1use 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#[derive(Clone)]
43pub struct SpacecraftDynamics {
44 pub orbital_dyn: OrbitalDynamics,
45 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 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 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 let osc_sc = ctx.set_with_delta_seconds(delta_t_s, state);
183 let mut d_x = OVector::<f64, Const<90>>::zeros();
184
185 match ctx.stm {
187 Some(stm) => {
188 let (state, grad) = self.dual_eom(delta_t_s, &osc_sc, almanac)?;
190
191 let stm_dt = stm * grad;
193
194 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 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 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 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 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 let total_thrust = (thrust_throttle_lvl * thruster.thrust_N) * 1e-3; (
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 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 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 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 d_x[i + 3] += model_frc[i] / total_mass;
329 for j in 0..3 {
331 grad[(i + 3, j)] += model_grad[(i, j)] / total_mass;
332 }
333 }
334 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}