Skip to main content

stem_branch/
lib.rs

1//! Native Rust port of stem-branch's solar ephemeris core.
2//!
3//! Computes the Sun's geocentric ecliptic state from the full VSOP87D Earth
4//! series plus a JPL DE441-fitted correction polynomial and IAU2000B nutation.
5//! Input is a Julian Ephemeris Day in Terrestrial Time (JDE/TT); the UTC<->TT /
6//! ΔT boundary is the caller's responsibility.
7//!
8//! This is a faithful translation of the TypeScript `solarEclipticState`; the
9//! VSOP87D and IAU2000B coefficient tables are generated from the same source
10//! by `scripts/gen-rust-solar-data.mjs`.
11
12#![forbid(unsafe_code)]
13
14mod delta_t;
15mod julian;
16mod lunisolar;
17mod new_moon;
18mod nutation_data;
19mod solar_terms;
20mod vsop87d_earth;
21
22pub use delta_t::delta_t_for_year;
23pub use julian::{jd_from_ymd, ymd_from_jd};
24pub use lunisolar::{
25    gregorian_to_lunisolar, lunar_months_for_year, lunar_new_year, CivilDate, LunarMonth,
26    LunisolarDate,
27};
28pub use new_moon::{find_new_moons_in_range, new_moon_jde};
29pub use solar_terms::{find_solar_term_moment, SOLAR_TERM_LONGITUDES};
30
31use core::f64::consts::{PI, TAU};
32use nutation_data::NUT_COEFFS;
33use vsop87d_earth::{EARTH_L, EARTH_R};
34
35/// Radians per arcsecond (π / 180 / 3600).
36const ARCSEC_TO_RAD: f64 = PI / 180.0 / 3600.0;
37/// Degrees per radian.
38const RAD_TO_DEG: f64 = 180.0 / PI;
39
40/// Geocentric solar ecliptic state at a Julian Ephemeris Day (TT).
41#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct SolarState {
43    /// Geocentric ecliptic longitude, mean equinox of date (no nutation/
44    /// aberration), in degrees `[0, 360)`.
45    pub true_longitude_degrees: f64,
46    /// Apparent geocentric ecliptic longitude — `true` plus IAU2000B nutation
47    /// in longitude and aberration (true equinox of date), in degrees `[0, 360)`.
48    pub apparent_longitude_degrees: f64,
49    /// Sun–Earth distance in astronomical units.
50    pub radius_au: f64,
51}
52
53/// Compute the Sun's geocentric ecliptic state at the given Julian Ephemeris
54/// Day in Terrestrial Time.
55pub fn solar_ecliptic_state(jde_tt: f64) -> SolarState {
56    let tau = (jde_tt - 2451545.0) / 365250.0; // Julian millennia from J2000 (TT)
57    let t = (jde_tt - 2451545.0) / 36525.0; // Julian centuries from J2000 (TT)
58
59    // VSOP87D heliocentric longitude (rad) and radius (AU).
60    let mut lon = eval_vsop_series(EARTH_L, tau);
61    let r = eval_vsop_series(EARTH_R, tau);
62
63    // DE441-fitted even-polynomial correction (arcseconds -> radians). The
64    // literal 206264.806 mirrors the TypeScript source.
65    let tau2 = tau * tau;
66    lon += (-0.106674 - 0.616597 * tau2 + 0.315446 * tau2 * tau2 - 0.050315 * tau2 * tau2 * tau2)
67        / 206264.806;
68
69    // Heliocentric -> geocentric (+180°): mean equinox of date before nutation
70    // and aberration.
71    let geo_true = lon + PI;
72
73    // Apparent place: + IAU2000B nutation in longitude + aberration.
74    let (dl, dlp, df, dd, dom) = delaunay_args(t);
75    let dpsi = nutation_dpsi(dl, dlp, df, dd, dom, t);
76    let apparent = geo_true + dpsi * ARCSEC_TO_RAD + (-20.4898 / r) * ARCSEC_TO_RAD;
77
78    SolarState {
79        true_longitude_degrees: normalize_radians(geo_true) * RAD_TO_DEG,
80        apparent_longitude_degrees: normalize_radians(apparent) * RAD_TO_DEG,
81        radius_au: r,
82    }
83}
84
85/// Evaluate a VSOP87 series: a sum over powers of `tau`, each power weighting a
86/// sum of `A * cos(B + C * tau)` terms.
87fn eval_vsop_series(series: &[&[[f64; 3]]], tau: f64) -> f64 {
88    let mut result = 0.0;
89    let mut tau_pow = 1.0;
90    for terms in series {
91        let mut sum = 0.0;
92        for term in *terms {
93            sum += term[0] * (term[1] + term[2] * tau).cos();
94        }
95        result += sum * tau_pow;
96        tau_pow *= tau;
97    }
98    result
99}
100
101/// The five Delaunay fundamental arguments (radians) at Julian century `t` (TT).
102/// Returns `(l, l', F, D, Ω)`. Source: IERS Conventions (2010), Table 5.2a.
103fn delaunay_args(t: f64) -> (f64, f64, f64, f64, f64) {
104    let t2 = t * t;
105    let t3 = t2 * t;
106    let t4 = t3 * t;
107    // `% 1296000.0` (arcseconds in 360°) reduces each argument; the f64 `%`
108    // operator matches JavaScript's truncated remainder for a faithful port.
109    let l = ((485868.249036 + 1717915923.2178 * t + 31.8792 * t2 + 0.051635 * t3
110        - 0.00024470 * t4)
111        % 1296000.0)
112        * ARCSEC_TO_RAD;
113    let lp = ((1287104.79305 + 129596581.0481 * t - 0.5532 * t2 + 0.000136 * t3 - 0.00001149 * t4)
114        % 1296000.0)
115        * ARCSEC_TO_RAD;
116    let f = ((335779.526232 + 1739527262.8478 * t - 12.7512 * t2 - 0.001037 * t3
117        + 0.00000417 * t4)
118        % 1296000.0)
119        * ARCSEC_TO_RAD;
120    let d = ((1072260.70369 + 1602961601.2090 * t - 6.3706 * t2 + 0.006593 * t3 - 0.00003169 * t4)
121        % 1296000.0)
122        * ARCSEC_TO_RAD;
123    let om = ((450160.398036 - 6962890.5431 * t + 7.4722 * t2 + 0.007702 * t3 - 0.00005939 * t4)
124        % 1296000.0)
125        * ARCSEC_TO_RAD;
126    (l, lp, f, d, om)
127}
128
129/// Nutation in longitude (Δψ) in arcseconds, IAU2000B (77 lunisolar terms).
130fn nutation_dpsi(l: f64, lp: f64, f: f64, d: f64, om: f64, t: f64) -> f64 {
131    let mut dpsi = 0.0;
132    for row in NUT_COEFFS {
133        let arg = row[0] * l + row[1] * lp + row[2] * f + row[3] * d + row[4] * om;
134        dpsi += (row[5] + row[6] * t) * arg.sin();
135    }
136    // 0.1 microarcseconds -> arcseconds.
137    dpsi / 1e7
138}
139
140/// Normalize an angle in radians to `[0, 2π)`.
141fn normalize_radians(rad: f64) -> f64 {
142    ((rad % TAU) + TAU) % TAU
143}