1use super::{
20 DynamicsAlmanacSnafu, DynamicsAstroSnafu, DynamicsError, DynamicsPlanetarySnafu, ForceModel,
21};
22use crate::cosmic::{AstroPhysicsSnafu, Frame, Spacecraft};
23use crate::dynamics::nrlmsise00::msise00_density;
24use crate::io::space_weather::SpaceWeatherData;
25use crate::linalg::{Matrix4x3, Vector3};
26use anise::almanac::Almanac;
27use anise::constants::frames::IAU_EARTH_FRAME;
28use anise::errors::OrientationSnafu;
29use hifitime::Unit;
30use serde::{Deserialize, Serialize};
31use serde_dhall::StaticType;
32use snafu::ResultExt;
33use std::fmt;
34use std::sync::Arc;
35use trig_const::{ln, sqrt};
36
37#[cfg(feature = "python")]
38use pyo3::prelude::*;
39#[cfg(feature = "python")]
40use pyo3::types::PyType;
41
42pub mod nrlmsise00;
43
44const SIN_30_DEG: f64 = 0.5;
45const COS_30_DEG: f64 = sqrt(3.0_f64) / 2.0_f64;
46
47#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
50struct HpNode {
51 alt_km: f64,
52 min_density_kg_m3: f64,
53 max_density_kg_m3: f64,
54}
55
56impl HpNode {
57 const fn ln_min_density(&self) -> f64 {
58 ln(self.min_density_kg_m3)
59 }
60 const fn ln_max_density(&self) -> f64 {
61 ln(self.max_density_kg_m3)
62 }
63}
64
65const HP_TABLE: &[HpNode] = &[
67 HpNode {
68 alt_km: 100.0,
69 min_density_kg_m3: 4.974e-07,
70 max_density_kg_m3: 4.974e-07,
71 },
72 HpNode {
73 alt_km: 120.0,
74 min_density_kg_m3: 2.490e-08,
75 max_density_kg_m3: 2.490e-08,
76 },
77 HpNode {
78 alt_km: 140.0,
79 min_density_kg_m3: 3.840e-09,
80 max_density_kg_m3: 3.840e-09,
81 },
82 HpNode {
83 alt_km: 160.0,
84 min_density_kg_m3: 1.170e-09,
85 max_density_kg_m3: 1.170e-09,
86 },
87 HpNode {
88 alt_km: 180.0,
89 min_density_kg_m3: 4.820e-10,
90 max_density_kg_m3: 5.220e-10,
91 },
92 HpNode {
93 alt_km: 200.0,
94 min_density_kg_m3: 2.260e-10,
95 max_density_kg_m3: 2.620e-10,
96 },
97 HpNode {
98 alt_km: 240.0,
99 min_density_kg_m3: 6.880e-11,
100 max_density_kg_m3: 9.380e-11,
101 },
102 HpNode {
103 alt_km: 280.0,
104 min_density_kg_m3: 2.570e-11,
105 max_density_kg_m3: 4.180e-11,
106 },
107 HpNode {
108 alt_km: 320.0,
109 min_density_kg_m3: 1.090e-11,
110 max_density_kg_m3: 2.060e-11,
111 },
112 HpNode {
113 alt_km: 360.0,
114 min_density_kg_m3: 4.980e-12,
115 max_density_kg_m3: 1.070e-11,
116 },
117 HpNode {
118 alt_km: 400.0,
119 min_density_kg_m3: 2.380e-12,
120 max_density_kg_m3: 5.820e-12,
121 },
122 HpNode {
123 alt_km: 440.0,
124 min_density_kg_m3: 1.180e-12,
125 max_density_kg_m3: 3.250e-12,
126 },
127 HpNode {
128 alt_km: 480.0,
129 min_density_kg_m3: 6.020e-13,
130 max_density_kg_m3: 1.860e-12,
131 },
132 HpNode {
133 alt_km: 520.0,
134 min_density_kg_m3: 3.150e-13,
135 max_density_kg_m3: 1.080e-12,
136 },
137 HpNode {
138 alt_km: 560.0,
139 min_density_kg_m3: 1.680e-13,
140 max_density_kg_m3: 6.400e-13,
141 },
142 HpNode {
143 alt_km: 600.0,
144 min_density_kg_m3: 9.100e-14,
145 max_density_kg_m3: 3.830e-13,
146 },
147 HpNode {
148 alt_km: 680.0,
149 min_density_kg_m3: 2.820e-14,
150 max_density_kg_m3: 1.440e-13,
151 },
152 HpNode {
153 alt_km: 760.0,
154 min_density_kg_m3: 9.200e-15,
155 max_density_kg_m3: 5.760e-14,
156 },
157 HpNode {
158 alt_km: 840.0,
159 min_density_kg_m3: 3.100e-15,
160 max_density_kg_m3: 2.400e-14,
161 },
162 HpNode {
163 alt_km: 920.0,
164 min_density_kg_m3: 1.100e-15,
165 max_density_kg_m3: 1.050e-14,
166 },
167 HpNode {
168 alt_km: 1000.0,
169 min_density_kg_m3: 4.000e-16,
170 max_density_kg_m3: 4.800e-15,
171 },
172];
173
174#[derive(Clone, Debug, Serialize, Deserialize, StaticType)]
176#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
177pub enum AtmDensity {
178 Constant(f64),
183
184 Exponential {
195 rho0_kg_m3: f64,
197 ref_alt_km: f64,
199 scale_height_km: f64,
201 },
202
203 StdAtm {
211 max_alt_km: f64,
213 },
214
215 NRLMSISE00 { weather: SpaceWeatherData },
221
222 HarrisPriester {
228 n_parameter: usize,
230 },
231}
232
233#[cfg(feature = "python")]
234#[cfg_attr(feature = "python", pymethods)]
235impl AtmDensity {
236 #[classmethod]
242 fn earth_exponential(_cls: &Bound<'_, PyType>) -> Self {
243 AtmDensity::Exponential {
244 rho0_kg_m3: 3.614e-13,
245 ref_alt_km: 700.000,
246 scale_height_km: 88.667,
247 }
248 }
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize, StaticType)]
253#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
254pub struct Drag {
255 pub density: AtmDensity,
257 pub frame: Frame,
259 pub estimate: bool,
261}
262
263impl Drag {
264 pub fn earth_exp(almanac: &Almanac) -> Result<Arc<Self>, DynamicsError> {
270 Ok(Arc::new(Self {
271 density: AtmDensity::Exponential {
272 rho0_kg_m3: 3.614e-13,
273 ref_alt_km: 700.000,
274 scale_height_km: 88.667,
275 },
276 frame: almanac
277 .frame_info(IAU_EARTH_FRAME)
278 .context(DynamicsPlanetarySnafu {
279 action: "planetary data from third body not loaded",
280 })?,
281 estimate: false,
282 }))
283 }
284
285 pub fn std_atm1976(almanac: &Almanac) -> Result<Arc<Self>, DynamicsError> {
290 Ok(Arc::new(Self {
291 density: AtmDensity::StdAtm {
292 max_alt_km: 1_000.0,
293 },
294 frame: almanac
295 .frame_info(IAU_EARTH_FRAME)
296 .context(DynamicsPlanetarySnafu {
297 action: "planetary data from third body not loaded",
298 })?,
299 estimate: false,
300 }))
301 }
302
303 fn rho_kg_m3(&self, ctx: &Spacecraft, almanac: &Almanac) -> Result<f64, DynamicsError> {
305 let osc_drag_frame =
306 almanac
307 .transform_to(ctx.orbit, self.frame, None)
308 .context(DynamicsAlmanacSnafu {
309 action: "transforming into drag frame",
310 })?;
311
312 let rho_kg_m3 = match &self.density {
313 AtmDensity::Constant(rho) => *rho,
314
315 AtmDensity::Exponential {
316 rho0_kg_m3,
317 scale_height_km,
318 ref_alt_km,
319 } => {
320 let altitude_km = osc_drag_frame
321 .altitude_km()
322 .context(AstroPhysicsSnafu)
323 .context(DynamicsAstroSnafu)?;
324 rho0_kg_m3 * (-(altitude_km - ref_alt_km) / scale_height_km).exp()
325 }
326
327 AtmDensity::StdAtm { max_alt_km } => {
328 let altitude_km = osc_drag_frame
329 .altitude_km()
330 .context(AstroPhysicsSnafu)
331 .context(DynamicsAstroSnafu)?;
332
333 if altitude_km > *max_alt_km {
334 10.0_f64.powf((-7e-5) * altitude_km - 14.464)
336 } else {
337 let scale = (altitude_km - 526.8000) / 292.8563;
340 let logdensity =
341 0.34047 * scale.powi(6) - 0.5889 * scale.powi(5) - 0.5269 * scale.powi(4)
342 + 1.0036 * scale.powi(3)
343 + 0.60713 * scale.powi(2)
344 - 2.3024 * scale
345 - 12.575;
346
347 10.0_f64.powf(logdensity)
349 }
350 }
351
352 AtmDensity::NRLMSISE00 { weather } => {
353 let (lat_deg, long_deg, alt_km) = osc_drag_frame
354 .latlongalt()
355 .context(AstroPhysicsSnafu)
356 .context(DynamicsAstroSnafu)?;
357
358 let lst_h = almanac.local_solar_time(osc_drag_frame, None).context(
359 DynamicsAlmanacSnafu {
360 action: "computing local solar time",
361 },
362 )?;
363
364 let epoch = ctx.orbit.epoch;
365
366 let sw = weather.msise_weather(epoch);
367
368 msise00_density(
369 sw,
370 lst_h.to_unit(Unit::Hour),
371 lat_deg,
372 long_deg,
373 alt_km,
374 ctx.orbit.epoch,
375 )?
376 .total_mass_density_kg_m3
377 }
378
379 AtmDensity::HarrisPriester { n_parameter } => {
380 let altitude_km = osc_drag_frame
381 .altitude_km()
382 .context(AstroPhysicsSnafu)
383 .context(DynamicsAstroSnafu)?;
384
385 if altitude_km < HP_TABLE[0].alt_km
386 || altitude_km > HP_TABLE[HP_TABLE.len() - 1].alt_km
387 {
388 0.0
389 } else {
390 let idx = HP_TABLE
392 .windows(2)
393 .position(|w| altitude_km >= w[0].alt_km && altitude_km <= w[1].alt_km)
394 .unwrap_or(0);
395
396 let n0 = &HP_TABLE[idx];
397 let n1 = &HP_TABLE[idx + 1];
398
399 let h_min =
401 (n0.alt_km - n1.alt_km) / (n1.ln_min_density() - n0.ln_min_density());
402 let h_max =
403 (n0.alt_km - n1.alt_km) / (n1.ln_max_density() - n0.ln_max_density());
404
405 let rho_min = n0.min_density_kg_m3 * (-(altitude_km - n0.alt_km) / h_min).exp();
406 let rho_max = n0.max_density_kg_m3 * (-(altitude_km - n0.alt_km) / h_max).exp();
407
408 let u_sun = almanac
410 .sun_unit_vector(ctx.orbit.epoch, self.frame, None)
411 .context(DynamicsAlmanacSnafu {
412 action: "fetching sun position for Harris-Priester model",
413 })?;
414
415 let u_bulge = Vector3::new(
417 u_sun.x * COS_30_DEG - u_sun.y * SIN_30_DEG,
418 u_sun.x * SIN_30_DEG + u_sun.y * COS_30_DEG,
419 u_sun.z,
420 );
421
422 let u_pos = osc_drag_frame.r_hat();
424 let cos_psi = u_pos.dot(&u_bulge).clamp(-1.0, 1.0);
425
426 let cos_half_psi = ((1.0 + cos_psi) / 2.0).sqrt();
428 let mod_factor = cos_half_psi.powi(*n_parameter as i32);
429
430 rho_min + (rho_max - rho_min) * mod_factor
431 }
432 }
433 };
434
435 Ok(rho_kg_m3)
436 }
437}
438
439impl fmt::Display for Drag {
440 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
441 write!(
442 f,
443 "\tDrag density {:?} in frame {}",
444 self.density, self.frame
445 )
446 }
447}
448
449impl ForceModel for Drag {
450 fn estimation_index(&self) -> Option<usize> {
451 if self.estimate { Some(7) } else { None }
452 }
453
454 fn eom(&self, ctx: &Spacecraft, almanac: &Almanac) -> Result<Vector3<f64>, DynamicsError> {
455 let integration_frame = ctx.orbit.frame;
456
457 let drag_frame = almanac
458 .frame_info(self.frame)
459 .context(DynamicsPlanetarySnafu {
460 action: "fetching drag frame information",
461 })?;
462
463 let osc_drag_frame =
464 almanac
465 .transform_to(ctx.orbit, self.frame, None)
466 .context(DynamicsAlmanacSnafu {
467 action: "transforming into drag frame",
468 })?;
469
470 let rho_kg_m3 = self.rho_kg_m3(ctx, almanac)?;
471
472 let v_km_s = osc_drag_frame.velocity_km_s;
473
474 let accel_drag_frame_kg_km_s2 = -0.5
476 * 1e3
477 * rho_kg_m3
478 * ctx.drag.coeff_drag
479 * ctx.drag.area_m2
480 * v_km_s.norm()
481 * v_km_s;
482
483 let accel_integr_frame = almanac
484 .rotate(drag_frame, integration_frame, ctx.orbit.epoch)
485 .context(OrientationSnafu {
486 action: "rotating drafg force into integration frame",
487 })
488 .context(DynamicsAlmanacSnafu {
489 action: "rotating drag force into integration frame",
490 })?
491 * accel_drag_frame_kg_km_s2;
492
493 Ok(accel_integr_frame)
495 }
496
497 fn gradient(
500 &self,
501 ctx: &Spacecraft,
502 almanac: &Almanac,
503 ) -> Result<(Vector3<f64>, Matrix4x3<f64>), DynamicsError> {
504 let dx = self.eom(ctx, almanac)?;
505
506 let mut grad = Matrix4x3::zeros();
507
508 for j in 0..3 {
510 let h = 6.0e-6 * ctx.orbit.radius_km[j].abs().max(1.0);
512
513 let mut ctx_plus = *ctx;
514 ctx_plus.orbit.radius_km[j] += h;
515 let f_plus = self.eom(&ctx_plus, almanac)?;
516
517 let mut ctx_minus = *ctx;
518 ctx_minus.orbit.radius_km[j] -= h;
519 let f_minus = self.eom(&ctx_minus, almanac)?;
520
521 let df_dr = (f_plus - f_minus) / (2.0 * h);
522 for i in 0..3 {
523 grad[(i, j)] = df_dr[i];
524 }
525 }
526
527 let wrt_cd = dx / ctx.drag.coeff_drag;
532 for j in 0..3 {
533 grad[(3, j)] = wrt_cd[j];
534 }
535
536 Ok((dx, grad))
537 }
538}
539
540#[cfg(feature = "python")]
541#[cfg_attr(feature = "python", pymethods)]
542impl Drag {
543 #[pyo3(signature = (density, frame, estimate=true))]
544 #[new]
545 fn py_new(density: AtmDensity, frame: Frame, estimate: bool) -> Self {
546 Self {
547 density,
548 frame,
549 estimate,
550 }
551 }
552
553 fn __str__(&self) -> String {
554 format!("{self}")
555 }
556
557 fn __repr__(&self) -> String {
558 format!("{self} @ {self:p}")
559 }
560}