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 guid_law.next(&mut state, almanac.clone());
174 Ok(state)
175 } else {
176 Ok(next_state)
177 }
178 }
179
180 fn eom(
181 &self,
182 delta_t_s: f64,
183 state: &OVector<f64, Const<90>>,
184 ctx: &Self::StateType,
185 almanac: Arc<Almanac>,
186 ) -> Result<OVector<f64, Const<90>>, DynamicsError> {
187 let osc_sc = ctx.set_with_delta_seconds(delta_t_s, state);
189 let mut d_x = OVector::<f64, Const<90>>::zeros();
190
191 match ctx.stm {
193 Some(stm) => {
194 let (state, grad) = self.dual_eom(delta_t_s, &osc_sc, almanac)?;
196
197 let stm_dt = stm * grad;
199
200 for (i, val) in state.iter().enumerate() {
202 d_x[i] = *val;
203 }
204
205 for (i, val) in stm_dt.iter().copied().enumerate() {
206 d_x[i + <Spacecraft as State>::Size::dim()] = val;
207 }
208 }
209 None => {
210 for (i, val) in self
212 .orbital_dyn
213 .eom(&osc_sc.orbit, almanac.clone())?
214 .iter()
215 .copied()
216 .enumerate()
217 {
218 d_x[i] = val;
219 }
220
221 for model in &self.force_models {
223 let model_frc = model.eom(&osc_sc, almanac.clone())? / osc_sc.mass_kg();
224 for i in 0..3 {
225 d_x[i + 3] += model_frc[i];
226 }
227 }
228 }
229 };
230
231 if let Some(guid_law) = &self.guid_law {
233 let (thrust_force, prop_rate) = {
234 if osc_sc.thruster.is_none() {
235 return Err(DynamicsError::DynamicsGuidance {
236 source: GuidanceError::NoThrustersDefined,
237 });
238 }
239 let thruster = osc_sc.thruster.unwrap();
240 let thrust_throttle_lvl =
241 guid_law.throttle(&osc_sc).context(DynamicsGuidanceSnafu)?;
242 if !(0.0..=1.0).contains(&thrust_throttle_lvl) {
243 return Err(DynamicsError::DynamicsGuidance {
244 source: GuidanceError::ThrottleRatio {
245 ratio: thrust_throttle_lvl,
246 },
247 });
248 } else if thrust_throttle_lvl > 0.0 {
249 let thrust_inertial =
251 guid_law.direction(&osc_sc).context(DynamicsGuidanceSnafu)?;
252 if (thrust_inertial.norm() - 1.0).abs() > NORM_ERR {
253 let (alpha, delta) = ra_dec_from_unit_vector(thrust_inertial);
254 return Err(DynamicsError::DynamicsGuidance {
255 source: GuidanceError::InvalidDirection {
256 x: thrust_inertial[0],
257 y: thrust_inertial[1],
258 z: thrust_inertial[2],
259 in_plane_deg: alpha.to_degrees(),
260 out_of_plane_deg: delta.to_degrees(),
261 },
262 });
263 } else if thrust_inertial.norm().is_normal() {
264 let total_thrust = (thrust_throttle_lvl * thruster.thrust_N) * 1e-3; (
267 thrust_inertial * total_thrust,
268 if self.decrement_mass {
269 let prop_usage = thrust_throttle_lvl * thruster.thrust_N
270 / (thruster.isp_s * STD_GRAVITY);
271 -prop_usage
272 } else {
273 0.0
274 },
275 )
276 } else {
277 warn!(
278 "Abnormal thrust direction vector\t|u| = {}",
279 thrust_inertial.norm()
280 );
281 (Vector3::zeros(), 0.0)
282 }
283 } else {
284 (Vector3::zeros(), 0.0)
285 }
286 };
287
288 for i in 0..3 {
289 d_x[i + 3] += thrust_force[i] / osc_sc.mass_kg();
290 }
291 d_x[8] += prop_rate;
292 }
293 Ok(d_x)
294 }
295
296 fn dual_eom(
297 &self,
298 delta_t_s: f64,
299 ctx: &Self::StateType,
300 almanac: Arc<Almanac>,
301 ) -> Result<(OVector<f64, Const<9>>, OMatrix<f64, Const<9>, Const<9>>), DynamicsError> {
302 let mut d_x = OVector::<f64, Const<9>>::zeros();
305 let mut grad = OMatrix::<f64, Const<9>, Const<9>>::zeros();
306
307 let (orb_state, orb_grad) =
308 self.orbital_dyn
309 .dual_eom(delta_t_s, &ctx.orbit, almanac.clone())?;
310
311 for (i, val) in orb_state.iter().enumerate() {
313 d_x[i] = *val;
314 }
315
316 for i in 0..6 {
317 for j in 0..6 {
318 grad[(i, j)] = orb_grad[(i, j)];
319 }
320 }
321
322 if self.guid_law.is_some() {
323 return Err(DynamicsError::DynamicsAstro {
324 source: AstroError::PartialsUndefined,
325 });
326 }
327
328 let total_mass = ctx.mass_kg();
330 for model in &self.force_models {
331 let (model_frc, model_grad) = model.dual_eom(ctx, almanac.clone())?;
332 for i in 0..3 {
333 d_x[i + 3] += model_frc[i] / total_mass;
335 for j in 0..3 {
337 grad[(i + 3, j)] += model_grad[(i, j)] / total_mass;
338 }
339 }
340 if let Some(idx) = model.estimation_index() {
342 for j in 0..3 {
343 grad[(j + 3, idx)] += model_grad[(3, j)] / total_mass;
344 }
345 }
346 }
347
348 Ok((d_x, grad))
349 }
350}