use std::f64::consts::PI;
use crate::common::{CoordinateFrame, StateVector, WGS72, Wgs, calc_period, deg2rad};
use crate::gp::GenPerturbElementSet;
use crate::time::{DateError, DateTime, utc2jday};
#[derive(Default, Clone)]
pub struct Sgp4 {
pub wgs: Wgs,
pub gp: GenPerturbElementSet,
pub jd0: f64,
pub jdfrac0: f64,
pub deep_space: bool,
pub brouwer0: BrouwerMeanElements,
pub atm_params: AtmDragParams,
pub zonal_params: EarthZonalParams,
pub solar_params: ThirdBodyParams,
pub lunar_params: ThirdBodyParams,
pub whole_day_resonance: bool,
pub whole_day_resonance_params: WholeDayResonanceParams,
pub half_day_resonance: bool,
pub half_day_resonance_params: HalfDayResonanceParams,
}
#[derive(Default, Clone, Copy)]
pub struct BrouwerMeanElements {
pub i: f64,
pub theta: f64,
pub raan: f64,
pub e: f64,
pub beta: f64,
pub omega: f64,
pub m: f64,
pub n: f64,
pub a: f64,
pub period: f64,
}
#[derive(Default, Clone, Copy)]
pub struct AtmDragParams {
pub hp: f64,
pub q0: f64,
pub s: f64,
pub zeta: f64,
pub eta: f64,
pub c1: f64,
pub c3: f64,
pub c4: f64,
pub c5: f64,
pub d2: f64,
pub d3: f64,
pub d4: f64,
}
#[derive(Default, Clone, Copy)]
pub struct EarthZonalParams {
pub m_dot: f64,
pub omega_dot: f64,
pub raan_dot: f64,
}
#[derive(Default, Clone, Copy)]
pub struct ThirdBodyParams {
pub cos_i: f64,
pub sin_i: f64,
pub e: f64,
pub n: f64,
pub cos_omega: f64,
pub sin_omega: f64,
pub raan: f64,
pub m: f64,
pub beta: f64,
pub c: f64,
pub x1: f64,
pub x2: f64,
pub x3: f64,
pub x4: f64,
pub x5: f64,
pub x6: f64,
pub x7: f64,
pub x8: f64,
pub z1: f64,
pub z2: f64,
pub z3: f64,
pub z11: f64,
pub z13: f64,
pub z21: f64,
pub z23: f64,
pub z22: f64,
pub z12: f64,
pub z31: f64,
pub z32: f64,
pub z33: f64,
pub e_dot: f64,
pub i_dot: f64,
pub m_dot: f64,
pub omega_dot: f64,
pub raan_dot: f64,
}
#[derive(Default, Clone, Copy)]
pub struct HalfDayResonanceParams {
pub theta_g: f64,
pub lam0: f64,
pub lam0_dot: f64,
pub d2201: f64,
pub d2211: f64,
pub d3210: f64,
pub d3222: f64,
pub d5220: f64,
pub d5232: f64,
pub d4422: f64,
pub d5421: f64,
pub d5433: f64,
pub d4410: f64,
}
#[derive(Default, Clone, Copy)]
pub struct WholeDayResonanceParams {
pub theta_g: f64,
pub lam0: f64,
pub lam0_dot: f64,
pub lam31: f64,
pub lam22: f64,
pub lam33: f64,
pub delta1: f64,
pub delta2: f64,
pub delta3: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Sgp4Error {
InvalidMeanMotion,
InvalidMeanEccentricity,
InvalidPerturbedEccentricity,
InvalidSemilatusRectum,
SatelliteDecayed,
InvalidDateTime(DateError),
NonFiniteState,
}
const XPDOTP: f64 = 1440.0 / (2.0 * PI);
const RPTIM: f64 = 4.375_269_088_011_3e-3;
pub fn init_sgp4(gp: &GenPerturbElementSet, wgs: Option<&Wgs>) -> Result<Sgp4, Sgp4Error> {
let wgs_sgp4 = if let Some(wgs_passed) = wgs {
*wgs_passed
} else {
WGS72
};
let i0 = deg2rad(gp.inclination); let n0_kozai = gp.mean_motion / XPDOTP; let e0 = gp.eccentricity; let omega0 = deg2rad(gp.argument_of_perigee); let raan0 = deg2rad(gp.right_ascension_of_ascending_node); let m0 = deg2rad(gp.mean_anomaly);
let (jd0, jdfrac0) = utc2jday(&gp.epoch_datetime).map_err(Sgp4Error::InvalidDateTime)?;
let theta0 = i0.cos();
let beta0 = (1. - e0.powi(2)).sqrt();
let a1 = (wgs_sgp4.ke / n0_kozai).powf(2. / 3.);
let delta1 = (3. / 2.) * (wgs_sgp4.k2 / a1.powf(2.)) * (3. * i0.cos().powf(2.) - 1.)
/ (1. - e0.powf(2.)).powf(3. / 2.);
let a2 = a1 * (1. - (1. / 3.) * delta1 - delta1.powf(2.) - (134. / 81.) * delta1.powf(3.));
let delta0 = (3. / 2.) * (wgs_sgp4.k2 / a2.powf(2.)) * (3. * i0.cos().powf(2.) - 1.)
/ (1. - e0.powf(2.)).powf(3. / 2.);
let n0 = n0_kozai / (1. + delta0); let a0 = (wgs_sgp4.ke / n0).powf(2. / 3.); let a0_km = a0 * wgs_sgp4.r_earth_eq; let period0 = calc_period(a0_km, wgs_sgp4.mu);
let brouwer0 = BrouwerMeanElements {
i: i0,
theta: theta0,
raan: raan0,
e: e0,
beta: beta0,
omega: omega0,
m: m0,
n: n0,
a: a0,
period: period0,
};
let atm_params = init_atm_effects(&wgs_sgp4, gp, &brouwer0);
let zonal_params = init_zonal_effects(&wgs_sgp4, &brouwer0);
let mut deep_space = false;
if period0 >= 225. {
deep_space = true;
}
let (lunar_params, solar_params) =
init_lunar_solar_effects(deep_space, jd0, jdfrac0, &brouwer0);
let mut whole_day_resonance = false;
let mut half_day_resonance = false;
let mut whole_day_resonance_params = WholeDayResonanceParams::default();
let mut half_day_resonance_params = HalfDayResonanceParams::default();
if (n0 > 0.0034906585) && (n0 < 0.0052359877) {
whole_day_resonance = true;
whole_day_resonance_params = init_earth_gravity_resonance_wholeday(
jd0,
jdfrac0,
&brouwer0,
&zonal_params,
&lunar_params,
&solar_params,
);
}
if (8.26e-3..=9.24e-3).contains(&n0) && (e0 >= 0.5) {
half_day_resonance = true;
half_day_resonance_params = init_earth_gravity_resonance_halfday(
jd0,
jdfrac0,
&brouwer0,
&zonal_params,
&lunar_params,
&solar_params,
);
}
let sgp4 = Sgp4 {
wgs: wgs_sgp4,
gp: gp.clone(),
jd0,
jdfrac0,
deep_space,
brouwer0,
atm_params,
zonal_params,
lunar_params,
solar_params,
whole_day_resonance,
whole_day_resonance_params,
half_day_resonance,
half_day_resonance_params,
};
sgp4_prop_delta(&sgp4, 0.0)?;
Ok(sgp4)
}
fn init_atm_effects(
wgs: &Wgs,
gp: &GenPerturbElementSet,
brouwer0: &BrouwerMeanElements,
) -> AtmDragParams {
let a30 = -wgs.j3; let q0 = (120. + wgs.r_earth_eq) / wgs.r_earth_eq;
let rp = brouwer0.a * (1. - brouwer0.e); let hp = (rp - 1.) * wgs.r_earth_eq;
let s: f64 = if hp >= 156. {
(78. + wgs.r_earth_eq) / wgs.r_earth_eq
} else if hp >= 98. {
(hp - 78. + wgs.r_earth_eq) / wgs.r_earth_eq
} else {
(20. + wgs.r_earth_eq) / wgs.r_earth_eq
};
let zeta = 1. / (brouwer0.a - s);
let eta = brouwer0.a * brouwer0.e * zeta;
let psisq = (1. - eta.powi(2)).abs();
let c2_1 = (q0 - s).powi(4) * zeta.powi(4) * brouwer0.n * psisq.powf(-3.5);
let c2_2 = brouwer0.a
* (1. + (3. / 2.) * eta.powi(2) + 4. * brouwer0.e * eta + brouwer0.e * eta.powi(3));
let c2_3 = (3. / 2.)
* (wgs.k2 * zeta / psisq)
* (-(1. / 2.) + (3. / 2.) * brouwer0.theta.powi(2))
* (8. + 24. * eta.powi(2) + 3. * eta.powi(4));
let c2 = c2_1 * (c2_2 + c2_3);
let c1 = gp.bstar * c2;
let c3 = if brouwer0.e > 1.0e-4 {
((q0 - s).powf(4.) * zeta.powf(5.) * a30 * brouwer0.n * brouwer0.i.sin())
/ (wgs.k2 * brouwer0.e)
} else {
0.0
};
let c4_1 = 2.
* brouwer0.n
* (q0 - s).powi(4)
* zeta.powi(4)
* brouwer0.a
* brouwer0.beta.powi(2)
* psisq.powf(-3.5);
let c4_2 = 2. * eta * (1. + brouwer0.e * eta) + 0.5 * brouwer0.e + 0.5 * eta.powi(3);
let c4_3 = 2. * wgs.k2 * zeta / (brouwer0.a * psisq);
let c4_4 = 3.
* (1. - 3. * brouwer0.theta.powi(2))
* (1. + 3. / 2. * eta.powi(2) - 2. * brouwer0.e * eta - 0.5 * brouwer0.e * eta.powi(3));
let c4_5 = 3. / 4.
* (1. - brouwer0.theta.powi(2))
* (2. * eta.powi(2) - brouwer0.e * eta - brouwer0.e * eta.powi(3))
* (2. * brouwer0.omega).cos();
let c4 = c4_1 * (c4_2 - c4_3 * (c4_4 + c4_5));
let c5_1 = 2.
* (q0 - s).powi(4)
* zeta.powi(4)
* brouwer0.a
* brouwer0.beta.powi(2)
* psisq.powf(-3.5);
let c5_2 = 1. + 11. / 4. * eta * (eta + brouwer0.e) + brouwer0.e * eta.powi(3);
let c5 = c5_1 * c5_2;
let d2 = 4. * brouwer0.a * zeta * c1.powi(2);
let d3 = 4. / 3. * brouwer0.a * zeta.powi(2) * (17. * brouwer0.a + s) * c1.powi(3);
let d4 =
2. / 3. * brouwer0.a.powi(2) * zeta.powi(3) * (221. * brouwer0.a + 31. * s) * c1.powi(4);
AtmDragParams {
hp,
q0,
s,
zeta,
eta,
c1,
c3,
c4,
c5,
d2,
d3,
d4,
}
}
fn init_zonal_effects(wgs: &Wgs, brouwer0: &BrouwerMeanElements) -> EarthZonalParams {
let m_dot_1 = 3. * wgs.k2 * (-1. + 3. * brouwer0.theta.powi(2))
/ (2. * brouwer0.a.powi(2) * brouwer0.beta.powi(3));
let m_dot_2 =
3. * wgs.k2.powi(2) * (13. - 78. * brouwer0.theta.powi(2) + 137. * brouwer0.theta.powi(4))
/ (16. * brouwer0.a.powi(4) * brouwer0.beta.powi(7));
let m_dot = (m_dot_1 + m_dot_2) * brouwer0.n;
let omega_dot_1 = -3. * wgs.k2 * (1. - 5. * brouwer0.theta.powi(2))
/ (2. * brouwer0.a.powi(2) * brouwer0.beta.powi(4));
let omega_dot_2 =
3. * wgs.k2.powi(2) * (7. - 114. * brouwer0.theta.powi(2) + 395. * brouwer0.theta.powi(4))
/ (16. * brouwer0.a.powi(4) * brouwer0.beta.powi(8));
let omega_dot_3 =
5. * wgs.k4 * (3. - 36. * brouwer0.theta.powi(2) + 49. * brouwer0.theta.powi(4))
/ (4. * brouwer0.a.powi(4) * brouwer0.beta.powi(8));
let omega_dot = (omega_dot_1 + omega_dot_2 + omega_dot_3) * brouwer0.n;
let raan_dot_1 = -3. * wgs.k2 * brouwer0.theta / (brouwer0.a.powi(2) * brouwer0.beta.powi(4));
let raan_dot_2 = 3. * wgs.k2.powi(2) * (4. * brouwer0.theta - 19. * brouwer0.theta.powi(3))
/ (2. * brouwer0.a.powi(4) * brouwer0.beta.powi(8));
let raan_dot_3 = 5. * wgs.k4 * brouwer0.theta * (3. - 7. * brouwer0.theta.powi(2))
/ (2. * brouwer0.a.powi(4) * brouwer0.beta.powi(8));
let raan_dot = (raan_dot_1 + raan_dot_2 + raan_dot_3) * brouwer0.n;
EarthZonalParams {
m_dot,
omega_dot,
raan_dot,
}
}
fn init_lunar_solar_effects(
deep_space: bool,
jd0: f64,
jdfrac0: f64,
brouwer0: &BrouwerMeanElements,
) -> (ThirdBodyParams, ThirdBodyParams) {
if !deep_space {
return (ThirdBodyParams::default(), ThirdBodyParams::default());
}
let epoch_sm = 2415020.0;
let e_m = 0.05490;
let n_m = 1.5835218e-4;
let c_m = 4.7968065e-7;
let raan_me0 = 4.5236020;
let raan_me0_dot = -9.2422029e-4;
let u_me0 = 5.8351514;
let u_me0_dot = 0.0019443680;
let m_m0 = 4.7199672;
let m_m0_dot = 0.22997150;
let sin_i_s = 0.39785416;
let cos_i_s = 0.91744867;
let e_s = 0.01675;
let n_s = 1.19459e-5;
let raan_s = 0.0;
let sin_omega_s = -0.98088458;
let cos_omega_s = 0.1945905;
let c_s = 2.9864797e-6;
let m_s0 = 6.2565837;
let m_s0_dot = 0.017201977;
let delta_t = jd0 + jdfrac0 - epoch_sm;
let raan_me = (raan_me0 + raan_me0_dot * delta_t).rem_euclid(2.0 * PI);
let sin_raan_me = raan_me.sin();
let cos_raan_me = raan_me.cos();
let cos_i_m = 0.91375164 - 0.03568096 * cos_raan_me;
let sin_i_m = (1.0 - cos_i_m * cos_i_m).sqrt();
let gamma_m = u_me0 + u_me0_dot * delta_t;
let sin_raan_m = 0.089683511 * sin_raan_me / sin_i_m;
let cos_raan_m = (1.0 - sin_raan_m * sin_raan_m).sqrt();
let raan_m = sin_raan_m.atan2(cos_raan_m);
let mut zx = 0.39785416 * sin_raan_me / sin_i_m;
let zy = cos_raan_m * cos_raan_me + 0.91744867 * sin_raan_m * sin_raan_me;
zx = zx.atan2(zy);
let omega_m = gamma_m + zx - raan_me;
let sin_omega_m = omega_m.sin();
let cos_omega_m = omega_m.cos();
let m_m = (m_m0 + m_m0_dot * delta_t - gamma_m).rem_euclid(2.0 * PI);
let m_s = (m_s0 + m_s0_dot * delta_t).rem_euclid(2.0 * PI);
let lunar_params = calc_lunar_solar_secular_rates(
&ThirdBodyParams {
cos_i: cos_i_m,
sin_i: sin_i_m,
e: e_m,
n: n_m,
cos_omega: cos_omega_m,
sin_omega: sin_omega_m,
raan: raan_m,
m: m_m,
c: c_m,
..Default::default()
},
brouwer0,
);
let solar_params = calc_lunar_solar_secular_rates(
&ThirdBodyParams {
cos_i: cos_i_s,
sin_i: sin_i_s,
e: e_s,
n: n_s,
cos_omega: cos_omega_s,
sin_omega: sin_omega_s,
raan: raan_s,
m: m_s,
c: c_s,
..Default::default()
},
brouwer0,
);
(lunar_params, solar_params)
}
fn calc_lunar_solar_secular_rates(
body: &ThirdBodyParams,
brouwer0: &BrouwerMeanElements,
) -> ThirdBodyParams {
let cos_raan_diff = (brouwer0.raan - body.raan).cos();
let sin_raan_diff = (brouwer0.raan - body.raan).sin();
let cos_omega0 = brouwer0.omega.cos();
let sin_omega0 = brouwer0.omega.sin();
let cos_i0 = brouwer0.i.cos();
let sin_i0 = brouwer0.i.sin();
let beta_x = (1. - body.e.powi(2)).sqrt();
let a1 = body.cos_omega * cos_raan_diff + body.sin_omega * body.cos_i * sin_raan_diff;
let a3 = -body.sin_omega * cos_raan_diff + body.cos_omega * body.cos_i * sin_raan_diff;
let a7 = -body.cos_omega * sin_raan_diff + body.sin_omega * body.cos_i * cos_raan_diff;
let a8 = body.sin_omega * body.sin_i;
let a9 = body.sin_omega * sin_raan_diff + body.cos_omega * body.cos_i * cos_raan_diff;
let a10 = body.cos_omega * body.sin_i;
let a2 = a7 * cos_i0 + a8 * sin_i0;
let a4 = a9 * cos_i0 + a10 * sin_i0;
let a5 = -a7 * sin_i0 + a8 * cos_i0;
let a6 = -a9 * sin_i0 + a10 * cos_i0;
let x1 = a1 * cos_omega0 + a2 * sin_omega0;
let x2 = a3 * cos_omega0 + a4 * sin_omega0;
let x3 = -a1 * sin_omega0 + a2 * cos_omega0;
let x4 = -a3 * sin_omega0 + a4 * cos_omega0;
let x5 = a5 * sin_omega0;
let x6 = a6 * sin_omega0;
let x7 = a5 * cos_omega0;
let x8 = a6 * cos_omega0;
let z31 = 12. * x1.powi(2) - 3. * x3.powi(2);
let z32 = 24. * x1 * x2 - 6. * x3 * x4;
let z33 = 12. * x2.powi(2) - 3. * x4.powi(2);
let z1 = 6. * (a1.powi(2) + a2.powi(2)) + (1. + brouwer0.e.powi(2)) * z31;
let z2 = 12. * (a1 * a3 + a2 * a4) + (1. + brouwer0.e.powi(2)) * z32;
let z3 = 6. * (a3.powi(2) + a4.powi(2)) + (1. + brouwer0.e.powi(2)) * z33;
let z11 = -6. * a1 * a5 + brouwer0.e.powi(2) * (-24. * x1 * x7 - 6. * x3 * x5);
let z13 = -6. * a3 * a6 + brouwer0.e.powi(2) * (-24. * x2 * x8 - 6. * x4 * x6);
let z21 = 6. * a2 * a5 + brouwer0.e.powi(2) * (24. * x1 * x5 - 6. * x3 * x7);
let z23 = 6. * a4 * a6 + brouwer0.e.powi(2) * (24. * x2 * x6 - 6. * x4 * x8);
let z22 = 6. * a4 * a5
+ 6. * a2 * a6
+ brouwer0.e.powi(2) * (24. * x2 * x5 + 24. * x1 * x6 - 6. * x4 * x7 - 6. * x3 * x8);
let z12 = -6. * a1 * a6
- 6. * a3 * a5
- brouwer0.e.powi(2) * (24. * x2 * x7 + 24. * x1 * x8 + 6. * x3 * x6 + 6. * x4 * x5);
let e_x_dot =
-15. * body.c * body.n * (brouwer0.e * brouwer0.beta / brouwer0.n) * (x1 * x3 + x2 * x4);
let i_x_dot = (-body.c * body.n / (2. * brouwer0.n * brouwer0.beta)) * (z11 + z13);
let m_x_dot = (-body.c * body.n / brouwer0.n) * (z1 + z3 - 14. - 6. * brouwer0.e.powi(2));
let raan_rate_valid = brouwer0.i >= deg2rad(3.) && brouwer0.i <= PI - deg2rad(3.);
let mut raan_x_dot = 0.;
if raan_rate_valid {
raan_x_dot = body.c * body.n / (2. * brouwer0.n * brouwer0.beta * sin_i0) * (z21 + z23);
}
let mut omega_x_dot = body.c * body.n * brouwer0.beta / brouwer0.n * (z31 + z33 - 6.);
if raan_rate_valid {
omega_x_dot -= raan_x_dot * cos_i0;
}
ThirdBodyParams {
beta: beta_x,
x1,
x2,
x3,
x4,
x5,
x6,
x7,
x8,
z1,
z2,
z3,
z11,
z13,
z21,
z23,
z22,
z12,
z31,
z32,
z33,
e_dot: e_x_dot,
i_dot: i_x_dot,
m_dot: m_x_dot,
raan_dot: raan_x_dot,
omega_dot: omega_x_dot,
..*body
}
}
fn init_earth_gravity_resonance_halfday(
jd0: f64,
jdfrac0: f64,
brouwer0: &BrouwerMeanElements,
zonal_params: &EarthZonalParams,
lunar_params: &ThirdBodyParams,
solar_params: &ThirdBodyParams,
) -> HalfDayResonanceParams {
let cos_i0 = brouwer0.i.cos();
let sin_i0 = brouwer0.i.sin();
let c22s22 = 1.7891679e-6;
let c32s32 = 3.7393792e-7;
let c44s44 = 7.3636953e-9;
let c52s52 = 1.1428639e-7;
let c54s54 = 2.1765803e-9;
let f220 = (3. / 4.) * (1. + cos_i0).powi(2);
let f221 = (3. / 2.) * sin_i0.powi(2);
let f321 = (15. / 8.) * sin_i0 * (1. - 2. * cos_i0 - 3. * cos_i0.powi(2));
let f322 = (-15. / 8.) * sin_i0 * (1. + 2. * cos_i0 - 3. * cos_i0.powi(2));
let f441 = (105. / 4.) * sin_i0.powi(2) * (1. + cos_i0).powi(2);
let f442 = (315. / 8.) * sin_i0.powi(4);
let f522 = (315. / 32.)
* (sin_i0.powi(3) - 2. * sin_i0.powi(3) * cos_i0 - 5. * sin_i0.powi(3) * cos_i0.powi(2)
+ sin_i0 * ((-2. / 3.) + (4. / 3.) * cos_i0 + 2. * cos_i0.powi(2)));
let f523 = (105. / 16.)
* sin_i0
* (1. + 2. * cos_i0
- 3. * cos_i0.powi(2)
- (3. / 2.) * sin_i0.powi(2) * (1. + 2. * cos_i0 - 5. * cos_i0.powi(2)));
let f542 = (945. / 32.)
* sin_i0
* (2. - 8. * cos_i0 + cos_i0.powi(2) * (-12. + 8. * cos_i0 + 10. * cos_i0.powi(2)));
let f543 = (945. / 32.)
* sin_i0
* (cos_i0.powi(2) * (12. + 8. * cos_i0 - 10. * cos_i0.powi(2)) - 2. - 8. * cos_i0);
let g211: f64;
let g201 = -0.306 - 0.44 * (brouwer0.e - 0.64);
let g310: f64;
let g322: f64;
let g410: f64;
let g422: f64;
let g520: f64;
let g521: f64;
let g532: f64;
let g533: f64;
if brouwer0.e <= 0.65 {
g211 = 3.616 - 13.247 * brouwer0.e + 16.29 * brouwer0.e.powi(2);
g310 = -19.302 + 117.39 * brouwer0.e - 228.419 * brouwer0.e.powi(2)
+ 156.591 * brouwer0.e.powi(3);
g322 = -18.9068 + 109.7927 * brouwer0.e - 214.6334 * brouwer0.e.powi(2)
+ 146.5816 * brouwer0.e.powi(3);
g410 = -41.122 + 242.694 * brouwer0.e - 471.094 * brouwer0.e.powi(2)
+ 313.953 * brouwer0.e.powi(3);
g422 = -146.407 + 841.88 * brouwer0.e - 1629.014 * brouwer0.e.powi(2)
+ 1083.435 * brouwer0.e.powi(3);
g520 = -532.114 + 3017.977 * brouwer0.e - 5740.032 * brouwer0.e.powi(2)
+ 3708.276 * brouwer0.e.powi(3);
} else {
g211 = -72.099 + 331.819 * brouwer0.e - 508.738 * brouwer0.e.powi(2)
+ 266.724 * brouwer0.e.powi(3);
g310 = -346.844 + 1582.851 * brouwer0.e - 2415.925 * brouwer0.e.powi(2)
+ 1246.113 * brouwer0.e.powi(3);
g322 = -342.585 + 1554.908 * brouwer0.e - 2366.899 * brouwer0.e.powi(2)
+ 1215.972 * brouwer0.e.powi(3);
g410 = -1052.797 + 4758.686 * brouwer0.e - 7193.992 * brouwer0.e.powi(2)
+ 3651.957 * brouwer0.e.powi(3);
g422 = -3581.69 + 16178.11 * brouwer0.e - 24462.77 * brouwer0.e.powi(2)
+ 12422.52 * brouwer0.e.powi(3);
if brouwer0.e < 0.715 {
g520 = 1464.74 - 4664.75 * brouwer0.e + 3763.64 * brouwer0.e.powi(2);
} else {
g520 = -5149.66 + 29936.92 * brouwer0.e - 54087.36 * brouwer0.e.powi(2)
+ 31324.56 * brouwer0.e.powi(3);
}
}
if brouwer0.e < 0.7 {
g521 = -822.71072 + 4568.6173 * brouwer0.e - 8491.4146 * brouwer0.e.powi(2)
+ 5337.524 * brouwer0.e.powi(3);
g532 = -853.666 + 4690.25 * brouwer0.e - 8624.77 * brouwer0.e.powi(2)
+ 5341.4 * brouwer0.e.powi(3);
g533 = -919.2277 + 4988.61 * brouwer0.e - 9064.77 * brouwer0.e.powi(2)
+ 5542.21 * brouwer0.e.powi(3);
} else {
g521 = -51752.104 + 218913.95 * brouwer0.e - 309468.16 * brouwer0.e.powi(2)
+ 146349.42 * brouwer0.e.powi(3);
g532 = -40023.88 + 170470.89 * brouwer0.e - 242699.48 * brouwer0.e.powi(2)
+ 115605.82 * brouwer0.e.powi(3);
g533 = -37995.78 + 161616.52 * brouwer0.e - 229838.2 * brouwer0.e.powi(2)
+ 109377.94 * brouwer0.e.powi(3);
}
let d2201 = 3. * brouwer0.n.powi(2) / brouwer0.a.powi(2) * (c22s22 * f220 * g201);
let d2211 = 3. * brouwer0.n.powi(2) / brouwer0.a.powi(2) * (c22s22 * f221 * g211);
let d3210 = 3. * brouwer0.n.powi(2) / brouwer0.a.powi(3) * (c32s32 * f321 * g310);
let d3222 = 3. * brouwer0.n.powi(2) / brouwer0.a.powi(3) * (c32s32 * f322 * g322);
let d5220 = 3. * brouwer0.n.powi(2) / brouwer0.a.powi(5) * (c52s52 * f522 * g520);
let d5232 = 3. * brouwer0.n.powi(2) / brouwer0.a.powi(5) * (c52s52 * f523 * g532);
let d4422 = 6. * brouwer0.n.powi(2) / brouwer0.a.powi(4) * (c44s44 * f442 * g422); let d5421 = 6. * brouwer0.n.powi(2) / brouwer0.a.powi(5) * (c54s54 * f542 * g521); let d5433 = 6. * brouwer0.n.powi(2) / brouwer0.a.powi(5) * (c54s54 * f543 * g533); let d4410 = 6. * brouwer0.n.powi(2) / brouwer0.a.powi(4) * (c44s44 * f441 * g410);
let theta_g = calc_theta_g(jd0, jdfrac0);
let lam0 = (brouwer0.m + 2. * brouwer0.raan - 2. * theta_g).rem_euclid(2.0 * PI);
let lam0_dot = zonal_params.m_dot
+ (lunar_params.m_dot + solar_params.m_dot)
+ 2. * zonal_params.raan_dot
+ 2. * (lunar_params.raan_dot + solar_params.raan_dot)
- 2. * RPTIM;
HalfDayResonanceParams {
theta_g,
lam0,
lam0_dot,
d2201,
d2211,
d3210,
d3222,
d5220,
d5232,
d4422,
d5421,
d5433,
d4410,
}
}
fn init_earth_gravity_resonance_wholeday(
jd0: f64,
jdfrac0: f64,
brouwer0: &BrouwerMeanElements,
zonal_params: &EarthZonalParams,
lunar_params: &ThirdBodyParams,
solar_params: &ThirdBodyParams,
) -> WholeDayResonanceParams {
let cos_i0 = brouwer0.i.cos();
let sin_i0 = brouwer0.i.sin();
let q31 = 2.1460748e-6;
let q22 = 1.7891679e-6;
let q33 = 2.2123015e-7;
let lam31 = 0.13130908;
let lam22 = 2.88431980;
let lam33 = 0.37448087;
let f220 = (3. / 4.) * (1. + cos_i0).powi(2);
let f311 = (15. / 16.) * sin_i0.powi(2) * (1. + 3. * cos_i0) - (3. / 4.) * (1. + cos_i0);
let f330 = (15. / 8.) * (1. + cos_i0).powi(3);
let g200 = 1. - (5. / 2.) * brouwer0.e.powi(2) + (13. / 16.) * brouwer0.e.powi(4);
let g310 = 1. + 2. * brouwer0.e.powi(2);
let g300 = 1. - 6. * brouwer0.e.powi(2) + (423. / 64.) * brouwer0.e.powi(4);
let delta1 = (3. * brouwer0.n.powi(2) / brouwer0.a.powi(3)) * f311 * g310 * q31;
let delta2 = (6. * brouwer0.n.powi(2) / brouwer0.a.powi(2)) * f220 * g200 * q22;
let delta3 = (9. * brouwer0.n.powi(2) / brouwer0.a.powi(3)) * f330 * g300 * q33;
let theta_g = calc_theta_g(jd0, jdfrac0);
let lam0 = brouwer0.m + brouwer0.raan + brouwer0.omega - theta_g;
let lam0_dot_1 = zonal_params.m_dot
+ (lunar_params.m_dot + solar_params.m_dot)
+ zonal_params.raan_dot
+ (lunar_params.raan_dot + solar_params.raan_dot);
let lam0_dot_2 =
zonal_params.omega_dot + (lunar_params.omega_dot + solar_params.omega_dot) - RPTIM;
let lam0_dot = lam0_dot_1 + lam0_dot_2;
WholeDayResonanceParams {
theta_g,
lam0,
lam0_dot,
lam31,
lam22,
lam33,
delta1,
delta2,
delta3,
}
}
fn calc_theta_g(jd0: f64, jdfrac0: f64) -> f64 {
let tut1 = (jd0 + jdfrac0 - 2451545.0) / 36525.0;
let temp = -6.2e-6 * tut1.powi(3)
+ 0.093104 * tut1.powi(2)
+ (876600.0 * 3600.0 + 8640184.812866) * tut1
+ 67310.54841;
(deg2rad(temp / 240.0)).rem_euclid(2.0 * PI) }
pub fn sgp4_prop_datetime(sgp4: &Sgp4, datetime: &DateTime) -> Result<StateVector, Sgp4Error> {
let (jd_prop, jdfrac_prop) = utc2jday(datetime).map_err(Sgp4Error::InvalidDateTime)?;
let delta_t = ((jd_prop - sgp4.jd0) + (jdfrac_prop - sgp4.jdfrac0)) * 1440.;
sgp4_prop_delta(sgp4, delta_t)
}
pub fn sgp4_prop_delta(sgp4: &Sgp4, delta_t: f64) -> Result<StateVector, Sgp4Error> {
let mut m: f64;
let mut omega: f64;
let mut raan: f64;
let mut e = sgp4.brouwer0.e;
let mut i = sgp4.brouwer0.i;
let mut n = sgp4.brouwer0.n;
let m_df = sgp4.brouwer0.m + sgp4.brouwer0.n * delta_t + sgp4.zonal_params.m_dot * delta_t;
let omega_df = sgp4.brouwer0.omega + sgp4.zonal_params.omega_dot * delta_t;
let raan_df = sgp4.brouwer0.raan + sgp4.zonal_params.raan_dot * delta_t;
let delta_omega: f64;
let delta_m: f64;
if sgp4.deep_space || sgp4.atm_params.hp < 220. || sgp4.brouwer0.e <= 1.0e-4 {
delta_omega = 0.;
delta_m = 0.;
} else {
delta_omega = sgp4.gp.bstar * sgp4.atm_params.c3 * sgp4.brouwer0.omega.cos() * delta_t;
delta_m = (-2. / 3.)
* (sgp4.atm_params.q0 - sgp4.atm_params.s).powi(4)
* sgp4.gp.bstar
* sgp4.atm_params.zeta.powi(4)
* (1. / (sgp4.brouwer0.e * sgp4.atm_params.eta))
* ((1. + sgp4.atm_params.eta * m_df.cos()).powi(3)
- (1. + sgp4.atm_params.eta * sgp4.brouwer0.m.cos()).powi(3));
}
m = m_df + delta_omega + delta_m;
omega = omega_df - delta_omega - delta_m;
raan = raan_df
- (21. / 2.)
* (sgp4.brouwer0.n * sgp4.wgs.k2 * sgp4.brouwer0.theta
/ (sgp4.brouwer0.a.powi(2) * sgp4.brouwer0.beta.powi(2)))
* sgp4.atm_params.c1
* delta_t.powi(2);
if sgp4.deep_space {
m += (sgp4.lunar_params.m_dot + sgp4.solar_params.m_dot) * delta_t;
omega += (sgp4.lunar_params.omega_dot + sgp4.solar_params.omega_dot) * delta_t;
raan += (sgp4.lunar_params.raan_dot + sgp4.solar_params.raan_dot) * delta_t;
e += (sgp4.lunar_params.e_dot + sgp4.solar_params.e_dot) * delta_t;
i += (sgp4.lunar_params.i_dot + sgp4.solar_params.i_dot) * delta_t;
}
if sgp4.half_day_resonance {
let mut lami = sgp4.half_day_resonance_params.lam0;
let mut ni = sgp4.brouwer0.n;
let mut lami_dot: f64;
let mut ni_dot: f64;
let mut lami_ddot: f64;
let mut ni_ddot: f64;
let step = if delta_t >= 0.0 { 720.0 } else { -720.0 };
let em_steps = (delta_t / step).floor() as i32;
let t_em = delta_t - em_steps as f64 * step;
(lami_dot, ni_dot, lami_ddot, ni_ddot) = half_day_euler_maclaurin_step(
lami,
ni,
sgp4.brouwer0.omega,
&sgp4.half_day_resonance_params,
);
for em_step in 0..em_steps {
lami += lami_dot * step + 0.5 * lami_ddot * 518400.;
ni += ni_dot * step + 0.5 * ni_ddot * 518400.;
let omegai =
sgp4.brouwer0.omega + sgp4.zonal_params.omega_dot * (em_step + 1) as f64 * step;
(lami_dot, ni_dot, lami_ddot, ni_ddot) =
half_day_euler_maclaurin_step(lami, ni, omegai, &sgp4.half_day_resonance_params);
}
lami = lami + (lami_dot * t_em) + (0.5 * lami_ddot * t_em.powi(2));
ni = ni + (ni_dot * t_em) + (0.5 * ni_ddot * t_em.powi(2));
let theta_t =
(sgp4.half_day_resonance_params.theta_g + RPTIM * delta_t).rem_euclid(2.0 * PI);
n = ni;
m = lami - 2. * raan + 2. * theta_t;
} else if sgp4.whole_day_resonance {
let mut lami = sgp4.whole_day_resonance_params.lam0;
let mut ni = sgp4.brouwer0.n;
let mut lami_dot: f64;
let mut ni_dot: f64;
let mut lami_ddot: f64;
let mut ni_ddot: f64;
let step = if delta_t >= 0.0 { 720.0 } else { -720.0 };
let em_steps = (delta_t / step).floor() as i32;
let t_em = delta_t - em_steps as f64 * step;
(lami_dot, ni_dot, lami_ddot, ni_ddot) =
whole_day_euler_maclaurin_step(lami, ni, &sgp4.whole_day_resonance_params);
for _ in 0..em_steps {
lami += lami_dot * step + 0.5 * lami_ddot * 518400.;
ni += ni_dot * step + 0.5 * ni_ddot * 518400.;
(lami_dot, ni_dot, lami_ddot, ni_ddot) =
whole_day_euler_maclaurin_step(lami, ni, &sgp4.whole_day_resonance_params);
}
lami = lami + (lami_dot * t_em) + (0.5 * lami_ddot * t_em.powi(2));
ni = ni + (ni_dot * t_em) + (0.5 * ni_ddot * t_em.powi(2));
let theta_t =
(sgp4.whole_day_resonance_params.theta_g + RPTIM * delta_t).rem_euclid(2.0 * PI);
n = ni;
m = lami - raan - omega + theta_t;
}
if n <= 0.0 {
return Err(Sgp4Error::InvalidMeanMotion);
}
let a: f64;
let il_atm: f64 = if sgp4.deep_space || sgp4.atm_params.hp < 220. {
e += -sgp4.gp.bstar * (sgp4.atm_params.c4 * delta_t);
let a_1 = 1. - sgp4.atm_params.c1 * delta_t; a = (sgp4.wgs.ke / n).powf(2. / 3.) * a_1.powi(2);
n = sgp4.wgs.ke / a.powf(1.5);
let il_1 = 3. / 2. * sgp4.atm_params.c1 * delta_t.powi(2);
sgp4.brouwer0.n * il_1
} else {
e += -sgp4.gp.bstar
* (sgp4.atm_params.c4 * delta_t
+ sgp4.atm_params.c5 * (m.sin() - sgp4.brouwer0.m.sin()));
let a_1 = 1. - sgp4.atm_params.c1 * delta_t - sgp4.atm_params.d2 * delta_t.powi(2);
let a_2 = -sgp4.atm_params.d3 * delta_t.powi(3) - sgp4.atm_params.d4 * delta_t.powi(4);
a = (sgp4.wgs.ke / n).powf(2. / 3.) * (a_1 + a_2).powi(2);
n = sgp4.wgs.ke / a.powf(1.5);
let il_1 = 3. / 2. * sgp4.atm_params.c1 * delta_t.powi(2);
let il_2 = (sgp4.atm_params.d2 + 2. * sgp4.atm_params.c1.powi(2)) * delta_t.powi(3);
let il_3 = 1. / 4.
* (3. * sgp4.atm_params.d3
+ 12. * sgp4.atm_params.c1 * sgp4.atm_params.d2
+ 10. * sgp4.atm_params.c1.powi(3))
* delta_t.powi(4);
let il_4 = 1. / 5.
* (3. * sgp4.atm_params.d4
+ 12. * sgp4.atm_params.c1 * sgp4.atm_params.d3
+ 6. * sgp4.atm_params.d2.powi(2)
+ 30. * sgp4.atm_params.c1.powi(2) * sgp4.atm_params.d2
+ 15. * sgp4.atm_params.c1.powi(4))
* delta_t.powi(5);
sgp4.brouwer0.n * (il_1 + il_2 + il_3 + il_4)
};
if !(-0.001..1.0).contains(&e) {
return Err(Sgp4Error::InvalidMeanEccentricity);
}
if e < 1.0e-6 {
e = 1.0e-6;
}
m += il_atm;
let lm = (m + omega + raan) % (2.0 * PI);
raan %= 2.0 * PI;
omega %= 2.0 * PI;
m = (lm - omega - raan) % (2.0 * PI);
if sgp4.deep_space {
let m_m = sgp4.lunar_params.m + sgp4.lunar_params.n * delta_t;
let m_s = sgp4.solar_params.m + sgp4.solar_params.n * delta_t;
let f_m = m_m + 2. * sgp4.lunar_params.e * m_m.sin();
let f_s = m_s + 2. * sgp4.solar_params.e * m_s.sin();
let f2_m = 0.5 * f_m.sin().powi(2) - 0.25;
let f2_s = 0.5 * f_s.sin().powi(2) - 0.25;
let f3_m = -0.5 * f_m.sin() * f_m.cos();
let f3_s = -0.5 * f_s.sin() * f_s.cos();
let delta_e_m = -(30. * sgp4.brouwer0.beta * sgp4.lunar_params.c * sgp4.brouwer0.e
/ sgp4.brouwer0.n)
* (f2_m
* (sgp4.lunar_params.x2 * sgp4.lunar_params.x3
+ sgp4.lunar_params.x1 * sgp4.lunar_params.x4)
+ f3_m
* (sgp4.lunar_params.x2 * sgp4.lunar_params.x4
- sgp4.lunar_params.x1 * sgp4.lunar_params.x3));
let delta_e_s = -(30. * sgp4.brouwer0.beta * sgp4.solar_params.c * sgp4.brouwer0.e
/ sgp4.brouwer0.n)
* (f2_s
* (sgp4.solar_params.x2 * sgp4.solar_params.x3
+ sgp4.solar_params.x1 * sgp4.solar_params.x4)
+ f3_s
* (sgp4.solar_params.x2 * sgp4.solar_params.x4
- sgp4.solar_params.x1 * sgp4.solar_params.x3));
let delta_i_m = -(sgp4.lunar_params.c / sgp4.brouwer0.n / sgp4.brouwer0.beta)
* (f2_m * sgp4.lunar_params.z12
+ f3_m * (sgp4.lunar_params.z13 - sgp4.lunar_params.z11));
let delta_i_s = -(sgp4.solar_params.c / sgp4.brouwer0.n / sgp4.brouwer0.beta)
* (f2_s * sgp4.solar_params.z12
+ f3_s * (sgp4.solar_params.z13 - sgp4.solar_params.z11));
let delta_m_m = -(2. * sgp4.lunar_params.c / sgp4.brouwer0.n)
* (f2_m * sgp4.lunar_params.z2 + f3_m * (sgp4.lunar_params.z3 - sgp4.lunar_params.z1)
- 3. * sgp4.lunar_params.e * f_m.sin() * (7. + 3. * sgp4.brouwer0.e.powi(2)));
let delta_m_s = -(2. * sgp4.solar_params.c / sgp4.brouwer0.n)
* (f2_s * sgp4.solar_params.z2 + f3_s * (sgp4.solar_params.z3 - sgp4.solar_params.z1)
- 3. * sgp4.solar_params.e * f_s.sin() * (7. + 3. * sgp4.brouwer0.e.powi(2)));
let delta_raan_m = (sgp4.lunar_params.c / sgp4.brouwer0.n / sgp4.brouwer0.beta)
* (f2_m * sgp4.lunar_params.z22
+ f3_m * (sgp4.lunar_params.z23 - sgp4.lunar_params.z21)); let delta_raan_s = (sgp4.solar_params.c / sgp4.brouwer0.n / sgp4.brouwer0.beta)
* (f2_s * sgp4.solar_params.z22
+ f3_s * (sgp4.solar_params.z23 - sgp4.solar_params.z21)); let delta_omega_m = (2. * sgp4.brouwer0.beta * sgp4.lunar_params.c / sgp4.brouwer0.n)
* (f2_m * sgp4.lunar_params.z32
+ f3_m * (sgp4.lunar_params.z33 - sgp4.lunar_params.z31)
- 9. * sgp4.lunar_params.e * f_m.sin()); let delta_omega_s = (2. * sgp4.brouwer0.beta * sgp4.solar_params.c / sgp4.brouwer0.n)
* (f2_s * sgp4.solar_params.z32
+ f3_s * (sgp4.solar_params.z33 - sgp4.solar_params.z31)
- 9. * sgp4.solar_params.e * f_s.sin()); let delta_e_ls = delta_e_m + delta_e_s;
let delta_i_ls = delta_i_m + delta_i_s;
let delta_m_ls = delta_m_m + delta_m_s;
let delta_raan_ls = delta_raan_m + delta_raan_s;
let delta_omega_ls = delta_omega_m + delta_omega_s;
e += delta_e_ls;
if !(0.0..1.0).contains(&e) {
return Err(Sgp4Error::InvalidPerturbedEccentricity);
}
i += delta_i_ls;
if i >= 0.2 {
raan += delta_raan_ls / i.sin();
omega += delta_omega_ls - delta_raan_ls * i.cos() / i.sin();
m += delta_m_ls;
} else {
let alpha = i.sin() * raan.sin()
+ raan.cos() * delta_raan_ls
+ i.cos() * raan.sin() * delta_i_ls;
let beta = i.sin() * raan.cos() - raan.sin() * delta_raan_ls
+ i.cos() * raan.cos() * delta_i_ls;
let raan_old = raan;
let m_omega_raan =
m + omega + delta_m_ls + delta_omega_ls + (i.cos() - delta_i_ls * i.sin()) * raan;
raan = alpha.atan2(beta);
if (raan_old - raan).abs() > PI {
if raan < raan_old {
raan += 2.0 * PI;
} else {
raan -= 2.0 * PI;
}
}
m += delta_m_ls;
omega = m_omega_raan - m - i.cos() * raan;
}
}
if sgp4.deep_space && i < 0.0 {
i = -i;
raan += PI;
omega -= PI;
}
let il = m + omega + raan;
let beta_update = (1. - e.powi(2)).sqrt();
let a30 = -sgp4.wgs.j3; let axn = e * omega.cos();
let mut one_plus_cos_i = 1. + i.cos();
if one_plus_cos_i.abs() < 1.5e-12 {
one_plus_cos_i = 1.5e-12;
}
let ill = a30 * i.sin() / (8. * sgp4.wgs.k2 * a * beta_update.powi(2))
* e
* omega.cos()
* (3. + 5. * i.cos())
/ one_plus_cos_i;
let aynl = a30 * i.sin() / (4. * sgp4.wgs.k2 * a * beta_update.powi(2));
let ilt = il + ill;
let ayn = e * omega.sin() + aynl;
let u = (ilt - raan).rem_euclid(2.0 * PI);
let mut e_omega = u;
let mut delta_e_omega: f64;
for _ in 0..10 {
delta_e_omega = (u - ayn * e_omega.cos() + axn * e_omega.sin() - e_omega)
/ (1. - ayn * e_omega.sin() - axn * e_omega.cos());
if delta_e_omega.abs() >= 0.95 {
if delta_e_omega > 0.0 {
delta_e_omega = 0.95;
} else {
delta_e_omega = -0.95;
}
}
e_omega += delta_e_omega;
if delta_e_omega.abs() < 1e-12 {
break;
}
}
e = (axn.powi(2) + ayn.powi(2)).sqrt();
let pl = a * (1. - e.powi(2));
if pl < 0.0 {
return Err(Sgp4Error::InvalidSemilatusRectum);
}
let cos_ecc_anomaly = (axn * e_omega.cos() + ayn * e_omega.sin()) / e;
let sin_ecc_anomaly = (axn * e_omega.sin() - ayn * e_omega.cos()) / e;
let r = a * (1. - e * cos_ecc_anomaly);
let r_dot = sgp4.wgs.ke * a.sqrt() * e * sin_ecc_anomaly / r;
let r_f_dot = sgp4.wgs.ke * pl.sqrt() / r;
let cos_u = a / r
* (e_omega.cos() - axn + ayn * (e * sin_ecc_anomaly) / (1. + (1. - e.powi(2)).sqrt()));
let sin_u = a / r
* (e_omega.sin() - ayn - axn * (e * sin_ecc_anomaly) / (1. + (1. - e.powi(2)).sqrt()));
let u = sin_u.atan2(cos_u);
let delta_r = sgp4.wgs.k2 / (2. * pl) * (1. - i.cos().powi(2)) * (2. * u).cos();
let delta_u = -sgp4.wgs.k2 / (4. * pl.powi(2)) * (7. * i.cos().powi(2) - 1.) * (2. * u).sin();
let delta_raan = 3. * sgp4.wgs.k2 * i.cos() / (2. * pl.powi(2)) * (2. * u).sin();
let delta_i = 3. * sgp4.wgs.k2 * i.cos() / (2. * pl.powi(2)) * i.sin() * (2. * u).cos();
let delta_r_dot = -sgp4.wgs.k2 * n / pl * (1. - i.cos().powi(2)) * (2. * u).sin();
let delta_r_f_dot = sgp4.wgs.k2 * n / pl
* ((1. - i.cos().powi(2)) * (2. * u).cos() - 3. / 2. * (1. - 3. * i.cos().powi(2)));
let rk = r
* (1.
- 3. / 2. * sgp4.wgs.k2 * (1. - e.powi(2)).sqrt() / pl.powi(2)
* (3. * i.cos().powi(2) - 1.))
+ delta_r;
if rk < 1.0 {
return Err(Sgp4Error::SatelliteDecayed);
}
let uk = u + delta_u;
let raan_k = raan + delta_raan;
let i_k = i + delta_i;
let r_dot_k = r_dot + delta_r_dot;
let r_f_dot_k = r_f_dot + delta_r_f_dot;
let mx = -raan_k.sin() * i_k.cos();
let my = raan_k.cos() * i_k.cos();
let mz = i_k.sin();
let nx = raan_k.cos();
let ny = raan_k.sin();
let nz = 0.;
let ux = mx * uk.sin() + nx * uk.cos();
let uy = my * uk.sin() + ny * uk.cos();
let uz = mz * uk.sin() + nz * uk.cos();
let vx = mx * uk.cos() - nx * uk.sin();
let vy = my * uk.cos() - ny * uk.sin();
let vz = mz * uk.cos() - nz * uk.sin();
let rx = rk * ux * sgp4.wgs.r_earth_eq;
let ry = rk * uy * sgp4.wgs.r_earth_eq;
let rz = rk * uz * sgp4.wgs.r_earth_eq;
let r_dot_x = (r_dot_k * ux + r_f_dot_k * vx) * sgp4.wgs.r_earth_eq / 60.;
let r_dot_y = (r_dot_k * uy + r_f_dot_k * vy) * sgp4.wgs.r_earth_eq / 60.;
let r_dot_z = (r_dot_k * uz + r_f_dot_k * vz) * sgp4.wgs.r_earth_eq / 60.;
if ![rx, ry, rz, r_dot_x, r_dot_y, r_dot_z]
.iter()
.all(|x| x.is_finite())
{
return Err(Sgp4Error::NonFiniteState);
}
Ok(StateVector {
r_x: rx,
r_y: ry,
r_z: rz,
v_x: r_dot_x,
v_y: r_dot_y,
v_z: r_dot_z,
coordinate_frame: CoordinateFrame::TEME,
})
}
fn half_day_euler_maclaurin_step(
lami: f64,
ni: f64,
omegai: f64,
half_day_resonance_params: &HalfDayResonanceParams,
) -> (f64, f64, f64, f64) {
let g22 = 5.7686396;
let g32 = 0.95240898;
let g44 = 1.8014998;
let g52 = 1.0508330;
let g54 = 4.4108898;
let lami_dot = ni + half_day_resonance_params.lam0_dot;
let ni_dot_2201 = half_day_resonance_params.d2201 * (2. * omegai + lami - g22).sin();
let ni_dot_2211 = half_day_resonance_params.d2211 * (lami - g22).sin();
let ni_dot_3210 = half_day_resonance_params.d3210 * (omegai + lami - g32).sin();
let ni_dot_3222 = half_day_resonance_params.d3222 * (-omegai + lami - g32).sin();
let ni_dot_5220 = half_day_resonance_params.d5220 * (omegai + lami - g52).sin();
let ni_dot_5232 = half_day_resonance_params.d5232 * (-omegai + lami - g52).sin();
let ni_dot_4422 = half_day_resonance_params.d4422 * (2. * lami - g44).sin();
let ni_dot_5421 = half_day_resonance_params.d5421 * (omegai + 2. * lami - g54).sin();
let ni_dot_5433 = half_day_resonance_params.d5433 * (-omegai + 2. * lami - g54).sin();
let ni_dot_4410 = half_day_resonance_params.d4410 * (2. * omegai + 2. * lami - g44).sin();
let ni_dot = ni_dot_2201
+ ni_dot_2211
+ ni_dot_3210
+ ni_dot_3222
+ ni_dot_5220
+ ni_dot_5232
+ ni_dot_4422
+ ni_dot_5421
+ ni_dot_5433
+ ni_dot_4410;
let lami_ddot = ni_dot;
let ni_ddot_2201 = 1. * half_day_resonance_params.d2201 * (2. * omegai + lami - g22).cos();
let ni_ddot_2211 = 1. * half_day_resonance_params.d2211 * (lami - g22).cos();
let ni_ddot_3210 = 1. * half_day_resonance_params.d3210 * (omegai + lami - g32).cos();
let ni_ddot_3222 = 1. * half_day_resonance_params.d3222 * (-omegai + lami - g32).cos();
let ni_ddot_5220 = 1. * half_day_resonance_params.d5220 * (omegai + lami - g52).cos();
let ni_ddot_5232 = 1. * half_day_resonance_params.d5232 * (-omegai + lami - g52).cos();
let ni_ddot_4422 = 2. * half_day_resonance_params.d4422 * (2. * lami - g44).cos();
let ni_ddot_5421 = 2. * half_day_resonance_params.d5421 * (omegai + 2. * lami - g54).cos();
let ni_ddot_5433 = 2. * half_day_resonance_params.d5433 * (-omegai + 2. * lami - g54).cos();
let ni_ddot_4410 = 2. * half_day_resonance_params.d4410 * (2. * omegai + 2. * lami - g44).cos();
let ni_ddot = lami_dot
* (ni_ddot_2201
+ ni_ddot_2211
+ ni_ddot_3210
+ ni_ddot_3222
+ ni_ddot_5220
+ ni_ddot_5232
+ ni_ddot_4422
+ ni_ddot_5421
+ ni_ddot_5433
+ ni_ddot_4410);
(lami_dot, ni_dot, lami_ddot, ni_ddot)
}
fn whole_day_euler_maclaurin_step(
lami: f64,
ni: f64,
whole_day_resonance_params: &WholeDayResonanceParams,
) -> (f64, f64, f64, f64) {
let lami_dot = ni + whole_day_resonance_params.lam0_dot;
let ni_dot_1 =
whole_day_resonance_params.delta1 * (lami - whole_day_resonance_params.lam31).sin();
let ni_dot_2 =
whole_day_resonance_params.delta2 * (2. * (lami - whole_day_resonance_params.lam22)).sin();
let ni_dot_3 =
whole_day_resonance_params.delta3 * (3. * (lami - whole_day_resonance_params.lam33)).sin();
let ni_dot = ni_dot_1 + ni_dot_2 + ni_dot_3;
let lami_ddot = ni_dot;
let ni_ddot_1 =
whole_day_resonance_params.delta1 * (lami - whole_day_resonance_params.lam31).cos();
let ni_ddot_2 = 2.
* whole_day_resonance_params.delta2
* (2. * (lami - whole_day_resonance_params.lam22)).cos();
let ni_ddot_3 = 3.
* whole_day_resonance_params.delta3
* (3. * (lami - whole_day_resonance_params.lam33)).cos();
let ni_ddot = lami_dot * (ni_ddot_1 + ni_ddot_2 + ni_ddot_3);
(lami_dot, ni_dot, lami_ddot, ni_ddot)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gp::{from_omm_kvn_string, from_tle_string};
use crate::time::Timezone;
use serde::Deserialize;
use std::collections::HashMap;
use toml::from_str;
#[derive(Deserialize)]
struct ValladoCases {
test: HashMap<String, ValladoCase>,
}
#[derive(Deserialize)]
struct ValladoCase {
name: String,
tle: String,
#[serde(default)]
#[allow(dead_code)]
start_mins_from_epoch: f64,
#[serde(default)]
#[allow(dead_code)]
end_mins_from_epoch: f64,
#[serde(default)]
#[allow(dead_code)]
delta_time_mins: f64,
ephem: String,
#[serde(default)]
exception: bool,
}
struct EphemRow {
t_mins: f64,
rx: f64,
ry: f64,
rz: f64,
vx: f64,
vy: f64,
vz: f64,
datetime: DateTime,
}
fn parse_vallado_datetime(cols: &[&str]) -> Option<DateTime> {
if cols.len() < 11 {
return None;
}
let year = cols[7].parse().ok()?;
let month = cols[8].parse().ok()?;
let day = cols[9].parse().ok()?;
let time_str = cols[10..].join(" ");
let parts: Vec<&str> = time_str
.split(':')
.map(str::trim)
.filter(|part| !part.is_empty())
.collect();
if parts.len() != 3 {
return None;
}
Some(DateTime {
year,
month,
day,
hour: parts[0].parse().ok()?,
minute: parts[1].parse().ok()?,
second: parts[2].parse().ok()?,
timezone: Timezone::UTC,
})
}
fn parse_vallado_ephem(ephem: &str, epoch: DateTime) -> Vec<EphemRow> {
ephem
.lines()
.filter_map(|line| {
let cols: Vec<&str> = line.split_whitespace().collect();
if cols.len() < 7 {
return None;
}
let datetime = if cols.len() >= 11 {
parse_vallado_datetime(&cols).unwrap_or_else(|| {
panic!("failed to parse Vallado calendar stamp: {line}");
})
} else {
epoch
};
Some(EphemRow {
t_mins: cols[0].parse().ok()?,
rx: cols[1].parse().ok()?,
ry: cols[2].parse().ok()?,
rz: cols[3].parse().ok()?,
vx: cols[4].parse().ok()?,
vy: cols[5].parse().ok()?,
vz: cols[6].parse().ok()?,
datetime,
})
})
.collect()
}
fn align_datetime_to_t_mins(
datetime: DateTime,
jd0: f64,
jdfrac0: f64,
t_mins: f64,
) -> DateTime {
let (jd, jdfrac) = utc2jday(&datetime).expect("propagation datetime must be UTC");
let recovered_mins = ((jd - jd0) + (jdfrac - jdfrac0)) * 1440.0;
let delta_mins = t_mins - recovered_mins;
assert!(
delta_mins.abs() * 60.0 < 1.0e-3,
"Vallado UTC stamp is more than 1 ms from t_mins={t_mins} (recovered {recovered_mins})"
);
DateTime {
second: datetime.second + delta_mins * 60.0,
..datetime
}
}
const VALLADO_STATE_TOL_KM: f64 = 1e-6;
fn assert_state_near(key: &str, name: &str, t_mins: f64, got: &StateVector, row: &EphemRow) {
let checks = [
("Position x", got.r_x, row.rx),
("Position y", got.r_y, row.ry),
("Position z", got.r_z, row.rz),
("Velocity x", got.v_x, row.vx),
("Velocity y", got.v_y, row.vy),
("Velocity z", got.v_z, row.vz),
];
for (label, value, expected) in checks {
assert!(
(value - expected).abs() < VALLADO_STATE_TOL_KM,
"{key}: {name}\n- {label} mismatch {value} vs {expected} (t_mins = {t_mins})"
);
}
}
fn iss_sgp4() -> Sgp4 {
from_tle_string(
"\
1 25544U 98067A 08264.51782528 -.00002182 -00100-2 -11606-4 0 2921
2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537
",
)
.expect("ISS TLE should parse")
.into_iter()
.next()
.expect("ISS TLE should yield one propagator")
}
fn utc_j2000() -> DateTime {
DateTime {
year: 2000,
month: 1,
day: 1,
hour: 12,
minute: 0,
second: 0.0,
timezone: Timezone::UTC,
}
}
#[test]
fn test_invalid_date_time() {
let mut gp = GenPerturbElementSet {
epoch_datetime: DateTime {
timezone: Timezone::UT1,
..utc_j2000()
},
mean_motion: 15.0,
..GenPerturbElementSet::default()
};
assert!(matches!(
init_sgp4(&gp, None),
Err(Sgp4Error::InvalidDateTime(DateError::DateNotUTC))
));
gp.epoch_datetime = DateTime {
year: 1500,
..utc_j2000()
};
assert!(matches!(
init_sgp4(&gp, None),
Err(Sgp4Error::InvalidDateTime(DateError::DateTooEarly))
));
let sgp4 = iss_sgp4();
let mut datetime = sgp4.gp.epoch_datetime;
datetime.timezone = Timezone::UT1;
assert!(matches!(
sgp4_prop_datetime(&sgp4, &datetime),
Err(Sgp4Error::InvalidDateTime(DateError::DateNotUTC))
));
datetime = DateTime {
year: 1500,
..utc_j2000()
};
assert!(matches!(
sgp4_prop_datetime(&sgp4, &datetime),
Err(Sgp4Error::InvalidDateTime(DateError::DateTooEarly))
));
}
#[test]
fn test_invalid_mean_motion() {
let gp = GenPerturbElementSet {
epoch_datetime: utc_j2000(),
mean_motion: 0.0,
..GenPerturbElementSet::default()
};
assert!(matches!(
init_sgp4(&gp, None),
Err(Sgp4Error::InvalidMeanMotion)
));
let mut sgp4 = iss_sgp4();
sgp4.brouwer0.n = 0.0;
assert!(matches!(
sgp4_prop_delta(&sgp4, 0.0),
Err(Sgp4Error::InvalidMeanMotion)
));
}
#[test]
fn test_invalid_mean_eccentricity() {
let mut sgp4 = iss_sgp4();
sgp4.brouwer0.e = 1.5;
assert!(matches!(
sgp4_prop_delta(&sgp4, 0.0),
Err(Sgp4Error::InvalidMeanEccentricity)
));
sgp4.brouwer0.e = -0.5;
assert!(matches!(
sgp4_prop_delta(&sgp4, 0.0),
Err(Sgp4Error::InvalidMeanEccentricity)
));
}
#[test]
fn test_invalid_perturbed_eccentricity() {
let mut sgp4 = iss_sgp4();
sgp4.deep_space = true;
sgp4.lunar_params.c = 1.0e6;
sgp4.lunar_params.x1 = 1.0;
sgp4.lunar_params.x2 = 1.0;
sgp4.lunar_params.x3 = 1.0;
sgp4.lunar_params.x4 = 1.0;
assert!(matches!(
sgp4_prop_delta(&sgp4, 0.0),
Err(Sgp4Error::InvalidPerturbedEccentricity)
));
}
#[test]
fn test_invalid_semilatus_rectum() {
let mut sgp4 = iss_sgp4();
sgp4.brouwer0.e = 0.9999;
assert!(matches!(
sgp4_prop_delta(&sgp4, 0.0),
Err(Sgp4Error::InvalidSemilatusRectum)
));
}
#[test]
fn test_satellite_decayed() {
let mut sgp4 = iss_sgp4();
sgp4.brouwer0.n = 1.0;
assert!(matches!(
sgp4_prop_delta(&sgp4, 0.0),
Err(Sgp4Error::SatelliteDecayed)
));
}
#[test]
fn test_non_finite_state() {
let mut sgp4 = iss_sgp4();
sgp4.brouwer0.i = f64::NAN;
assert!(matches!(
sgp4_prop_delta(&sgp4, 0.0),
Err(Sgp4Error::NonFiniteState)
));
}
#[test]
fn test_sgp4_vallado_cases() {
let content = std::fs::read_to_string("test/vallado_cases.toml")
.expect("could not read test/vallado_cases.toml");
let cases: ValladoCases =
from_str(&content).expect("could not parse test/vallado_cases.toml");
let mut keys: Vec<&String> = cases.test.keys().collect();
keys.sort();
for key in keys {
let case = &cases.test[key];
if case.exception {
assert!(
from_tle_string(&case.tle).is_err(),
"case {key}: expected initialization to return Err"
);
continue;
}
let sgp4s = from_tle_string(&case.tle).expect("Vallado TLE should parse");
assert!(!sgp4s.is_empty(), "case {key}: TLE failed to parse");
let sgp4 = &sgp4s[0];
for row in parse_vallado_ephem(&case.ephem, sgp4.gp.epoch_datetime) {
let state_delta = sgp4_prop_delta(sgp4, row.t_mins).unwrap_or_else(|err| {
panic!("{key}: {} at t_mins={}: {err:?}", case.name, row.t_mins)
});
assert_state_near(key, &case.name, row.t_mins, &state_delta, &row);
let datetime =
align_datetime_to_t_mins(row.datetime, sgp4.jd0, sgp4.jdfrac0, row.t_mins);
let state_datetime = sgp4_prop_datetime(sgp4, &datetime).unwrap_or_else(|err| {
panic!(
"{key}: {} datetime at t_mins={}: {err:?}",
case.name, row.t_mins
)
});
assert_state_near(key, &case.name, row.t_mins, &state_datetime, &row);
}
}
}
#[test]
fn test_parse_vallado_datetime() {
let epoch = DateTime {
year: 2000,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0.0,
timezone: Timezone::UTC,
};
let compact = " 360.00000000 -7154.03120202 -3783.17682504 -3536.19412294 4.741887409 -4.151817765 -2.093935425 2000 6 28 0:50:19.733571";
let spaced_seconds = "0.0 8827.0 -41223.0 3.63 3.00 0.64 0.00 2004 2 9 10:20: 1.494254";
let spaced_hms =
"1844040.0 -31652.0 -66335.0 12774.0 1.71 1.91 -0.60 2009 7 2 9: 0: 0.000282";
let no_stamp = " 0.00000000 7022.46529266 -1400.08296755 0.03995155 1.893841015 6.405893759 4.534807250";
let compact_row = &parse_vallado_ephem(compact, epoch)[0];
let compact_dt = compact_row.datetime;
assert_eq!(compact_dt.year, 2000);
assert_eq!(compact_dt.month, 6);
assert_eq!(compact_dt.day, 28);
assert_eq!(compact_dt.hour, 0);
assert_eq!(compact_dt.minute, 50);
assert!((compact_dt.second - 19.733571).abs() < 1e-9);
assert_eq!(compact_dt.timezone, Timezone::UTC);
let spaced_row = &parse_vallado_ephem(spaced_seconds, epoch)[0];
let spaced_dt = spaced_row.datetime;
assert_eq!(spaced_dt.hour, 10);
assert_eq!(spaced_dt.minute, 20);
assert!((spaced_dt.second - 1.494254).abs() < 1e-9);
let hms_row = &parse_vallado_ephem(spaced_hms, epoch)[0];
let hms_dt = hms_row.datetime;
assert_eq!(hms_dt.year, 2009);
assert_eq!(hms_dt.hour, 9);
assert_eq!(hms_dt.minute, 0);
assert!((hms_dt.second - 0.000282).abs() < 1e-12);
let epoch_row = &parse_vallado_ephem(no_stamp, epoch)[0];
assert_eq!(epoch_row.datetime, epoch);
}
#[derive(Deserialize)]
struct PythonSgp4Cases {
test: HashMap<String, PythonSgp4Case>,
}
#[derive(Deserialize)]
struct PythonSgp4Case {
name: String,
omm: String,
time_delta: Vec<f64>,
time_utc: Vec<String>,
x_teme_m: Vec<f64>,
y_teme_m: Vec<f64>,
z_teme_m: Vec<f64>,
vx_teme_m_per_s: Vec<f64>,
vy_teme_m_per_s: Vec<f64>,
vz_teme_m_per_s: Vec<f64>,
}
const PYTHON_SGP4_POS_TOL_M: f64 = 1e-3;
const PYTHON_SGP4_VEL_TOL_M_S: f64 = 1e-3;
fn parse_iso_utc(text: &str) -> DateTime {
let (date, time) = text
.split_once('T')
.unwrap_or_else(|| panic!("UTC timestamp missing T: {text}"));
let date_parts: Vec<&str> = date.split('-').collect();
let time_parts: Vec<&str> = time.split(':').collect();
assert!(
date_parts.len() == 3 && time_parts.len() == 3,
"UTC timestamp has the wrong shape: {text}"
);
DateTime {
year: date_parts[0].parse().expect("year"),
month: date_parts[1].parse().expect("month"),
day: date_parts[2].parse().expect("day"),
hour: time_parts[0].parse().expect("hour"),
minute: time_parts[1].parse().expect("minute"),
second: time_parts[2].parse().expect("second"),
timezone: Timezone::UTC,
}
}
fn assert_python_sgp4_state(
key: &str,
name: &str,
path: &str,
t_mins: f64,
got: &StateVector,
expected: &[f64; 6],
) {
let checks = [
(
"Position x",
got.r_x * 1000.0,
expected[0],
PYTHON_SGP4_POS_TOL_M,
),
(
"Position y",
got.r_y * 1000.0,
expected[1],
PYTHON_SGP4_POS_TOL_M,
),
(
"Position z",
got.r_z * 1000.0,
expected[2],
PYTHON_SGP4_POS_TOL_M,
),
(
"Velocity x",
got.v_x * 1000.0,
expected[3],
PYTHON_SGP4_VEL_TOL_M_S,
),
(
"Velocity y",
got.v_y * 1000.0,
expected[4],
PYTHON_SGP4_VEL_TOL_M_S,
),
(
"Velocity z",
got.v_z * 1000.0,
expected[5],
PYTHON_SGP4_VEL_TOL_M_S,
),
];
for (label, value, reference, tol) in checks {
assert!(
(value - reference).abs() < tol,
"{key}: {name} {path}\n- {label} mismatch {value} vs {reference} (t_mins = {t_mins})"
);
}
}
#[test]
fn test_sgp4_python_sgp4_cases() {
let content = std::fs::read_to_string("test/python-sgp4_cases.toml")
.expect("could not read test/python-sgp4_cases.toml");
let cases: PythonSgp4Cases =
from_str(&content).expect("could not parse test/python-sgp4_cases.toml");
let mut keys: Vec<&String> = cases.test.keys().collect();
keys.sort();
for key in keys {
let case = &cases.test[key];
let n = case.time_delta.len();
assert!(n > 0, "{key}: no propagation samples");
assert_eq!(case.time_utc.len(), n, "{key}: time_utc length");
assert_eq!(case.x_teme_m.len(), n, "{key}: x_teme_m length");
assert_eq!(case.y_teme_m.len(), n, "{key}: y_teme_m length");
assert_eq!(case.z_teme_m.len(), n, "{key}: z_teme_m length");
assert_eq!(case.vx_teme_m_per_s.len(), n, "{key}: vx length");
assert_eq!(case.vy_teme_m_per_s.len(), n, "{key}: vy length");
assert_eq!(case.vz_teme_m_per_s.len(), n, "{key}: vz length");
let sgp4s = from_omm_kvn_string(&case.omm)
.unwrap_or_else(|err| panic!("{key}: {} failed to parse: {err:?}", case.name));
assert_eq!(sgp4s.len(), 1, "{key}: expected one OMM");
let sgp4 = &sgp4s[0];
for i in 0..n {
let expected = [
case.x_teme_m[i],
case.y_teme_m[i],
case.z_teme_m[i],
case.vx_teme_m_per_s[i],
case.vy_teme_m_per_s[i],
case.vz_teme_m_per_s[i],
];
let t_mins = case.time_delta[i];
let state_delta = sgp4_prop_delta(sgp4, t_mins).unwrap_or_else(|err| {
panic!("{key}: {} delta at t_mins={t_mins}: {err:?}", case.name)
});
assert_python_sgp4_state(key, &case.name, "delta", t_mins, &state_delta, &expected);
let datetime = parse_iso_utc(&case.time_utc[i]);
let state_utc = sgp4_prop_datetime(sgp4, &datetime).unwrap_or_else(|err| {
panic!("{key}: {} utc at t_mins={t_mins}: {err:?}", case.name)
});
assert_python_sgp4_state(key, &case.name, "utc", t_mins, &state_utc, &expected);
}
}
}
}