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>>,
48 pub guid_law: Option<Arc<dyn GuidanceLaw>>,
49 pub decrement_mass: bool,
50}
51
52impl SpacecraftDynamics {
53 pub fn from_guidance_law(orbital_dyn: OrbitalDynamics, guid_law: Arc<dyn GuidanceLaw>) -> Self {
56 Self {
57 orbital_dyn,
58 guid_law: Some(guid_law),
59 force_models: Vec::new(),
60 decrement_mass: true,
61 }
62 }
63
64 pub fn from_guidance_law_no_decr(
67 orbital_dyn: OrbitalDynamics,
68 guid_law: Arc<dyn GuidanceLaw>,
69 ) -> Self {
70 Self {
71 orbital_dyn,
72 guid_law: Some(guid_law),
73 force_models: Vec::new(),
74 decrement_mass: false,
75 }
76 }
77
78 pub fn new(orbital_dyn: OrbitalDynamics) -> Self {
80 Self {
81 orbital_dyn,
82 guid_law: None,
83 force_models: Vec::new(),
84 decrement_mass: true,
85 }
86 }
87
88 pub fn from_model(orbital_dyn: OrbitalDynamics, force_model: Arc<dyn ForceModel>) -> Self {
90 Self {
91 orbital_dyn,
92 guid_law: None,
93 force_models: vec![force_model],
94 decrement_mass: true,
95 }
96 }
97
98 pub fn from_models(
100 orbital_dyn: OrbitalDynamics,
101 force_models: Vec<Arc<dyn ForceModel>>,
102 ) -> Self {
103 let mut me = Self::new(orbital_dyn);
104 me.force_models = force_models;
105 me
106 }
107
108 pub fn guidance_achieved(&self, state: &Spacecraft) -> Result<bool, GuidanceError> {
110 match &self.guid_law {
111 Some(guid_law) => guid_law.achieved(state),
112 None => Err(GuidanceError::NoGuidanceObjectiveDefined),
113 }
114 }
115
116 pub fn with_guidance_law(&self, guid_law: Arc<dyn GuidanceLaw>) -> Self {
118 Self {
119 orbital_dyn: self.orbital_dyn.clone(),
120 guid_law: Some(guid_law),
121 force_models: self.force_models.clone(),
122 decrement_mass: self.decrement_mass,
123 }
124 }
125}
126
127impl fmt::Display for SpacecraftDynamics {
128 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
129 let force_models: String = if self.force_models.is_empty() {
130 "No force models;".to_string()
131 } else {
132 self.force_models
133 .iter()
134 .fold(String::new(), |mut output, x| {
135 let _ = write!(output, "{x}; ");
136 output
137 })
138 };
139 write!(
140 f,
141 "Spacecraft dynamics (with guidance = {}): {} {}",
142 self.guid_law.is_some(),
143 force_models,
144 self.orbital_dyn
145 )
146 }
147}
148
149impl Dynamics for SpacecraftDynamics {
150 type HyperdualSize = Const<9>;
151 type StateType = Spacecraft;
152
153 fn finally(
154 &self,
155 next_state: Self::StateType,
156 almanac: Arc<Almanac>,
157 ) -> Result<Self::StateType, DynamicsError> {
158 if next_state.mass.prop_mass_kg < 0.0 {
159 error!("negative prop mass at {}", next_state.epoch());
160 return Err(DynamicsError::FuelExhausted {
161 sc: Box::new(next_state),
162 });
163 }
164
165 if let Some(guid_law) = &self.guid_law {
166 let mut state = next_state;
167 guid_law.next(&mut state, almanac.clone());
169 Ok(state)
170 } else {
171 Ok(next_state)
172 }
173 }
174
175 fn eom(
176 &self,
177 delta_t_s: f64,
178 state: &OVector<f64, Const<90>>,
179 ctx: &Self::StateType,
180 almanac: Arc<Almanac>,
181 ) -> Result<OVector<f64, Const<90>>, DynamicsError> {
182 let osc_sc = ctx.set_with_delta_seconds(delta_t_s, state);
184 let mut d_x = OVector::<f64, Const<90>>::zeros();
185
186 match ctx.stm {
188 Some(stm) => {
189 let (state, grad) = self.dual_eom(delta_t_s, &osc_sc, almanac)?;
191
192 let stm_dt = stm * grad;
194
195 for (i, val) in state.iter().enumerate() {
197 d_x[i] = *val;
198 }
199
200 for (i, val) in stm_dt.iter().copied().enumerate() {
201 d_x[i + <Spacecraft as State>::Size::dim()] = val;
202 }
203 }
204 None => {
205 for (i, val) in self
207 .orbital_dyn
208 .eom(&osc_sc.orbit, almanac.clone())?
209 .iter()
210 .copied()
211 .enumerate()
212 {
213 d_x[i] = val;
214 }
215
216 for model in &self.force_models {
218 let model_frc = model.eom(&osc_sc, almanac.clone())? / osc_sc.mass_kg();
219 for i in 0..3 {
220 d_x[i + 3] += model_frc[i];
221 }
222 }
223 }
224 };
225
226 if let Some(guid_law) = &self.guid_law {
228 let (thrust_force, prop_rate) = {
229 if osc_sc.thruster.is_none() {
230 return Err(DynamicsError::DynamicsGuidance {
231 source: GuidanceError::NoThrustersDefined,
232 });
233 }
234 let thruster = osc_sc.thruster.unwrap();
235 let thrust_throttle_lvl =
236 guid_law.throttle(&osc_sc).context(DynamicsGuidanceSnafu)?;
237 if !(0.0..=1.0).contains(&thrust_throttle_lvl) {
238 return Err(DynamicsError::DynamicsGuidance {
239 source: GuidanceError::ThrottleRatio {
240 ratio: thrust_throttle_lvl,
241 },
242 });
243 } else if thrust_throttle_lvl > 0.0 {
244 let thrust_inertial =
246 guid_law.direction(&osc_sc).context(DynamicsGuidanceSnafu)?;
247 if (thrust_inertial.norm() - 1.0).abs() > NORM_ERR {
248 let (alpha, delta) = ra_dec_from_unit_vector(thrust_inertial);
249 return Err(DynamicsError::DynamicsGuidance {
250 source: GuidanceError::InvalidDirection {
251 x: thrust_inertial[0],
252 y: thrust_inertial[1],
253 z: thrust_inertial[2],
254 in_plane_deg: alpha.to_degrees(),
255 out_of_plane_deg: delta.to_degrees(),
256 },
257 });
258 } else if thrust_inertial.norm().is_normal() {
259 let total_thrust = (thrust_throttle_lvl * thruster.thrust_N) * 1e-3; (
262 thrust_inertial * total_thrust,
263 if self.decrement_mass {
264 let prop_usage = thrust_throttle_lvl * thruster.thrust_N
265 / (thruster.isp_s * STD_GRAVITY);
266 -prop_usage
267 } else {
268 0.0
269 },
270 )
271 } else {
272 warn!(
273 "Abnormal thrust direction vector\t|u| = {}",
274 thrust_inertial.norm()
275 );
276 (Vector3::zeros(), 0.0)
277 }
278 } else {
279 (Vector3::zeros(), 0.0)
280 }
281 };
282
283 for i in 0..3 {
284 d_x[i + 3] += thrust_force[i] / osc_sc.mass_kg();
285 }
286 d_x[8] += prop_rate;
287 }
288 Ok(d_x)
289 }
290
291 fn dual_eom(
292 &self,
293 delta_t_s: f64,
294 ctx: &Self::StateType,
295 almanac: Arc<Almanac>,
296 ) -> Result<(OVector<f64, Const<9>>, OMatrix<f64, Const<9>, Const<9>>), DynamicsError> {
297 let mut d_x = OVector::<f64, Const<9>>::zeros();
300 let mut grad = OMatrix::<f64, Const<9>, Const<9>>::zeros();
301
302 let (orb_state, orb_grad) =
303 self.orbital_dyn
304 .dual_eom(delta_t_s, &ctx.orbit, almanac.clone())?;
305
306 for (i, val) in orb_state.iter().enumerate() {
308 d_x[i] = *val;
309 }
310
311 for i in 0..6 {
312 for j in 0..6 {
313 grad[(i, j)] = orb_grad[(i, j)];
314 }
315 }
316
317 if self.guid_law.is_some() {
318 return Err(DynamicsError::DynamicsAstro {
319 source: AstroError::PartialsUndefined,
320 });
321 }
322
323 let total_mass = ctx.mass_kg();
325 for model in &self.force_models {
326 let (model_frc, model_grad) = model.dual_eom(ctx, almanac.clone())?;
327 for i in 0..3 {
328 d_x[i + 3] += model_frc[i] / total_mass;
330 for j in 0..3 {
332 grad[(i + 3, j)] += model_grad[(i, j)] / total_mass;
333 }
334 }
335 if let Some(idx) = model.estimation_index() {
337 for j in 0..3 {
338 grad[(j + 3, idx)] += model_grad[(3, j)] / total_mass;
339 }
340 }
341 }
342
343 Ok((d_x, grad))
344 }
345}