#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(any(feature = "std", feature = "libm")))]
compile_error!("either feature \"std\" or feature \"libm\" must be enabled");
#[cfg(all(feature = "std", feature = "libm"))]
compile_error!("feature \"std\" and feature \"libm\" cannot be enabled at the same time");
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(not(feature = "std"))]
use num_traits::Float;
mod deep_space;
mod gp;
mod model;
mod near_earth;
mod propagator;
mod third_body;
mod tle;
pub use chrono;
pub use deep_space::ResonanceState;
pub use gp::Error;
pub use model::afspc_epoch_to_sidereal_time;
pub use model::iau_epoch_to_sidereal_time;
pub use model::Geopotential;
pub use model::WGS72;
pub use model::WGS84;
pub use propagator::Constants;
pub use propagator::Orbit;
pub use propagator::Prediction;
pub use tle::julian_years_since_j2000;
pub use tle::julian_years_since_j2000_afspc_compatibility_mode;
pub use tle::Classification;
pub use tle::DatetimeToMinutesSinceEpochError;
pub use tle::Elements;
pub use tle::Error as TleError;
pub use tle::ErrorLine as TleErrorLine;
pub use tle::ErrorWhat as TleErrorWhat;
pub use tle::MinutesSinceEpoch;
pub use tle::MinutesSinceEpochToDatetimeError;
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub use tle::parse_2les;
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub use tle::parse_3les;
#[derive(Debug, Clone, PartialEq)]
pub enum KozaiElementsError {
NegativeKozaiMeanMotion,
NegativeBrouwerMeanMotion,
}
impl core::fmt::Display for KozaiElementsError {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
KozaiElementsError::NegativeKozaiMeanMotion => formatter
.write_str("The Kozai mean motion calculated from epoch elements is negative"),
KozaiElementsError::NegativeBrouwerMeanMotion => formatter
.write_str("The Brouwer mean motion calculated from epoch elements is negative"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for KozaiElementsError {}
#[derive(Debug, Clone, PartialEq)]
pub struct OutOfRangeEpochEccentricity(pub f64);
impl core::fmt::Display for OutOfRangeEpochEccentricity {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_fmt(core::format_args!(
"The epoch eccentricity ({}) is outside the range [0, 1[",
self.0
))
}
}
#[cfg(feature = "std")]
impl std::error::Error for OutOfRangeEpochEccentricity {}
#[derive(Debug, Clone, PartialEq)]
pub enum ElementsError {
KozaiElementsError(KozaiElementsError),
OutOfRangeEpochEccentricity(OutOfRangeEpochEccentricity),
}
impl core::fmt::Display for ElementsError {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ElementsError::KozaiElementsError(error) => error.fmt(formatter),
ElementsError::OutOfRangeEpochEccentricity(error) => error.fmt(formatter),
}
}
}
impl From<KozaiElementsError> for ElementsError {
fn from(value: KozaiElementsError) -> Self {
Self::KozaiElementsError(value)
}
}
impl From<OutOfRangeEpochEccentricity> for ElementsError {
fn from(value: OutOfRangeEpochEccentricity) -> Self {
Self::OutOfRangeEpochEccentricity(value)
}
}
#[cfg(feature = "std")]
impl std::error::Error for ElementsError {}
impl Orbit {
pub fn from_kozai_elements(
geopotential: &Geopotential,
inclination: f64,
right_ascension: f64,
eccentricity: f64,
argument_of_perigee: f64,
mean_anomaly: f64,
kozai_mean_motion: f64,
) -> core::result::Result<Self, KozaiElementsError> {
if kozai_mean_motion <= 0.0 {
Err(KozaiElementsError::NegativeKozaiMeanMotion)
} else {
let mean_motion = {
let a1 = (geopotential.ke / kozai_mean_motion).powf(2.0 / 3.0);
let p0 = 0.75 * geopotential.j2 * (3.0 * inclination.cos().powi(2) - 1.0)
/ (1.0 - eccentricity.powi(2)).powf(3.0 / 2.0);
let d1 = p0 / a1.powi(2);
let d0 = p0
/ (a1 * (1.0 - d1.powi(2) - d1 * (1.0 / 3.0 + 134.0 * d1.powi(2) / 81.0)))
.powi(2);
kozai_mean_motion / (1.0 + d0)
};
if mean_motion <= 0.0 {
Err(KozaiElementsError::NegativeBrouwerMeanMotion)
} else {
Ok(propagator::Orbit {
inclination,
right_ascension,
eccentricity,
argument_of_perigee,
mean_anomaly,
mean_motion,
})
}
}
}
}
impl Constants {
pub fn new(
geopotential: Geopotential,
epoch_to_sidereal_time: impl Fn(f64) -> f64,
epoch: f64,
drag_term: f64,
orbit_0: propagator::Orbit,
) -> core::result::Result<Self, OutOfRangeEpochEccentricity> {
if orbit_0.eccentricity < 0.0 || orbit_0.eccentricity >= 1.0 {
Err(OutOfRangeEpochEccentricity(orbit_0.eccentricity))
} else {
let p1 = orbit_0.inclination.cos();
let p2 = 1.0 - orbit_0.eccentricity.powi(2);
let k6 = 3.0 * p1.powi(2) - 1.0;
let a0 = (geopotential.ke / orbit_0.mean_motion).powf(2.0 / 3.0);
let p3 = a0 * (1.0 - orbit_0.eccentricity);
let (s, p6) = {
let p4 = geopotential.ae * (p3 - 1.0);
let p5 = if p4 < 98.0 {
20.0
} else if p4 < 156.0 {
p4 - 78.0
} else {
78.0
};
(
p5 / geopotential.ae + 1.0,
((120.0 - p5) / geopotential.ae).powi(4),
)
};
let xi = 1.0 / (a0 - s);
let p7 = p6 * xi.powi(4);
let eta = a0 * orbit_0.eccentricity * xi;
let p8 = (1.0 - eta.powi(2)).abs();
let p9 = p7 / p8.powf(3.5);
let c1 = drag_term
* (p9
* orbit_0.mean_motion
* (a0
* (1.0
+ 1.5 * eta.powi(2)
+ orbit_0.eccentricity * eta * (4.0 + eta.powi(2)))
+ 0.375 * geopotential.j2 * xi / p8
* k6
* (8.0 + 3.0 * eta.powi(2) * (8.0 + eta.powi(2)))));
let p10 = 1.0 / (a0 * p2).powi(2);
let b0 = p2.sqrt();
let p11 = 1.5 * geopotential.j2 * p10 * orbit_0.mean_motion;
let p12 = 0.5 * p11 * geopotential.j2 * p10;
let p13 = -0.46875 * geopotential.j4 * p10.powi(2) * orbit_0.mean_motion;
let p14 = -p11 * p1
+ (0.5 * p12 * (4.0 - 19.0 * p1.powi(2)) + 2.0 * p13 * (3.0 - 7.0 * p1.powi(2)))
* p1;
let k14 = -0.5 * p11 * (1.0 - 5.0 * p1.powi(2))
+ 0.0625 * p12 * (7.0 - 114.0 * p1.powi(2) + 395.0 * p1.powi(4))
+ p13 * (3.0 - 36.0 * p1.powi(2) + 49.0 * p1.powi(4));
let p15 = orbit_0.mean_motion
+ 0.5 * p11 * b0 * k6
+ 0.0625 * p12 * b0 * (13.0 - 78.0 * p1.powi(2) + 137.0 * p1.powi(4));
let c4 = drag_term
* (2.0
* orbit_0.mean_motion
* p9
* a0
* p2
* (eta * (2.0 + 0.5 * eta.powi(2))
+ orbit_0.eccentricity * (0.5 + 2.0 * eta.powi(2))
- geopotential.j2 * xi / (a0 * p8)
* (-3.0
* k6
* (1.0 - 2.0 * orbit_0.eccentricity * eta
+ eta.powi(2) * (1.5 - 0.5 * orbit_0.eccentricity * eta))
+ 0.75
* (1.0 - p1.powi(2))
* (2.0 * eta.powi(2)
- orbit_0.eccentricity * eta * (1.0 + eta.powi(2)))
* (2.0 * orbit_0.argument_of_perigee).cos())));
let k0 = 3.5 * p2 * (-p11 * p1) * c1;
let k1 = 1.5 * c1;
if orbit_0.mean_motion > 2.0 * core::f64::consts::PI / 225.0 {
Ok(near_earth::constants(
geopotential,
drag_term,
orbit_0,
p1,
a0,
s,
xi,
eta,
c1,
c4,
k0,
k1,
k6,
k14,
p2,
p3,
p7,
p9,
p14,
p15,
))
} else {
Ok(deep_space::constants(
geopotential,
epoch_to_sidereal_time,
epoch,
orbit_0,
p1,
a0,
c1,
b0,
c4,
k0,
k1,
k14,
p2,
p14,
p15,
))
}
}
}
pub fn from_elements(elements: &Elements) -> core::result::Result<Self, ElementsError> {
Ok(Constants::new(
WGS84,
iau_epoch_to_sidereal_time,
elements.epoch(),
elements.drag_term,
Orbit::from_kozai_elements(
&WGS84,
elements.inclination * (core::f64::consts::PI / 180.0),
elements.right_ascension * (core::f64::consts::PI / 180.0),
elements.eccentricity,
elements.argument_of_perigee * (core::f64::consts::PI / 180.0),
elements.mean_anomaly * (core::f64::consts::PI / 180.0),
elements.mean_motion * (core::f64::consts::PI / 720.0),
)?,
)?)
}
pub fn from_elements_afspc_compatibility_mode(
elements: &Elements,
) -> core::result::Result<Self, ElementsError> {
Ok(Constants::new(
WGS72,
afspc_epoch_to_sidereal_time,
elements.epoch_afspc_compatibility_mode(),
elements.drag_term,
Orbit::from_kozai_elements(
&WGS72,
elements.inclination * (core::f64::consts::PI / 180.0),
elements.right_ascension * (core::f64::consts::PI / 180.0),
elements.eccentricity,
elements.argument_of_perigee * (core::f64::consts::PI / 180.0),
elements.mean_anomaly * (core::f64::consts::PI / 180.0),
elements.mean_motion * (core::f64::consts::PI / 720.0),
)?,
)?)
}
pub fn initial_state(&self) -> Option<ResonanceState> {
match &self.method {
propagator::Method::NearEarth { .. } => None,
propagator::Method::DeepSpace { resonant, .. } => match resonant {
propagator::Resonant::No { .. } => None,
propagator::Resonant::Yes { lambda_0, .. } => {
Some(ResonanceState::new(self.orbit_0.mean_motion, *lambda_0))
}
},
}
}
#[allow(clippy::many_single_char_names)]
pub fn propagate_from_state(
&self,
t: MinutesSinceEpoch,
state: Option<&mut ResonanceState>,
afspc_compatibility_mode: bool,
) -> core::result::Result<Prediction, gp::Error> {
let p22 =
self.orbit_0.right_ascension + self.right_ascension_dot * t.0 + self.k0 * t.0.powi(2);
let p23 = self.orbit_0.argument_of_perigee + self.argument_of_perigee_dot * t.0;
let (orbit, a, p32, p33, p34, p35, p36) = match &self.method {
propagator::Method::NearEarth {
a0,
k2,
k3,
k4,
k5,
k6,
high_altitude,
} => {
assert!(
state.is_none(),
"state must be None with a near-earth propagator",
);
self.near_earth_orbital_elements(
*a0,
*k2,
*k3,
*k4,
*k5,
*k6,
high_altitude,
t.0,
p22,
p23,
)
}
propagator::Method::DeepSpace {
eccentricity_dot,
inclination_dot,
solar_perturbations,
lunar_perturbations,
resonant,
} => self.deep_space_orbital_elements(
*eccentricity_dot,
*inclination_dot,
solar_perturbations,
lunar_perturbations,
resonant,
state,
t.0,
p22,
p23,
afspc_compatibility_mode,
),
}?;
let p37 = 1.0 / (a * (1.0 - orbit.eccentricity.powi(2)));
let axn = orbit.eccentricity * orbit.argument_of_perigee.cos();
let ayn = orbit.eccentricity * orbit.argument_of_perigee.sin() + p37 * p32;
let p38 = (orbit.mean_anomaly + orbit.argument_of_perigee + p37 * p35 * axn)
% (2.0 * core::f64::consts::PI);
let mut ew = p38;
for _ in 0..10 {
let delta = (p38 - ayn * ew.cos() + axn * ew.sin() - ew)
/ (1.0 - ew.cos() * axn - ew.sin() * ayn);
if delta.abs() < 1.0e-12 {
break;
}
ew += delta.clamp(-0.95, 0.95);
}
let p39 = axn.powi(2) + ayn.powi(2);
let pl = a * (1.0 - p39);
if pl < 0.0 {
Err(gp::Error::NegativeSemiLatusRectum { t: t.0 })
} else {
let p40 = axn * ew.sin() - ayn * ew.cos();
let r = a * (1.0 - (axn * ew.cos() + ayn * ew.sin()));
let r_dot = a.sqrt() * p40 / r;
let b = (1.0 - p39).sqrt();
let p41 = p40 / (1.0 + b);
let p42 = a / r * (ew.sin() - ayn - axn * p41);
let p43 = a / r * (ew.cos() - axn + ayn * p41);
let u = p42.atan2(p43);
let p44 = 2.0 * p43 * p42;
let p45 = 1.0 - 2.0 * p42.powi(2);
let p46 = 0.5 * self.geopotential.j2 / pl / pl;
let rk = r * (1.0 - 1.5 * p46 * b * p36)
+ 0.5 * (0.5 * self.geopotential.j2 / pl) * p33 * p45;
let uk = u - 0.25 * p46 * p34 * p44;
let inclination_k = orbit.inclination
+ 1.5 * p46 * orbit.inclination.cos() * orbit.inclination.sin() * p45;
let right_ascension_k =
orbit.right_ascension + 1.5 * p46 * orbit.inclination.cos() * p44;
let rk_dot = r_dot
- orbit.mean_motion * (0.5 * self.geopotential.j2 / pl) * p33 * p44
/ self.geopotential.ke;
let rfk_dot = pl.sqrt() / r
+ orbit.mean_motion * (0.5 * self.geopotential.j2 / pl) * (p33 * p45 + 1.5 * p36)
/ self.geopotential.ke;
let u0 = -right_ascension_k.sin() * inclination_k.cos() * uk.sin()
+ right_ascension_k.cos() * uk.cos();
let u1 = right_ascension_k.cos() * inclination_k.cos() * uk.sin()
+ right_ascension_k.sin() * uk.cos();
let u2 = inclination_k.sin() * uk.sin();
Ok(Prediction {
position: [
rk * u0 * self.geopotential.ae,
rk * u1 * self.geopotential.ae,
rk * u2 * self.geopotential.ae,
],
velocity: [
(rk_dot * u0
+ rfk_dot
* (-right_ascension_k.sin() * inclination_k.cos() * uk.cos()
- right_ascension_k.cos() * uk.sin()))
* (self.geopotential.ae * self.geopotential.ke / 60.0),
(rk_dot * u1
+ rfk_dot
* (right_ascension_k.cos() * inclination_k.cos() * uk.cos()
- right_ascension_k.sin() * uk.sin()))
* (self.geopotential.ae * self.geopotential.ke / 60.0),
(rk_dot * u2 + rfk_dot * (inclination_k.sin() * uk.cos()))
* (self.geopotential.ae * self.geopotential.ke / 60.0),
],
})
}
}
pub fn propagate(&self, t: MinutesSinceEpoch) -> core::result::Result<Prediction, gp::Error> {
self.propagate_from_state(t, self.initial_state().as_mut(), false)
}
pub fn propagate_afspc_compatibility_mode(
&self,
t: MinutesSinceEpoch,
) -> core::result::Result<Prediction, gp::Error> {
self.propagate_from_state(t, self.initial_state().as_mut(), true)
}
}