apsis-rs/crates/orbit/apsis-sgp4/src/satellite.rs

417 lines
15 KiB
Rust

//! The satellite record and its initialisation from mean elements.
use crate::error::{PropagationError, PropagationErrorKind};
use crate::init::{self, Elements};
use crate::model::GravityModel;
use crate::state::State;
use apsis_tle::TwoLineElement;
use core::f64::consts::PI;
/// The number of radians in a revolution.
pub(crate) const TWO_PI: f64 = 2.0 * PI;
/// The number of minutes in a day.
pub(crate) const MINUTES_PER_DAY: f64 = 1_440.0;
/// Whether the near-Earth or the deep space branch applies.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Method {
/// Period below 225 minutes.
NearEarth,
/// Period at or above 225 minutes.
DeepSpace,
}
/// A satellite prepared for propagation.
///
/// Construct one with [`Satellite::from_element`], then call
/// [`Satellite::propagate_minutes`] or [`Satellite::propagate_at`] for each
/// instant of interest. The initialisation work, including the mean motion
/// correction and every coefficient that depends only on the elements, is
/// done once at construction.
#[derive(Debug, Clone)]
pub struct Satellite {
/// The model this satellite was initialised against.
pub(crate) model: GravityModel,
/// Corrected mean motion, in radians per minute.
pub(crate) no_unkozai: f64,
/// Eccentricity.
pub(crate) ecco: f64,
/// Inclination, in radians.
pub(crate) inclo: f64,
/// Right ascension of the ascending node, in radians.
pub(crate) nodeo: f64,
/// Argument of perigee, in radians.
pub(crate) argpo: f64,
/// Mean anomaly, in radians.
pub(crate) mo: f64,
/// `BSTAR` drag term, in inverse Earth radii.
pub(crate) bstar: f64,
/// Epoch as a Modified Julian Date on the TAI scale.
pub(crate) epoch_mjd: f64,
/// Which branch of the theory applies.
pub(crate) method: Method,
/// Set when the short period terms are omitted.
pub(crate) isimp: bool,
/// `3 cos^2 i - 1`.
pub(crate) con41: f64,
/// `1 - cos^2 i`.
pub(crate) x1mth2: f64,
/// `7 cos^2 i - 1`.
pub(crate) x7thm1: f64,
/// `-0.5 j3 / j2 sin i`.
pub(crate) aycof: f64,
/// The `j3` contribution to the argument of latitude.
pub(crate) xlcof: f64,
/// First drag coefficient.
pub(crate) cc1: f64,
/// Fourth drag coefficient.
pub(crate) cc4: f64,
/// Fifth drag coefficient.
pub(crate) cc5: f64,
/// Second order drag coefficient.
pub(crate) d2: f64,
/// Third order drag coefficient.
pub(crate) d3: f64,
/// Fourth order drag coefficient.
pub(crate) d4: f64,
/// `(1 + eta cos M)^3` at epoch.
pub(crate) delmo: f64,
/// `eta`.
pub(crate) eta: f64,
/// Contribution of drag to the argument of perigee.
pub(crate) omgcof: f64,
/// `sin M` at epoch.
pub(crate) sinmao: f64,
/// Quadratic coefficient of the time polynomial.
pub(crate) t2cof: f64,
/// Cubic coefficient of the time polynomial.
pub(crate) t3cof: f64,
/// Quartic coefficient of the time polynomial.
pub(crate) t4cof: f64,
/// Quintic coefficient of the time polynomial.
pub(crate) t5cof: f64,
/// Contribution of drag to the mean anomaly.
pub(crate) xmcof: f64,
/// Rate of change of the mean anomaly, in radians per minute.
pub(crate) mdot: f64,
/// Rate of change of the argument of perigee, in radians per minute.
pub(crate) argpdot: f64,
/// Rate of change of the right ascension, in radians per minute.
pub(crate) nodedot: f64,
/// Quadratic term of the right ascension.
pub(crate) nodecf: f64,
/// Greenwich sidereal time at the epoch, in radians.
pub(crate) gsto: f64,
/// Deep space resonance coefficients, all zero for near-Earth orbits.
pub(crate) ds: crate::deep_space::DeepSpace,
}
impl Satellite {
/// Initialises a satellite from a parsed element set.
///
/// Returns `None` when the mean elements do not describe an orbit the
/// model can propagate, for example when the eccentricity is at least
/// one. A caller that needs the reason should use
/// [`Satellite::try_from_element`].
pub fn from_element(element: &TwoLineElement, model: GravityModel) -> Option<Self> {
Self::try_from_element(element, model).ok()
}
/// Initialises a satellite from a parsed element set.
///
/// Returns an error describing which element is not propagatable when
/// the elements are rejected.
pub fn try_from_element(
element: &TwoLineElement,
model: GravityModel,
) -> Result<Self, PropagationError> {
let elements = Elements::from_tle(element).ok_or_else(|| {
PropagationError::new(PropagationErrorKind::InvalidElements)
.with_detail("element set epoch is outside the leap second table")
})?;
Self::try_from_elements(&elements, model)
}
/// Initialises a satellite from mean elements already in model units.
pub fn try_from_elements(
elements: &Elements,
model: GravityModel,
) -> Result<Self, PropagationError> {
init::validate(elements)?;
Ok(Self::initialise(elements, model))
}
/// Returns the epoch of the elements as a Modified Julian Date on the
/// TAI scale.
pub fn epoch_mjd(&self) -> f64 {
self.epoch_mjd
}
/// Returns `true` when the deep space branch applies.
pub fn is_deep_space(&self) -> bool {
self.method == Method::DeepSpace
}
/// Returns the number of minutes from the element set epoch to the
/// given instant.
///
/// The instant is given as a Modified Julian Date on the TAI scale, the
/// same scale as [`Satellite::epoch_mjd`].
pub fn minutes_since_epoch(&self, tai_mjd: f64) -> f64 {
(tai_mjd - self.epoch_mjd) * MINUTES_PER_DAY
}
/// Returns the state of the satellite at a given instant.
///
/// The instant is a Modified Julian Date on the TAI scale.
pub fn propagate_at(&self, tai_mjd: f64) -> Result<State, PropagationError> {
self.propagate_minutes(self.minutes_since_epoch(tai_mjd))
}
/// Returns the state of the satellite at a number of minutes from the
/// element set epoch.
///
/// Negative values propagate backwards. The result is in the True
/// Equator Mean Equinox frame, with position in kilometers and velocity
/// in kilometers per second.
///
/// Returns [`PropagationErrorKind::Decayed`](crate::PropagationErrorKind::Decayed)
/// when the propagated radius falls below one Earth radius, which the
/// model treats as the satellite having re-entered.
pub fn propagate_minutes(&self, minutes: f64) -> Result<State, PropagationError> {
crate::propagate::propagate(self, minutes)
}
/// Returns the Kepler constant of the model, in radians per minute per
/// Earth radius to the three halves.
pub fn xke(&self) -> f64 {
self.model.xke()
}
}
impl Satellite {
/// Builds the record, including every coefficient that depends only on
/// the epoch elements.
fn initialise(elements: &Elements, model: GravityModel) -> Self {
let xke = model.xke();
let j2 = model.j2;
let j4 = model.j4;
let ecco = elements.eccentricity;
let inclo = elements.inclination;
let no_kozai = elements.mean_motion;
let no_unkozai = init::unkozai_mean_motion(no_kozai, ecco, inclo, model);
let method = if init::is_deep_space(no_unkozai) {
Method::DeepSpace
} else {
Method::NearEarth
};
let cosio = inclo.cos();
let sinio = inclo.sin();
let cosio2 = cosio * cosio;
let cosio4 = cosio2 * cosio2;
let omeosq = 1.0 - ecco * ecco;
let rteosq = omeosq.sqrt();
let ao = (xke / no_unkozai).powf(2.0 / 3.0);
let po = ao * omeosq;
let posq = po * po;
let rp = ao * (1.0 - ecco);
let con42 = 1.0 - 5.0 * cosio2;
let con41 = -con42 - cosio2 - cosio2;
// The aerodynamic parameters. `sfour` is the effective radius at
// which the atmosphere is treated as having constant density, and
// `qzms24` is the corresponding density scale raised to the fourth
// power.
let ss = 78.0 / model.radius + 1.0;
let qzms2t = ((120.0 - 78.0) / model.radius).powi(4);
let mut isimp = false;
if rp < 220.0 / model.radius + 1.0 {
isimp = true;
}
let mut sfour = ss;
let mut qzms24 = qzms2t;
let perige = (rp - 1.0) * model.radius;
if perige < 156.0 {
sfour = perige - 78.0;
if perige < 98.0 {
sfour = 20.0;
}
qzms24 = ((120.0 - sfour) / model.radius).powi(4);
sfour = sfour / model.radius + 1.0;
}
let pinvsq = 1.0 / posq;
let tsi = 1.0 / (ao - sfour);
let eta = ao * ecco * tsi;
let etasq = eta * eta;
let eeta = ecco * eta;
let psisq = (1.0 - etasq).abs();
let coef = qzms24 * tsi.powi(4);
let coef1 = coef / psisq.powf(3.5);
let cc2 = coef1
* no_unkozai
* (ao * (1.0 + 1.5 * etasq + eeta * (4.0 + etasq))
+ 0.375 * j2 * tsi / psisq * con41 * (8.0 + 3.0 * etasq * (8.0 + etasq)));
let cc1 = elements.bstar * cc2;
let mut cc3 = 0.0;
if ecco > 1.0e-4 {
cc3 = -2.0 * coef * tsi * model.j3_over_j2() * no_unkozai * sinio / ecco;
}
let x1mth2 = 1.0 - cosio2;
let cc4 = 2.0
* no_unkozai
* coef1
* ao
* omeosq
* (eta * (2.0 + 0.5 * etasq) + ecco * (0.5 + 2.0 * etasq)
- j2 * tsi / (ao * psisq)
* (-3.0 * con41 * (1.0 - 2.0 * eeta + etasq * (1.5 - 0.5 * eeta))
+ 0.75
* x1mth2
* (2.0 * etasq - eeta * (1.0 + etasq))
* (2.0 * elements.argument_of_perigee).cos()));
let cc5 = 2.0 * coef1 * ao * omeosq * (1.0 + 2.75 * (etasq + eeta) + eeta * etasq);
let temp1 = 1.5 * j2 * pinvsq * no_unkozai;
let temp2 = 0.5 * temp1 * j2 * pinvsq;
let temp3 = -0.46875 * j4 * pinvsq * pinvsq * no_unkozai;
let mdot = no_unkozai
+ 0.5 * temp1 * rteosq * con41
+ 0.0625 * temp2 * rteosq * (13.0 - 78.0 * cosio2 + 137.0 * cosio4);
let argpdot = -0.5 * temp1 * con42
+ 0.0625 * temp2 * (7.0 - 114.0 * cosio2 + 395.0 * cosio4)
+ temp3 * (3.0 - 36.0 * cosio2 + 49.0 * cosio4);
let xhdot1 = -temp1 * cosio;
let nodedot = xhdot1
+ (0.5 * temp2 * (4.0 - 19.0 * cosio2) + 2.0 * temp3 * (3.0 - 7.0 * cosio2)) * cosio;
let omgcof = elements.bstar * cc3 * elements.argument_of_perigee.cos();
let mut xmcof = 0.0;
if ecco > 1.0e-4 {
xmcof = -2.0 / 3.0 * coef * elements.bstar / eeta;
}
let nodecf = 3.5 * omeosq * xhdot1 * cc1;
let t2cof = 1.5 * cc1;
// The divisor guards the singularity at inclination pi, where the
// expression is finite in the limit. `1.5e-12` is the threshold used
// by the reference implementation.
const INCLINATION_GUARD: f64 = 1.5e-12;
let xlcof = if (cosio + 1.0).abs() > INCLINATION_GUARD {
-0.25 * model.j3_over_j2() * sinio * (3.0 + 5.0 * cosio) / (1.0 + cosio)
} else {
-0.25 * model.j3_over_j2() * sinio * (3.0 + 5.0 * cosio) / INCLINATION_GUARD
};
let aycof = -0.5 * model.j3_over_j2() * sinio;
let delmotemp = 1.0 + eta * elements.mean_anomaly.cos();
let delmo = delmotemp * delmotemp * delmotemp;
let sinmao = elements.mean_anomaly.sin();
let x7thm1 = 7.0 * cosio2 - 1.0;
let mut satellite = Self {
model,
no_unkozai,
ecco,
inclo,
nodeo: elements.right_ascension,
argpo: elements.argument_of_perigee,
mo: elements.mean_anomaly,
bstar: elements.bstar,
epoch_mjd: elements.epoch_mjd,
method,
isimp,
con41,
x1mth2,
x7thm1,
aycof,
xlcof,
cc1,
cc4,
cc5,
d2: 0.0,
d3: 0.0,
d4: 0.0,
delmo,
eta,
omgcof,
sinmao,
t2cof,
t3cof: 0.0,
t4cof: 0.0,
t5cof: 0.0,
xmcof,
mdot,
argpdot,
nodedot,
nodecf,
gsto: gstime(elements.epoch_mjd + 2_400_000.5),
ds: crate::deep_space::DeepSpace::ZERO,
};
// The higher order drag terms are omitted for the simplified branch.
if !isimp {
let cc1sq = cc1 * cc1;
let d2 = 4.0 * ao * tsi * cc1sq;
let temp = d2 * tsi * cc1 / 3.0;
let d3 = (17.0 * ao + sfour) * temp;
let d4 = 0.5 * temp * ao * tsi * (221.0 * ao + 31.0 * sfour) * cc1;
satellite.d2 = d2;
satellite.d3 = d3;
satellite.d4 = d4;
satellite.t3cof = d2 + 2.0 * cc1sq;
satellite.t4cof = 0.25 * (3.0 * d3 + cc1 * (12.0 * d2 + 10.0 * cc1sq));
satellite.t5cof = 0.2
* (3.0 * d4 + 12.0 * cc1 * d3 + 6.0 * d2 * d2 + 15.0 * cc1sq * (2.0 * d2 + cc1sq));
}
if method == Method::DeepSpace {
satellite.isimp = true;
satellite.ds = crate::deep_space::initialise(&satellite);
}
satellite
}
}
/// Returns Greenwich sidereal time for a UT1 Julian Date, in radians.
///
/// The expression is the one used by the reference implementation. It
/// expands the ratio of universal time to sidereal time and is accurate for
/// the purpose of initialising the deep space resonance, which is the only
/// place the value is used.
///
/// Source: Vallado et al., *Revisiting Spacetrack Report #3*, AIAA
/// 2006-6753, 2006.
pub(crate) fn gstime(julian_date_ut1: f64) -> f64 {
let centuries = (julian_date_ut1 - 2_451_545.0) / 36_525.0;
let mut seconds = -6.2e-6 * centuries * centuries * centuries
+ 0.093_104 * centuries * centuries
+ (876_600.0 * 3_600.0 + 8_640_184.812_866) * centuries
+ 67_310.548_41;
seconds = (seconds * (PI / 180.0) / 240.0) % TWO_PI;
if seconds < 0.0 {
seconds += TWO_PI;
}
seconds
}
/// Returns the propagation error for a mean motion that is not positive.
pub(crate) fn non_positive_mean_motion(mean_motion: f64) -> PropagationError {
PropagationError::new(PropagationErrorKind::InvalidElements)
.with_detail(format!("propagated mean motion is {mean_motion}"))
}