Skip to main content

deep_time/dt/
mars.rs

1//! Mars time-scale constants and conversion methods (MSD, MTC, Ls, LMST, LTST, Mars Year).
2
3use crate::{Dt, Real, Scale, cos, floor_f, rem_euclid_f, sin};
4
5/// mean length of one Martian sol in Earth seconds.
6/// Current NASA GISS Mars24 value (updated 2025-01-07): 1.0274912517 Earth days.
7pub const MARS_SOL_LENGTH_SEC: Real = 88_775.244_146_88;
8
9/// Martian mean sol length in attoseconds
10/// (88_775.24414688 s × 10¹⁸, integer matching the current NASA divisor).
11pub const MARS_SOL_ATTOS: i128 = 88_775_244_146_880_000_000_000;
12
13/// Precomputed numerical values of the Mars reference epoch on the TT scale (seconds since J2000).
14pub const MARS_REF_TT: Dt = Dt::new(-3_976_386_952, 650_560_000_000_000_000);
15pub const MARS_REF_TT_ATTOS: i128 = MARS_REF_TT.to_attos();
16
17/// Areocentric solar longitude (Ls) constants from the current NASA GISS Mars24
18/// algorithm (AM2000 short series, updated 2025-01-07).
19///
20/// Ls = 0°   → northern vernal equinox (Martian northern spring begins)
21/// Ls = 90°  → northern summer solstice
22/// Ls = 180° → northern autumnal equinox
23/// Ls = 270° → northern winter solstice
24pub const MARS_LS_M0: Real = f!(19.3871);
25pub const MARS_LS_M_RATE: Real = f!(0.52402073);
26pub const MARS_LS_ALPHA_FMS0: Real = f!(270.3871);
27pub const MARS_LS_ALPHA_FMS_RATE: Real = f!(0.524038496);
28
29/// Equation-of-Time coefficients for LTST (from NASA GISS Mars24 / AM2000).
30pub const MARS_EOT_COEFF_2LS: Real = f!(2.861);
31pub const MARS_EOT_COEFF_4LS: Real = f!(-0.071);
32pub const MARS_EOT_COEFF_6LS: Real = f!(0.002);
33
34/// Mars Year epoch: JD 2435208.456 TT (northern vernal equinox Ls = 0° on 1955 April 11).
35///
36/// This is the Clancy et al. (2000) definition used by NASA, ESA, LMD Mars Climate
37/// Database, and every modern Mars mission paper as of 2026.
38pub const MARS_YEAR_EPOCH_JD: Real = f!(2435208.456);
39
40/// Length of one Mars tropical year in Earth days (NASA GISS Mars24, 2025).
41///
42/// This is the interval between successive northern vernal equinoxes.
43pub const MARS_TROPICAL_YEAR_DAYS: Real = f!(686.9725);
44
45impl Dt {
46    /// helper: elapsed attoseconds since the Mars MSD reference epoch (JD 2405522.0028779 TT).
47    #[inline]
48    pub(crate) const fn to_attos_since_mars_msd_epoch(numerical_tt: Dt) -> i128 {
49        numerical_tt.to_attos() - MARS_REF_TT_ATTOS
50    }
51
52    /// Returns the Mars Sol Date (MSD) as a tuple of integer sols and the fractional part of a sol.
53    ///
54    /// - The computation follows the canonical NASA GISS / AM2000 formulation and works for any input
55    ///  [`Scale`].
56    /// - Leap seconds are automatically accounted for when converting from UTC.
57    pub const fn to_msd(&self, current: Scale) -> (i64, u128) {
58        let tt = self.to(current, Scale::TT);
59        let elapsed = Self::to_attos_since_mars_msd_epoch(tt);
60        let whole_sols = elapsed.div_euclid(MARS_SOL_ATTOS);
61        let frac_attos = elapsed.rem_euclid(MARS_SOL_ATTOS) as u128;
62
63        (Dt::clamp_i128_to_i64(whole_sols), frac_attos)
64    }
65
66    /// Returns Mars Coordinated Time (MTC) as a [`Dt`] representing
67    /// seconds into the current sol (range `[0, one Martian sol)`).
68    #[inline]
69    pub const fn to_mtc(&self, current: Scale) -> Dt {
70        let (_, frac_attos) = self.to_msd(current);
71        Dt::from_attos(frac_attos as i128, Scale::TAI)
72    }
73
74    /// Creates a `Dt` (in TT) from an Mars Sol Date using full library precision.
75    pub const fn from_msd(whole_sols: i64, frac_attos: u128) -> Self {
76        let elapsed_attos = (whole_sols as i128) * MARS_SOL_ATTOS + frac_attos as i128;
77        let tt = MARS_REF_TT.add(Dt::from_attos(elapsed_attos, Scale::TAI));
78        Self::from(tt.sec, tt.attos, Scale::TT)
79    }
80
81    /// Creates a `Dt` (in TT) from a floating-point Mars Sol Date.
82    /// Non-exact Real.
83    pub const fn from_msd_f(msd: Real) -> Self {
84        let whole = floor_f(msd) as i64;
85        let frac = msd - f!(whole);
86        let frac_span = Dt::from_sec_f(frac * MARS_SOL_LENGTH_SEC);
87        Self::from_msd(whole, frac_span.to_attos() as u128)
88    }
89
90    /// Returns the Mars Sol Date (MSD) as a floating-point value (matches NASA Mars24 output).
91    /// Non-exact Real.
92    #[inline]
93    pub const fn to_msd_f(&self, current: Scale) -> Real {
94        let (whole, frac) = self.to_msd(current);
95        f!(whole) + Dt::attos_to_sec_f(frac) / MARS_SOL_LENGTH_SEC
96    }
97
98    /// Returns the Areocentric Solar Longitude `Ls` in degrees (range [0, 360)).
99    ///
100    /// Ls is the angular position of the Sun as measured eastward from the Martian
101    /// vernal equinox in Mars's orbital plane. It is the standard index of Martian
102    /// seasonal progression used in all mission planning, science operations, and
103    /// atmospheric modeling. Due to orbital eccentricity, northern spring + summer
104    /// last ~381 Earth days while autumn + winter last ~306 Earth days.
105    ///
106    /// - Ls = 0°   → northern vernal equinox (northern spring begins)
107    /// - Ls = 90°  → northern summer solstice
108    /// - Ls = 180° → northern autumnal equinox
109    /// - Ls = 270° → northern winter solstice
110    ///
111    /// Reproduces the short-series analytic model (B-1 through B-5) used
112    /// by the current NASA GISS Mars24 Sunclock algorithm, which is based on
113    /// Allison & McEwen (2000) with the seven largest planetary perturbations.
114    ///
115    /// Source: NASA Goddard Institute for Space Studies (GISS)  
116    /// Title:   Mars24 Sunclock — Algorithm and Worked Examples  
117    /// URL:     https://www.giss.nasa.gov/tools/mars24/help/algorithm.html  
118    /// Updated: 2025-01-07
119    ///
120    /// Works for any input [`Scale`] because it internally converts to TT.
121    pub const fn to_mars_ls(&self, current: Scale) -> Real {
122        let tt = self.to(current, Scale::TT);
123
124        // Δt_J2000 = days since J2000.0 TT
125        let jd_tt = tt.to_jd_f();
126        let dt_j2000 = jd_tt - f!(2451545.0);
127
128        // B-1: Mean anomaly M (degrees)
129        let m = MARS_LS_M0 + MARS_LS_M_RATE * dt_j2000;
130
131        // B-2: Right ascension of the Fictitious Mean Sun
132        let alpha_fms = MARS_LS_ALPHA_FMS0 + MARS_LS_ALPHA_FMS_RATE * dt_j2000;
133
134        // B-3: Planetary perturbation sum (PBS)
135        let pbs = Self::mars_perturber_sum(dt_j2000);
136
137        // B-4: Equation of Center (ν − M) in degrees
138        let eq_center = (f!(10.691) + f!(3.0e-7) * dt_j2000) * sin(m.to_radians())
139            + f!(0.623) * sin((f!(2.0) * m).to_radians())
140            + f!(0.050) * sin((f!(3.0) * m).to_radians())
141            + f!(0.005) * sin((f!(4.0) * m).to_radians())
142            + f!(0.0005) * sin((f!(5.0) * m).to_radians())
143            + pbs;
144
145        // B-5: Areocentric solar longitude
146        let mut ls = alpha_fms + eq_center;
147
148        // Normalize to [0, 360)
149        ls = ls % f!(360.0);
150        if ls < f!(0.0) {
151            ls += f!(360.0);
152        }
153        ls
154    }
155
156    #[inline]
157    const fn mars_perturber_sum(dt_j2000: Real) -> Real {
158        let base = f!(0.985626) * dt_j2000;
159
160        let mut sum = f!(0.0);
161
162        sum += f!(0.0071) * cos(base / f!(2.2353) + f!(49.409));
163        sum += f!(0.0057) * cos(base / f!(2.7543) + f!(168.173));
164        sum += f!(0.0039) * cos(base / f!(1.1177) + f!(191.837));
165        sum += f!(0.0037) * cos(base / f!(15.7866) + f!(21.736));
166        sum += f!(0.0021) * cos(base / f!(2.1354) + f!(15.704));
167        sum += f!(0.0020) * cos(base / f!(2.4694) + f!(95.528));
168        sum += f!(0.0018) * cos(base / f!(32.8493) + f!(49.095));
169
170        sum
171    }
172
173    /// Returns Local Mean Solar Time (LMST) at the given planetocentric east longitude
174    /// as a `Dt` representing seconds into the current Martian sol (range [0, one sol)).
175    ///
176    /// LMST is the uniform mean solar time adjusted for longitude.
177    ///
178    /// Longitude is east-positive (standard planetocentric convention, 0–360° E).
179    /// Internally converts to TT and uses the current NASA GISS Mars24 definition of MST.
180    pub const fn to_mars_lmst(&self, current: Scale, east_longitude_deg: Real) -> Dt {
181        let tt = self.to(current, Scale::TT);
182        let jd_tt = tt.to_jd_f();
183
184        // MST in hours (0–24) — prime-meridian mean solar time (NASA Mars24 formula)
185        let mst = (f!(24.0)
186            * ((jd_tt - f!(2451549.5)) / f!(1.0274912517) + f!(44796.0) - f!(0.0009626)))
187            % f!(24.0);
188
189        // Convert east-positive longitude to west-positive (NASA convention)
190        let lambda_west = rem_euclid_f(-east_longitude_deg, f!(360.0));
191
192        // LMST in hours
193        let mut lmst_hours = mst - lambda_west / f!(15.0);
194        if lmst_hours < f!(0.0) {
195            lmst_hours += f!(24.0);
196        }
197
198        // Convert hours → seconds into the sol and return as Dt (consistent with to_mtc)
199        let seconds_into_sol = lmst_hours * f!(3600.0);
200        Dt::from_sec_f(seconds_into_sol)
201    }
202
203    /// Returns Local True Solar Time (LTST) at the given planetocentric east longitude
204    /// as a [`Dt`] representing seconds into the current Martian sol (range [0, one sol)).
205    ///
206    /// LTST is the actual sundial time (true solar time) at the location — what a
207    /// local observer would see on a sundial. It equals LMST plus the Equation of Time.
208    ///
209    /// Longitude is east-positive (standard planetocentric convention, 0–360° E).
210    pub const fn to_mars_ltst(&self, current: Scale, east_longitude_deg: Real) -> Dt {
211        let lmst = self.to_mars_lmst(current, east_longitude_deg);
212
213        // We already have Ls; reuse it for EOT
214        let ls = self.to_mars_ls(current);
215
216        // Equation of center (ν − M) — same term used in to_mars_ls
217        let dt_j2000 = self.to(current, Scale::TT).to_jd_f() - f!(2451545.0);
218        let m = MARS_LS_M0 + MARS_LS_M_RATE * dt_j2000;
219        let pbs = Self::mars_perturber_sum(dt_j2000);
220        let eq_center = (f!(10.691) + f!(3.0e-7) * dt_j2000) * sin(m.to_radians())
221            + f!(0.623) * sin((f!(2.0) * m).to_radians())
222            + f!(0.050) * sin((f!(3.0) * m).to_radians())
223            + f!(0.005) * sin((f!(4.0) * m).to_radians())
224            + f!(0.0005) * sin((f!(5.0) * m).to_radians())
225            + pbs;
226
227        // Equation of Time in degrees (NASA GISS / AM2000)
228        let eot_deg = MARS_EOT_COEFF_2LS * sin(f!(2.0) * ls.to_radians())
229            + MARS_EOT_COEFF_4LS * sin(f!(4.0) * ls.to_radians())
230            + MARS_EOT_COEFF_6LS * sin(f!(6.0) * ls.to_radians())
231            - eq_center;
232
233        // Convert EOT to seconds (1° = 3600 s / 15 = 240 s per degree)
234        let eot_seconds = eot_deg * f!(240.0);
235
236        // LTST = LMST + EOT (as duration)
237        lmst.add(Dt::from_sec_f(eot_seconds))
238    }
239
240    /// Returns the integer Mars Year (MY) for this instant.
241    ///
242    /// Mars Year numbering follows the standard Clancy et al. (2000) system:
243    /// - Mars Year 1 begins at the northern vernal equinox (Ls = 0°) on 1955 April 11.
244    /// - Each Mars Year is one tropical year on Mars (686.9725 Earth days).
245    /// - Current missions operate in Mars Year 36–37 (as of 2026).
246    ///
247    /// This is the canonical year count used in all Mars science literature,
248    /// mission reports, and atmospheric databases.
249    ///
250    /// Source: Clancy et al. (2000), *J. Geophys. Res.: Planets* 105(E4), 9553–9572;
251    /// confirmed in NASA GISS Mars24 Technical Notes (2025) and LMD Mars Climate Database.
252    ///
253    /// To get the fractional progress through the year, simply use:
254    /// `self.to_mars_ls(current) / 360.0`
255    pub const fn to_mars_year(&self, current: Scale) -> i64 {
256        let tt = self.to(current, Scale::TT);
257        let jd_tt = tt.to_jd_f();
258
259        let days_since_epoch = jd_tt - MARS_YEAR_EPOCH_JD;
260        let years_elapsed = floor_f(days_since_epoch / MARS_TROPICAL_YEAR_DAYS);
261
262        1 + (years_elapsed as i64)
263    }
264}