Skip to main content

stem_branch/
moon.rs

1//! ELP/MPP02 Moon ephemeris ("ELP revisited", Chapront & Francou 2003; parameter
2//! set corr=1, fitted to DE405/DE406). Faithful port of the TypeScript `elpmpp02`
3//! computation and the `getMoonPosition` wrapper. The coefficient tables live in
4//! the generated `elpmpp02_data.rs`.
5
6use core::f64::consts::PI;
7
8use crate::elpmpp02_data::{
9    MAIN_DIST, MAIN_LAT, MAIN_LONG, PERT_DIST_T0, PERT_DIST_T1, PERT_DIST_T2, PERT_DIST_T3,
10    PERT_LAT_T0, PERT_LAT_T1, PERT_LAT_T2, PERT_LONG_T0, PERT_LONG_T1, PERT_LONG_T2, PERT_LONG_T3,
11};
12use crate::{
13    delaunay_args, normalize_radians, nutation_deps, nutation_dpsi, ARCSEC_TO_RAD, RAD_TO_DEG,
14};
15
16const TWO_PI: f64 = 2.0 * PI;
17const DEG: f64 = PI / 180.0;
18/// Arcseconds to radians (used for argument construction).
19const SEC: f64 = PI / 648000.0;
20
21// ── Correction parameters (corr=1) ────────────────────────────────────────
22const DW1_0: f64 = -0.07008;
23const DW2_0: f64 = 0.20794;
24const DW3_0: f64 = -0.07215;
25const DEART_0: f64 = -0.00033;
26const DPERI: f64 = -0.00749;
27const DW1_1: f64 = -0.35106;
28const DGAM: f64 = 0.00085;
29const DE: f64 = -0.00006;
30const DEART_1: f64 = 0.00732;
31const DEP: f64 = 0.00224;
32const DW2_1: f64 = 0.08017;
33const DW3_1: f64 = -0.04317;
34const DW1_2: f64 = -0.03743;
35const DW1_3: f64 = -0.00018865;
36const DW1_4: f64 = -0.00001024;
37const DW2_2: f64 = 0.00470602;
38const DW2_3: f64 = -0.00025213;
39const DW3_2: f64 = -0.00261070;
40const DW3_3: f64 = -0.00010712;
41
42// ── Derived constants ──────────────────────────────────────────────────────
43const AM: f64 = 0.074801329;
44const ALPHA: f64 = 0.002571881;
45const DTSM: f64 = 2.0 * ALPHA / (3.0 * AM);
46const XA: f64 = 2.0 * ALPHA / 3.0;
47const W11_RATE: f64 = 1732559343.73604;
48const W11: f64 = W11_RATE * SEC;
49const BP: [[f64; 2]; 5] = [
50    [0.311079095, -0.103837907],
51    [-0.004482398, 0.000668287],
52    [-0.001102485, -0.001298072],
53    [0.001056062, -0.000178028],
54    [0.000050928, -0.000037342],
55];
56const W21_RATE: f64 = (14643420.3171 + DW2_1) * SEC;
57const W31_RATE: f64 = (-6967919.5383 + DW3_1) * SEC;
58const X2: f64 = W21_RATE / W11;
59const X3: f64 = W31_RATE / W11;
60const Y2: f64 = AM * BP[0][0] + XA * BP[4][0];
61const Y3: f64 = AM * BP[0][1] + XA * BP[4][1];
62const D21: f64 = X2 - Y2;
63const D22: f64 = W11 * BP[1][0];
64const D23: f64 = W11 * BP[2][0];
65const D24: f64 = W11 * BP[3][0];
66const D25: f64 = Y2 / AM;
67const D31: f64 = X3 - Y3;
68const D32: f64 = W11 * BP[1][1];
69const D33: f64 = W11 * BP[2][1];
70const D34: f64 = W11 * BP[3][1];
71const D35: f64 = Y3 / AM;
72const CW2_1: f64 = D21 * DW1_1 + D25 * DEART_1 + D22 * DGAM + D23 * DE + D24 * DEP;
73const CW3_1: f64 = D31 * DW1_1 + D35 * DEART_1 + D32 * DGAM + D33 * DE + D34 * DEP;
74const DELNU_NU: f64 = (0.55604 + DW1_1) * SEC / W11;
75const DELE: f64 = (0.01789 + DE) * SEC;
76const DELG: f64 = (-0.08066 + DGAM) * SEC;
77const DELNP_NU: f64 = (-0.06424 + DEART_1) * SEC / W11;
78const DELEP: f64 = (-0.12879 + DEP) * SEC;
79const FB1: f64 = -AM * DELNU_NU + DELNP_NU;
80const FB2: f64 = DELG;
81const FB3: f64 = DELE;
82const FB4: f64 = DELEP;
83const FB5: f64 = -XA * DELNU_NU + DTSM * DELNP_NU;
84const FA_DIST: f64 = 1.0 - 2.0 / 3.0 * DELNU_NU;
85const RA0: f64 = 384747.961370173 / 384747.980674318;
86
87fn mod2pi(x: f64) -> f64 {
88    x - TWO_PI * ((x + PI) / TWO_PI).floor()
89}
90
91struct ElpArguments {
92    w1: f64,
93    d: f64,
94    f: f64,
95    l: f64,
96    lp: f64,
97    zeta: f64,
98    me: f64,
99    ve: f64,
100    em: f64,
101    ma: f64,
102    ju: f64,
103    sa: f64,
104    ur: f64,
105    ne: f64,
106}
107
108fn compute_elp_arguments(t: f64) -> ElpArguments {
109    let t2 = t * t;
110    let t3 = t2 * t;
111    let t4 = t3 * t;
112
113    let w10 = (-142.0 + 18.0 / 60.0 + (59.95571 + DW1_0) / 3600.0) * DEG;
114    let w1_1 = mod2pi((W11_RATE + DW1_1) * t * SEC);
115    let w12 = mod2pi((-6.8084 + DW1_2) * t2 * SEC);
116    let w13 = mod2pi((0.006604 + DW1_3) * t3 * SEC);
117    let w14 = mod2pi((-3.169e-5 + DW1_4) * t4 * SEC);
118
119    let w20 = (83.0 + 21.0 / 60.0 + (11.67475 + DW2_0) / 3600.0) * DEG;
120    let w21 = mod2pi((14643420.3171 + DW2_1 + CW2_1) * t * SEC);
121    let w22 = mod2pi((-38.2631 + DW2_2) * t2 * SEC);
122    let w23 = mod2pi((-0.045047 + DW2_3) * t3 * SEC);
123    let w24 = mod2pi(0.00021301 * t4 * SEC);
124
125    let w30 = (125.0 + 2.0 / 60.0 + (40.39816 + DW3_0) / 3600.0) * DEG;
126    let w31 = mod2pi((-6967919.5383 + DW3_1 + CW3_1) * t * SEC);
127    let w32 = mod2pi((6.359 + DW3_2) * t2 * SEC);
128    let w33 = mod2pi((0.007625 + DW3_3) * t3 * SEC);
129    let w34 = mod2pi(-3.586e-5 * t4 * SEC);
130
131    let ea0 = (100.0 + 27.0 / 60.0 + (59.13885 + DEART_0) / 3600.0) * DEG;
132    let ea1 = mod2pi((129597742.293 + DEART_1) * t * SEC);
133    let ea2 = mod2pi(-0.0202 * t2 * SEC);
134    let ea3 = mod2pi(9e-6 * t3 * SEC);
135    let ea4 = mod2pi(1.5e-7 * t4 * SEC);
136
137    let p0 = (102.0 + 56.0 / 60.0 + (14.45766 + DPERI) / 3600.0) * DEG;
138    let p1 = mod2pi(1161.24342 * t * SEC);
139    let p2 = mod2pi(0.529265 * t2 * SEC);
140    let p3 = mod2pi(-1.1814e-4 * t3 * SEC);
141    let p4 = mod2pi(1.1379e-5 * t4 * SEC);
142
143    let w1 = w10 + w1_1 + w12 + w13 + w14;
144    let w2 = w20 + w21 + w22 + w23 + w24;
145    let w3 = w30 + w31 + w32 + w33 + w34;
146    let ea = ea0 + ea1 + ea2 + ea3 + ea4;
147    let pomp = p0 + p1 + p2 + p3 + p4;
148
149    let me = (-108.0 + 15.0 / 60.0 + 3.216919 / 3600.0) * DEG + mod2pi(538101628.66888 * t * SEC);
150    let ve = (-179.0 + 58.0 / 60.0 + 44.758419 / 3600.0) * DEG + mod2pi(210664136.45777 * t * SEC);
151    let em = (100.0 + 27.0 / 60.0 + 59.13885 / 3600.0) * DEG + mod2pi(129597742.293 * t * SEC);
152    let ma = (-5.0 + 26.0 / 60.0 + 3.642778 / 3600.0) * DEG + mod2pi(68905077.65936 * t * SEC);
153    let ju = (34.0 + 21.0 / 60.0 + 5.379392 / 3600.0) * DEG + mod2pi(10925660.57335 * t * SEC);
154    let sa = (50.0 + 4.0 / 60.0 + 38.902495 / 3600.0) * DEG + mod2pi(4399609.33632 * t * SEC);
155    let ur = (-46.0 + 3.0 / 60.0 + 4.354234 / 3600.0) * DEG + mod2pi(1542482.57845 * t * SEC);
156    let ne = (-56.0 + 20.0 / 60.0 + 56.808371 / 3600.0) * DEG + mod2pi(786547.897 * t * SEC);
157
158    ElpArguments {
159        w1: mod2pi(w1),
160        d: mod2pi(w1 - ea + PI),
161        f: mod2pi(w1 - w3),
162        l: mod2pi(w1 - w2),
163        lp: mod2pi(ea - pomp),
164        zeta: mod2pi(w1 + 0.02438029560881907 * t),
165        me: mod2pi(me),
166        ve: mod2pi(ve),
167        em: mod2pi(em),
168        ma: mod2pi(ma),
169        ju: mod2pi(ju),
170        sa: mod2pi(sa),
171        ur: mod2pi(ur),
172        ne: mod2pi(ne),
173    }
174}
175
176/// Main-problem series (4 Delaunay multipliers + amplitude-correction columns).
177fn main_sum(terms: &[[f64; 10]], args: &ElpArguments, is_sine: bool, fa: f64) -> f64 {
178    let mut sum = 0.0;
179    for t in terms {
180        let phase = t[0] * args.d + t[1] * args.f + t[2] * args.l + t[3] * args.lp;
181        let a_eff = fa * t[4] + FB1 * t[5] + FB2 * t[6] + FB3 * t[7] + FB4 * t[8] + FB5 * t[9];
182        sum += a_eff * if is_sine { phase.sin() } else { phase.cos() };
183    }
184    sum
185}
186
187/// Perturbation series (13 argument multipliers + amplitude + phase offset).
188fn pert_sum(terms: &[[f64; 15]], args: &ElpArguments) -> f64 {
189    let mut sum = 0.0;
190    for t in terms {
191        let phase = t[14]
192            + t[0] * args.d
193            + t[1] * args.f
194            + t[2] * args.l
195            + t[3] * args.lp
196            + t[4] * args.me
197            + t[5] * args.ve
198            + t[6] * args.em
199            + t[7] * args.ma
200            + t[8] * args.ju
201            + t[9] * args.sa
202            + t[10] * args.ur
203            + t[11] * args.ne
204            + t[12] * args.zeta;
205        sum += t[13] * phase.sin();
206    }
207    sum
208}
209
210/// Geocentric ecliptic position (longitude rad, latitude rad, distance km) in
211/// the J2000.0 mean-ecliptic frame.
212fn compute_moon_position(t: f64) -> (f64, f64, f64) {
213    let args = compute_elp_arguments(t);
214
215    let main_long = main_sum(MAIN_LONG, &args, true, 1.0);
216    let main_lat = main_sum(MAIN_LAT, &args, true, 1.0);
217    let main_dist = main_sum(MAIN_DIST, &args, false, FA_DIST);
218
219    let pl0 = pert_sum(PERT_LONG_T0, &args);
220    let pl1 = pert_sum(PERT_LONG_T1, &args);
221    let pl2 = pert_sum(PERT_LONG_T2, &args);
222    let pl3 = pert_sum(PERT_LONG_T3, &args);
223    let pb0 = pert_sum(PERT_LAT_T0, &args);
224    let pb1 = pert_sum(PERT_LAT_T1, &args);
225    let pb2 = pert_sum(PERT_LAT_T2, &args);
226    let pd0 = pert_sum(PERT_DIST_T0, &args);
227    let pd1 = pert_sum(PERT_DIST_T1, &args);
228    let pd2 = pert_sum(PERT_DIST_T2, &args);
229    let pd3 = pert_sum(PERT_DIST_T3, &args);
230
231    let t2 = t * t;
232    let t3 = t2 * t;
233
234    let long_m = args.w1 + main_long + pl0 + mod2pi(pl1 * t) + mod2pi(pl2 * t2) + mod2pi(pl3 * t3);
235    let lat_m = main_lat + pb0 + mod2pi(pb1 * t) + mod2pi(pb2 * t2);
236    let r = RA0 * (main_dist + pd0 + pd1 * t + pd2 * t2 + pd3 * t3);
237
238    // Rectangular in the ELP computation frame.
239    let cos_lat = lat_m.cos();
240    let x0 = r * long_m.cos() * cos_lat;
241    let y0 = r * long_m.sin() * cos_lat;
242    let z0 = r * lat_m.sin();
243
244    // Chapront P,Q precession to the J2000.0 mean ecliptic.
245    let t4 = t3 * t;
246    let t5 = t4 * t;
247    let p = 0.10180391e-4 * t + 0.47020439e-6 * t2 - 0.5417367e-9 * t3 - 0.2507948e-11 * t4
248        + 0.463486e-14 * t5;
249    let q = -0.113469002e-3 * t + 0.12372674e-6 * t2 + 0.12654170e-8 * t3
250        - 0.1371808e-11 * t4
251        - 0.320334e-14 * t5;
252    let sq = (1.0 - p * p - q * q).sqrt();
253    let p11 = 1.0 - 2.0 * p * p;
254    let p12 = 2.0 * p * q;
255    let p13 = 2.0 * p * sq;
256    let p21 = 2.0 * p * q;
257    let p22 = 1.0 - 2.0 * q * q;
258    let p23 = -2.0 * q * sq;
259    let p31 = -2.0 * p * sq;
260    let p32 = 2.0 * q * sq;
261    let p33 = 1.0 - 2.0 * p * p - 2.0 * q * q;
262
263    let x = p11 * x0 + p12 * y0 + p13 * z0;
264    let y = p21 * x0 + p22 * y0 + p23 * z0;
265    let z = p31 * x0 + p32 * y0 + p33 * z0;
266
267    let dist = (x * x + y * y + z * z).sqrt();
268    let longitude = y.atan2(x);
269    let latitude = (z / dist).asin();
270    (longitude, latitude, dist)
271}
272
273fn precession_in_longitude(t: f64) -> f64 {
274    let t2 = t * t;
275    5028.796195 * t + 1.1054348 * t2 - 0.00007964 * t2 * t
276}
277
278fn mean_obliquity(t: f64) -> f64 {
279    let t2 = t * t;
280    (84381.448 - 46.8150 * t - 0.00059 * t2 + 0.001813 * t2 * t) * ARCSEC_TO_RAD
281}
282
283fn true_obliquity(t: f64) -> f64 {
284    let (l, lp, f, d, om) = delaunay_args(t);
285    mean_obliquity(t) + nutation_deps(l, lp, f, d, om, t) * ARCSEC_TO_RAD
286}
287
288fn ecliptic_to_equatorial(lambda: f64, beta: f64, eps: f64) -> (f64, f64) {
289    let (sin_lam, cos_lam) = lambda.sin_cos();
290    let (sin_beta, cos_beta) = beta.sin_cos();
291    let (sin_eps, cos_eps) = eps.sin_cos();
292    let ra = (sin_lam * cos_eps - sin_beta / cos_beta * sin_eps).atan2(cos_lam);
293    let dec = (sin_beta * cos_eps + cos_beta * sin_eps * sin_lam).asin();
294    (ra, dec)
295}
296
297/// Apparent geocentric position of the Moon at a Julian Ephemeris Day (TT):
298/// ELP/MPP02 geometric place, precessed to the ecliptic of date and corrected
299/// for nutation in longitude, then resolved to equatorial coordinates.
300#[derive(Debug, Clone, Copy, PartialEq)]
301pub struct MoonState {
302    /// Apparent ecliptic longitude of date, degrees `[0, 360)`.
303    pub longitude_degrees: f64,
304    /// Ecliptic latitude, degrees.
305    pub latitude_degrees: f64,
306    /// Geocentric distance, kilometres.
307    pub distance_km: f64,
308    /// Apparent right ascension, degrees `[0, 360)`.
309    pub ra_degrees: f64,
310    /// Apparent declination, degrees.
311    pub dec_degrees: f64,
312}
313
314/// Compute the Moon's apparent geocentric position at the given Julian Ephemeris
315/// Day in Terrestrial Time.
316#[must_use]
317pub fn moon_position(jde_tt: f64) -> MoonState {
318    let t = (jde_tt - 2451545.0) / 36525.0;
319    let (geo_lon, lat, dist) = compute_moon_position(t);
320
321    let p_a = precession_in_longitude(t);
322    let (l, lp, f, d, om) = delaunay_args(t);
323    let dpsi = nutation_dpsi(l, lp, f, d, om, t);
324    let lon = normalize_radians(geo_lon + (p_a + dpsi) * ARCSEC_TO_RAD);
325
326    let eps = true_obliquity(t);
327    let (ra, dec) = ecliptic_to_equatorial(lon, lat, eps);
328
329    MoonState {
330        longitude_degrees: (lon * RAD_TO_DEG).rem_euclid(360.0),
331        latitude_degrees: lat * RAD_TO_DEG,
332        distance_km: dist,
333        ra_degrees: (ra * RAD_TO_DEG).rem_euclid(360.0),
334        dec_degrees: dec * RAD_TO_DEG,
335    }
336}