1use super::angle::SiderealAngle;
2use super::gmst::GMST;
3use crate::scales::{TT, UT1};
4use crate::TimeResult;
5use celestial_core::Location;
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct LMST {
13 angle: SiderealAngle,
14 location: Location,
15}
16
17impl LMST {
18 pub fn from_ut1_tt_and_location(ut1: &UT1, tt: &TT, location: &Location) -> TimeResult<Self> {
19 let gmst = GMST::from_ut1_and_tt(ut1, tt)?;
20
21 let lmst_rad = gmst.radians() + location.longitude;
22
23 use celestial_core::angle::wrap_0_2pi;
24 let lmst_normalized = wrap_0_2pi(lmst_rad);
25
26 let angle = SiderealAngle::from_radians_exact(lmst_normalized);
27
28 Ok(Self {
29 angle,
30 location: *location,
31 })
32 }
33
34 pub fn from_hours(hours: f64, location: &Location) -> Self {
35 Self {
36 angle: SiderealAngle::from_hours(hours),
37 location: *location,
38 }
39 }
40
41 pub fn from_degrees(degrees: f64, location: &Location) -> Self {
42 Self {
43 angle: SiderealAngle::from_degrees(degrees),
44 location: *location,
45 }
46 }
47
48 pub fn from_radians(radians: f64, location: &Location) -> Self {
49 Self {
50 angle: SiderealAngle::from_radians(radians),
51 location: *location,
52 }
53 }
54
55 pub fn j2000(location: &Location) -> TimeResult<Self> {
56 let ut1 = UT1::j2000();
57 let tt = TT::j2000();
58 Self::from_ut1_tt_and_location(&ut1, &tt, location)
59 }
60
61 pub fn angle(&self) -> SiderealAngle {
62 self.angle
63 }
64
65 pub fn location(&self) -> Location {
66 self.location
67 }
68
69 pub fn hours(&self) -> f64 {
70 self.angle.hours()
71 }
72
73 pub fn degrees(&self) -> f64 {
74 self.angle.degrees()
75 }
76
77 pub fn radians(&self) -> f64 {
78 self.angle.radians()
79 }
80
81 pub fn hour_angle_to_target(&self, target_ra_hours: f64) -> f64 {
82 self.angle.hour_angle_to_target(target_ra_hours)
83 }
84
85 pub fn to_gmst(&self) -> GMST {
86 let longitude_hours = self.location.longitude * 12.0 / celestial_core::constants::PI;
87
88 let gmst_hours = self.hours() - longitude_hours;
89
90 GMST::from_hours(gmst_hours)
91 }
92}
93
94impl std::fmt::Display for LMST {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 let lat_deg = self.location.latitude * celestial_core::constants::RAD_TO_DEG;
97 let lon_deg = self.location.longitude * celestial_core::constants::RAD_TO_DEG;
98 write!(
99 f,
100 "LMST {} at ({:.4}°, {:.4}°)",
101 self.angle, lat_deg, lon_deg
102 )
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn mauna_kea() -> Location {
111 Location::from_degrees(19.8283, -155.4783, 4145.0).unwrap()
112 }
113
114 fn greenwich() -> Location {
115 Location::greenwich()
116 }
117
118 #[test]
119 fn test_lmst_at_greenwich_equals_gmst() {
120 let ut1 = UT1::j2000();
122 let tt = TT::j2000();
123 let location = greenwich();
124
125 let gmst = GMST::from_ut1_and_tt(&ut1, &tt).unwrap();
126 let lmst = LMST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
127
128 assert!(
130 (lmst.hours() - gmst.hours()).abs() < 1e-14,
131 "LMST at Greenwich should equal GMST: LMST={}, GMST={}",
132 lmst.hours(),
133 gmst.hours()
134 );
135 }
136
137 #[test]
138 fn test_lmst_longitude_correction() {
139 let ut1 = UT1::j2000();
141 let tt = TT::j2000();
142
143 let greenwich_loc = greenwich();
144 let east_15deg = Location::from_degrees(0.0, 15.0, 0.0).unwrap(); let west_15deg = Location::from_degrees(0.0, -15.0, 0.0).unwrap(); let lmst_greenwich = LMST::from_ut1_tt_and_location(&ut1, &tt, &greenwich_loc).unwrap();
148 let lmst_east = LMST::from_ut1_tt_and_location(&ut1, &tt, &east_15deg).unwrap();
149 let lmst_west = LMST::from_ut1_tt_and_location(&ut1, &tt, &west_15deg).unwrap();
150
151 let diff_east = lmst_east.hours() - lmst_greenwich.hours();
153 assert!(
154 (diff_east - 1.0).abs() < 1e-12,
155 "15°E should be +1 hour: {}",
156 diff_east
157 );
158
159 let diff_west = lmst_west.hours() - lmst_greenwich.hours();
161 assert!(
162 (diff_west + 1.0).abs() < 1e-12,
163 "15°W should be -1 hour: {}",
164 diff_west
165 );
166 }
167
168 #[test]
169 fn test_lmst_mauna_kea() {
170 let ut1 = UT1::j2000();
172 let tt = TT::j2000();
173 let location = mauna_kea();
174
175 let gmst = GMST::from_ut1_and_tt(&ut1, &tt).unwrap();
176 let lmst = LMST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
177
178 let expected_offset = -155.4783 / 15.0; let actual_offset = lmst.hours() - gmst.hours();
181
182 assert!(
183 (actual_offset - expected_offset).abs() < 1e-10,
184 "Mauna Kea LMST offset incorrect: expected={}, actual={}",
185 expected_offset,
186 actual_offset
187 );
188 }
189
190 #[test]
191 fn test_lmst_j2000() {
192 let location = mauna_kea();
193 let lmst = LMST::j2000(&location).unwrap();
194
195 let hours = lmst.hours();
197 assert!(
198 (0.0..24.0).contains(&hours),
199 "LMST should be in [0, 24) hours: {}",
200 hours
201 );
202 }
203
204 #[test]
205 fn test_lmst_hour_angle_calculation() {
206 let location = mauna_kea();
207 let lmst = LMST::from_hours(12.0, &location);
208 let target_ra = 6.0;
209 let hour_angle = lmst.hour_angle_to_target(target_ra);
210 assert_eq!(hour_angle, 6.0);
211 }
212
213 #[test]
214 fn test_lmst_to_gmst_roundtrip() {
215 let location = mauna_kea();
216 let original_gmst = GMST::from_hours(15.5);
217
218 let longitude_hours = location.longitude * 12.0 / celestial_core::constants::PI;
220 let lmst_hours = original_gmst.hours() + longitude_hours;
221 let lmst = LMST::from_hours(lmst_hours, &location);
222 let recovered_gmst = lmst.to_gmst();
223
224 assert!(
225 (recovered_gmst.hours() - original_gmst.hours()).abs() < 1e-14,
226 "GMST->LMST->GMST roundtrip failed: original={}, recovered={}",
227 original_gmst.hours(),
228 recovered_gmst.hours()
229 );
230 }
231
232 #[test]
233 fn test_lmst_from_constructors() {
234 let location = mauna_kea();
235
236 let hours = 14.5;
238 let degrees = hours * 15.0;
239 let radians = hours * celestial_core::constants::PI / 12.0;
240
241 let lmst_hours = LMST::from_hours(hours, &location);
242 let lmst_degrees = LMST::from_degrees(degrees, &location);
243 let lmst_radians = LMST::from_radians(radians, &location);
244
245 assert!((lmst_hours.hours() - lmst_degrees.hours()).abs() < 1e-14);
246 assert!((lmst_hours.hours() - lmst_radians.hours()).abs() < 1e-14);
247 assert_eq!(lmst_hours.location(), location);
248 assert_eq!(lmst_degrees.location(), location);
249 assert_eq!(lmst_radians.location(), location);
250 }
251
252 #[test]
253 fn test_lmst_display() {
254 let location = mauna_kea();
255 let lmst = LMST::from_hours(12.5, &location);
256 let display = format!("{}", lmst);
257
258 assert!(display.contains("LMST"));
260 assert!(display.contains("19.8283")); assert!(display.contains("-155.4783")); }
263
264 #[test]
265 fn test_lmst_location_enforcement() {
266 let location = mauna_kea();
268 let lmst = LMST::from_hours(12.0, &location);
269
270 let stored_location = lmst.location();
272 assert_eq!(stored_location.latitude, location.latitude);
273 assert_eq!(stored_location.longitude, location.longitude);
274 assert_eq!(stored_location.height, location.height);
275 }
276
277 #[test]
278 fn test_extreme_longitudes() {
279 let ut1 = UT1::j2000();
280 let tt = TT::j2000();
281
282 let east_extreme = Location::from_degrees(0.0, 180.0, 0.0).unwrap(); let west_extreme = Location::from_degrees(0.0, -180.0, 0.0).unwrap(); let gmst = GMST::from_ut1_and_tt(&ut1, &tt).unwrap();
287 let lmst_east = LMST::from_ut1_tt_and_location(&ut1, &tt, &east_extreme).unwrap();
288 let lmst_west = LMST::from_ut1_tt_and_location(&ut1, &tt, &west_extreme).unwrap();
289
290 let diff_east = lmst_east.hours() - gmst.hours();
292 let expected_east = if diff_east < 0.0 {
293 diff_east + 24.0
294 } else {
295 diff_east
296 };
297 assert!(
298 (expected_east - 12.0).abs() < 1e-12,
299 "180°E should be +12 hours"
300 );
301
302 let diff_west = lmst_west.hours() - gmst.hours();
304 let expected_west = if diff_west > 12.0 {
305 diff_west - 24.0
306 } else {
307 diff_west
308 };
309 assert!(
310 (expected_west + 12.0).abs() < 1e-12,
311 "180°W should be -12 hours"
312 );
313 }
314
315 #[test]
316 fn test_lmst_constructors_and_accessors() {
317 let location = mauna_kea();
318
319 let lmst_deg = LMST::from_degrees(90.0, &location);
321 assert_eq!(lmst_deg.degrees(), 90.0);
322 assert_eq!(lmst_deg.hours(), 6.0);
323
324 let lmst_rad = LMST::from_radians(celestial_core::constants::PI * 1.5, &location);
326 assert!((lmst_rad.radians() - celestial_core::constants::PI * 1.5).abs() < 1e-15);
327 assert_eq!(lmst_rad.hours(), 18.0);
328
329 let angle = lmst_deg.angle();
331 assert_eq!(angle.degrees(), 90.0);
332
333 let stored_location = lmst_deg.location();
335 assert_eq!(stored_location.latitude, location.latitude);
336 assert_eq!(stored_location.longitude, location.longitude);
337 assert_eq!(stored_location.height, location.height);
338
339 let degrees = lmst_deg.degrees();
341 assert_eq!(degrees, 90.0);
342 }
343}