277 lines
12 KiB
Rust
277 lines
12 KiB
Rust
//! The celestial-to-terrestrial transformation.
|
|
//!
|
|
//! This module composes the frame bias, precession, nutation, Earth rotation
|
|
//! angle, and polar motion into the rotation that carries a vector from the
|
|
//! inertial frame to the Earth-fixed frame.
|
|
//!
|
|
//! Two intermediate frames appear in the composition. The **celestial
|
|
//! intermediate frame** is the true equator and equinox of date; the
|
|
//! rotation from the inertial frame to it is the bias, precession, and
|
|
//! nutation. The **terrestrial intermediate frame** is that frame carried to
|
|
//! the Earth's prime meridian by the Earth rotation angle. The polar motion
|
|
//! is applied last, because it is the motion of the pole within the Earth
|
|
//! rather than of the Earth within space.
|
|
//!
|
|
//! The elementary rotations follow the convention of the reference: `rz`
|
|
//! rotates the frame about its third axis, and composing `a * b` applies `b`
|
|
//! first. That convention is what makes the products below read in the order
|
|
//! the rotations are physically applied.
|
|
//!
|
|
//! Source: the composition follows the IAU SOFA library and its ERFA
|
|
//! derivative. The bias and precession are `eraPmat06`, the nutation matrix
|
|
//! is `eraNumat`, the rotation angle is `eraEra00`, the sidereal time for
|
|
//! the equinox-based composition is `eraGmst06`, and the polar motion is
|
|
//! `eraPom00`, composed by `eraC2t00b`.
|
|
|
|
use super::nutation::nutation_2000a_with_2006_adjustment;
|
|
use super::precession::{frame_to_mean_of_date, mean_obliquity, rotation_z};
|
|
use apsis_math::Mat3;
|
|
use core::f64::consts::PI;
|
|
|
|
/// Converts arcseconds to radians.
|
|
const ARCSECOND_TO_RADIAN: f64 = PI / (180.0 * 3_600.0);
|
|
|
|
/// Returns the bias-precession-nutation matrix for a time.
|
|
///
|
|
/// The matrix carries a vector from the celestial reference frame to the
|
|
/// true equator and equinox of date. It is built from the four precession
|
|
/// angles with the nutation added to the last two, which is the form the
|
|
/// reference uses.
|
|
///
|
|
/// Source: as implemented by `eraPnm06a` and `eraFw2m`.
|
|
pub fn bias_precession_nutation_matrix(t: f64) -> Mat3 {
|
|
let angles = super::precession::precession_angles(t);
|
|
let nutation = nutation_2000a_with_2006_adjustment(t);
|
|
frame_to_mean_of_date(&super::precession::PrecessionAngles {
|
|
gamma_bar: angles.gamma_bar,
|
|
phi_bar: angles.phi_bar,
|
|
psi_bar: angles.psi_bar + nutation.longitude,
|
|
epsilon: angles.epsilon + nutation.obliquity,
|
|
})
|
|
}
|
|
|
|
/// Returns the complementary terms of the equation of the equinoxes.
|
|
///
|
|
/// The equation of the equinoxes has, besides the projection of the nutation
|
|
/// in longitude onto the true equator, a set of small periodic terms from
|
|
/// the same theory. They reach about twice the constant term of the
|
|
/// osculating nutation, which is a millisecond of time, so they matter for a
|
|
/// sidereal time that is quoted to that precision.
|
|
///
|
|
/// Source: the MHB2000 complementary terms, as implemented by `eraEect00`.
|
|
pub fn equation_of_the_equinoxes_complementary_terms(t: f64) -> f64 {
|
|
use super::eect_table::{CONSTANT_TERMS, LINEAR_TERMS};
|
|
|
|
// The complementary terms use the IERS 2003 forms of two arguments,
|
|
// which differ from the ones the nutation series was fitted with.
|
|
let arguments = [
|
|
super::fundamental::mean_anomaly_moon(t),
|
|
super::fundamental::mean_anomaly_sun_iers2003(t),
|
|
super::fundamental::mean_latitude_moon(t),
|
|
super::fundamental::mean_elongation_moon_sun_iers2003(t),
|
|
super::fundamental::mean_longitude_node(t),
|
|
super::fundamental::mean_longitude_venus(t),
|
|
super::fundamental::mean_longitude_earth(t),
|
|
super::fundamental::general_precession(t),
|
|
];
|
|
|
|
let mut constant = 0.0;
|
|
for term in CONSTANT_TERMS.iter() {
|
|
let argument: f64 = term
|
|
.argument
|
|
.iter()
|
|
.zip(arguments.iter())
|
|
.map(|(multiplier, value)| f64::from(*multiplier) * value)
|
|
.sum();
|
|
let (sine, cosine) = argument.sin_cos();
|
|
constant += term.coefficients[0] * sine + term.coefficients[1] * cosine;
|
|
}
|
|
|
|
let mut linear = 0.0;
|
|
for term in LINEAR_TERMS.iter() {
|
|
let argument: f64 = term
|
|
.argument
|
|
.iter()
|
|
.zip(arguments.iter())
|
|
.map(|(multiplier, value)| f64::from(*multiplier) * value)
|
|
.sum();
|
|
let (sine, cosine) = argument.sin_cos();
|
|
linear += term.coefficients[0] * sine + term.coefficients[1] * cosine;
|
|
}
|
|
|
|
// The tabulated coefficients are in arcseconds.
|
|
(constant + linear * t) * ARCSECOND_TO_RADIAN
|
|
}
|
|
|
|
/// Returns the equation of the equinoxes for a time.
|
|
///
|
|
/// The equation of the equinoxes is the difference between the true and mean
|
|
/// equinoxes measured along the true equator. It is the quantity a sidereal
|
|
/// time must be corrected by to become the hour angle of the true equinox.
|
|
///
|
|
/// The value is the projection of the nutation in longitude onto the true
|
|
/// equator, `dpsi cos(eps)`, plus the complementary terms of the theory.
|
|
pub fn equation_of_the_equinoxes(t: f64) -> f64 {
|
|
let nutation = nutation_2000a_with_2006_adjustment(t);
|
|
let obliquity = mean_obliquity(t);
|
|
nutation.longitude * obliquity.cos() + equation_of_the_equinoxes_complementary_terms(t)
|
|
}
|
|
|
|
/// The polar motion of the Earth, as two angles and the rotation.
|
|
///
|
|
/// The polar motion is the motion of the rotation pole within the Earth. It
|
|
/// is measured daily and published by the International Earth Rotation and
|
|
/// Reference Systems Service, so it cannot be computed from a model and is
|
|
/// supplied by the caller.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
|
pub struct PolarMotion {
|
|
/// The displacement of the pole along the Greenwich meridian, in
|
|
/// radians.
|
|
pub x: f64,
|
|
/// The displacement of the pole along the meridian ninety degrees west,
|
|
/// in radians.
|
|
pub y: f64,
|
|
/// The rotation of the terrestrial frame about the pole, in radians.
|
|
///
|
|
/// This is the terrestrial intermediate origin locator, which is small
|
|
/// and is often taken as zero.
|
|
pub s: f64,
|
|
}
|
|
|
|
impl PolarMotion {
|
|
/// The zero polar motion, which is the approximation used when the
|
|
/// published values are not available.
|
|
///
|
|
/// The error from omitting the polar motion is under a tenth of an
|
|
/// arcsecond, well below the nutation but above the precision of a
|
|
/// modern transformation.
|
|
pub const NONE: Self = Self {
|
|
x: 0.0,
|
|
y: 0.0,
|
|
s: 0.0,
|
|
};
|
|
|
|
/// Creates a polar motion from its three angles.
|
|
pub const fn new(x: f64, y: f64, s: f64) -> Self {
|
|
Self { x, y, s }
|
|
}
|
|
|
|
/// Returns `true` when the angles are usable.
|
|
pub fn is_valid(self) -> bool {
|
|
self.x.is_finite() && self.y.is_finite() && self.s.is_finite()
|
|
}
|
|
|
|
/// Returns the rotation matrix of the polar motion.
|
|
///
|
|
/// The rotation is a turn about the third axis by the locator, then
|
|
/// about the second by minus the `x` displacement, then about the first
|
|
/// by minus the `y` displacement. The signs are the ones the reference
|
|
/// uses, which carry the terrestrial frame into alignment with the
|
|
/// celestial pole.
|
|
///
|
|
/// Source: as implemented by `eraPom00`, which applies the rotations in
|
|
/// sequence by composing each on the left of the accumulator.
|
|
pub fn matrix(self) -> Mat3 {
|
|
rotation_x(-self.y) * rotation_y(-self.x) * rotation_z(self.s)
|
|
}
|
|
}
|
|
|
|
/// Returns the intermediate-to-terrestrial matrix for a time.
|
|
///
|
|
/// The intermediate frame is the true equator and equinox of date, and the
|
|
/// terrestrial frame is the Earth-fixed frame at the same date. This uses
|
|
/// the equinox-based approach, in which the rotation from one to the other
|
|
/// is Greenwich apparent sidereal time.
|
|
///
|
|
/// The time arguments are the UT1 and TT Julian dates.
|
|
///
|
|
/// Source: the composition is the one `eraC2t06a` produces for the equinox
|
|
/// based chain, with the polar motion applied separately by the caller of
|
|
/// the full transformation.
|
|
pub fn intermediate_to_terrestrial_matrix(ut1_julian_date: f64, tt_julian_date: f64) -> Mat3 {
|
|
let apparent_sidereal = greenwich_apparent_sidereal_time(ut1_julian_date, tt_julian_date);
|
|
rotation_z(apparent_sidereal)
|
|
}
|
|
|
|
/// Returns Greenwich apparent sidereal time for a UT1 and a TT Julian date.
|
|
///
|
|
/// This is the mean sidereal time plus the equation of the equinoxes, so it
|
|
/// is the hour angle of the true equinox.
|
|
///
|
|
/// Source: as implemented by `eraGst06a`.
|
|
pub fn greenwich_apparent_sidereal_time(ut1_julian_date: f64, tt_julian_date: f64) -> f64 {
|
|
let t = (tt_julian_date - 2_451_545.0) / 36_525.0;
|
|
let mean = greenwich_mean_sidereal_time(ut1_julian_date, tt_julian_date);
|
|
(mean + equation_of_the_equinoxes(t)).rem_euclid(2.0 * PI)
|
|
}
|
|
|
|
/// Returns Greenwich mean sidereal time for a UT1 and a TT Julian date.
|
|
///
|
|
/// The relation is the Earth rotation angle plus the accumulated precession
|
|
/// in right ascension, which is the quantity that relates the equinox to the
|
|
/// non-rotating origin.
|
|
///
|
|
/// Source: as implemented by `eraGmst06`.
|
|
pub fn greenwich_mean_sidereal_time(ut1_julian_date: f64, tt_julian_date: f64) -> f64 {
|
|
let t = (tt_julian_date - 2_451_545.0) / 36_525.0;
|
|
// The accumulated precession in right ascension, in arcseconds.
|
|
let arcseconds = 0.014_506
|
|
+ t * (4_612.156_534
|
|
+ t * (1.391_581_7
|
|
+ t * (-0.000_000_44 + t * (-0.000_029_956 + t * (-0.000_000_036_8)))));
|
|
(earth_rotation_angle(ut1_julian_date) + arcseconds * ARCSECOND_TO_RADIAN).rem_euclid(2.0 * PI)
|
|
}
|
|
|
|
/// Returns the Earth rotation angle for a UT1 Julian date.
|
|
///
|
|
/// This is the linear expression of the International Astronomical Union,
|
|
/// which is exact to the precision the angle is known.
|
|
///
|
|
/// Source: IERS Conventions (2010), equation 8.32.
|
|
pub fn earth_rotation_angle(ut1_julian_date: f64) -> f64 {
|
|
let days = ut1_julian_date - 2_451_545.0;
|
|
let turns = 0.779_057_273_264_0 + 1.002_737_811_911_354_6 * days;
|
|
(turns.rem_euclid(1.0)) * 2.0 * PI
|
|
}
|
|
|
|
/// Returns the celestial-to-terrestrial matrix for a time.
|
|
///
|
|
/// This composes the whole chain: the frame bias, the precession, the
|
|
/// nutation, the Earth rotation, and the polar motion.
|
|
///
|
|
/// The time arguments are the UT1 and TT Julian dates, and the polar motion
|
|
/// is supplied by the caller because it is measured rather than modelled.
|
|
///
|
|
/// Source: the composition is the one `eraC2t06a` produces.
|
|
pub fn celestial_to_terrestrial_matrix(
|
|
ut1_julian_date: f64,
|
|
tt_julian_date: f64,
|
|
polar_motion: PolarMotion,
|
|
) -> Mat3 {
|
|
let t = (tt_julian_date - 2_451_545.0) / 36_525.0;
|
|
let bias_precession_nutation = bias_precession_nutation_matrix(t);
|
|
let sidereal = greenwich_apparent_sidereal_time(ut1_julian_date, tt_julian_date);
|
|
// The composition is the one `eraC2tcio` produces: the bias, precession,
|
|
// and nutation first, then the Earth rotation, then the polar motion,
|
|
// with each new rotation composed on the left of the accumulator.
|
|
polar_motion.matrix() * rotation_z(sidereal) * bias_precession_nutation
|
|
}
|
|
|
|
/// Returns a rotation of `angle` about the first axis, in radians.
|
|
///
|
|
/// The convention matches the reference helper `eraRx`: the rotation is
|
|
/// applied to the frame, so the matrix is the transpose of the one that
|
|
/// would rotate a vector by the same angle.
|
|
fn rotation_x(angle: f64) -> Mat3 {
|
|
let (sine, cosine) = angle.sin_cos();
|
|
Mat3::from_rows([1.0, 0.0, 0.0], [0.0, cosine, sine], [0.0, -sine, cosine])
|
|
}
|
|
|
|
/// Returns a rotation of `angle` about the second axis, in radians.
|
|
///
|
|
/// The convention is the one of [`rotation_x`].
|
|
fn rotation_y(angle: f64) -> Mat3 {
|
|
let (sine, cosine) = angle.sin_cos();
|
|
Mat3::from_rows([cosine, 0.0, -sine], [0.0, 1.0, 0.0], [sine, 0.0, cosine])
|
|
}
|