siderust/astro/
precession.rs

1//! # Equatorial Precession utilities
2//!
3//! This module implements **precession of the Earth’s mean equator and equinox**
4//! following the *short series* formulation in chapter 20 of Jean Meeus’
5//! *Astronomical Algorithms* (2nd ed.).  The algorithm is sufficient for most
6//! practical astronomical work: its errors remain below **0.1 arc‑second** for
7//! epochs within ±5 Julian centuries of **J2000.0**. That is, the years
8//! **1500 → 2500**.
9//!
10//! ## What is precession?
11//! The rotation axis of the Earth is not fixed in inertial space: the torques
12//! produced by the gravitational attraction of the Sun and the Moon on the
13//! Earth’s equatorial bulge make the axis describe a **slow conical motion**, a
14//! gyroscope under external torque.  The effect is known as **lunisolar
15//! precession** and has a period of about **25 770 years** (*the Platonic year*).
16//! A smaller contribution, **planetary precession**, is produced by the tidal
17//! forces of the other planets.
18//!
19//! In equatorial coordinates (right ascension *α*, declination *δ*) this causes
20//! the celestial poles and the equinox to *drift* at roughly **50″ · yr⁻¹**. If
21//! the apparent or mean position of a star is referred to two different epochs
22//! it must therefore be **precessed** to the desired date before it can be
23//! compared with catalogued data or with another observation.
24//!
25//! ## Why do we need a numerical model?
26//! Astronomical catalogues adopt a fixed **reference epoch** (e.g. B1950.0 or
27//! J2000.0). Precise pointing, orbit determination or reduction of
28//! observational data to standard coordinates all require a transformation
29//! between the catalogue epoch and the observation epoch.  Performing that
30//! transformation with an accuracy of a few milliarc‑seconds – small enough for
31//! sub‑arc‑second telescopes and for most amateur‑level applications, is the
32//! purpose of the present code.
33//!
34//! ## How is precession computed here?
35//! Meeus expresses precession as three **Euler‑like rotation angles**
36//! (ζ *zeta*, *z* and θ *theta*) around the axes *z → x → z*.  For a time span of
37//! *t* Julian centuries (*T* denotes the starting epoch measured from J2000.0)
38//! the angles are expanded as low‑order polynomials in *t* and *T* (**eqs. 20.2
39//! and 20.3**).  They are first given in arc‑seconds:
40//!
41//! ```text
42//! ζ  = ( 2306.2181 + 1.39656 T − 0.000139 T² ) t
43//!      + ( 0.30188 − 0.000344 T ) t² + 0.017998 t³
44//! z  = ( 2306.2181 + 1.39656 T − 0.000139 T² ) t
45//!      + ( 1.09468 + 0.000066 T ) t² + 0.018203 t³
46//! θ  = ( 2004.3109 − 0.85330 T − 0.000217 T² ) t
47//!      − ( 0.42665 + 0.000217 T ) t² − 0.041833 t³
48//! ```
49//!
50//! Those angles are converted to radians and the **right‑hand rotation
51//! (ζ, θ, −z)** is applied to the input equatorial coordinates using
52//! Meeus eq. 20.4.
53//!
54//! ## Precession vs Nutation
55//! | Aspect            | **Precession**                                   | **Nutation**                                   |
56//! |-------------------|--------------------------------------------------|------------------------------------------------|
57//! | Physical cause    | Long‑term torque on the equatorial bulge by Sun and Moon, plus planetary tidal forces | Short‑term periodic torque variations mainly from the Moon’s changing distance, inclination and the Sun’s apparent motion |
58//! | Character         | **Secular** (monotonic drift, ≈ 50″ · yr⁻¹)       | **Periodic** (dominant 18.6 yr term of ±9″, plus many smaller terms) |
59//! | Time scale        | ~25 770 yr cycle                                 | Hours → decades (hundreds of terms)            |
60//! | Typical magnitude | 0.014° per year                                  | up to 9 arc‑seconds peak‑to‑peak               |
61//! | Modelling         | Low‑order polynomials (this module)              | Large trigonometric series (IAU 1980/2000B, ELP 2000) |
62//! | When to apply?    | **Always** when comparing coordinates referred to different epochs | When sub‑arc‑second precision or true‑of‑date coordinates are required |
63//!
64//! In other words, **precession is the steady drift** of the reference frame;
65//! **nutation is the superposed wobble**. The two effects must be added to
66//! obtain the *true* or *apparent* equator and equinox of date.  This module
67//! provides the precession step; nutation would be applied subsequently by a
68//! dedicated routine.
69//!
70//! ## Public API
71//! * [`precess_from_j2000`] – common convenience for the transformation
72//!   *J2000.0 → date*.
73//! * [`precess_equatorial`] – general *epoch → epoch* interface.
74//!
75//! ## Accuracy & limitations
76//! The short series is adequate for most handbook‑level work (<0.1″ error up
77//! to ±5 centuries).  For **modern astrometry below the milli‑arc‑second
78//! level** the full IAU 2006 precession‑nutation model should be preferred.
79//!
80//! Equatorial precession utilities (Meeus, *Astronomical Algorithms*,
81//! 2nd ed., ch. 20).  Accuracy of the **short model** is better than
82//! 0.1″ for |T| ≤ 5 centuries.  All angles are handled via the crate’s
83//! `Degrees` / `Radians` wrappers; right ascension is wrapped to
84//! **0 ≤ α < 2π**.
85//!
86//! Public API
87//! ----------
88//! - [`precess_from_j2000`] – convenience layer for the common case
89//!   “mean J2000.0 → given date”.
90//! - [`precess_equatorial`] – precess between *any* two epochs.
91
92use std::f64::consts::TAU;
93
94use crate::astro::JulianDate;
95use crate::coordinates::spherical::position;
96use crate::units::*;
97
98/* -------------------------------------------------------------------------
99 * Constants & small utilities
100 * ---------------------------------------------------------------------- */
101
102/// 85 ° in radians: threshold above which the `asin` formula for δ loses
103/// precision.  Switching to the alternative formula earlier does no harm
104/// and guarantees full numerical stability even closer to the pole.
105const NEAR_POLE_LIMIT: f64 = 85.0_f64.to_radians();
106
107/// Return (*t*, *t²*, *t³*).  Saves three multiplications when the caller
108/// needs all powers.
109#[inline]
110fn t_powers(t: f64) -> (f64, f64, f64) {
111    let t2 = t * t;
112    (t, t2, t2 * t)
113}
114
115/* -------------------------------------------------------------------------
116 * Precession coefficients (Meeus 20‑2/20‑3)
117 * ---------------------------------------------------------------------- */
118
119/// Compute the *short‑series* precession angles `ζ`, `z` and `θ` *(radians)*
120/// for a span of `span` Julian centuries, starting at epoch
121/// `epoch` (both measured from J2000.0).
122///
123/// Follows Meeus Eq. 20.2/20.3.  The trick of dividing **T** and *t* by
124/// 3600 converts the published coefficients (arcseconds) into *degrees*,
125/// avoiding one extra division.
126#[inline]
127#[allow(non_snake_case)]
128fn short_series_coefficients(epoch: Centuries, span: Centuries) -> (Radians, Radians, Radians) {
129    // Meeus uses full Julian centuries, *not* divided by 3600.
130    let T = epoch.value();
131    let (t, t2, t3) = t_powers(span.value());
132
133    // 20.2 / 20.3 – all results in **arc-seconds**.
134    let zeta_as = (2306.2181 + 1.39656 * T - 0.000139 * T * T) * t
135        + (0.30188 - 0.000344 * T) * t2
136        + 0.017_998 * t3;
137
138    let z_as = (2306.2181 + 1.39656 * T - 0.000139 * T * T) * t
139        + (1.09468 + 0.000066 * T) * t2
140        + 0.018_203 * t3;
141
142    let theta_as = (2004.3109 - 0.85330 * T - 0.000217 * T * T) * t
143        - (0.42665 + 0.000217 * T) * t2
144        - 0.041_833 * t3;
145
146    // arc-seconds → degrees → radians
147    let as_to_rad = |a: f64| Degrees::new(a / 3600.0).to::<Radian>();
148    (as_to_rad(zeta_as), as_to_rad(z_as), as_to_rad(theta_as))
149}
150
151/* -------------------------------------------------------------------------
152 * Core rotation (Meeus 20‑4)
153 * ---------------------------------------------------------------------- */
154
155/// Rotate a single equatorial coordinate by the precession angles.
156/// Returns *(α, δ)* **in radians**.
157#[inline]
158fn rotate_equatorial(
159    ra: Radians,
160    dec: Radians,
161    zeta: Radians,
162    z: Radians,
163    theta: Radians,
164) -> (Radians, Radians) {
165    // Pre‑compute repeated terms with `sin_cos` for a small perf gain.
166    let (sin_ra_zeta, cos_ra_zeta) = (ra + zeta).sin_cos();
167    let (sin_dec, cos_dec) = dec.sin_cos();
168    let (sin_th, cos_th) = theta.sin_cos();
169
170    // Meeus Eq. 20.4
171    let a = cos_dec * sin_ra_zeta;
172    let b = cos_th * cos_dec * cos_ra_zeta - sin_th * sin_dec;
173    let c = sin_th * cos_dec * cos_ra_zeta + cos_th * sin_dec;
174
175    // New right ascension, wrapped to 0–2π.
176    let mut new_ra = a.atan2(b) + z.value();
177    new_ra = new_ra.rem_euclid(TAU);
178
179    // New declination: pole‑safe formula when |δ| > 85 °.
180    let new_dec = if dec.value().abs() > NEAR_POLE_LIMIT {
181        let mut d = (a * a + b * b).sqrt().acos();
182        if dec.value().is_sign_negative() {
183            d = -d;
184        }
185        d
186    } else {
187        c.asin()
188    };
189
190    (Radians::new(new_ra), Radians::new(new_dec))
191}
192
193/* -------------------------------------------------------------------------
194 * Public API
195 * ---------------------------------------------------------------------- */
196
197/// **J2000.0 → to_jd** shortcut.
198///
199/// Transforms mean right ascension & declination supplied at the standard
200/// reference epoch (JD 2 451 545.0) to the mean equator & equinox of `to_jd`.
201#[inline]
202pub fn precess_from_j2000<U: LengthUnit>(
203    mean_position: position::Equatorial<U>,
204    to_jd: JulianDate,
205) -> position::Equatorial<U> {
206    precess_equatorial(mean_position, JulianDate::J2000, to_jd)
207}
208
209/// **Epoch → epoch** precession.
210///
211/// * `position` – (α, δ, r) referred to the mean equator/equinox of `from_jd`.
212/// * `from_jd`, `to_jd` – Julian Days of source and target epochs.
213///
214/// Returns the coordinates referred to `to_jd`.
215pub fn precess_equatorial<U: LengthUnit>(
216    position: position::Equatorial<U>,
217    from_jd: JulianDate,
218    to_jd: JulianDate,
219) -> position::Equatorial<U> {
220    let ra0 = position.ra().to::<Radian>();
221    let dec0 = position.dec().to::<Radian>();
222
223    let from_centuries = from_jd.julian_centuries();
224    let centuries_span = to_jd.julian_centuries() - from_centuries;
225
226    let (zeta, z, theta) = short_series_coefficients(from_centuries, centuries_span);
227
228    let (ra, dec) = rotate_equatorial(ra0, dec0, zeta, z, theta);
229
230    position::Equatorial::<U>::new(ra, dec, position.distance)
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    #[test]
237    fn sirius_2025() {
238        // Sirius: α0 ≈ 101.287°, δ0 ≈ −16.716° (J2000.0, Hipparcos)
239        use crate::bodies::catalog::SIRIUS;
240
241        // Target epoch: 2025‑05‑12 (JD 2 469 807.5)
242        let prec = precess_from_j2000(
243            SIRIUS.target.get_position().clone(),
244            JulianDate::new(2_460_807.5),
245        );
246
247        // Expected (Meeus short model): α ≈ 101.84557°, δ ≈ −16.77182°
248        assert!(
249            (prec.ra().value() - 101.57047).abs() < 3e-5,
250            "current α ≈ {}",
251            prec.ra()
252        );
253        assert!(
254            (prec.dec().value() + 16.74409).abs() < 1e-5,
255            "current δ ≈ {}",
256            prec.dec()
257        );
258    }
259}