1use super::angle::SiderealAngle;
2use super::gast::GAST;
3use super::gmst::GMST;
4use super::lmst::LMST;
5use crate::scales::{TT, UT1};
6use crate::transforms::nutation::NutationCalculator;
7use crate::TimeResult;
8use celestial_core::angle::wrap_0_2pi;
9use celestial_core::Location;
10
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Copy, PartialEq)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16pub struct LAST {
17 angle: SiderealAngle,
18 location: Location,
19}
20
21impl LAST {
22 pub fn from_ut1_tt_and_location(ut1: &UT1, tt: &TT, location: &Location) -> TimeResult<Self> {
23 let gast = GAST::from_ut1_and_tt(ut1, tt)?;
24
25 let last_rad = gast.radians() + location.longitude;
26
27 let last_normalized = wrap_0_2pi(last_rad);
28
29 let angle = SiderealAngle::from_radians_exact(last_normalized);
30
31 Ok(Self {
32 angle,
33 location: *location,
34 })
35 }
36
37 pub fn from_lmst_and_equation_of_equinoxes(
38 ut1: &UT1,
39 tt: &TT,
40 location: &Location,
41 ) -> TimeResult<Self> {
42 Self::from_ut1_tt_and_location(ut1, tt, location)
43 }
44
45 pub fn from_hours(hours: f64, location: &Location) -> Self {
46 Self {
47 angle: SiderealAngle::from_hours(hours),
48 location: *location,
49 }
50 }
51
52 pub fn from_degrees(degrees: f64, location: &Location) -> Self {
53 Self {
54 angle: SiderealAngle::from_degrees(degrees),
55 location: *location,
56 }
57 }
58
59 pub fn from_radians(radians: f64, location: &Location) -> Self {
60 Self {
61 angle: SiderealAngle::from_radians(radians),
62 location: *location,
63 }
64 }
65
66 pub fn j2000(location: &Location) -> TimeResult<Self> {
67 let ut1 = UT1::j2000();
68 let tt = TT::j2000();
69 Self::from_ut1_tt_and_location(&ut1, &tt, location)
70 }
71
72 pub fn angle(&self) -> SiderealAngle {
73 self.angle
74 }
75
76 pub fn location(&self) -> Location {
77 self.location
78 }
79
80 pub fn hours(&self) -> f64 {
81 self.angle.hours()
82 }
83
84 pub fn degrees(&self) -> f64 {
85 self.angle.degrees()
86 }
87
88 pub fn radians(&self) -> f64 {
89 self.angle.radians()
90 }
91
92 pub fn hour_angle_to_target(&self, target_ra_hours: f64) -> f64 {
93 self.angle.hour_angle_to_target(target_ra_hours)
94 }
95
96 pub fn to_gast(&self) -> GAST {
97 let longitude_hours = self.location.longitude * 12.0 / celestial_core::constants::PI;
98
99 let gast_hours = self.hours() - longitude_hours;
100
101 GAST::from_hours(gast_hours)
102 }
103
104 pub fn to_lmst(&self, tt: &TT) -> TimeResult<LMST> {
105 let nutation = tt.nutation_iau2006a()?;
106
107 let jd = tt.to_julian_date();
108 let mean_obliquity = celestial_core::obliquity::iau_2006_mean_obliquity(jd.jd1(), jd.jd2());
109
110 let ee_rad = nutation.nutation_longitude() * libm::cos(mean_obliquity);
111 let ee_hours = ee_rad * 12.0 / celestial_core::constants::PI;
112
113 let lmst_hours = self.hours() - ee_hours;
114
115 Ok(LMST::from_hours(lmst_hours, &self.location))
116 }
117
118 pub fn to_gmst(&self, tt: &TT) -> TimeResult<GMST> {
119 let lmst = self.to_lmst(tt)?;
120
121 Ok(lmst.to_gmst())
122 }
123}
124
125impl std::fmt::Display for LAST {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 let lat_deg = self.location.latitude * celestial_core::constants::RAD_TO_DEG;
128 let lon_deg = self.location.longitude * celestial_core::constants::RAD_TO_DEG;
129 write!(
130 f,
131 "LAST {} at ({:.4}°, {:.4}°)",
132 self.angle, lat_deg, lon_deg
133 )
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 fn mauna_kea() -> Location {
142 Location::from_degrees(19.8283, -155.4783, 4145.0).unwrap()
143 }
144
145 fn greenwich() -> Location {
146 Location::greenwich()
147 }
148
149 #[test]
150 fn test_last_at_greenwich_equals_gast() {
151 let ut1 = UT1::j2000();
153 let tt = TT::j2000();
154 let location = greenwich();
155
156 let gast = GAST::from_ut1_and_tt(&ut1, &tt).unwrap();
157 let last = LAST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
158
159 assert!(
161 (last.hours() - gast.hours()).abs() < 1e-14,
162 "LAST at Greenwich should equal GAST: LAST={}, GAST={}",
163 last.hours(),
164 gast.hours()
165 );
166 }
167
168 #[test]
169 fn test_last_method_consistency() {
170 let ut1 = UT1::j2000();
172 let tt = TT::j2000();
173 let location = mauna_kea();
174
175 let last_method1 = LAST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
176 let last_method2 = LAST::from_lmst_and_equation_of_equinoxes(&ut1, &tt, &location).unwrap();
177
178 assert!(
180 (last_method1.hours() - last_method2.hours()).abs() < 1e-14,
181 "LAST calculation methods should match: method1={}, method2={}",
182 last_method1.hours(),
183 last_method2.hours()
184 );
185 }
186
187 #[test]
188 fn test_last_longitude_correction() {
189 let ut1 = UT1::j2000();
191 let tt = TT::j2000();
192
193 let greenwich_loc = greenwich();
194 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 last_greenwich = LAST::from_ut1_tt_and_location(&ut1, &tt, &greenwich_loc).unwrap();
198 let last_east = LAST::from_ut1_tt_and_location(&ut1, &tt, &east_15deg).unwrap();
199 let last_west = LAST::from_ut1_tt_and_location(&ut1, &tt, &west_15deg).unwrap();
200
201 let diff_east = last_east.hours() - last_greenwich.hours();
203 assert!(
204 (diff_east - 1.0).abs() < 1e-12,
205 "15°E should be +1 hour: {}",
206 diff_east
207 );
208
209 let diff_west = last_west.hours() - last_greenwich.hours();
211 assert!(
212 (diff_west + 1.0).abs() < 1e-12,
213 "15°W should be -1 hour: {}",
214 diff_west
215 );
216 }
217
218 #[test]
219 fn test_last_vs_gast_longitude() {
220 let ut1 = UT1::j2000();
222 let tt = TT::j2000();
223 let location = mauna_kea();
224
225 let last = LAST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
226 let gast = GAST::from_ut1_and_tt(&ut1, &tt).unwrap();
227
228 let longitude_hours = location.longitude * 12.0 / celestial_core::constants::PI;
230
231 let expected_last = gast.hours() + longitude_hours;
233 let expected_last_normalized = ((expected_last % 24.0) + 24.0) % 24.0;
234
235 assert!(
236 (last.hours() - expected_last_normalized).abs() < 1e-14,
237 "LAST = GAST + longitude: LAST={}, GAST={}, longitude={}, expected={}",
238 last.hours(),
239 gast.hours(),
240 longitude_hours,
241 expected_last_normalized
242 );
243 }
244
245 #[test]
246 fn test_last_mauna_kea() {
247 let ut1 = UT1::j2000();
249 let tt = TT::j2000();
250 let location = mauna_kea();
251
252 let gast = GAST::from_ut1_and_tt(&ut1, &tt).unwrap();
253 let last = LAST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
254
255 let expected_offset = -155.4783 / 15.0; let actual_offset = last.hours() - gast.hours();
258
259 assert!(
260 (actual_offset - expected_offset).abs() < 1e-10,
261 "Mauna Kea LAST offset incorrect: expected={}, actual={}",
262 expected_offset,
263 actual_offset
264 );
265 }
266
267 #[test]
268 fn test_last_j2000() {
269 let location = mauna_kea();
270 let last = LAST::j2000(&location).unwrap();
271
272 let hours = last.hours();
274 assert!(
275 (0.0..24.0).contains(&hours),
276 "LAST should be in [0, 24) hours: {}",
277 hours
278 );
279 }
280
281 #[test]
282 fn test_last_hour_angle_calculation() {
283 let location = mauna_kea();
284 let last = LAST::from_hours(12.0, &location);
285 let target_ra = 6.0;
286 let hour_angle = last.hour_angle_to_target(target_ra);
287 assert_eq!(hour_angle, 6.0);
288 }
289
290 #[test]
291 fn test_last_to_gast_roundtrip() {
292 let location = mauna_kea();
293 let original_gast = GAST::from_hours(15.5);
294
295 let longitude_hours = location.longitude * 12.0 / celestial_core::constants::PI;
297 let last_hours = original_gast.hours() + longitude_hours;
298 let last = LAST::from_hours(last_hours, &location);
299 let recovered_gast = last.to_gast();
300
301 assert!(
302 (recovered_gast.hours() - original_gast.hours()).abs() < 1e-14,
303 "GAST->LAST->GAST roundtrip failed: original={}, recovered={}",
304 original_gast.hours(),
305 recovered_gast.hours()
306 );
307 }
308
309 #[test]
310 fn test_last_to_lmst_conversion() {
311 let ut1 = UT1::j2000();
312 let tt = TT::j2000();
313 let location = mauna_kea();
314
315 let original_lmst = LMST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
316
317 let last = LAST::from_lmst_and_equation_of_equinoxes(&ut1, &tt, &location).unwrap();
319 let recovered_lmst = last.to_lmst(&tt).unwrap();
320
321 assert!(
324 (recovered_lmst.hours() - original_lmst.hours()).abs() < 1e-7,
325 "LMST->LAST->LMST roundtrip failed: original={}, recovered={}",
326 original_lmst.hours(),
327 recovered_lmst.hours()
328 );
329 }
330
331 #[test]
332 fn test_last_from_constructors() {
333 let location = mauna_kea();
334
335 let hours = 14.5;
337 let degrees = hours * 15.0;
338 let radians = hours * celestial_core::constants::PI / 12.0;
339
340 let last_hours = LAST::from_hours(hours, &location);
341 let last_degrees = LAST::from_degrees(degrees, &location);
342 let last_radians = LAST::from_radians(radians, &location);
343
344 assert!((last_hours.hours() - last_degrees.hours()).abs() < 1e-14);
345 assert!((last_hours.hours() - last_radians.hours()).abs() < 1e-14);
346 assert_eq!(last_hours.location(), location);
347 assert_eq!(last_degrees.location(), location);
348 assert_eq!(last_radians.location(), location);
349 }
350
351 #[test]
352 fn test_last_display() {
353 let location = mauna_kea();
354 let last = LAST::from_hours(12.5, &location);
355 let display = format!("{}", last);
356
357 assert!(display.contains("LAST"));
359 assert!(display.contains("19.8283")); assert!(display.contains("-155.4783")); }
362
363 #[test]
364 fn test_last_location_enforcement() {
365 let location = mauna_kea();
367 let last = LAST::from_hours(12.0, &location);
368
369 let stored_location = last.location();
371 assert_eq!(stored_location.latitude, location.latitude);
372 assert_eq!(stored_location.longitude, location.longitude);
373 assert_eq!(stored_location.height, location.height);
374 }
375
376 #[test]
377 fn test_extreme_longitudes() {
378 let ut1 = UT1::j2000();
379 let tt = TT::j2000();
380
381 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 gast = GAST::from_ut1_and_tt(&ut1, &tt).unwrap();
386 let last_east = LAST::from_ut1_tt_and_location(&ut1, &tt, &east_extreme).unwrap();
387 let last_west = LAST::from_ut1_tt_and_location(&ut1, &tt, &west_extreme).unwrap();
388
389 let diff_east = last_east.hours() - gast.hours();
391 let expected_east = if diff_east < 0.0 {
392 diff_east + 24.0
393 } else {
394 diff_east
395 };
396 assert!(
397 (expected_east - 12.0).abs() < 1e-12,
398 "180°E should be +12 hours"
399 );
400
401 let diff_west = last_west.hours() - gast.hours();
403 let expected_west = if diff_west > 12.0 {
404 diff_west - 24.0
405 } else {
406 diff_west
407 };
408 assert!(
409 (expected_west + 12.0).abs() < 1e-12,
410 "180°W should be -12 hours"
411 );
412 }
413
414 #[test]
415 fn test_last_equation_of_equinoxes_range() {
416 let ut1 = UT1::j2000();
418 let tt = TT::j2000();
419 let location = mauna_kea();
420
421 let last = LAST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
422 let lmst = LMST::from_ut1_tt_and_location(&ut1, &tt, &location).unwrap();
423
424 let ee_hours = last.hours() - lmst.hours();
426 let ee_hours_normalized = if ee_hours > 12.0 {
427 ee_hours - 24.0
428 } else if ee_hours < -12.0 {
429 ee_hours + 24.0
430 } else {
431 ee_hours
432 };
433
434 assert!(
435 ee_hours_normalized.abs() < 0.001, "Equation of equinoxes too large: {} hours = {} seconds",
437 ee_hours_normalized,
438 ee_hours_normalized * 3600.0
439 );
440 }
441
442 #[test]
443 fn test_last_constructors_and_accessors() {
444 let location = greenwich();
445
446 let last_deg = LAST::from_degrees(45.0, &location);
448 assert_eq!(last_deg.degrees(), 45.0);
449 assert_eq!(last_deg.hours(), 3.0);
450
451 let last_rad = LAST::from_radians(celestial_core::constants::PI / 4.0, &location);
453 assert!((last_rad.radians() - celestial_core::constants::PI / 4.0).abs() < 1e-15);
454 assert_eq!(last_rad.hours(), 3.0);
455
456 let angle = last_deg.angle();
458 assert_eq!(angle.degrees(), 45.0);
459
460 let stored_location = last_deg.location();
462 assert_eq!(stored_location.latitude, location.latitude);
463 assert_eq!(stored_location.longitude, location.longitude);
464 assert_eq!(stored_location.height, location.height);
465
466 let degrees = last_deg.degrees();
468 assert_eq!(degrees, 45.0);
469 }
470}