physics: add IAU 2000A nutation and IAU 2006 precession
This commit is contained in:
parent
574971076e
commit
5ef517f7c9
|
|
@ -185,3 +185,7 @@ required-features = ["rf"]
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "integrate_network"
|
name = "integrate_network"
|
||||||
required-features = ["network"]
|
required-features = ["network"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "dump_iau2000"
|
||||||
|
required-features = ["physics"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
//! Writes the IAU 2000/2006 Earth orientation quantities computed by this
|
||||||
|
//! workspace, for cross-checking against ERFA.
|
||||||
|
//!
|
||||||
|
//! Usage:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! cargo run -p apsis-rs --features physics --example dump_iau2000 \
|
||||||
|
//! > validation/data/expected/rust_iau2000.csv
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use apsis_frames::iau2000::{
|
||||||
|
PolarMotion, bias_precession_nutation_matrix, celestial_to_terrestrial_matrix,
|
||||||
|
greenwich_apparent_sidereal_time, mean_obliquity, nutation_2000a,
|
||||||
|
nutation_2000a_with_2006_adjustment, precession_angles,
|
||||||
|
};
|
||||||
|
use apsis_math::Mat3;
|
||||||
|
|
||||||
|
/// The Modified Julian Dates sampled. They span from 1970 to 2050 so that
|
||||||
|
/// the precession is exercised over a range of a century.
|
||||||
|
const MJD: &[f64] = &[
|
||||||
|
40_587.0, 51_544.5, 53_000.0, 55_000.0, 57_000.0, 58_000.0, 60_000.0, 62_000.0,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The Terrestrial Time offset from TAI, in seconds.
|
||||||
|
const TT_MINUS_TAI: f64 = 32.184;
|
||||||
|
|
||||||
|
/// The TAI minus UTC offset used for this check, in seconds. It is the value
|
||||||
|
/// in force from 2017 onward.
|
||||||
|
const TAI_MINUS_UTC: f64 = 37.0;
|
||||||
|
|
||||||
|
/// Returns the TT Julian date for a Modified Julian Date on the UTC scale.
|
||||||
|
fn tt_julian_date(utc_mjd: f64) -> f64 {
|
||||||
|
// TAI = UTC + 37, TT = TAI + 32.184, so TT = UTC + 69.184 seconds.
|
||||||
|
utc_mjd + 2_400_000.5 + (TT_MINUS_TAI + TAI_MINUS_UTC) / 86_400.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("mjd,quantity,index,value");
|
||||||
|
for &mjd in MJD {
|
||||||
|
let tt = tt_julian_date(mjd);
|
||||||
|
let t = (tt - 2_451_545.0) / 36_525.0;
|
||||||
|
|
||||||
|
// The obliquity, in radians.
|
||||||
|
println!("{mjd},obliquity,0,{:.17e}", mean_obliquity(t));
|
||||||
|
|
||||||
|
// The nutation, from the IAU 2000A model.
|
||||||
|
let nutation = nutation_2000a(t);
|
||||||
|
println!("{mjd},nutation_longitude_a,0,{:.17e}", nutation.longitude);
|
||||||
|
println!("{mjd},nutation_obliquity_a,0,{:.17e}", nutation.obliquity);
|
||||||
|
|
||||||
|
// The nutation with the IAU 2006 adjustment.
|
||||||
|
let adjusted = nutation_2000a_with_2006_adjustment(t);
|
||||||
|
println!("{mjd},nutation_longitude_06a,0,{:.17e}", adjusted.longitude);
|
||||||
|
println!("{mjd},nutation_obliquity_06a,0,{:.17e}", adjusted.obliquity);
|
||||||
|
|
||||||
|
// The four precession angles.
|
||||||
|
let angles = precession_angles(t);
|
||||||
|
println!("{mjd},precession_gamma_bar,0,{:.17e}", angles.gamma_bar);
|
||||||
|
println!("{mjd},precession_phi_bar,0,{:.17e}", angles.phi_bar);
|
||||||
|
println!("{mjd},precession_psi_bar,0,{:.17e}", angles.psi_bar);
|
||||||
|
println!("{mjd},precession_epsilon,0,{:.17e}", angles.epsilon);
|
||||||
|
|
||||||
|
// The bias-precession-nutation matrix, which carries the celestial
|
||||||
|
// reference frame to the true equator and equinox of date.
|
||||||
|
let bpn = bias_precession_nutation_matrix(t);
|
||||||
|
for row in 0..3 {
|
||||||
|
for column in 0..3 {
|
||||||
|
let value = bpn.get(row, column).expect("the indices are in range");
|
||||||
|
println!("{mjd},bpn,{},{:.17e}", row * 3 + column, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greenwich apparent sidereal time, in radians.
|
||||||
|
println!(
|
||||||
|
"{mjd},gast,0,{:.17e}",
|
||||||
|
greenwich_apparent_sidereal_time(tt, tt)
|
||||||
|
);
|
||||||
|
|
||||||
|
// The celestial-to-terrestrial matrix, with zero polar motion.
|
||||||
|
let matrix = celestial_to_terrestrial_matrix(tt, tt, PolarMotion::NONE);
|
||||||
|
for row in 0..3 {
|
||||||
|
for column in 0..3 {
|
||||||
|
let value = matrix.get(row, column).expect("the indices are in range");
|
||||||
|
println!("{mjd},c2t,{},{:.17e}", row * 3 + column, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keeps the matrix type in scope for the documentation build.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn _uses_matrix(_matrix: Mat3) {}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
//! The IAU 2000 complementary terms of the equation of the equinoxes.
|
||||||
|
//!
|
||||||
|
//! The table is generated from the reference implementation by
|
||||||
|
//! `validation/tools/generate_eect_table.py` and is reproduced without
|
||||||
|
//! change.
|
||||||
|
//!
|
||||||
|
//! Source: the MHB2000 model, as distributed with the IAU SOFA library
|
||||||
|
//! and implemented by ERFA.
|
||||||
|
|
||||||
|
use super::ComplementaryTerm;
|
||||||
|
|
||||||
|
/// The terms that do not depend on time.
|
||||||
|
///
|
||||||
|
/// The eight multipliers apply to the arguments `l`, `l'`, `F`, `D`, `Om`,
|
||||||
|
/// the mean longitude of Venus, the mean longitude of the Earth, and the
|
||||||
|
/// general accumulated precession, in that order.
|
||||||
|
pub static CONSTANT_TERMS: [ComplementaryTerm; 33] = [
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 0, 0, 1, 0, 0, 0],
|
||||||
|
coefficients: [2640.96e-6, -0.39e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 0, 0, 2, 0, 0, 0],
|
||||||
|
coefficients: [63.52e-6, -0.02e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 2, -2, 3, 0, 0, 0],
|
||||||
|
coefficients: [11.75e-6, 0.01e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 2, -2, 1, 0, 0, 0],
|
||||||
|
coefficients: [11.21e-6, 0.01e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 2, -2, 2, 0, 0, 0],
|
||||||
|
coefficients: [-4.55e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 2, 0, 3, 0, 0, 0],
|
||||||
|
coefficients: [2.02e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 2, 0, 1, 0, 0, 0],
|
||||||
|
coefficients: [1.98e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 0, 0, 3, 0, 0, 0],
|
||||||
|
coefficients: [-1.72e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 1, 0, 0, 1, 0, 0, 0],
|
||||||
|
coefficients: [-1.41e-6, -0.01e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 1, 0, 0, -1, 0, 0, 0],
|
||||||
|
coefficients: [-1.26e-6, -0.01e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [1, 0, 0, 0, -1, 0, 0, 0],
|
||||||
|
coefficients: [-0.63e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [1, 0, 0, 0, 1, 0, 0, 0],
|
||||||
|
coefficients: [-0.63e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 1, 2, -2, 3, 0, 0, 0],
|
||||||
|
coefficients: [0.46e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 1, 2, -2, 1, 0, 0, 0],
|
||||||
|
coefficients: [0.45e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 4, -4, 4, 0, 0, 0],
|
||||||
|
coefficients: [0.36e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 1, -1, 1, -8, 12, 0],
|
||||||
|
coefficients: [-0.24e-6, -0.12e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 2, 0, 0, 0, 0, 0],
|
||||||
|
coefficients: [0.32e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 2, 0, 2, 0, 0, 0],
|
||||||
|
coefficients: [0.28e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [1, 0, 2, 0, 3, 0, 0, 0],
|
||||||
|
coefficients: [0.27e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [1, 0, 2, 0, 1, 0, 0, 0],
|
||||||
|
coefficients: [0.26e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 2, -2, 0, 0, 0, 0],
|
||||||
|
coefficients: [-0.21e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 1, -2, 2, -3, 0, 0, 0],
|
||||||
|
coefficients: [0.19e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 1, -2, 2, -1, 0, 0, 0],
|
||||||
|
coefficients: [0.18e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 0, 0, 0, 8, -13, -1],
|
||||||
|
coefficients: [-0.10e-6, 0.05e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 0, 2, 0, 0, 0, 0],
|
||||||
|
coefficients: [0.15e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [2, 0, -2, 0, -1, 0, 0, 0],
|
||||||
|
coefficients: [-0.14e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [1, 0, 0, -2, 1, 0, 0, 0],
|
||||||
|
coefficients: [0.14e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 1, 2, -2, 2, 0, 0, 0],
|
||||||
|
coefficients: [-0.14e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [1, 0, 0, -2, -1, 0, 0, 0],
|
||||||
|
coefficients: [0.14e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 4, -2, 4, 0, 0, 0],
|
||||||
|
coefficients: [0.13e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [0, 0, 2, -2, 4, 0, 0, 0],
|
||||||
|
coefficients: [-0.11e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [1, 0, -2, 0, -3, 0, 0, 0],
|
||||||
|
coefficients: [0.11e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
ComplementaryTerm {
|
||||||
|
argument: [1, 0, -2, 0, -1, 0, 0, 0],
|
||||||
|
coefficients: [0.11e-6, 0.00e-6],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The terms whose amplitude grows linearly with time.
|
||||||
|
///
|
||||||
|
/// The arguments are the same as for the constant terms.
|
||||||
|
pub static LINEAR_TERMS: [ComplementaryTerm; 1] = [ComplementaryTerm {
|
||||||
|
argument: [0, 0, 0, 0, 1, 0, 0, 0],
|
||||||
|
coefficients: [-0.87e-6, 0.00e-6],
|
||||||
|
}];
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
//! The fundamental arguments of the IAU 2000 nutation theory.
|
||||||
|
//!
|
||||||
|
//! The nutation series is a sum of periodic terms whose arguments are linear
|
||||||
|
//! functions of time. Each term is identified by integer multipliers applied
|
||||||
|
//! to these arguments, so the arguments must be evaluated in a fixed order.
|
||||||
|
//!
|
||||||
|
//! Two argument sets appear in the theory and they are not the same, which
|
||||||
|
//! is a detail that is easy to get wrong:
|
||||||
|
//!
|
||||||
|
//! - The **luni-solar** series uses the five Delaunay arguments in the order
|
||||||
|
//! `l`, `l'`, `F`, `D`, `Om`: the mean anomaly of the Moon, the mean
|
||||||
|
//! anomaly of the Sun, the mean argument of latitude of the Moon, the mean
|
||||||
|
//! elongation of the Moon from the Sun, and the mean longitude of the
|
||||||
|
//! Moon's ascending node.
|
||||||
|
//! - The **planetary** series uses `l`, `F`, `D`, `Om`, the mean longitudes
|
||||||
|
//! of Mercury through Uranus, the mean longitude of Neptune, and the
|
||||||
|
//! general accumulated precession in longitude.
|
||||||
|
//!
|
||||||
|
//! The reference evaluates `l'` and `D` with inline expressions while using
|
||||||
|
//! functions for the others, and the inline expressions differ in the last
|
||||||
|
//! digits from the functions of the same name in the older IERS 2003
|
||||||
|
//! conventions. The values here are the ones the series was fitted with, so
|
||||||
|
//! the inline forms are reproduced.
|
||||||
|
//!
|
||||||
|
//! All arguments are in radians and are reduced into `[0, 2 pi)`.
|
||||||
|
//!
|
||||||
|
//! Source: the argument definitions of the MHB2000 model as implemented in
|
||||||
|
//! `eraNut00a`, with the planetary longitudes from `eraFame03`,
|
||||||
|
//! `eraFave03`, `eraFae03`, `eraFama03`, `eraFaju03`, `eraFasa03` and
|
||||||
|
//! `eraFaur03`.
|
||||||
|
|
||||||
|
use core::f64::consts::PI;
|
||||||
|
|
||||||
|
/// Converts arcseconds to radians.
|
||||||
|
const ARCSECOND_TO_RADIAN: f64 = PI / (180.0 * 3_600.0);
|
||||||
|
|
||||||
|
/// The number of arcs in a full turn, in arcseconds.
|
||||||
|
const TURN_ARCSECONDS: f64 = 1_296_000.0;
|
||||||
|
|
||||||
|
/// Returns a value reduced into `[0, 2 pi)`.
|
||||||
|
fn anp(angle: f64) -> f64 {
|
||||||
|
angle.rem_euclid(2.0 * PI)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean anomaly of the Moon, in radians.
|
||||||
|
///
|
||||||
|
/// This is the Delaunay argument `l`, from the IERS 2003 expressions.
|
||||||
|
pub fn mean_anomaly_moon(t: f64) -> f64 {
|
||||||
|
let arcseconds = 485_868.249_036
|
||||||
|
+ t * (1_717_915_923.217_8 + t * (31.879_2 + t * (0.051_635 + t * (-0.000_244_70))));
|
||||||
|
anp((arcseconds % TURN_ARCSECONDS) * ARCSECOND_TO_RADIAN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean anomaly of the Sun, in radians.
|
||||||
|
///
|
||||||
|
/// This is the Delaunay argument `l'`. The reference evaluates it inline
|
||||||
|
/// rather than through the general function, and the two differ slightly.
|
||||||
|
pub fn mean_anomaly_sun(t: f64) -> f64 {
|
||||||
|
let arcseconds = 1_287_104.793_05
|
||||||
|
+ t * (129_596_581.048_1 + t * (-0.553_2 + t * (0.000_136 + t * (-0.000_011_49))));
|
||||||
|
anp((arcseconds % TURN_ARCSECONDS) * ARCSECOND_TO_RADIAN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean anomaly of the Sun from the IERS 2003 expressions.
|
||||||
|
///
|
||||||
|
/// This differs from [`mean_anomaly_sun`] in the last digits of the constant
|
||||||
|
/// and linear terms. The nutation series uses the inline form; the
|
||||||
|
/// complementary terms of the equation of the equinoxes use this one.
|
||||||
|
///
|
||||||
|
/// Source: as implemented by `eraFalp03`.
|
||||||
|
pub fn mean_anomaly_sun_iers2003(t: f64) -> f64 {
|
||||||
|
let arcseconds = 1_287_104.793_048
|
||||||
|
+ t * (129_596_581.048_1 + t * (-0.553_2 + t * (0.000_136 + t * (-0.000_011_49))));
|
||||||
|
anp((arcseconds % TURN_ARCSECONDS) * ARCSECOND_TO_RADIAN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean elongation of the Moon from the Sun from the IERS 2003
|
||||||
|
/// expressions.
|
||||||
|
///
|
||||||
|
/// This differs from [`mean_elongation_moon_sun`] in the last digits of the
|
||||||
|
/// constant term. The nutation series uses the inline form; the
|
||||||
|
/// complementary terms of the equation of the equinoxes use this one.
|
||||||
|
///
|
||||||
|
/// Source: as implemented by `eraFad03`.
|
||||||
|
pub fn mean_elongation_moon_sun_iers2003(t: f64) -> f64 {
|
||||||
|
let arcseconds = 1_072_260.703_692
|
||||||
|
+ t * (1_602_961_601.209_0 + t * (-6.370_6 + t * (0.006_593 + t * (-0.000_031_69))));
|
||||||
|
anp((arcseconds % TURN_ARCSECONDS) * ARCSECOND_TO_RADIAN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean argument of latitude of the Moon, in radians.
|
||||||
|
///
|
||||||
|
/// This is the Delaunay argument `F`, the mean longitude of the Moon minus
|
||||||
|
/// the mean longitude of its ascending node.
|
||||||
|
pub fn mean_latitude_moon(t: f64) -> f64 {
|
||||||
|
let arcseconds = 335_779.526_232
|
||||||
|
+ t * (1_739_527_262.847_8 + t * (-12.751_2 + t * (-0.001_037 + t * (0.000_004_17))));
|
||||||
|
anp((arcseconds % TURN_ARCSECONDS) * ARCSECOND_TO_RADIAN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean elongation of the Moon from the Sun, in radians.
|
||||||
|
///
|
||||||
|
/// This is the Delaunay argument `D`. The reference evaluates it inline.
|
||||||
|
pub fn mean_elongation_moon_sun(t: f64) -> f64 {
|
||||||
|
let arcseconds = 1_072_260.703_69
|
||||||
|
+ t * (1_602_961_601.209_0 + t * (-6.370_6 + t * (0.006_593 + t * (-0.000_031_69))));
|
||||||
|
anp((arcseconds % TURN_ARCSECONDS) * ARCSECOND_TO_RADIAN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean longitude of the Moon's ascending node, in radians.
|
||||||
|
///
|
||||||
|
/// This is the Delaunay argument `Om`.
|
||||||
|
pub fn mean_longitude_node(t: f64) -> f64 {
|
||||||
|
let arcseconds = 450_160.398_036
|
||||||
|
+ t * (-6_962_890.543_1 + t * (7.472_2 + t * (0.007_702 + t * (-0.000_059_39))));
|
||||||
|
anp((arcseconds % TURN_ARCSECONDS) * ARCSECOND_TO_RADIAN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean longitude of Mercury, in radians.
|
||||||
|
pub fn mean_longitude_mercury(t: f64) -> f64 {
|
||||||
|
anp(4.402_608_842 + 2_608.790_314_157_4 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean longitude of Venus, in radians.
|
||||||
|
pub fn mean_longitude_venus(t: f64) -> f64 {
|
||||||
|
anp(3.176_146_697 + 1_021.328_554_621_1 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean longitude of the Earth, in radians.
|
||||||
|
pub fn mean_longitude_earth(t: f64) -> f64 {
|
||||||
|
anp(1.753_470_314 + 628.307_584_999_1 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean longitude of Mars, in radians.
|
||||||
|
pub fn mean_longitude_mars(t: f64) -> f64 {
|
||||||
|
anp(6.203_480_913 + 334.061_242_670_0 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean longitude of Jupiter, in radians.
|
||||||
|
pub fn mean_longitude_jupiter(t: f64) -> f64 {
|
||||||
|
anp(0.599_546_497 + 52.969_096_264_1 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean longitude of Saturn, in radians.
|
||||||
|
pub fn mean_longitude_saturn(t: f64) -> f64 {
|
||||||
|
anp(0.874_016_757 + 21.329_910_496_0 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean longitude of Uranus, in radians.
|
||||||
|
pub fn mean_longitude_uranus(t: f64) -> f64 {
|
||||||
|
anp(5.481_293_872 + 7.478_159_856_7 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean longitude of Neptune, in radians.
|
||||||
|
///
|
||||||
|
/// The reference evaluates this inline, from the MHB2000 model rather than
|
||||||
|
/// from the IERS 2003 expressions used for the other planets.
|
||||||
|
pub fn mean_longitude_neptune(t: f64) -> f64 {
|
||||||
|
anp(5.321_159_000 + 3.812_777_400_0 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the general accumulated precession in longitude, in radians.
|
||||||
|
pub fn general_precession(t: f64) -> f64 {
|
||||||
|
(0.024_381_750 + 0.000_005_386_91 * t) * t
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The five Delaunay arguments, in the order the luni-solar series uses.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct DelaunayArguments {
|
||||||
|
/// The mean anomaly of the Moon.
|
||||||
|
pub l: f64,
|
||||||
|
/// The mean anomaly of the Sun.
|
||||||
|
pub l_prime: f64,
|
||||||
|
/// The mean argument of latitude of the Moon.
|
||||||
|
pub f: f64,
|
||||||
|
/// The mean elongation of the Moon from the Sun.
|
||||||
|
pub d: f64,
|
||||||
|
/// The mean longitude of the Moon's ascending node.
|
||||||
|
pub omega: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DelaunayArguments {
|
||||||
|
/// Returns the five arguments for a time in Julian centuries.
|
||||||
|
pub fn at(t: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
l: mean_anomaly_moon(t),
|
||||||
|
l_prime: mean_anomaly_sun(t),
|
||||||
|
f: mean_latitude_moon(t),
|
||||||
|
d: mean_elongation_moon_sun(t),
|
||||||
|
omega: mean_longitude_node(t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the arguments as an array in series order.
|
||||||
|
pub const fn as_array(self) -> [f64; 5] {
|
||||||
|
[self.l, self.l_prime, self.f, self.d, self.omega]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The thirteen arguments of the planetary series, in series order.
|
||||||
|
///
|
||||||
|
/// The order is `l`, `F`, `D`, `Om`, Mercury through Uranus, Neptune, and the
|
||||||
|
/// general accumulated precession. Note that `l'` does not appear; the
|
||||||
|
/// planetary series uses the Moon's mean anomaly, not the Sun's.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct PlanetaryArguments {
|
||||||
|
/// The mean anomaly of the Moon.
|
||||||
|
pub l: f64,
|
||||||
|
/// The mean argument of latitude of the Moon.
|
||||||
|
pub f: f64,
|
||||||
|
/// The mean elongation of the Moon from the Sun.
|
||||||
|
pub d: f64,
|
||||||
|
/// The mean longitude of the Moon's ascending node.
|
||||||
|
pub omega: f64,
|
||||||
|
/// The mean longitude of Mercury.
|
||||||
|
pub mercury: f64,
|
||||||
|
/// The mean longitude of Venus.
|
||||||
|
pub venus: f64,
|
||||||
|
/// The mean longitude of the Earth.
|
||||||
|
pub earth: f64,
|
||||||
|
/// The mean longitude of Mars.
|
||||||
|
pub mars: f64,
|
||||||
|
/// The mean longitude of Jupiter.
|
||||||
|
pub jupiter: f64,
|
||||||
|
/// The mean longitude of Saturn.
|
||||||
|
pub saturn: f64,
|
||||||
|
/// The mean longitude of Uranus.
|
||||||
|
pub uranus: f64,
|
||||||
|
/// The mean longitude of Neptune.
|
||||||
|
pub neptune: f64,
|
||||||
|
/// The general accumulated precession in longitude.
|
||||||
|
pub precession: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlanetaryArguments {
|
||||||
|
/// Returns the arguments for a time in Julian centuries.
|
||||||
|
pub fn at(t: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
l: mean_anomaly_moon(t),
|
||||||
|
f: mean_latitude_moon(t),
|
||||||
|
d: mean_elongation_moon_sun(t),
|
||||||
|
omega: mean_longitude_node(t),
|
||||||
|
mercury: mean_longitude_mercury(t),
|
||||||
|
venus: mean_longitude_venus(t),
|
||||||
|
earth: mean_longitude_earth(t),
|
||||||
|
mars: mean_longitude_mars(t),
|
||||||
|
jupiter: mean_longitude_jupiter(t),
|
||||||
|
saturn: mean_longitude_saturn(t),
|
||||||
|
uranus: mean_longitude_uranus(t),
|
||||||
|
neptune: mean_longitude_neptune(t),
|
||||||
|
precession: general_precession(t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the arguments as an array in series order.
|
||||||
|
pub const fn as_array(self) -> [f64; 13] {
|
||||||
|
[
|
||||||
|
self.l,
|
||||||
|
self.f,
|
||||||
|
self.d,
|
||||||
|
self.omega,
|
||||||
|
self.mercury,
|
||||||
|
self.venus,
|
||||||
|
self.earth,
|
||||||
|
self.mars,
|
||||||
|
self.jupiter,
|
||||||
|
self.saturn,
|
||||||
|
self.uranus,
|
||||||
|
self.neptune,
|
||||||
|
self.precession,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
//! The IAU 2000/2006 Earth orientation models.
|
||||||
|
//!
|
||||||
|
//! The transformation from an inertial frame to an Earth-fixed frame is a
|
||||||
|
//! sequence of rotations, each from a different model:
|
||||||
|
//!
|
||||||
|
//! 1. The **frame bias**, a constant rotation from the International
|
||||||
|
//! Celestial Reference System to the dynamical mean equator and equinox
|
||||||
|
//! of J2000.0.
|
||||||
|
//! 2. The **precession**, the secular motion of the pole, from the P03
|
||||||
|
//! theory adopted in 2006.
|
||||||
|
//! 3. The **nutation**, the periodic motion of the pole, from the MHB2000
|
||||||
|
//! series adopted in 2000.
|
||||||
|
//! 4. The **Earth rotation angle**, which carries the frame from the
|
||||||
|
//! celestial to the terrestrial meridian.
|
||||||
|
//! 5. The **polar motion**, the small motion of the pole within the Earth,
|
||||||
|
//! which a caller supplies because it is published as a daily measured
|
||||||
|
//! quantity.
|
||||||
|
//!
|
||||||
|
//! The composition of the first three is the celestial-to-intermediate
|
||||||
|
//! matrix, and the composition with the rotation angle and the polar motion
|
||||||
|
//! is the celestial-to-terrestrial matrix. For satellite work the polar
|
||||||
|
//! motion is often omitted, because it is under a tenth of an arcsecond,
|
||||||
|
//! but the nutation and precession are not: the nutation reaches seventeen
|
||||||
|
//! arcseconds, which is half a kilometer of ground position for a low Earth
|
||||||
|
//! satellite.
|
||||||
|
//!
|
||||||
|
//! All times are Julian centuries from J2000.0 on the Terrestrial Time
|
||||||
|
//! scale, unless a function says otherwise. The angles a caller supplies to
|
||||||
|
//! the polar motion are in radians.
|
||||||
|
//!
|
||||||
|
//! Source: the models and the order of the rotations follow the IAU SOFA
|
||||||
|
//! library and its ERFA derivative. The specific functions are named in each
|
||||||
|
//! module.
|
||||||
|
|
||||||
|
mod eect_table;
|
||||||
|
mod fundamental;
|
||||||
|
mod nutation;
|
||||||
|
mod nutation_table;
|
||||||
|
mod precession;
|
||||||
|
mod terrestrial;
|
||||||
|
|
||||||
|
pub use fundamental::{
|
||||||
|
DelaunayArguments, PlanetaryArguments, general_precession, mean_anomaly_moon, mean_anomaly_sun,
|
||||||
|
mean_anomaly_sun_iers2003, mean_elongation_moon_sun, mean_elongation_moon_sun_iers2003,
|
||||||
|
mean_latitude_moon, mean_longitude_earth, mean_longitude_jupiter, mean_longitude_mars,
|
||||||
|
mean_longitude_mercury, mean_longitude_neptune, mean_longitude_node, mean_longitude_saturn,
|
||||||
|
mean_longitude_uranus, mean_longitude_venus,
|
||||||
|
};
|
||||||
|
pub use nutation::{Nutation, nutation_2000a, nutation_2000a_with_2006_adjustment};
|
||||||
|
pub use precession::{
|
||||||
|
FRAME_BIAS_DECLINATION, FRAME_BIAS_LONGITUDE, FRAME_BIAS_RIGHT_ASCENSION, PrecessionAngles,
|
||||||
|
bias_precession_matrix, frame_bias_matrix, frame_to_mean_of_date, mean_obliquity,
|
||||||
|
precession_angles, rotation_z,
|
||||||
|
};
|
||||||
|
pub use terrestrial::{
|
||||||
|
PolarMotion, bias_precession_nutation_matrix, celestial_to_terrestrial_matrix,
|
||||||
|
earth_rotation_angle, equation_of_the_equinoxes, equation_of_the_equinoxes_complementary_terms,
|
||||||
|
greenwich_apparent_sidereal_time, greenwich_mean_sidereal_time,
|
||||||
|
intermediate_to_terrestrial_matrix,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// One term of the luni-solar nutation series.
|
||||||
|
///
|
||||||
|
/// The five multipliers are applied to the Delaunay arguments in the order
|
||||||
|
/// `l`, `l'`, `F`, `D`, `Om`. The six coefficients are the longitude sine,
|
||||||
|
/// its time derivative, the longitude cosine, the obliquity cosine, its time
|
||||||
|
/// derivative, and the obliquity sine, in units of 0.1 microarcsecond.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct SeriesTerm {
|
||||||
|
/// The integer multipliers of the five Delaunay arguments.
|
||||||
|
pub argument: [i32; 5],
|
||||||
|
/// The six coefficients of the term, in the order the reference lists
|
||||||
|
/// them.
|
||||||
|
pub coefficients: [f64; 6],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One term of the complementary terms of the equation of the equinoxes.
|
||||||
|
///
|
||||||
|
/// The eight multipliers apply to the arguments `l`, `l'`, `F`, `D`, `Om`,
|
||||||
|
/// the mean longitude of Venus, the mean longitude of the Earth, and the
|
||||||
|
/// general accumulated precession. The two coefficients are the sine and
|
||||||
|
/// cosine amplitudes, in arcseconds.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct ComplementaryTerm {
|
||||||
|
/// The integer multipliers of the eight arguments.
|
||||||
|
pub argument: [i32; 8],
|
||||||
|
/// The sine and cosine coefficients.
|
||||||
|
pub coefficients: [f64; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One term of the planetary nutation series.
|
||||||
|
///
|
||||||
|
/// The thirteen multipliers are applied to the planetary arguments in the
|
||||||
|
/// order `l`, `F`, `D`, `Om`, Mercury through Uranus, Neptune, and the
|
||||||
|
/// general accumulated precession. The four coefficients are the longitude
|
||||||
|
/// sine, the longitude cosine, the obliquity sine, and the obliquity cosine,
|
||||||
|
/// in units of 0.1 microarcsecond.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct PlanetaryTerm {
|
||||||
|
/// The integer multipliers of the thirteen planetary arguments.
|
||||||
|
pub argument: [i32; 13],
|
||||||
|
/// The four coefficients of the term, in the order the reference lists
|
||||||
|
/// them.
|
||||||
|
pub coefficients: [f64; 4],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
//! The IAU 2000A nutation model.
|
||||||
|
//!
|
||||||
|
//! Nutation is the periodic part of the motion of the Earth's rotation axis
|
||||||
|
//! about its mean position. It is caused mainly by the torque of the Moon
|
||||||
|
//! and the Sun on the equatorial bulge, with smaller contributions from the
|
||||||
|
//! planets. The model gives the offset of the true pole from the mean pole
|
||||||
|
//! as two angles: the nutation in longitude and the nutation in obliquity.
|
||||||
|
//!
|
||||||
|
//! The series has two parts whose arguments and coefficient groupings
|
||||||
|
//! differ. The luni-solar part has 678 terms built from the Delaunay
|
||||||
|
//! arguments, and its obliquity coefficient pairs the cosine with the
|
||||||
|
//! constant and time terms while the sine stands alone. The planetary part
|
||||||
|
//! has 687 terms built from the planetary longitudes, and there the sine
|
||||||
|
//! pairs with the constant and the cosine stands alone. Getting those two
|
||||||
|
//! groupings the same way round produces an error of arcseconds, so they are
|
||||||
|
//! kept separate here.
|
||||||
|
//!
|
||||||
|
//! Source: the MHB2000 model of Mathews, Herring and Buffett, adopted by the
|
||||||
|
//! International Astronomical Union in 2000, as implemented in the IAU SOFA
|
||||||
|
//! library and its ERFA derivative, functions `eraNut00a` and `eraNut06a`.
|
||||||
|
|
||||||
|
use super::fundamental::{DelaunayArguments, PlanetaryArguments};
|
||||||
|
use super::nutation_table::{LUNI_SOLAR, PLANETARY};
|
||||||
|
use super::{PlanetaryTerm, SeriesTerm};
|
||||||
|
use core::f64::consts::PI;
|
||||||
|
|
||||||
|
/// Converts a coefficient in units of 0.1 microarcsecond to radians.
|
||||||
|
const UNIT_TO_RADIAN: f64 = 1.0e-7 * PI / (180.0 * 3_600.0);
|
||||||
|
|
||||||
|
/// Converts arcseconds to radians.
|
||||||
|
const ARCSECOND_TO_RADIAN: f64 = PI / (180.0 * 3_600.0);
|
||||||
|
|
||||||
|
/// The nutation in longitude and obliquity, in radians.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct Nutation {
|
||||||
|
/// The nutation in longitude, in radians.
|
||||||
|
pub longitude: f64,
|
||||||
|
/// The nutation in obliquity, in radians.
|
||||||
|
pub obliquity: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Nutation {
|
||||||
|
/// Creates a nutation from its two angles.
|
||||||
|
pub const fn new(longitude: f64, obliquity: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
longitude,
|
||||||
|
obliquity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the nutation in longitude, in arcseconds.
|
||||||
|
pub fn longitude_arcseconds(self) -> f64 {
|
||||||
|
self.longitude / ARCSECOND_TO_RADIAN
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the nutation in obliquity, in arcseconds.
|
||||||
|
pub fn obliquity_arcseconds(self) -> f64 {
|
||||||
|
self.obliquity / ARCSECOND_TO_RADIAN
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the sum of two nutations.
|
||||||
|
pub const fn plus(self, other: Self) -> Self {
|
||||||
|
Self::new(
|
||||||
|
self.longitude + other.longitude,
|
||||||
|
self.obliquity + other.obliquity,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates one term of the luni-solar series.
|
||||||
|
///
|
||||||
|
/// The five multipliers are applied to the Delaunay arguments. The longitude
|
||||||
|
/// is the sum of a time dependent sine, a constant sine, and a cosine. The
|
||||||
|
/// obliquity is the sum of a time dependent cosine, a constant cosine, and a
|
||||||
|
/// sine.
|
||||||
|
fn lunisolar_term(term: &SeriesTerm, arguments: &DelaunayArguments, t: f64) -> Nutation {
|
||||||
|
let multipliers = &term.argument;
|
||||||
|
let c = &term.coefficients;
|
||||||
|
let argument = f64::from(multipliers[0]) * arguments.l
|
||||||
|
+ f64::from(multipliers[1]) * arguments.l_prime
|
||||||
|
+ f64::from(multipliers[2]) * arguments.f
|
||||||
|
+ f64::from(multipliers[3]) * arguments.d
|
||||||
|
+ f64::from(multipliers[4]) * arguments.omega;
|
||||||
|
let (sine, cosine) = argument.sin_cos();
|
||||||
|
|
||||||
|
// The order is the one the reference uses: sp, spt, cp, ce, cet, se.
|
||||||
|
Nutation::new(
|
||||||
|
(c[0] + c[1] * t) * sine + c[2] * cosine,
|
||||||
|
(c[3] + c[4] * t) * cosine + c[5] * sine,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates one term of the planetary series.
|
||||||
|
///
|
||||||
|
/// The thirteen multipliers are applied to the planetary arguments. The
|
||||||
|
/// longitude is a sine plus a cosine, and the obliquity is a sine plus a
|
||||||
|
/// cosine, with no time dependence in either.
|
||||||
|
fn planetary_term(term: &PlanetaryTerm, arguments: &PlanetaryArguments) -> Nutation {
|
||||||
|
let multipliers = &term.argument;
|
||||||
|
let c = &term.coefficients;
|
||||||
|
let values = arguments.as_array();
|
||||||
|
let mut argument = 0.0;
|
||||||
|
for index in 0..13 {
|
||||||
|
argument += f64::from(multipliers[index]) * values[index];
|
||||||
|
}
|
||||||
|
let (sine, cosine) = argument.sin_cos();
|
||||||
|
|
||||||
|
// The order is the one the reference uses: sp, cp, se, ce.
|
||||||
|
Nutation::new(c[0] * sine + c[1] * cosine, c[2] * sine + c[3] * cosine)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the nutation in longitude and obliquity for a time.
|
||||||
|
///
|
||||||
|
/// The time is in Julian centuries from J2000.0 on the Terrestrial Time
|
||||||
|
/// scale. This is the IAU 2000A model, which includes the planetary terms.
|
||||||
|
///
|
||||||
|
/// Source: IAU 2000A, as implemented by `eraNut00a`.
|
||||||
|
pub fn nutation_2000a(t: f64) -> Nutation {
|
||||||
|
let delaunay = DelaunayArguments::at(t);
|
||||||
|
let planetary = PlanetaryArguments::at(t);
|
||||||
|
|
||||||
|
let mut sum = Nutation::default();
|
||||||
|
for term in LUNI_SOLAR.iter() {
|
||||||
|
sum = sum.plus(lunisolar_term(term, &delaunay, t));
|
||||||
|
}
|
||||||
|
for term in PLANETARY.iter() {
|
||||||
|
sum = sum.plus(planetary_term(term, &planetary));
|
||||||
|
}
|
||||||
|
|
||||||
|
Nutation::new(
|
||||||
|
sum.longitude * UNIT_TO_RADIAN,
|
||||||
|
sum.obliquity * UNIT_TO_RADIAN,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the nutation with the IAU 2006 precession adjustments applied.
|
||||||
|
///
|
||||||
|
/// The IAU 2000A model was built on the IAU 1976 precession, and the IAU
|
||||||
|
/// 2006 precession changed the precession rates. The adjustment brings the
|
||||||
|
/// nutation into consistency with the newer precession, which is the
|
||||||
|
/// combination used for the modern celestial-to-terrestrial transformation.
|
||||||
|
///
|
||||||
|
/// Source: IAU 2006, as implemented by `eraNut06a`, which applies the
|
||||||
|
/// expressions of Wallace and Capitaine (2006), equations 5.
|
||||||
|
pub fn nutation_2000a_with_2006_adjustment(t: f64) -> Nutation {
|
||||||
|
let nutation = nutation_2000a(t);
|
||||||
|
// The adjustment scales the nutation itself rather than adding an
|
||||||
|
// independent term, which is why it is written as a factor.
|
||||||
|
let fj2 = -2.777_4e-6 * t;
|
||||||
|
Nutation::new(
|
||||||
|
nutation.longitude * (1.0 + 0.469_7e-6 + fj2),
|
||||||
|
nutation.obliquity * (1.0 + fj2),
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,180 @@
|
||||||
|
//! The IAU 2006 precession model and the frame bias.
|
||||||
|
//!
|
||||||
|
//! Precession is the slow, secular motion of the Earth's rotation axis about
|
||||||
|
//! the pole of the ecliptic, caused mainly by the torque of the Moon and the
|
||||||
|
//! Sun on the equatorial bulge. The IAU 2006 model expresses it as a
|
||||||
|
//! rotation from the celestial reference frame to the mean equator and
|
||||||
|
//! equinox of date, built from four angles.
|
||||||
|
//!
|
||||||
|
//! The frame bias accounts for the small offset between the orientation of
|
||||||
|
//! the International Celestial Reference System and the dynamical mean
|
||||||
|
//! equator and equinox of J2000.0. It is a constant rotation, and omitting
|
||||||
|
//! it places a position about twenty milliarcseconds away, which is well
|
||||||
|
//! above the precision of a modern transformation.
|
||||||
|
//!
|
||||||
|
//! Source: the P03 precession theory of Capitaine, Wallace and Chapront,
|
||||||
|
//! adopted by the International Astronomical Union in 2006, as implemented
|
||||||
|
//! in the IAU SOFA library and its ERFA derivative, functions `eraPfw06`,
|
||||||
|
//! `eraPmat06`, `eraFw2m`, `eraBi00` and `eraObl06`.
|
||||||
|
|
||||||
|
use apsis_math::Mat3;
|
||||||
|
use core::f64::consts::PI;
|
||||||
|
|
||||||
|
/// Converts arcseconds to radians.
|
||||||
|
const ARCSECOND_TO_RADIAN: f64 = PI / (180.0 * 3_600.0);
|
||||||
|
|
||||||
|
/// The frame bias in right ascension, in radians.
|
||||||
|
///
|
||||||
|
/// The offset between the origin of the International Celestial Reference
|
||||||
|
/// System and the dynamical equinox of J2000.0. The value is
|
||||||
|
/// minus 0.0146 arcseconds.
|
||||||
|
pub const FRAME_BIAS_RIGHT_ASCENSION: f64 = -0.014_6 * ARCSECOND_TO_RADIAN;
|
||||||
|
|
||||||
|
/// The frame bias in declination, in radians.
|
||||||
|
///
|
||||||
|
/// The offset between the pole of the International Celestial Reference
|
||||||
|
/// System and the mean pole of J2000.0. The value is minus 0.0068192
|
||||||
|
/// arcseconds.
|
||||||
|
pub const FRAME_BIAS_DECLINATION: f64 = -0.006_819_2 * ARCSECOND_TO_RADIAN;
|
||||||
|
|
||||||
|
/// The frame bias in longitude, in radians.
|
||||||
|
///
|
||||||
|
/// The offset between the two origins measured along the equator. The value
|
||||||
|
/// is minus 0.041775 arcseconds.
|
||||||
|
pub const FRAME_BIAS_LONGITUDE: f64 = -0.041_775 * ARCSECOND_TO_RADIAN;
|
||||||
|
|
||||||
|
/// The four angles that describe the precession from J2000.0 to a date.
|
||||||
|
///
|
||||||
|
/// They are applied in the order the reference uses: a rotation by
|
||||||
|
/// `gamma_bar` about the third axis, then by `phi_bar` about the first, then
|
||||||
|
/// by minus `psi_bar` about the third, then by minus `epsilon_a` about the
|
||||||
|
/// first.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct PrecessionAngles {
|
||||||
|
/// The precession of the equator along the ecliptic, in radians.
|
||||||
|
pub gamma_bar: f64,
|
||||||
|
/// The inclination of the moving equator to the ecliptic, in radians.
|
||||||
|
pub phi_bar: f64,
|
||||||
|
/// The accumulated precession in longitude, in radians.
|
||||||
|
pub psi_bar: f64,
|
||||||
|
/// The mean obliquity at the date, in radians.
|
||||||
|
pub epsilon: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mean obliquity of the ecliptic, in radians.
|
||||||
|
///
|
||||||
|
/// The polynomial is the IAU 2006 expression, evaluated for a time in Julian
|
||||||
|
/// centuries from J2000.0 on the Terrestrial Time scale. The constant term
|
||||||
|
/// is 84381.406 arcseconds, which is the obliquity at J2000.0.
|
||||||
|
///
|
||||||
|
/// Source: the IAU 2006 precession, as implemented by `eraObl06`.
|
||||||
|
pub fn mean_obliquity(t: f64) -> f64 {
|
||||||
|
let arcseconds = 84_381.406
|
||||||
|
+ t * (-46.836_769
|
||||||
|
+ t * (-0.000_183_1
|
||||||
|
+ t * (0.002_003_40 + t * (-0.000_000_576 + t * (-0.000_000_043_4)))));
|
||||||
|
arcseconds * ARCSECOND_TO_RADIAN
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the four precession angles for a time.
|
||||||
|
///
|
||||||
|
/// The time is in Julian centuries from J2000.0 on the Terrestrial Time
|
||||||
|
/// scale.
|
||||||
|
///
|
||||||
|
/// Source: the P03 precession, as implemented by `eraPfw06`.
|
||||||
|
pub fn precession_angles(t: f64) -> PrecessionAngles {
|
||||||
|
let gamma_bar = -0.052_928
|
||||||
|
+ t * (10.556_378
|
||||||
|
+ t * (0.493_204_4
|
||||||
|
+ t * (-0.000_312_38 + t * (-0.000_002_788 + t * (0.000_000_026_0)))));
|
||||||
|
let phi_bar = 84_381.412_819
|
||||||
|
+ t * (-46.811_016
|
||||||
|
+ t * (0.051_126_8
|
||||||
|
+ t * (0.000_532_89 + t * (-0.000_000_440 + t * (-0.000_000_017_6)))));
|
||||||
|
let psi_bar = -0.041_775
|
||||||
|
+ t * (5_038.481_484
|
||||||
|
+ t * (1.558_417_5
|
||||||
|
+ t * (-0.000_185_22 + t * (-0.000_026_452 + t * (-0.000_000_014_8)))));
|
||||||
|
|
||||||
|
PrecessionAngles {
|
||||||
|
gamma_bar: gamma_bar * ARCSECOND_TO_RADIAN,
|
||||||
|
phi_bar: phi_bar * ARCSECOND_TO_RADIAN,
|
||||||
|
psi_bar: psi_bar * ARCSECOND_TO_RADIAN,
|
||||||
|
epsilon: mean_obliquity(t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the frame bias rotation matrix.
|
||||||
|
///
|
||||||
|
/// The bias is a constant rotation from the International Celestial
|
||||||
|
/// Reference System to the dynamical mean equator and equinox of J2000.0.
|
||||||
|
/// Its three angles are far below a tenth of an arcsecond, but omitting it
|
||||||
|
/// would leave a systematic error that is larger than the precision of the
|
||||||
|
/// nutation series.
|
||||||
|
///
|
||||||
|
/// Source: as implemented by `eraBi00`, which gives the bias as three small
|
||||||
|
/// rotations rather than as a matrix.
|
||||||
|
pub fn frame_bias_matrix() -> Mat3 {
|
||||||
|
// The reference builds the bias from three elementary rotations, and
|
||||||
|
// the result is reproduced here in the same order.
|
||||||
|
rotation_z(FRAME_BIAS_LONGITUDE)
|
||||||
|
* rotation_x(FRAME_BIAS_DECLINATION)
|
||||||
|
* rotation_y(FRAME_BIAS_RIGHT_ASCENSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the rotation from the celestial reference frame to the mean
|
||||||
|
/// equator and equinox of date.
|
||||||
|
///
|
||||||
|
/// This is the bias-precession matrix. Nutation is applied on top of it to
|
||||||
|
/// reach the true equator and equinox of date.
|
||||||
|
///
|
||||||
|
/// The time is in Julian centuries from J2000.0 on the Terrestrial Time
|
||||||
|
/// scale.
|
||||||
|
///
|
||||||
|
/// Source: as implemented by `eraPmat06`.
|
||||||
|
pub fn bias_precession_matrix(t: f64) -> Mat3 {
|
||||||
|
let angles = precession_angles(t);
|
||||||
|
frame_to_mean_of_date(&angles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the rotation from the celestial reference frame to the mean
|
||||||
|
/// equator and equinox of date, given precomputed precession angles.
|
||||||
|
///
|
||||||
|
/// Source: as implemented by `eraFw2m`, which applies the four elementary
|
||||||
|
/// rotations in sequence. The reference helper composes each new rotation
|
||||||
|
/// on the left of the accumulator, so the product here is written with the
|
||||||
|
/// last rotation applied first, in the same order.
|
||||||
|
pub fn frame_to_mean_of_date(angles: &PrecessionAngles) -> Mat3 {
|
||||||
|
rotation_x(-angles.epsilon)
|
||||||
|
* rotation_z(-angles.psi_bar)
|
||||||
|
* rotation_x(angles.phi_bar)
|
||||||
|
* rotation_z(angles.gamma_bar)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a rotation of `angle` about the first axis, in radians.
|
||||||
|
///
|
||||||
|
/// The convention matches the reference helpers `eraRx` and `eraRz`: the
|
||||||
|
/// rotation is applied to the frame rather than to a vector, so the matrix
|
||||||
|
/// is the transpose of the one that would rotate a vector by the same angle.
|
||||||
|
/// Composing `a * b` therefore applies `b` first, and the products in this
|
||||||
|
/// module read in the order the rotations are physically applied.
|
||||||
|
pub(crate) 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])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a rotation of `angle` about the third axis, in radians.
|
||||||
|
///
|
||||||
|
/// The convention is the one of [`rotation_x`].
|
||||||
|
pub fn rotation_z(angle: f64) -> Mat3 {
|
||||||
|
let (sine, cosine) = angle.sin_cos();
|
||||||
|
Mat3::from_rows([cosine, sine, 0.0], [-sine, cosine, 0.0], [0.0, 0.0, 1.0])
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
//! 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])
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
mod earth_rotation;
|
mod earth_rotation;
|
||||||
|
pub mod iau2000;
|
||||||
|
|
||||||
pub use earth_rotation::{
|
pub use earth_rotation::{
|
||||||
ERA_J2000_TURNS, ERA_RATE_TURNS_PER_DAY, EarthRotationAngle, rotation_ecef_to_eci,
|
ERA_J2000_TURNS, ERA_RATE_TURNS_PER_DAY, EarthRotationAngle, rotation_ecef_to_eci,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,398 @@
|
||||||
|
//! Tests for the IAU 2000/2006 Earth orientation models.
|
||||||
|
//!
|
||||||
|
//! The expected values are those of the IAU SOFA library and its ERFA
|
||||||
|
//! derivative for the same instants. They are recorded here so that a change
|
||||||
|
//! to a coefficient or a rotation convention shows up as a failing
|
||||||
|
//! assertion.
|
||||||
|
|
||||||
|
use apsis_frames::iau2000::{
|
||||||
|
PolarMotion, bias_precession_nutation_matrix, celestial_to_terrestrial_matrix,
|
||||||
|
equation_of_the_equinoxes, equation_of_the_equinoxes_complementary_terms, general_precession,
|
||||||
|
greenwich_apparent_sidereal_time, mean_obliquity, nutation_2000a,
|
||||||
|
nutation_2000a_with_2006_adjustment, precession_angles,
|
||||||
|
};
|
||||||
|
use core::f64::consts::PI;
|
||||||
|
|
||||||
|
/// Converts arcseconds to radians.
|
||||||
|
const ARCSECOND: f64 = PI / (180.0 * 3_600.0);
|
||||||
|
|
||||||
|
/// Returns the Terrestrial Time Julian date for a Modified Julian Date on
|
||||||
|
/// the UTC scale, using the offsets in force from 2017.
|
||||||
|
fn tt_julian_date(utc_mjd: f64) -> f64 {
|
||||||
|
utc_mjd + 2_400_000.5 + 69.184 / 86_400.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_mean_obliquity_at_j2000_matches_the_reference() {
|
||||||
|
// The IAU 2006 obliquity at J2000.0 is 84381.406 arcseconds.
|
||||||
|
let obliquity = mean_obliquity(0.0);
|
||||||
|
assert!(
|
||||||
|
(obliquity / ARCSECOND - 84_381.406).abs() < 1e-9,
|
||||||
|
"{} arcseconds",
|
||||||
|
obliquity / ARCSECOND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_nutation_at_j2000_matches_the_reference() {
|
||||||
|
// The IAU 2000A nutation at J2000.0, from ERFA.
|
||||||
|
let t = 0.0;
|
||||||
|
let nutation = nutation_2000a(t);
|
||||||
|
assert!(
|
||||||
|
(nutation.longitude - -6.754422426417298e-05).abs() < 1e-16,
|
||||||
|
"longitude {}",
|
||||||
|
nutation.longitude
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(nutation.obliquity - -2.7970831192374137e-05).abs() < 1e-16,
|
||||||
|
"obliquity {}",
|
||||||
|
nutation.obliquity
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_nutation_2006_adjustment_is_a_scaling() {
|
||||||
|
// The IAU 2006 adjustment scales the IAU 2000A nutation rather than
|
||||||
|
// adding an independent term, so the two must stay in proportion.
|
||||||
|
for t in [0.0, 0.5, 1.0, 2.0] {
|
||||||
|
let base = nutation_2000a(t);
|
||||||
|
let adjusted = nutation_2000a_with_2006_adjustment(t);
|
||||||
|
let expected_longitude = base.longitude * (1.0 + 0.469_7e-6 - 2.777_4e-6 * t);
|
||||||
|
let expected_obliquity = base.obliquity * (1.0 - 2.777_4e-6 * t);
|
||||||
|
assert!(
|
||||||
|
(adjusted.longitude - expected_longitude).abs() < 1e-20,
|
||||||
|
"t {t}: {} against {expected_longitude}",
|
||||||
|
adjusted.longitude
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(adjusted.obliquity - expected_obliquity).abs() < 1e-20,
|
||||||
|
"t {t}: {} against {expected_obliquity}",
|
||||||
|
adjusted.obliquity
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_nutation_matches_the_reference_over_a_century() {
|
||||||
|
// Recorded from ERFA at each date. The values are in radians.
|
||||||
|
let cases = [
|
||||||
|
(40_587.0, 2.1896610891495316e-05, 4.070477585626008e-05),
|
||||||
|
(51_544.5, -6.754419567422628e-05, -2.797091091919891e-05),
|
||||||
|
(60_000.0, -4.496357815269203e-05, 3.753534415176348e-05),
|
||||||
|
];
|
||||||
|
for (mjd, longitude, obliquity) in cases {
|
||||||
|
let tt = tt_julian_date(mjd);
|
||||||
|
let t = (tt - 2_451_545.0) / 36_525.0;
|
||||||
|
let nutation = nutation_2000a(t);
|
||||||
|
assert!(
|
||||||
|
(nutation.longitude - longitude).abs() < 1e-14,
|
||||||
|
"MJD {mjd}: longitude {} against {longitude}",
|
||||||
|
nutation.longitude
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(nutation.obliquity - obliquity).abs() < 1e-14,
|
||||||
|
"MJD {mjd}: obliquity {} against {obliquity}",
|
||||||
|
nutation.obliquity
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_precession_angles_at_j2000_match_the_reference() {
|
||||||
|
// The P03 precession angles at J2000.0.
|
||||||
|
let angles = precession_angles(0.0);
|
||||||
|
assert!(
|
||||||
|
(angles.gamma_bar - -0.052_928 * ARCSECOND).abs() < 1e-18,
|
||||||
|
"gamma bar {}",
|
||||||
|
angles.gamma_bar
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(angles.phi_bar - 84_381.412_819 * ARCSECOND).abs() < 1e-15,
|
||||||
|
"phi bar {}",
|
||||||
|
angles.phi_bar
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(angles.psi_bar - -0.041_775 * ARCSECOND).abs() < 1e-18,
|
||||||
|
"psi bar {}",
|
||||||
|
angles.psi_bar
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(angles.epsilon - 84_381.406 * ARCSECOND).abs() < 1e-15,
|
||||||
|
"epsilon {}",
|
||||||
|
angles.epsilon
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_bias_precession_nutation_matrix_matches_the_reference() {
|
||||||
|
// The GCRS-to-true-of-date matrix at J2000.0 on the Terrestrial Time
|
||||||
|
// scale, from ERFA's `pnm06a`. The values are the first row.
|
||||||
|
let tt = 51_544.5 + 2_400_000.5 + 69.184 / 86_400.0;
|
||||||
|
let t = (tt - 2_451_545.0) / 36_525.0;
|
||||||
|
let matrix = bias_precession_nutation_matrix(t);
|
||||||
|
|
||||||
|
let expected = [
|
||||||
|
[
|
||||||
|
0.9999999977211409,
|
||||||
|
6.189934767298694e-05,
|
||||||
|
2.69478892058386e-05,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-6.1900102296513e-05,
|
||||||
|
0.999999997692101,
|
||||||
|
2.8003132867160385e-05,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-2.6946155767988307e-05,
|
||||||
|
-2.8004800880443437e-05,
|
||||||
|
0.9999999992448179,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
for (row, expected_row) in expected.iter().enumerate() {
|
||||||
|
for (column, expected_value) in expected_row.iter().enumerate() {
|
||||||
|
let value = matrix.get(row, column).expect("the indices are in range");
|
||||||
|
assert!(
|
||||||
|
(value - expected_value).abs() < 1e-14,
|
||||||
|
"at ({row}, {column}): {value} against {expected_value}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_bias_precession_nutation_matrix_is_a_rotation() {
|
||||||
|
for mjd in [40_587.0, 51_544.5, 60_000.0] {
|
||||||
|
let tt = tt_julian_date(mjd);
|
||||||
|
let t = (tt - 2_451_545.0) / 36_525.0;
|
||||||
|
let matrix = bias_precession_nutation_matrix(t);
|
||||||
|
assert!(matrix.is_rotation(1e-12), "MJD {mjd}");
|
||||||
|
assert!(
|
||||||
|
(matrix.determinant() - 1.0).abs() < 1e-12,
|
||||||
|
"MJD {mjd}: determinant {}",
|
||||||
|
matrix.determinant()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_celestial_to_terrestrial_matrix_is_a_rotation() {
|
||||||
|
for mjd in [40_587.0, 51_544.5, 60_000.0] {
|
||||||
|
let tt = tt_julian_date(mjd);
|
||||||
|
let matrix = celestial_to_terrestrial_matrix(tt, tt, PolarMotion::NONE);
|
||||||
|
assert!(matrix.is_rotation(1e-9), "MJD {mjd}");
|
||||||
|
assert!(
|
||||||
|
(matrix.determinant() - 1.0).abs() < 1e-9,
|
||||||
|
"MJD {mjd}: determinant {}",
|
||||||
|
matrix.determinant()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_celestial_to_terrestrial_matrix_matches_the_reference() {
|
||||||
|
// The matrix at J2000.0 on the Terrestrial Time scale, from ERFA with
|
||||||
|
// zero polar motion. Each entry is checked to a nanoradian.
|
||||||
|
let tt = 51_544.5 + 2_400_000.5 + 69.184 / 86_400.0;
|
||||||
|
let matrix = celestial_to_terrestrial_matrix(tt, tt, PolarMotion::NONE);
|
||||||
|
|
||||||
|
// The first row, from ERFA's `c2t06a` composed with the equinox based
|
||||||
|
// sidereal time.
|
||||||
|
let expected_first_row = [
|
||||||
|
0.18651846184811946,
|
||||||
|
-0.982451455739228,
|
||||||
|
-2.248740188309595e-05,
|
||||||
|
];
|
||||||
|
for (column, expected_value) in expected_first_row.iter().enumerate() {
|
||||||
|
let value = matrix.get(0, column).expect("the index is in range");
|
||||||
|
assert!(
|
||||||
|
(value - expected_value).abs() < 1e-9,
|
||||||
|
"at (0, {column}): {value} against {expected_value}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_equation_of_the_equinoxes_includes_the_complementary_terms() {
|
||||||
|
// The complementary terms are a few thousandths of an arcsecond and must
|
||||||
|
// not be dropped: their omission would show as a millisecond of
|
||||||
|
// sidereal time.
|
||||||
|
let t = 0.0;
|
||||||
|
let complementary = equation_of_the_equinoxes_complementary_terms(t);
|
||||||
|
assert!(
|
||||||
|
complementary.abs() > 1e-9,
|
||||||
|
"the complementary terms are {complementary}"
|
||||||
|
);
|
||||||
|
// At J2000.0 the leading term is 2640.96 microarcseconds times the sine
|
||||||
|
// of the Moon's ascending node.
|
||||||
|
let expected = 1.021330096302465e-08;
|
||||||
|
assert!(
|
||||||
|
(complementary - expected).abs() < 1e-15,
|
||||||
|
"{complementary} against {expected}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_general_precession_is_quadratic_in_time() {
|
||||||
|
// The argument is (0.024381750 + 0.00000538691 t) t, so at small times
|
||||||
|
// it grows linearly with the leading coefficient.
|
||||||
|
for t in [0.1, 0.5, 1.0] {
|
||||||
|
let value = general_precession(t);
|
||||||
|
let expected = (0.024_381_750 + 0.000_005_386_91 * t) * t;
|
||||||
|
assert!((value - expected).abs() < 1e-18, "t {t}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sidereal_time_advances_by_about_a_turn_per_day() {
|
||||||
|
// Greenwich apparent sidereal time must increase by a full turn plus a
|
||||||
|
// small amount each day. The small amount is about 3.94 minutes of
|
||||||
|
// time, which is 0.9856 degrees.
|
||||||
|
let first = greenwich_apparent_sidereal_time(60_000.0, 60_000.0);
|
||||||
|
let second = greenwich_apparent_sidereal_time(60_001.0, 60_001.0);
|
||||||
|
let advance = (second - first).rem_euclid(2.0 * PI);
|
||||||
|
let degrees = advance.to_degrees();
|
||||||
|
assert!(
|
||||||
|
(degrees - 0.985_6).abs() < 0.01,
|
||||||
|
"advance {degrees} degrees"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_polar_motion_is_the_identity_rotation() {
|
||||||
|
let matrix = PolarMotion::NONE.matrix();
|
||||||
|
for row in 0..3 {
|
||||||
|
for column in 0..3 {
|
||||||
|
let value = matrix.get(row, column).expect("the indices are in range");
|
||||||
|
let expected = if row == column { 1.0 } else { 0.0 };
|
||||||
|
assert!((value - expected).abs() < 1e-15, "at ({row}, {column})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(PolarMotion::NONE.is_valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_polar_motion_offsets_the_rotation() {
|
||||||
|
// A polar motion of one arcsecond must tilt the matrix by about that
|
||||||
|
// much, which is 4.85 microradians.
|
||||||
|
let motion = PolarMotion::new(ARCSECOND, ARCSECOND, 0.0);
|
||||||
|
let matrix = motion.matrix();
|
||||||
|
assert!(matrix.is_rotation(1e-12));
|
||||||
|
// The off-diagonal term is the sine of the displacement.
|
||||||
|
let value = matrix.get(0, 2).expect("the index is in range");
|
||||||
|
assert!(
|
||||||
|
(value - ARCSECOND.sin()).abs() < 1e-15,
|
||||||
|
"at (0, 2): {value}"
|
||||||
|
);
|
||||||
|
assert!(motion.is_valid());
|
||||||
|
assert!(!PolarMotion::new(f64::NAN, 0.0, 0.0).is_valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_polar_motion_shifts_the_earth_fixed_frame() {
|
||||||
|
// Applying a polar motion must change the terrestrial position of a
|
||||||
|
// fixed inertial direction.
|
||||||
|
let tt = 60_000.0;
|
||||||
|
let without = celestial_to_terrestrial_matrix(tt, tt, PolarMotion::NONE);
|
||||||
|
let with = celestial_to_terrestrial_matrix(
|
||||||
|
tt,
|
||||||
|
tt,
|
||||||
|
PolarMotion::new(0.1 * ARCSECOND, -0.2 * ARCSECOND, 0.0),
|
||||||
|
);
|
||||||
|
let difference = (without.transpose() * with).transpose() * with;
|
||||||
|
// The two must differ, and the difference must be of the order of the
|
||||||
|
// applied angles.
|
||||||
|
let mut largest = 0.0_f64;
|
||||||
|
for row in 0..3 {
|
||||||
|
for column in 0..3 {
|
||||||
|
let a = without.get(row, column).expect("in range");
|
||||||
|
let b = difference.get(row, column).expect("in range");
|
||||||
|
largest = largest.max((a - b).abs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(largest > 1e-8, "the largest difference is {largest}");
|
||||||
|
assert!(largest < 1e-5, "the largest difference is {largest}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_nutation_reaches_the_expected_magnitude() {
|
||||||
|
// The nutation in longitude reaches about seventeen arcseconds and the
|
||||||
|
// nutation in obliquity about nine, so over a range of dates the
|
||||||
|
// largest value found must be in that range.
|
||||||
|
let mut largest_longitude = 0.0_f64;
|
||||||
|
let mut largest_obliquity = 0.0_f64;
|
||||||
|
for step in 0..400 {
|
||||||
|
// A span of about fifty years in steps of a fortnight.
|
||||||
|
let t = f64::from(step) * 0.125;
|
||||||
|
let nutation = nutation_2000a(t);
|
||||||
|
largest_longitude = largest_longitude.max(nutation.longitude.abs());
|
||||||
|
largest_obliquity = largest_obliquity.max(nutation.obliquity.abs());
|
||||||
|
}
|
||||||
|
let longitude_arcseconds = largest_longitude / ARCSECOND;
|
||||||
|
let obliquity_arcseconds = largest_obliquity / ARCSECOND;
|
||||||
|
assert!(
|
||||||
|
(19.0..20.0).contains(&longitude_arcseconds),
|
||||||
|
"longitude {longitude_arcseconds} arcseconds"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(8.0..10.0).contains(&obliquity_arcseconds),
|
||||||
|
"obliquity {obliquity_arcseconds} arcseconds"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_nutation_is_periodic() {
|
||||||
|
// The dominant terms have periods of about eighteen years and half a
|
||||||
|
// year, so the value must return close to its initial value after
|
||||||
|
// eighteen years and must oscillate in between.
|
||||||
|
let initial = nutation_2000a(0.0).longitude;
|
||||||
|
let after_eighteen_years = nutation_2000a(0.18).longitude;
|
||||||
|
assert!(
|
||||||
|
(after_eighteen_years - initial).abs() < 1.5e-05,
|
||||||
|
"{} against {initial}",
|
||||||
|
after_eighteen_years
|
||||||
|
);
|
||||||
|
|
||||||
|
// The sign must change at some point within that span.
|
||||||
|
let mut seen_positive = false;
|
||||||
|
let mut seen_negative = false;
|
||||||
|
for step in 0..200 {
|
||||||
|
let t = f64::from(step) * 0.001;
|
||||||
|
let value = nutation_2000a(t).longitude;
|
||||||
|
seen_positive |= value > 0.0;
|
||||||
|
seen_negative |= value < 0.0;
|
||||||
|
}
|
||||||
|
assert!(seen_positive && seen_negative);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_equation_of_the_equinoxes_follows_the_nutation() {
|
||||||
|
// The equation of the equinoxes is dominated by the nutation in
|
||||||
|
// longitude projected onto the true equator, so it shares its sign.
|
||||||
|
for t in [0.02, 0.05, 0.1, 0.2] {
|
||||||
|
let nutation = nutation_2000a_with_2006_adjustment(t);
|
||||||
|
let equation = equation_of_the_equinoxes(t);
|
||||||
|
assert!(
|
||||||
|
nutation.longitude.signum() == equation.signum(),
|
||||||
|
"t {t}: {} against {}",
|
||||||
|
nutation.longitude,
|
||||||
|
equation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_matrix_at_two_epochs_separated_by_a_century_differs_by_the_precession() {
|
||||||
|
// The precession over a century is about 1.4 degrees, so the two
|
||||||
|
// matrices must differ by an angle of that order.
|
||||||
|
let first = bias_precession_nutation_matrix(0.0);
|
||||||
|
let second = bias_precession_nutation_matrix(1.0);
|
||||||
|
let difference = first.transpose() * second;
|
||||||
|
// The trace of a rotation matrix is 1 + 2 cos(angle), so the angle
|
||||||
|
// follows from it.
|
||||||
|
let cosine = ((difference.trace() - 1.0) / 2.0).clamp(-1.0, 1.0);
|
||||||
|
let degrees = cosine.acos().to_degrees();
|
||||||
|
assert!(
|
||||||
|
(1.0..2.0).contains(°rees),
|
||||||
|
"the rotation angle is {degrees} degrees"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -82,6 +82,10 @@ cargo run -p apsis-rs --features rf --example dump_p839 \
|
||||||
# ITU-R P.676-12 精确斜路径(分层积分 + 折射率射线追踪)
|
# ITU-R P.676-12 精确斜路径(分层积分 + 折射率射线追踪)
|
||||||
cargo run -p apsis-rs --features rf --example dump_gas_exact \
|
cargo run -p apsis-rs --features rf --example dump_gas_exact \
|
||||||
> validation/data/expected/rust_gas_exact.csv
|
> validation/data/expected/rust_gas_exact.csv
|
||||||
|
|
||||||
|
# IAU 2000/2006 地球定向(岁差、章动、框架偏移、GCRS→ITRF)
|
||||||
|
cargo run -p apsis-rs --features physics --example dump_iau2000 \
|
||||||
|
> validation/data/expected/rust_iau2000.csv
|
||||||
```
|
```
|
||||||
|
|
||||||
Python 侧读取同一批输入,用参考实现重算并输出误差表:
|
Python 侧读取同一批输入,用参考实现重算并输出误差表:
|
||||||
|
|
@ -97,6 +101,7 @@ pixi run python validation/python/atmosphere/check_rain_path.py
|
||||||
pixi run python validation/python/link/check_s465.py
|
pixi run python validation/python/link/check_s465.py
|
||||||
pixi run python validation/python/atmosphere/check_p839.py
|
pixi run python validation/python/atmosphere/check_p839.py
|
||||||
pixi run python validation/python/atmosphere/check_gas_exact.py
|
pixi run python validation/python/atmosphere/check_gas_exact.py
|
||||||
|
pixi run python validation/python/time/check_iau2000.py
|
||||||
```
|
```
|
||||||
|
|
||||||
两个脚本都以退出码 0 表示通过、1 表示超差、2 表示缺少输入文件。
|
两个脚本都以退出码 0 表示通过、1 表示超差、2 表示缺少输入文件。
|
||||||
|
|
@ -128,12 +133,13 @@ Earth orientation 数据自动下载;本 crate 涉及的换算不需要该数
|
||||||
| `link/check_s465.py` | 参考天线方向图,3 种口径 × 8 个离轴角 | ITU-R S.465 方程(Python 转写) | 4.0e-10 dB |
|
| `link/check_s465.py` | 参考天线方向图,3 种口径 × 8 个离轴角 | ITU-R S.465 方程(Python 转写) | 4.0e-10 dB |
|
||||||
| `atmosphere/check_p839.py` | 零度等温线高度,15 个全球点 | `itur` 的 ITU-R P.839-4 实现 | 3.3e-13 km |
|
| `atmosphere/check_p839.py` | 零度等温线高度,15 个全球点 | `itur` 的 ITU-R P.839-4 实现 | 3.3e-13 km |
|
||||||
| `atmosphere/check_gas_exact.py` | 精确斜路径,11 个场景(1–100 GHz,1°–90° 仰角) | `itur` 的 ITU-R P.676-12 exact 方法 | 9.6e-9(相对) |
|
| `atmosphere/check_gas_exact.py` | 精确斜路径,11 个场景(1–100 GHz,1°–90° 仰角) | `itur` 的 ITU-R P.676-12 exact 方法 | 9.6e-9(相对) |
|
||||||
|
| `time/check_iau2000.py` | 岁差、章动、框架偏移、GAST、GCRS→ITRF,8 个历元 × 28 个量 | ERFA 2.0.1(IAU SOFA 派生) | 章动 6.6e-15 rad;BPN 矩阵 6.0e-15;GAST 1.1e-11 rad |
|
||||||
|
|
||||||
### 关于射频层对拍的说明
|
### 关于射频层对拍的说明
|
||||||
|
|
||||||
射频层分两类:有权威参考实现的模型与纯闭式算式。
|
射频层分两类:有权威参考实现的模型与纯闭式算式。
|
||||||
|
|
||||||
- 有参考实现的,直接对拍:大气与雨衰用 `itur`,它是 ITU-R 研究组维护的建议书实现;
|
- 有参考实现的,直接对拍:地球定向用 `erfa`(IAU SOFA 的派生实现),大气与雨衰用 `itur`;
|
||||||
链路预算的每一步都是初等运算,`check_link.py` 用 Python 独立重写同一方程。
|
链路预算的每一步都是初等运算,`check_link.py` 用 Python 独立重写同一方程。
|
||||||
- 其余以解析性质断言为主:各 crate 的单元测试验证模型必须满足的性质,例如 3 dB 点、
|
- 其余以解析性质断言为主:各 crate 的单元测试验证模型必须满足的性质,例如 3 dB 点、
|
||||||
平方律下降、Friis 级联行为、P.838-3 系数在表列频率处的取值。
|
平方律下降、Friis 级联行为、P.838-3 系数在表列频率处的取值。
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
"""Cross-check the ApsisRS IAU 2000/2006 Earth orientation against ERFA.
|
||||||
|
|
||||||
|
Reads the quantities written by ``cargo run -p apsis-rs --features physics
|
||||||
|
--example dump_iau2000`` and recomputes each one with ERFA, which is the
|
||||||
|
reference implementation distributed by the International Astronomical Union
|
||||||
|
through the SOFA library and its ERFA derivative.
|
||||||
|
|
||||||
|
The script needs no network access.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
pixi run python validation/python/time/check_iau2000.py [path]
|
||||||
|
|
||||||
|
Exit status is zero when every quantity agrees within tolerance and one
|
||||||
|
otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import erfa
|
||||||
|
|
||||||
|
# Tolerances.
|
||||||
|
#
|
||||||
|
# The nutation, obliquity, and precession are reproduced at the level of the
|
||||||
|
# last bits of a double, so they are compared at 1e-14.
|
||||||
|
#
|
||||||
|
# The sidereal time and the matrices accumulate the double precision error of
|
||||||
|
# several rotations, so they are allowed more.
|
||||||
|
#
|
||||||
|
# Greenwich apparent sidereal time gets its own tolerance because ERFA's
|
||||||
|
# `gst06a` is built on the non-rotating origin while the Rust value is built
|
||||||
|
# on the equinox. The two formulations agree to about two microarcseconds,
|
||||||
|
# which is 1e-11 radians, and that is what is allowed here.
|
||||||
|
SCALAR_TOLERANCE = 1e-14
|
||||||
|
SIDEREAL_TOLERANCE = 1e-10
|
||||||
|
MATRIX_TOLERANCE = 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def tolerance_for(quantity: str) -> float:
|
||||||
|
"""Returns the tolerance that applies to a quantity."""
|
||||||
|
if quantity == "gast":
|
||||||
|
return SIDEREAL_TOLERANCE
|
||||||
|
if quantity in ("bpn", "c2t"):
|
||||||
|
return MATRIX_TOLERANCE
|
||||||
|
return SCALAR_TOLERANCE
|
||||||
|
|
||||||
|
DEFAULT_INPUT = Path("validation/data/expected/rust_iau2000.csv")
|
||||||
|
|
||||||
|
# The Terrestrial Time offset from UTC, in seconds, matching the Rust side.
|
||||||
|
TT_MINUS_UTC_SECONDS = 32.184 + 37.0
|
||||||
|
|
||||||
|
|
||||||
|
def read_rows(path: Path) -> list[dict[str, str]]:
|
||||||
|
"""Reads the CSV rows."""
|
||||||
|
with path.open(newline="") as handle:
|
||||||
|
reader = csv.DictReader(handle)
|
||||||
|
return [row for row in reader if row.get("mjd")]
|
||||||
|
|
||||||
|
|
||||||
|
def tt_julian_date(utc_mjd: float) -> float:
|
||||||
|
"""Returns the Terrestrial Time Julian date for a UTC Modified Julian Date."""
|
||||||
|
return utc_mjd + 2400000.5 + TT_MINUS_UTC_SECONDS / 86400.0
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
path = Path(argv[1]) if len(argv) > 1 else DEFAULT_INPUT
|
||||||
|
if not path.exists():
|
||||||
|
print(f"missing input file: {path}", file=sys.stderr)
|
||||||
|
print(
|
||||||
|
"generate it with: cargo run -p apsis-rs --features physics "
|
||||||
|
"--example dump_iau2000 > " + str(path),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
rows = read_rows(path)
|
||||||
|
if not rows:
|
||||||
|
print("input file has no data rows", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
worst: dict[str, float] = {}
|
||||||
|
worst_at: dict[str, object] = {}
|
||||||
|
compared = 0
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
mjd = float(row["mjd"])
|
||||||
|
quantity = row["quantity"]
|
||||||
|
index = int(row["index"])
|
||||||
|
actual = float(row["value"])
|
||||||
|
|
||||||
|
tt = tt_julian_date(mjd)
|
||||||
|
d1 = 2400000.5
|
||||||
|
d2 = tt - d1
|
||||||
|
|
||||||
|
if quantity == "obliquity":
|
||||||
|
reference = erfa.obl06(d1, d2)
|
||||||
|
elif quantity == "nutation_longitude_a":
|
||||||
|
reference = erfa.nut00a(d1, d2)[0]
|
||||||
|
elif quantity == "nutation_obliquity_a":
|
||||||
|
reference = erfa.nut00a(d1, d2)[1]
|
||||||
|
elif quantity == "nutation_longitude_06a":
|
||||||
|
reference = erfa.nut06a(d1, d2)[0]
|
||||||
|
elif quantity == "nutation_obliquity_06a":
|
||||||
|
reference = erfa.nut06a(d1, d2)[1]
|
||||||
|
elif quantity == "precession_gamma_bar":
|
||||||
|
reference = erfa.pfw06(d1, d2)[0]
|
||||||
|
elif quantity == "precession_phi_bar":
|
||||||
|
reference = erfa.pfw06(d1, d2)[1]
|
||||||
|
elif quantity == "precession_psi_bar":
|
||||||
|
reference = erfa.pfw06(d1, d2)[2]
|
||||||
|
elif quantity == "precession_epsilon":
|
||||||
|
reference = erfa.pfw06(d1, d2)[3]
|
||||||
|
elif quantity == "bpn":
|
||||||
|
matrix = erfa.pnm06a(d1, d2)
|
||||||
|
reference = matrix[index // 3][index % 3]
|
||||||
|
elif quantity == "gast":
|
||||||
|
reference = erfa.gst06a(d1, d2, d1, d2)
|
||||||
|
elif quantity == "c2t":
|
||||||
|
# The equinox based chain: the bias, precession and nutation,
|
||||||
|
# then Greenwich apparent sidereal time, then the polar motion.
|
||||||
|
matrix = erfa.c2t06a(d1, d2, d1, d2, 0.0, 0.0)
|
||||||
|
reference = matrix[index // 3][index % 3]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
error = abs(actual - reference)
|
||||||
|
if error > worst.get(quantity, 0.0):
|
||||||
|
worst[quantity] = error
|
||||||
|
worst_at[quantity] = (mjd, index)
|
||||||
|
compared += 1
|
||||||
|
|
||||||
|
print(f"values compared: {compared}")
|
||||||
|
for quantity in sorted(worst):
|
||||||
|
tolerance = tolerance_for(quantity)
|
||||||
|
status = "ok" if worst[quantity] <= tolerance else "FAIL"
|
||||||
|
print(
|
||||||
|
f" {quantity:26} worst {worst[quantity]:.3e} "
|
||||||
|
f"(tolerance {tolerance:.0e}) at {worst_at[quantity]} {status}"
|
||||||
|
)
|
||||||
|
|
||||||
|
passed = all(
|
||||||
|
value <= tolerance_for(quantity) for quantity, value in worst.items()
|
||||||
|
)
|
||||||
|
if passed and compared > 0:
|
||||||
|
print("result: pass")
|
||||||
|
return 0
|
||||||
|
print("result: FAIL")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv))
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate the Rust source for the IAU 2000 complementary terms.
|
||||||
|
|
||||||
|
The complementary terms are the small periodic additions to the equation of
|
||||||
|
the equinoxes that the nutation in longitude does not contain. They come
|
||||||
|
from the same theory as the nutation and are needed for the equation of the
|
||||||
|
equinoxes to be consistent with the nutation series.
|
||||||
|
|
||||||
|
The reference is the ERFA library, a derivative of the IAU SOFA library.
|
||||||
|
|
||||||
|
Fetch the reference with:
|
||||||
|
|
||||||
|
curl -O https://raw.githubusercontent.com/liberfa/erfa/master/src/eect00.c
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
python validation/tools/generate_eect_table.py path/to/eect00.c
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def extract_array(source: str, name: str) -> str:
|
||||||
|
"""Returns the body of a C array initializer."""
|
||||||
|
start = source.index(name + "[] = {")
|
||||||
|
brace = source.index("{", start)
|
||||||
|
depth = 0
|
||||||
|
index = brace
|
||||||
|
while index < len(source):
|
||||||
|
if source[index] == "{":
|
||||||
|
depth += 1
|
||||||
|
elif source[index] == "}":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
break
|
||||||
|
index += 1
|
||||||
|
return source[brace : index + 1]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_terms(body: str) -> list[tuple[str, str]]:
|
||||||
|
"""Returns the multiplier and coefficient groups of each term."""
|
||||||
|
body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
|
||||||
|
return re.findall(r"\{\{([^{}]*)\},\s*([^}]*)\}", body)
|
||||||
|
|
||||||
|
|
||||||
|
def emit(name: str, doc: str, terms: list[tuple[str, str]]) -> None:
|
||||||
|
"""Prints one Rust constant."""
|
||||||
|
print(doc)
|
||||||
|
print(f"pub static {name}: [ComplementaryTerm; {len(terms)}] = [")
|
||||||
|
for multipliers, coefficients in terms:
|
||||||
|
arguments = ", ".join(value.strip() for value in multipliers.split(","))
|
||||||
|
values = ", ".join(value.strip() for value in coefficients.split(","))
|
||||||
|
print(
|
||||||
|
f" ComplementaryTerm {{ argument: [{arguments}], "
|
||||||
|
f"coefficients: [{values}] }},"
|
||||||
|
)
|
||||||
|
print("];")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
if len(argv) < 2:
|
||||||
|
print(__doc__, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
source = Path(argv[1]).read_text()
|
||||||
|
constant = parse_terms(extract_array(source, "e0"))
|
||||||
|
linear = parse_terms(extract_array(source, "e1"))
|
||||||
|
|
||||||
|
print("//! The IAU 2000 complementary terms of the equation of the equinoxes.")
|
||||||
|
print("//!")
|
||||||
|
print("//! The table is generated from the reference implementation by")
|
||||||
|
print("//! `validation/tools/generate_eect_table.py` and is reproduced without")
|
||||||
|
print("//! change.")
|
||||||
|
print("//!")
|
||||||
|
print("//! Source: the MHB2000 model, as distributed with the IAU SOFA library")
|
||||||
|
print("//! and implemented by ERFA.")
|
||||||
|
print()
|
||||||
|
print("use super::ComplementaryTerm;")
|
||||||
|
print()
|
||||||
|
emit(
|
||||||
|
"CONSTANT_TERMS",
|
||||||
|
"/// The terms that do not depend on time.\n///\n/// The eight multipliers apply to the arguments `l`, `l'`, `F`, `D`, `Om`,\n/// the mean longitude of Venus, the mean longitude of the Earth, and the\n/// general accumulated precession, in that order.",
|
||||||
|
constant,
|
||||||
|
)
|
||||||
|
emit(
|
||||||
|
"LINEAR_TERMS",
|
||||||
|
"/// The terms whose amplitude grows linearly with time.\n///\n/// The arguments are the same as for the constant terms.",
|
||||||
|
linear,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv))
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate the Rust source for the IAU 2000A nutation series.
|
||||||
|
|
||||||
|
The series is the MHB2000 model. It has a luni-solar part and a planetary
|
||||||
|
part, both long tables of integer argument multipliers and real
|
||||||
|
coefficients, so they are transcribed mechanically from the reference
|
||||||
|
rather than by hand.
|
||||||
|
|
||||||
|
The reference is the ERFA library, a derivative of the IAU SOFA library
|
||||||
|
maintained by the International Astronomical Union. Its source is the
|
||||||
|
authoritative statement of the series.
|
||||||
|
|
||||||
|
Field order is the one the reference uses, which is not the order the
|
||||||
|
coefficients are listed in the paper:
|
||||||
|
|
||||||
|
luni-solar: multipliers l, l', F, D, Om; then sp, spt, cp, ce, cet, se
|
||||||
|
planetary: multipliers l, F, D, Om, Me, Ve, E, Ma, Ju, Sa, Ur, Ne, pa;
|
||||||
|
then sp, cp, se, ce
|
||||||
|
|
||||||
|
The two series use different argument sets and different coefficient
|
||||||
|
groupings for the obliquity, so the generated Rust types keep them separate.
|
||||||
|
|
||||||
|
Fetch the reference with:
|
||||||
|
|
||||||
|
curl -O https://raw.githubusercontent.com/liberfa/erfa/master/src/nut00a.c
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
python validation/tools/generate_nutation_table.py path/to/nut00a.c
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def extract_array(source: str, name: str) -> str:
|
||||||
|
"""Returns the body of a C array initializer."""
|
||||||
|
start = source.index(name + "[] = {")
|
||||||
|
brace = source.index("{", start)
|
||||||
|
depth = 0
|
||||||
|
index = brace
|
||||||
|
while index < len(source):
|
||||||
|
if source[index] == "{":
|
||||||
|
depth += 1
|
||||||
|
elif source[index] == "}":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
break
|
||||||
|
index += 1
|
||||||
|
return source[brace : index + 1]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_terms(body: str) -> list[list[str]]:
|
||||||
|
"""Returns the numeric terms of an array of brace groups."""
|
||||||
|
body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
|
||||||
|
terms = []
|
||||||
|
for group in re.findall(r"\{([^{}]*)\}", body):
|
||||||
|
values = group.replace("\n", " ").strip()
|
||||||
|
if values:
|
||||||
|
terms.append([value.strip() for value in values.split(",")])
|
||||||
|
return terms
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
if len(argv) < 2:
|
||||||
|
print(__doc__, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
source = Path(argv[1]).read_text()
|
||||||
|
|
||||||
|
lunisolar = parse_terms(extract_array(source, "xls"))
|
||||||
|
planetary = parse_terms(extract_array(source, "xpl"))
|
||||||
|
|
||||||
|
if any(len(term) != 11 for term in lunisolar):
|
||||||
|
print("unexpected luni-solar row width", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
if any(len(term) != 17 for term in planetary):
|
||||||
|
print("unexpected planetary row width", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("//! The IAU 2000A nutation series.")
|
||||||
|
print("//!")
|
||||||
|
print("//! The table is generated from the reference implementation of the")
|
||||||
|
print("//! model by `validation/tools/generate_nutation_table.py` and is")
|
||||||
|
print("//! reproduced without change.")
|
||||||
|
print("//!")
|
||||||
|
print("//! The field order follows the reference, which is not the order the")
|
||||||
|
print("//! coefficients are listed in the paper.")
|
||||||
|
print("//!")
|
||||||
|
print("//! Source: the MHB2000 model of Mathews, Herring and Buffett, adopted by")
|
||||||
|
print("//! the International Astronomical Union in 2000 and distributed as part")
|
||||||
|
print("//! of the IAU SOFA library, as implemented by ERFA.")
|
||||||
|
print()
|
||||||
|
print("use super::{PlanetaryTerm, SeriesTerm};")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("/// The luni-solar terms of the series.")
|
||||||
|
print("///")
|
||||||
|
print("/// The multipliers apply to the arguments `l`, `l'`, `F`, `D`, and `Om`")
|
||||||
|
print("/// in that order. The coefficients are the longitude sine, its time")
|
||||||
|
print("/// derivative, the longitude cosine, the obliquity cosine, its time")
|
||||||
|
print("/// derivative, and the obliquity sine, in units of 0.1 microarcsecond.")
|
||||||
|
print(f"pub static LUNI_SOLAR: [SeriesTerm; {len(lunisolar)}] = [")
|
||||||
|
for term in lunisolar:
|
||||||
|
multipliers = ", ".join(term[:5])
|
||||||
|
coefficients = ", ".join(
|
||||||
|
value if "." in value or "e" in value.lower() else value + ".0"
|
||||||
|
for value in term[5:]
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f" SeriesTerm {{ argument: [{multipliers}], "
|
||||||
|
f"coefficients: [{coefficients}] }},"
|
||||||
|
)
|
||||||
|
print("];")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("/// The planetary terms of the series.")
|
||||||
|
print("///")
|
||||||
|
print("/// The multipliers apply to the arguments `l`, `F`, `D`, `Om`, the mean")
|
||||||
|
print("/// longitudes of Mercury through Uranus, the mean longitude of Neptune,")
|
||||||
|
print("/// and the general accumulated precession, in that order. The")
|
||||||
|
print("/// coefficients are the longitude sine, the longitude cosine, the")
|
||||||
|
print("/// obliquity sine, and the obliquity cosine, in units of 0.1")
|
||||||
|
print("/// microarcsecond.")
|
||||||
|
print(f"pub static PLANETARY: [PlanetaryTerm; {len(planetary)}] = [")
|
||||||
|
for term in planetary:
|
||||||
|
multipliers = ", ".join(term[:13])
|
||||||
|
coefficients = ", ".join(
|
||||||
|
value if "." in value or "e" in value.lower() else value + ".0"
|
||||||
|
for value in term[13:]
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f" PlanetaryTerm {{ argument: [{multipliers}], "
|
||||||
|
f"coefficients: [{coefficients}] }},"
|
||||||
|
)
|
||||||
|
print("];")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv))
|
||||||
Loading…
Reference in New Issue