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}