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