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