181 lines
7.1 KiB
Rust
181 lines
7.1 KiB
Rust
//! Initialisation of the propagator from a two-line element set.
|
|
//!
|
|
//! The element set gives mean elements in degrees, revolutions per day, and
|
|
//! a drag term. The propagator works in radians, radians per minute, and
|
|
//! Earth radii, so the initialisation converts the inputs and precomputes
|
|
//! every coefficient that depends only on the elements.
|
|
//!
|
|
//! The quantity `no_unkozai` is the mean motion after the correction that
|
|
//! removes the effect of the Kozai mean motion convention used in the
|
|
//! element set. The correction follows from the second zonal harmonic and
|
|
//! is solved by a fixed point iteration, because the corrected value appears
|
|
//! on both sides of the relation.
|
|
//!
|
|
//! Source: the correction and the coefficient definitions follow Vallado et
|
|
//! al., *Revisiting Spacetrack Report #3*, AIAA 2006-6753, 2006.
|
|
|
|
use crate::error::{PropagationError, PropagationErrorKind};
|
|
use crate::model::GravityModel;
|
|
use apsis_tle::TwoLineElement;
|
|
use core::f64::consts::PI;
|
|
|
|
/// The number of minutes in a day.
|
|
const MINUTES_PER_DAY: f64 = 1_440.0;
|
|
|
|
/// The number of radians in a revolution.
|
|
const TWO_PI: f64 = 2.0 * PI;
|
|
|
|
/// The period, in minutes, at or above which the deep space branch is used.
|
|
///
|
|
/// The theory states 225 minutes. Satellites below that period are handled
|
|
/// by the near-Earth branch, which is the case for all low Earth orbits.
|
|
const DEEP_SPACE_PERIOD_MINUTES: f64 = 225.0;
|
|
|
|
/// The elements of a satellite in the units the propagator uses.
|
|
///
|
|
/// Angles are in radians and mean motion is in radians per minute. Helpers
|
|
/// convert a [`TwoLineElement`] into this form.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct Elements {
|
|
/// Epoch of the elements, expressed as a Modified Julian Date on the
|
|
/// scale the element set was written in.
|
|
pub epoch_mjd: f64,
|
|
/// `BSTAR` drag term, in inverse Earth radii.
|
|
pub bstar: f64,
|
|
/// Mean motion, in radians per minute.
|
|
pub mean_motion: f64,
|
|
/// Eccentricity, dimensionless.
|
|
pub eccentricity: f64,
|
|
/// Inclination, in radians.
|
|
pub inclination: f64,
|
|
/// Right ascension of the ascending node, in radians.
|
|
pub right_ascension: f64,
|
|
/// Argument of perigee, in radians.
|
|
pub argument_of_perigee: f64,
|
|
/// Mean anomaly, in radians.
|
|
pub mean_anomaly: f64,
|
|
}
|
|
|
|
impl Elements {
|
|
/// Converts a parsed element set into the units the propagator uses.
|
|
///
|
|
/// The epoch is taken as the UTC Modified Julian Date exactly as written
|
|
/// in the element set. The element set epoch is a UTC calendar instant,
|
|
/// but the propagator treats it as a uniform time origin, which is the
|
|
/// convention the theory was fitted under. Converting it to a uniform
|
|
/// scale first would shift the epoch by the leap second offset, about
|
|
/// 33 seconds, and that shift propagates into the solar and lunar series
|
|
/// of the deep space branch.
|
|
pub fn from_tle(element: &TwoLineElement) -> Option<Self> {
|
|
let epoch_mjd = element.epoch.mjd(apsis_time::TimeScale::Utc)?;
|
|
Some(Self {
|
|
epoch_mjd,
|
|
bstar: element.bstar,
|
|
mean_motion: element.mean_motion * TWO_PI / MINUTES_PER_DAY,
|
|
eccentricity: element.eccentricity,
|
|
inclination: element.inclination.to_radians(),
|
|
right_ascension: element.right_ascension.to_radians(),
|
|
argument_of_perigee: element.argument_of_perigee.to_radians(),
|
|
mean_anomaly: element.mean_anomaly.to_radians(),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Applies the Kozai to Brouwer mean motion correction.
|
|
///
|
|
/// The mean motion in an element set is a Kozai mean motion, which the
|
|
/// theory must convert to a Brouwer mean motion before use. The relation is
|
|
///
|
|
/// ```text
|
|
/// n0 = n / (1 + 1.5 k2 (3 cos^2 i - 1) / (a^2 sqrt(1 - e^2)) / a^2)
|
|
/// ```
|
|
///
|
|
/// written in the form used here as a fixed point iteration, because `a`
|
|
/// depends on `n0` through Kepler's third law. The iteration converges from
|
|
/// the unmodified mean motion in a few steps for every orbit that appears in
|
|
/// an element set.
|
|
/// Applies the Kozai to Brouwer mean motion correction.
|
|
///
|
|
/// The mean motion in an element set is a Kozai mean motion, which the
|
|
/// theory must convert to a Brouwer mean motion before use. Writing `no` for
|
|
/// the element set value and `xke` for the Kepler constant, the reference
|
|
/// implementation evaluates
|
|
///
|
|
/// ```text
|
|
/// ak = (xke / no)^(2/3)
|
|
/// d1 = 0.75 j2 (3 cos^2 i - 1) / (sqrt(1 - e^2) (1 - e^2))
|
|
/// del = d1 / ak^2
|
|
/// adel = ak (1 - del^2 - del (1/3 + 134 del^2 / 81))
|
|
/// del = d1 / adel^2
|
|
/// n0 = no / (1 + del)
|
|
/// ```
|
|
///
|
|
/// The second evaluation of `del` uses `adel` rather than `ak`, which is
|
|
/// what makes the correction consistent with the semi-major axis implied by
|
|
/// the corrected mean motion.
|
|
///
|
|
/// Source: Vallado et al., *Revisiting Spacetrack Report #3*, AIAA
|
|
/// 2006-6753, 2006.
|
|
pub fn unkozai_mean_motion(
|
|
mean_motion: f64,
|
|
eccentricity: f64,
|
|
inclination: f64,
|
|
model: GravityModel,
|
|
) -> f64 {
|
|
let xke = model.xke();
|
|
let j2 = model.j2;
|
|
let cosio = inclination.cos();
|
|
let cosio2 = cosio * cosio;
|
|
let omeosq = 1.0 - eccentricity * eccentricity;
|
|
let rteosq = omeosq.sqrt();
|
|
|
|
let ak = (xke / mean_motion).powf(2.0 / 3.0);
|
|
let d1 = 0.75 * j2 * (3.0 * cosio2 - 1.0) / (rteosq * omeosq);
|
|
let mut delta = d1 / (ak * ak);
|
|
let adel = ak * (1.0 - delta * delta - delta * (1.0 / 3.0 + 134.0 * delta * delta / 81.0));
|
|
delta = d1 / (adel * adel);
|
|
mean_motion / (1.0 + delta)
|
|
}
|
|
|
|
/// Validates the mean elements before initialisation.
|
|
///
|
|
/// Returns an error when the orbit cannot be propagated, for example when
|
|
/// the eccentricity does not describe a closed orbit or the mean motion is
|
|
/// not positive.
|
|
pub fn validate(elements: &Elements) -> Result<(), PropagationError> {
|
|
if !elements.mean_motion.is_finite() || elements.mean_motion <= 0.0 {
|
|
return Err(PropagationError::new(PropagationErrorKind::InvalidElements)
|
|
.with_detail(format!("mean motion is {}", elements.mean_motion)));
|
|
}
|
|
if !elements.eccentricity.is_finite()
|
|
|| elements.eccentricity < 0.0
|
|
|| elements.eccentricity >= 1.0
|
|
{
|
|
return Err(PropagationError::new(PropagationErrorKind::InvalidElements)
|
|
.with_detail(format!("eccentricity is {}", elements.eccentricity)));
|
|
}
|
|
for (name, value) in [
|
|
("inclination", elements.inclination),
|
|
("right ascension", elements.right_ascension),
|
|
("argument of perigee", elements.argument_of_perigee),
|
|
("mean anomaly", elements.mean_anomaly),
|
|
] {
|
|
if !value.is_finite() {
|
|
return Err(PropagationError::new(PropagationErrorKind::InvalidElements)
|
|
.with_detail(format!("{name} is {value}")));
|
|
}
|
|
}
|
|
if !elements.bstar.is_finite() {
|
|
return Err(PropagationError::new(PropagationErrorKind::InvalidElements)
|
|
.with_detail(format!("bstar is {}", elements.bstar)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns `true` when the elements describe a deep space orbit.
|
|
///
|
|
/// The test is on the orbital period, which follows from the mean motion.
|
|
pub fn is_deep_space(mean_motion: f64) -> bool {
|
|
TWO_PI / mean_motion >= DEEP_SPACE_PERIOD_MINUTES
|
|
}
|