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