1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#![allow(clippy::all, clippy::pedantic, clippy::restriction, warnings)]
#[cfg(feature = "mars")]
mod mars_tests {
use deep_time::{Dt, Real, Scale, f, mars::MARS_SOL_LENGTH_SEC};
#[test]
fn utc_leap_seconds_are_handled_in_mars_time() {
// One second before vs after a leap second insertion
let utc_pre = Dt::from_tai_sec(1_485_779_199);
let utc_post = Dt::from_tai_sec(1_485_779_200);
let msd_pre = utc_pre.to_msd_f();
let msd_post = utc_post.to_msd_f();
let diff_sols = (msd_post - msd_pre).abs();
assert!(
diff_sols > 1e-6 && diff_sols < 2e-5,
"MSD difference across leap second was {} sols (expected ~1.126e-5)",
diff_sols
);
}
#[test]
fn msd_exact_roundtrip_is_accurate() {
let test_points = [
Dt::from_sec(0, Scale::TAI),
Dt::from_sec(86_400 * 365, Scale::TAI),
Dt::from_sec(-86_400 * 365 * 10, Scale::TAI),
Dt::from_sec(1_000_000_000, Scale::TAI),
Dt::from_sec(-2_208_945_600, Scale::TAI),
];
for &p in &test_points {
let (whole, frac) = p.to_msd();
let back = Dt::from_msd(whole, frac);
let diff = back.to_diff_raw(p).to_sec_f().abs();
assert!(
diff < 5e-5, // ← relaxed for f64 JD precision (max observed error ≈ 13.7 µs)
"MSD round-trip error too large: {} s at {:?}",
diff,
p
);
}
}
#[test]
fn msd_float_roundtrip_is_accurate() {
let test_points = [
Dt::from_sec(0, Scale::TAI),
Dt::from_sec(86_400 * 365 * 100, Scale::TAI),
Dt::from_sec(1_000_000_000, Scale::TAI),
];
for &p in &test_points {
let msd_float = p.to_msd_f();
let back = Dt::from_msd_f(msd_float);
let diff = back.to_diff_raw(p).to_sec_f().abs();
assert!(
diff < 5e-5, // ← relaxed for f64 MSD path (max observed error ≈ 13.7 µs)
"MSD float round-trip error too large: {} s at {:?}",
diff,
p
);
}
}
#[test]
fn mtc_is_in_valid_range() {
let test_points = [
Dt::from_sec(0, Scale::TAI),
Dt::from_sec(86_400 * 365, Scale::TAI),
Dt::from_sec(1_000_000_000, Scale::TAI),
];
for &p in &test_points {
let mtc = p.to_mtc();
let mtc_sec = mtc.to_sec_f();
assert!(
mtc_sec >= 0.0 && mtc_sec < MARS_SOL_LENGTH_SEC,
"MTC out of range: {} s at {:?}",
mtc_sec,
p
);
}
}
#[test]
fn msd_at_j2000_is_correct() {
let tai = Dt::ZERO;
let (whole, frac) = tai.to_msd();
assert_eq!(whole, 44791, "Integer part of MSD at J2000 should be 44791");
}
#[test]
fn mars_ls_is_correct() {
// These test cases are the official worked examples published by NASA GISS
// in the Mars24 Sunclock algorithm documentation.
//
// Source: NASA Goddard Institute for Space Studies (GISS)
// Title: Mars24 Sunclock — Algorithm and Worked Examples
// URL: https://www.giss.nasa.gov/tools/mars24/help/algorithm.html
// Updated: 2025-01-07
//
// The short-series analytic model for Areocentric Solar Longitude (Ls)
// implemented in `to_mars_ls` is based on Allison & McEwen (2000) with
// the seven largest planetary perturbations. These two dates are the
// exact verification benchmarks provided by NASA.
const TOLERANCE: f64 = 0.01;
let date = Dt::from_ymd(2000, 1, 6, Scale::UTC, 0, 0, 0, 0);
let ls = date.to_mars_ls();
assert!(
(ls - 277.18758).abs() < TOLERANCE,
"2000-01-06 Ls is wrong, got: {}",
ls
);
let date = Dt::from_ymd(2004, 1, 3, Scale::UTC, 13, 46, 41, 0);
let ls = date.to_mars_ls();
assert!(
(ls - 327.32416).abs() < TOLERANCE,
"2004-01-03 Ls is wrong, got: {}",
ls
);
}
#[test]
fn mars_local_solar_times_are_correct() {
// Official NASA GISS Mars24 worked example (prime meridian)
//
// Source: NASA Goddard Institute for Space Studies (GISS)
// Title: Mars24 Sunclock — Algorithm and Worked Examples
// URL: https://www.giss.nasa.gov/tools/mars24/help/algorithm.html
// Updated: 2025-01-07
//
// Date: 2000-01-06 00:00:00 UTC
// Longitude: 0° E (prime meridian)
// Expected LMST: 23:59:39 Mars time → 86_379 seconds into the sol
// Expected LTST: 23:38:54 Mars time → 85_134 seconds into the sol
//
// These values are taken verbatim from the published NASA algorithm page
// (Table of worked examples, rows C-2/C-3 and C-4).
let date = Dt::from_ymd(2000, 1, 6, Scale::UTC, 0, 0, 0, 0);
let east_lon_deg = f!(0.0); // prime meridian
let lmst = date.to_mars_lmst(east_lon_deg);
let ltst = date.to_mars_ltst(east_lon_deg);
// Convert the returned Dt (seconds into the current sol) to a float for comparison
let lmst_sec = lmst.to_sec_f();
let ltst_sec = ltst.to_sec_f();
assert!(
(lmst_sec - f!(86379.0)).abs() < f!(0.5),
"LMST wrong, got {} seconds (expected ~86379)",
lmst_sec
);
assert!(
(ltst_sec - f!(85134.0)).abs() < f!(3.0),
"LTST wrong, got {} seconds (expected ~85134)",
ltst_sec
);
}
#[test]
fn mars_year_is_correct() {
// These dates are standard reference points in Mars science literature.
//
// Mars Year numbering follows the Clancy et al. (2000) convention:
// Mars Year 1 begins at the northern vernal equinox (Ls = 0°) on
// 1955 April 11 (JD 2435208.456 TT).
//
// The implementation uses:
// - Epoch JD from the Clancy definition (confirmed in Gangale's
// "Vernal Equinoxes of Mars" table and multiple sources).
// - Average Mars tropical year length 686.9725 Earth days
// (NASA GISS Mars24 Technical Notes).
//
// This produces the conventional integer Mars Year used by NASA,
// ESA, LMD Mars Climate Database, and peer-reviewed papers.
// Spirit (MER-A) landing: 2004-01-04 UTC
// Explicitly stated as Mars Year 26, Ls ≈ 328° in:
// Smith et al. (2006) "One Martian year of atmospheric observations
// using MER Mini-TES", J. Geophys. Res. Planets, 111(E12S13).
// https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2006JE002770
let spirit = Dt::from_ymd(2004, 1, 4, Scale::UTC, 0, 0, 0, 0);
assert_eq!(
spirit.to_mars_year(),
26,
"Spirit landing (2004-01-04) should be Mars Year 26"
);
// Perseverance (M2020) landing: 2021-02-18 UTC
// Landed in Mars Year 36 (widely used in mission papers and
// confirmed by the epoch + tropical year calculation; e.g.
// "Mars Year 36" references in post-landing MEDA/Mars 2020 literature).
let perseverance = Dt::from_ymd(2021, 2, 18, Scale::UTC, 0, 0, 0, 0);
assert_eq!(
perseverance.to_mars_year(),
36,
"Perseverance landing (2021-02-18) should be Mars Year 36"
);
}
}