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