1use crate::{
2 aberration::{apply_aberration, compute_earth_state, remove_aberration},
3 frames::{CIRSPosition, ICRSPosition},
4 transforms::CoordinateFrame,
5 CoordError, CoordResult, Distance,
6};
7use celestial_core::{matrix::RotationMatrix3, Angle, Vector3};
8use celestial_time::{transforms::NutationCalculator, TT};
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, PartialEq)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15pub struct GCRSPosition {
16 ra: Angle,
17 dec: Angle,
18 epoch: TT,
19 distance: Option<Distance>,
20}
21
22impl GCRSPosition {
23 pub fn new(ra: Angle, dec: Angle, epoch: TT) -> CoordResult<Self> {
24 let ra = ra.validate_right_ascension()?;
25 let dec = dec.validate_declination(false)?;
26
27 Ok(Self {
28 ra,
29 dec,
30 epoch,
31 distance: None,
32 })
33 }
34
35 pub fn with_distance(
36 ra: Angle,
37 dec: Angle,
38 epoch: TT,
39 distance: Distance,
40 ) -> CoordResult<Self> {
41 let mut pos = Self::new(ra, dec, epoch)?;
42 pos.distance = Some(distance);
43 Ok(pos)
44 }
45
46 pub fn from_degrees(ra_deg: f64, dec_deg: f64, epoch: TT) -> CoordResult<Self> {
47 Self::new(
48 Angle::from_degrees(ra_deg),
49 Angle::from_degrees(dec_deg),
50 epoch,
51 )
52 }
53
54 pub fn ra(&self) -> Angle {
55 self.ra
56 }
57
58 pub fn dec(&self) -> Angle {
59 self.dec
60 }
61
62 pub fn epoch(&self) -> TT {
63 self.epoch
64 }
65
66 pub fn distance(&self) -> Option<Distance> {
67 self.distance
68 }
69
70 pub fn set_distance(&mut self, distance: Distance) {
71 self.distance = Some(distance);
72 }
73
74 pub fn remove_distance(&mut self) {
75 self.distance = None;
76 }
77
78 pub fn unit_vector(&self) -> Vector3 {
79 let (sin_dec, cos_dec) = self.dec.sin_cos();
80 let (sin_ra, cos_ra) = self.ra.sin_cos();
81
82 Vector3::new(cos_dec * cos_ra, cos_dec * sin_ra, sin_dec)
83 }
84
85 pub fn from_unit_vector(unit: Vector3, epoch: TT) -> CoordResult<Self> {
86 let r = libm::sqrt(unit.x.powi(2) + unit.y.powi(2) + unit.z.powi(2));
87
88 if r == 0.0 {
89 return Err(CoordError::invalid_coordinate("Zero vector"));
90 }
91
92 let x = unit.x / r;
93 let y = unit.y / r;
94 let z = unit.z / r;
95
96 let d2 = x * x + y * y;
97 let ra = if d2 == 0.0 { 0.0 } else { libm::atan2(y, x) };
98 let dec = if z == 0.0 {
99 0.0
100 } else {
101 libm::atan2(z, libm::sqrt(d2))
102 };
103
104 Self::new(Angle::from_radians(ra), Angle::from_radians(dec), epoch)
105 }
106
107 pub fn to_cirs(&self) -> CoordResult<CIRSPosition> {
108 let npb_matrix = Self::gcrs_to_cirs_matrix(&self.epoch)?;
109
110 let gcrs_vec = self.unit_vector();
111 let cirs_vec = npb_matrix * gcrs_vec;
112
113 let mut cirs = CIRSPosition::from_unit_vector(cirs_vec, self.epoch)?;
114
115 if let Some(distance) = self.distance {
116 cirs.set_distance(distance);
117 }
118
119 Ok(cirs)
120 }
121
122 pub fn from_cirs(cirs: &CIRSPosition) -> CoordResult<Self> {
123 let npb_matrix = Self::gcrs_to_cirs_matrix(&cirs.epoch())?;
124 let cirs_to_gcrs = npb_matrix.transpose();
125
126 let cirs_vec = cirs.unit_vector();
127 let gcrs_vec = cirs_to_gcrs * cirs_vec;
128
129 let mut gcrs = Self::from_unit_vector(gcrs_vec, cirs.epoch())?;
130
131 if let Some(distance) = cirs.distance() {
132 gcrs.distance = Some(distance);
133 }
134
135 Ok(gcrs)
136 }
137
138 fn gcrs_to_cirs_matrix(epoch: &TT) -> CoordResult<RotationMatrix3> {
139 let jd = epoch.to_julian_date();
140 let t = celestial_core::utils::jd_to_centuries(jd.jd1(), jd.jd2());
141
142 let nutation = epoch
143 .nutation_iau2006a()
144 .map_err(|e| CoordError::CoreError {
145 message: format!("Nutation calculation failed: {}", e),
146 })?;
147
148 let precession_calc = celestial_core::precession::PrecessionIAU2006::new();
149 let npb_matrix = precession_calc.npb_matrix_iau2006a(
150 t,
151 nutation.nutation_longitude(),
152 nutation.nutation_obliquity(),
153 );
154
155 let cio_solution = celestial_core::CioSolution::calculate(&npb_matrix, t).map_err(|e| {
156 CoordError::CoreError {
157 message: format!("CIO calculation failed: {}", e),
158 }
159 })?;
160
161 let c2i_matrix = celestial_core::gcrs_to_cirs_matrix(
162 cio_solution.cip.x,
163 cio_solution.cip.y,
164 cio_solution.s,
165 );
166
167 Ok(c2i_matrix)
168 }
169}
170
171impl CoordinateFrame for GCRSPosition {
172 fn to_icrs(&self, _epoch: &TT) -> CoordResult<ICRSPosition> {
173 let gcrs_vec = self.unit_vector();
174
175 let earth_state = compute_earth_state(&self.epoch)?;
176 let sun_earth_dist = earth_state.heliocentric_position.magnitude();
177 let icrs_vec =
178 remove_aberration(gcrs_vec, earth_state.barycentric_velocity, sun_earth_dist);
179
180 let mut icrs = ICRSPosition::from_unit_vector(icrs_vec)?;
181
182 if let Some(distance) = self.distance {
183 icrs.set_distance(distance);
184 }
185
186 Ok(icrs)
187 }
188
189 fn from_icrs(icrs: &ICRSPosition, epoch: &TT) -> CoordResult<Self> {
190 let icrs_vec = icrs.unit_vector();
191
192 let earth_state = compute_earth_state(epoch)?;
193 let sun_earth_dist = earth_state.heliocentric_position.magnitude();
194 let gcrs_vec = apply_aberration(icrs_vec, earth_state.barycentric_velocity, sun_earth_dist);
195
196 let mut gcrs = Self::from_unit_vector(gcrs_vec, *epoch)?;
197
198 if let Some(distance) = icrs.distance() {
199 gcrs.distance = Some(distance);
200 }
201
202 Ok(gcrs)
203 }
204}
205
206impl std::fmt::Display for GCRSPosition {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 write!(
209 f,
210 "GCRS(RA={:.6}°, Dec={:.6}°, epoch=J{:.1}",
211 self.ra.degrees(),
212 self.dec.degrees(),
213 self.epoch.julian_year()
214 )?;
215
216 if let Some(distance) = self.distance {
217 write!(f, ", d={}", distance)?;
218 }
219
220 write!(f, ")")
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_gcrs_creation() {
230 let epoch = TT::j2000();
231 let pos = GCRSPosition::from_degrees(180.0, 45.0, epoch).unwrap();
232
233 assert_eq!(pos.ra().degrees(), 180.0);
234 assert_eq!(pos.dec().degrees(), 45.0);
235 assert_eq!(pos.epoch(), epoch);
236 assert_eq!(pos.distance(), None);
237 }
238
239 #[test]
240 fn test_gcrs_with_distance() {
241 let epoch = TT::j2000();
242 let distance = Distance::from_parsecs(10.0).unwrap();
243 let pos = GCRSPosition::with_distance(
244 Angle::from_degrees(90.0),
245 Angle::from_degrees(30.0),
246 epoch,
247 distance,
248 )
249 .unwrap();
250
251 assert_eq!(pos.distance().unwrap(), distance);
252 }
253
254 #[test]
255 fn test_unit_vector_conversion() {
256 let epoch = TT::j2000();
257
258 let vernal_equinox = GCRSPosition::from_degrees(0.0, 0.0, epoch).unwrap();
259 let unit_vec = vernal_equinox.unit_vector();
260
261 assert_eq!(unit_vec.x, 1.0);
262 assert_eq!(unit_vec.y, 0.0);
263 assert_eq!(unit_vec.z, 0.0);
264
265 let recovered = GCRSPosition::from_unit_vector(unit_vec, epoch).unwrap();
266 assert_eq!(recovered.ra().degrees(), vernal_equinox.ra().degrees());
267 assert_eq!(recovered.dec().degrees(), vernal_equinox.dec().degrees());
268 }
269
270 #[test]
271 fn test_icrs_to_gcrs_applies_aberration() {
272 let epoch = TT::j2000();
273 let icrs = ICRSPosition::from_degrees(90.0, 23.0).unwrap();
274
275 let gcrs = GCRSPosition::from_icrs(&icrs, &epoch).unwrap();
276
277 let sep_arcsec = icrs
278 .angular_separation(&ICRSPosition::from_unit_vector(gcrs.unit_vector()).unwrap())
279 .arcseconds();
280
281 assert!(
282 sep_arcsec > 15.0 && sep_arcsec < 25.0,
283 "ICRS→GCRS aberration should be ~20 arcsec, got {:.2} arcsec",
284 sep_arcsec
285 );
286 }
287
288 #[test]
289 fn test_gcrs_to_icrs_roundtrip() {
290 let test_positions = [
291 (0.0, 0.0),
292 (90.0, 0.0),
293 (180.0, 45.0),
294 (270.0, -60.0),
295 (45.0, 89.0),
296 ];
297
298 for (ra, dec) in test_positions {
299 let epoch = TT::j2000();
300 let icrs = ICRSPosition::from_degrees(ra, dec).unwrap();
301 let gcrs = GCRSPosition::from_icrs(&icrs, &epoch).unwrap();
302 let recovered = gcrs.to_icrs(&epoch).unwrap();
303
304 let diff_arcsec = icrs.angular_separation(&recovered).arcseconds();
306 assert!(
307 diff_arcsec < 1e-7,
308 "Roundtrip for ({}, {}) should be < 100 nano-arcsec, got {:.2e} arcsec",
309 ra,
310 dec,
311 diff_arcsec
312 );
313 }
314 }
315
316 #[test]
317 fn test_gcrs_to_cirs_to_gcrs_roundtrip() {
318 let epoch = TT::j2000();
319 let original = GCRSPosition::from_degrees(120.0, 30.0, epoch).unwrap();
320
321 let cirs = original.to_cirs().unwrap();
322 let recovered = GCRSPosition::from_cirs(&cirs).unwrap();
323
324 let sep_arcsec = {
327 let orig_vec = original.unit_vector();
328 let rec_vec = recovered.unit_vector();
329 let dot = orig_vec.x * rec_vec.x + orig_vec.y * rec_vec.y + orig_vec.z * rec_vec.z;
330 dot.clamp(-1.0, 1.0).acos() * 206264.806247
331 };
332 assert!(
333 sep_arcsec < 1e-10,
334 "GCRS→CIRS→GCRS roundtrip should be < 0.1 nano-arcsec, got {:.2e} arcsec",
335 sep_arcsec
336 );
337 }
338
339 #[test]
340 fn test_icrs_to_gcrs_to_cirs_chain() {
341 let epoch = TT::j2000();
345 let icrs = ICRSPosition::from_degrees(180.0, 45.0).unwrap();
346
347 let gcrs = GCRSPosition::from_icrs(&icrs, &epoch).unwrap();
348 let cirs_via_gcrs = gcrs.to_cirs().unwrap();
349
350 let cirs_direct = CIRSPosition::from_icrs(&icrs, &epoch).unwrap();
351
352 let ra_diff_arcsec =
355 (cirs_via_gcrs.ra().radians() - cirs_direct.ra().radians()).abs() * 206264.806247;
356 let dec_diff_arcsec =
357 (cirs_via_gcrs.dec().radians() - cirs_direct.dec().radians()).abs() * 206264.806247;
358
359 assert!(
361 ra_diff_arcsec < 0.1,
362 "RA difference (light deflection) should be < 0.1\", got {:.4}\"",
363 ra_diff_arcsec
364 );
365 assert!(
366 dec_diff_arcsec < 0.1,
367 "Dec difference (light deflection) should be < 0.1\", got {:.4}\"",
368 dec_diff_arcsec
369 );
370 }
371
372 #[test]
373 fn test_aberration_varies_with_epoch() {
374 let icrs = ICRSPosition::from_degrees(180.0, 45.0).unwrap();
375
376 let epoch_jan = TT::from_julian_date(celestial_time::JulianDate::new(2451545.0, 0.0));
377 let epoch_jul = TT::from_julian_date(celestial_time::JulianDate::new(2451545.0, 182.5));
378
379 let gcrs_jan = GCRSPosition::from_icrs(&icrs, &epoch_jan).unwrap();
380 let gcrs_jul = GCRSPosition::from_icrs(&icrs, &epoch_jul).unwrap();
381
382 let ra_diff_arcsec = (gcrs_jan.ra().degrees() - gcrs_jul.ra().degrees()).abs() * 3600.0;
383 let dec_diff_arcsec = (gcrs_jan.dec().degrees() - gcrs_jul.dec().degrees()).abs() * 3600.0;
384
385 assert!(
386 ra_diff_arcsec > 1.0 || dec_diff_arcsec > 1.0,
387 "Aberration should differ between epochs: RA={:.2}\", Dec={:.2}\"",
388 ra_diff_arcsec,
389 dec_diff_arcsec
390 );
391 }
392
393 #[test]
394 fn test_distance_preservation() {
395 let epoch = TT::j2000();
396 let distance = Distance::from_parsecs(100.0).unwrap();
397 let icrs = ICRSPosition::from_degrees_with_distance(90.0, 45.0, distance).unwrap();
398
399 let gcrs = GCRSPosition::from_icrs(&icrs, &epoch).unwrap();
400 assert_eq!(gcrs.distance().unwrap(), distance);
401
402 let cirs = gcrs.to_cirs().unwrap();
403 assert_eq!(cirs.distance().unwrap(), distance);
404
405 let recovered_gcrs = GCRSPosition::from_cirs(&cirs).unwrap();
406 assert_eq!(recovered_gcrs.distance().unwrap(), distance);
407
408 let recovered_icrs = recovered_gcrs.to_icrs(&epoch).unwrap();
409 assert_eq!(recovered_icrs.distance().unwrap(), distance);
410 }
411
412 #[test]
413 fn test_coordinate_validation() {
414 let epoch = TT::j2000();
415
416 assert!(GCRSPosition::from_degrees(0.0, 0.0, epoch).is_ok());
417 assert!(GCRSPosition::from_degrees(359.99, 89.99, epoch).is_ok());
418
419 assert!(GCRSPosition::from_degrees(0.0, 91.0, epoch).is_err());
420 assert!(GCRSPosition::from_degrees(0.0, -91.0, epoch).is_err());
421 }
422
423 #[test]
424 fn test_display_formatting() {
425 let epoch = TT::j2000();
426 let pos = GCRSPosition::from_degrees(123.456789, -67.123456, epoch).unwrap();
427 let display = format!("{}", pos);
428
429 assert!(display.contains("GCRS"));
430 assert!(display.contains("RA=123.456789°"));
431 assert!(display.contains("Dec=-67.123456°"));
432 assert!(display.contains("J2000.0"));
433 }
434
435 #[test]
436 fn test_from_unit_vector_north_pole() {
437 let epoch = TT::j2000();
438 let north_pole_vec = Vector3::new(0.0, 0.0, 1.0);
439 let pos = GCRSPosition::from_unit_vector(north_pole_vec, epoch).unwrap();
440
441 assert_eq!(pos.ra().radians(), 0.0);
442 assert_eq!(pos.dec().radians(), std::f64::consts::FRAC_PI_2);
443 }
444
445 #[test]
446 fn test_from_unit_vector_south_pole() {
447 let epoch = TT::j2000();
448 let south_pole_vec = Vector3::new(0.0, 0.0, -1.0);
449 let pos = GCRSPosition::from_unit_vector(south_pole_vec, epoch).unwrap();
450
451 assert_eq!(pos.ra().radians(), 0.0);
452 assert_eq!(pos.dec().radians(), -std::f64::consts::FRAC_PI_2);
453 }
454
455 #[test]
456 fn test_pole_roundtrip() {
457 let epoch = TT::j2000();
458
459 let north_pole = GCRSPosition::from_degrees(0.0, 90.0, epoch).unwrap();
460 let unit_vec = north_pole.unit_vector();
461 let recovered = GCRSPosition::from_unit_vector(unit_vec, epoch).unwrap();
462
463 assert_eq!(recovered.ra().radians(), 0.0);
464 assert_eq!(recovered.dec().degrees(), 90.0);
465
466 let south_pole = GCRSPosition::from_degrees(0.0, -90.0, epoch).unwrap();
467 let unit_vec = south_pole.unit_vector();
468 let recovered = GCRSPosition::from_unit_vector(unit_vec, epoch).unwrap();
469
470 assert_eq!(recovered.ra().radians(), 0.0);
471 assert_eq!(recovered.dec().degrees(), -90.0);
472 }
473}