1use crate::{transforms::CoordinateFrame, CoordError, CoordResult, Distance};
2use celestial_core::{Angle, Vector3};
3use celestial_time::TT;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq)]
9#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10pub struct ICRSPosition {
11 ra: Angle,
12 dec: Angle,
13 distance: Option<Distance>,
14}
15
16impl ICRSPosition {
17 pub fn new(ra: Angle, dec: Angle) -> CoordResult<Self> {
18 let ra = ra.validate_right_ascension()?;
19 let dec = dec.validate_declination(false)?;
20
21 Ok(Self {
22 ra,
23 dec,
24 distance: None,
25 })
26 }
27
28 pub fn with_distance(ra: Angle, dec: Angle, distance: Distance) -> CoordResult<Self> {
29 let mut pos = Self::new(ra, dec)?;
30 pos.distance = Some(distance);
31 Ok(pos)
32 }
33
34 pub fn from_degrees(ra_deg: f64, dec_deg: f64) -> CoordResult<Self> {
35 Self::new(Angle::from_degrees(ra_deg), Angle::from_degrees(dec_deg))
36 }
37
38 pub fn from_degrees_with_distance(
39 ra_deg: f64,
40 dec_deg: f64,
41 distance: Distance,
42 ) -> CoordResult<Self> {
43 Self::with_distance(
44 Angle::from_degrees(ra_deg),
45 Angle::from_degrees(dec_deg),
46 distance,
47 )
48 }
49
50 pub fn from_hours_degrees(ra_hours: f64, dec_deg: f64) -> CoordResult<Self> {
51 Self::new(Angle::from_hours(ra_hours), Angle::from_degrees(dec_deg))
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 distance(&self) -> Option<Distance> {
63 self.distance
64 }
65
66 pub fn set_distance(&mut self, distance: Distance) {
67 self.distance = Some(distance);
68 }
69
70 pub fn remove_distance(&mut self) {
71 self.distance = None;
72 }
73
74 pub fn unit_vector(&self) -> Vector3 {
75 let (sin_dec, cos_dec) = self.dec.sin_cos();
76 let (sin_ra, cos_ra) = self.ra.sin_cos();
77
78 Vector3::new(cos_dec * cos_ra, cos_dec * sin_ra, sin_dec)
79 }
80
81 pub fn position_vector(&self) -> CoordResult<Vector3> {
82 let distance = self.distance.ok_or_else(|| {
83 CoordError::invalid_coordinate("Distance required for position vector")
84 })?;
85
86 let unit = self.unit_vector();
87 let distance_au = distance.au();
88
89 Ok(Vector3::new(
90 unit.x * distance_au,
91 unit.y * distance_au,
92 unit.z * distance_au,
93 ))
94 }
95
96 pub fn from_unit_vector(unit: Vector3) -> CoordResult<Self> {
97 let r = libm::sqrt(unit.x.powi(2) + unit.y.powi(2) + unit.z.powi(2));
98
99 if r == 0.0 {
100 return Err(CoordError::invalid_coordinate("Zero vector"));
101 }
102
103 let x = unit.x / r;
104 let y = unit.y / r;
105 let z = unit.z / r;
106
107 let d2 = x * x + y * y;
108 let ra = if d2 == 0.0 { 0.0 } else { libm::atan2(y, x) };
109 let dec = if z == 0.0 {
110 0.0
111 } else {
112 libm::atan2(z, libm::sqrt(d2))
113 };
114
115 Self::new(Angle::from_radians(ra), Angle::from_radians(dec))
116 }
117
118 pub fn from_position_vector(pos: Vector3) -> CoordResult<Self> {
119 let distance_au = libm::sqrt(pos.x.powi(2) + pos.y.powi(2) + pos.z.powi(2));
120
121 if distance_au == 0.0 {
122 return Err(CoordError::invalid_coordinate("Zero position vector"));
123 }
124
125 let unit = Vector3::new(
126 pos.x / distance_au,
127 pos.y / distance_au,
128 pos.z / distance_au,
129 );
130
131 let mut icrs = Self::from_unit_vector(unit)?;
132 icrs.distance = Some(Distance::from_au(distance_au)?);
133
134 Ok(icrs)
135 }
136
137 pub fn angular_separation(&self, other: &Self) -> Angle {
138 let (sin_dec1, cos_dec1) = self.dec.sin_cos();
139 let (sin_dec2, cos_dec2) = other.dec.sin_cos();
140 let delta_ra = (self.ra - other.ra).radians();
141
142 let angle_rad = celestial_core::math::vincenty_angular_separation(
143 sin_dec1, cos_dec1, sin_dec2, cos_dec2, delta_ra,
144 );
145
146 Angle::from_radians(angle_rad)
147 }
148
149 pub fn is_near_pole(&self) -> bool {
150 self.dec.abs().degrees() > 89.0
151 }
152
153 pub fn position_uncertainty_arcsec(&self, parallax_error_mas: f64) -> Option<f64> {
175 self.distance.map(|_d| {
176 parallax_error_mas / 1000.0
178 })
179 }
180
181 pub fn distance_uncertainty_parsecs(&self, parallax_error_mas: f64) -> Option<f64> {
193 self.distance.map(|d| {
194 let parallax_mas = d.parallax_milliarcsec();
195 let relative_error = parallax_error_mas / parallax_mas;
196 d.parsecs() * relative_error
197 })
198 }
199}
200
201impl CoordinateFrame for ICRSPosition {
202 fn to_icrs(&self, _epoch: &TT) -> CoordResult<ICRSPosition> {
203 Ok(self.clone())
204 }
205
206 fn from_icrs(icrs: &ICRSPosition, _epoch: &TT) -> CoordResult<Self> {
207 Ok(icrs.clone())
208 }
209}
210
211impl ICRSPosition {
212 pub fn to_galactic(&self, epoch: &TT) -> CoordResult<crate::GalacticPosition> {
213 crate::GalacticPosition::from_icrs(self, epoch)
214 }
215
216 pub fn to_ecliptic(&self, epoch: &TT) -> CoordResult<crate::EclipticPosition> {
217 crate::EclipticPosition::from_icrs(self, epoch)
218 }
219}
220
221impl std::fmt::Display for ICRSPosition {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 write!(
224 f,
225 "ICRS(RA={:.6}°, Dec={:.6}°",
226 self.ra.degrees(),
227 self.dec.degrees()
228 )?;
229
230 if let Some(distance) = self.distance {
231 write!(f, ", d={}", distance)?;
232 }
233
234 write!(f, ")")
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::Distance;
242
243 #[test]
244 fn test_constructor_methods() {
245 let pos1 =
247 ICRSPosition::new(Angle::from_degrees(180.0), Angle::from_degrees(45.0)).unwrap();
248
249 assert_eq!(pos1.ra().degrees(), Angle::from_degrees(180.0).degrees());
250 assert_eq!(pos1.dec().degrees(), Angle::from_degrees(45.0).degrees());
251 assert_eq!(pos1.distance(), None);
252
253 let pos2 = ICRSPosition::from_degrees(90.0, -30.0).unwrap();
255 assert_eq!(pos2.ra().degrees(), Angle::from_degrees(90.0).degrees());
256 assert_eq!(pos2.dec().degrees(), Angle::from_degrees(-30.0).degrees());
257
258 let pos3 = ICRSPosition::from_hours_degrees(12.0, 60.0).unwrap();
260 assert_eq!(pos3.ra().hours(), 12.0);
261 assert_eq!(pos3.dec().degrees(), Angle::from_degrees(60.0).degrees());
262
263 let distance = Distance::from_parsecs(10.0).unwrap();
265 let pos4 = ICRSPosition::with_distance(
266 Angle::from_degrees(0.0),
267 Angle::from_degrees(0.0),
268 distance,
269 )
270 .unwrap();
271 assert_eq!(pos4.distance().unwrap(), distance);
272 }
273
274 #[test]
275 fn test_accessor_methods() {
276 let mut pos = ICRSPosition::from_degrees(270.0, 15.0).unwrap();
277 let distance = Distance::from_parsecs(5.0).unwrap();
278
279 assert_eq!(pos.ra().degrees(), Angle::from_degrees(270.0).degrees());
281 assert_eq!(pos.dec().degrees(), Angle::from_degrees(15.0).degrees());
282 assert_eq!(pos.distance(), None);
283
284 pos.set_distance(distance);
286 assert_eq!(pos.distance().unwrap(), distance);
287
288 pos.remove_distance();
290 assert_eq!(pos.distance(), None);
291 }
292
293 #[test]
294 fn test_unit_vector_conversion() {
295 let vernal_equinox = ICRSPosition::from_degrees(0.0, 0.0).unwrap();
297 let unit_vec = vernal_equinox.unit_vector();
298
299 assert_eq!(unit_vec.x, 1.0);
301 assert_eq!(unit_vec.y, 0.0);
302 assert_eq!(unit_vec.z, 0.0);
303
304 let north_pole = ICRSPosition::from_degrees(0.0, 90.0).unwrap();
306 let pole_vec = north_pole.unit_vector();
307
308 let expected_pole = ICRSPosition::from_degrees(0.0, 90.0).unwrap().unit_vector();
310 assert_eq!(pole_vec.x, expected_pole.x);
311 assert_eq!(pole_vec.y, expected_pole.y);
312 assert_eq!(pole_vec.z, expected_pole.z);
313 }
314
315 #[test]
316 fn test_coordinate_transformations() {
317 let pos = ICRSPosition::from_degrees(45.0, 30.0).unwrap();
318
319 let galactic = pos.to_galactic(&TT::j2000()).unwrap();
321 assert!(galactic.longitude().degrees() >= 0.0); let ecliptic = pos.to_ecliptic(&TT::j2000()).unwrap();
325 assert!(ecliptic.lambda().degrees() >= 0.0); }
327
328 #[test]
329 fn test_coordinate_frame_implementation() {
330 let pos = ICRSPosition::from_degrees(120.0, -45.0).unwrap();
331
332 let icrs_copy = pos.to_icrs(&TT::j2000()).unwrap();
334 assert_eq!(pos.ra().radians(), icrs_copy.ra().radians());
335 assert_eq!(pos.dec().radians(), icrs_copy.dec().radians());
336
337 let icrs_from = ICRSPosition::from_icrs(&pos, &TT::j2000()).unwrap();
339 assert_eq!(pos.ra().radians(), icrs_from.ra().radians());
340 assert_eq!(pos.dec().radians(), icrs_from.dec().radians());
341 }
342
343 #[test]
344 fn test_transformation_consistency() {
345 let test_positions = [
347 (200.0, -20.0),
348 (0.0, 0.0), (90.0, 0.0), (45.0, 60.0), ];
352
353 for (ra_deg, dec_deg) in test_positions {
354 let original = ICRSPosition::from_degrees(ra_deg, dec_deg).unwrap();
355
356 let galactic_1 = original.to_galactic(&TT::j2000()).unwrap();
357 let galactic_2 = original.to_galactic(&TT::j2000()).unwrap();
358 assert_eq!(
359 galactic_1.longitude().radians(),
360 galactic_2.longitude().radians()
361 );
362 assert_eq!(
363 galactic_1.latitude().radians(),
364 galactic_2.latitude().radians()
365 );
366
367 let ecliptic_1 = original.to_ecliptic(&TT::j2000()).unwrap();
368 let ecliptic_2 = original.to_ecliptic(&TT::j2000()).unwrap();
369 assert_eq!(ecliptic_1.lambda().radians(), ecliptic_2.lambda().radians());
370 assert_eq!(ecliptic_1.beta().radians(), ecliptic_2.beta().radians());
371 }
372 }
373
374 #[test]
375 fn test_coordinate_validation() {
376 assert!(ICRSPosition::from_degrees(0.0, 0.0).is_ok());
378 assert!(ICRSPosition::from_degrees(359.99, 89.99).is_ok());
379 assert!(ICRSPosition::from_degrees(180.0, -89.99).is_ok());
380
381 assert!(ICRSPosition::from_degrees(0.0, 91.0).is_err());
383 assert!(ICRSPosition::from_degrees(0.0, -91.0).is_err());
384 }
385
386 #[test]
387 fn test_distance_handling() {
388 let distance1 = Distance::from_parsecs(100.0).unwrap();
389 let _distance2 = Distance::from_parsecs(50.0).unwrap();
390
391 let pos = ICRSPosition::with_distance(
392 Angle::from_degrees(90.0),
393 Angle::from_degrees(45.0),
394 distance1,
395 )
396 .unwrap();
397
398 let galactic = pos.to_galactic(&TT::j2000()).unwrap();
400 assert_eq!(galactic.distance().unwrap(), distance1);
401
402 let ecliptic = pos.to_ecliptic(&TT::j2000()).unwrap();
403 assert_eq!(ecliptic.distance().unwrap(), distance1);
404 }
405
406 #[test]
407 fn test_additional_constructor_methods() {
408 let distance = Distance::from_parsecs(10.0).unwrap();
409
410 let pos = ICRSPosition::from_degrees_with_distance(120.0, 45.0, distance).unwrap();
412 assert_eq!(pos.ra().degrees(), Angle::from_degrees(120.0).degrees());
413 assert_eq!(pos.dec().degrees(), Angle::from_degrees(45.0).degrees());
414 assert_eq!(pos.distance().unwrap(), distance);
415 }
416
417 #[test]
418 fn test_vector_operations() {
419 let distance = Distance::from_au(5.0).unwrap();
420 let pos = ICRSPosition::with_distance(
421 Angle::from_degrees(0.0),
422 Angle::from_degrees(0.0),
423 distance,
424 )
425 .unwrap();
426
427 let pos_vec = pos.position_vector().unwrap();
429
430 let expected_x = distance.au() * pos.unit_vector().x;
432 let expected_y = distance.au() * pos.unit_vector().y;
433 let expected_z = distance.au() * pos.unit_vector().z;
434
435 assert_eq!(pos_vec.x, expected_x);
436 assert_eq!(pos_vec.y, expected_y);
437 assert_eq!(pos_vec.z, expected_z);
438
439 let recovered = ICRSPosition::from_position_vector(pos_vec).unwrap();
441
442 assert_eq!(recovered.ra().degrees(), pos.ra().degrees());
444 assert_eq!(recovered.dec().degrees(), pos.dec().degrees());
445
446 assert!(recovered.distance().is_some());
448
449 let unit_vec = pos.unit_vector();
451 let from_unit = ICRSPosition::from_unit_vector(unit_vec).unwrap();
452 assert_eq!(from_unit.ra().degrees(), pos.ra().degrees());
453 assert_eq!(from_unit.dec().degrees(), pos.dec().degrees());
454 assert_eq!(from_unit.distance(), None); }
456
457 #[test]
458 fn test_vector_error_cases() {
459 let zero_vec = Vector3::new(0.0, 0.0, 0.0);
461 assert!(ICRSPosition::from_unit_vector(zero_vec).is_err());
462
463 assert!(ICRSPosition::from_position_vector(zero_vec).is_err());
465
466 let pos_no_dist = ICRSPosition::from_degrees(0.0, 0.0).unwrap();
468 assert!(pos_no_dist.position_vector().is_err());
469 }
470
471 #[test]
472 fn test_angular_separation() {
473 let pos1 = ICRSPosition::from_degrees(0.0, 0.0).unwrap(); let pos2 = ICRSPosition::from_degrees(90.0, 0.0).unwrap(); let pos3 = ICRSPosition::from_degrees(0.0, 90.0).unwrap(); let sep_90 = pos1.angular_separation(&pos2);
479 celestial_core::test_helpers::assert_ulp_le(sep_90.degrees(), 90.0, 2, "90° separation");
480
481 let sep_pole = pos1.angular_separation(&pos3);
483 celestial_core::test_helpers::assert_ulp_le(sep_pole.degrees(), 90.0, 2, "Pole separation");
484
485 let sep_self = pos1.angular_separation(&pos1);
487 assert!(
488 sep_self.degrees().abs() < 1e-10,
489 "Self separation should be near zero"
490 );
491
492 let sep_12 = pos1.angular_separation(&pos2);
494 let sep_21 = pos2.angular_separation(&pos1);
495 assert_eq!(sep_12.degrees(), sep_21.degrees());
496 }
497
498 #[test]
499 fn test_pole_classification() {
500 let north_pole = ICRSPosition::from_degrees(0.0, 89.5).unwrap();
502 assert!(north_pole.is_near_pole());
503
504 let south_pole = ICRSPosition::from_degrees(0.0, -89.5).unwrap();
505 assert!(south_pole.is_near_pole());
506
507 let equator = ICRSPosition::from_degrees(0.0, 0.0).unwrap();
509 assert!(!equator.is_near_pole());
510
511 let mid_lat = ICRSPosition::from_degrees(0.0, 45.0).unwrap();
512 assert!(!mid_lat.is_near_pole());
513
514 let boundary = ICRSPosition::from_degrees(0.0, 89.0).unwrap();
516 assert!(!boundary.is_near_pole()); }
518
519 #[test]
520 fn test_position_uncertainty() {
521 let distance = Distance::from_parsecs(100.0).unwrap();
522 let pos_with_dist = ICRSPosition::with_distance(
523 Angle::from_degrees(0.0),
524 Angle::from_degrees(0.0),
525 distance,
526 )
527 .unwrap();
528
529 let uncertainty = pos_with_dist.position_uncertainty_arcsec(1.0); assert!(uncertainty.is_some());
532 assert!(uncertainty.unwrap() > 0.0);
533
534 let pos_no_dist = ICRSPosition::from_degrees(0.0, 0.0).unwrap();
536 let no_uncertainty = pos_no_dist.position_uncertainty_arcsec(1.0);
537 assert!(no_uncertainty.is_none());
538 }
539
540 #[test]
541 fn test_display_formatting() {
542 let pos_no_dist = ICRSPosition::from_degrees(123.456789, -67.123456).unwrap();
544 let display_no_dist = format!("{}", pos_no_dist);
545 assert!(display_no_dist.contains("RA=123.456789°"));
546 assert!(display_no_dist.contains("Dec=-67.123456°"));
547 assert!(!display_no_dist.contains("d="));
548
549 let distance = Distance::from_parsecs(25.0).unwrap();
551 let pos_with_dist = ICRSPosition::with_distance(
552 Angle::from_degrees(45.0),
553 Angle::from_degrees(30.0),
554 distance,
555 )
556 .unwrap();
557 let display_with_dist = format!("{}", pos_with_dist);
558 assert!(display_with_dist.contains("RA=45.000000°"));
559 assert!(display_with_dist.contains("Dec=30.000000°"));
560 assert!(display_with_dist.contains("d=25"));
561 }
562
563 #[test]
564 fn test_position_uncertainty_dimensional_analysis() {
565 let distance = Distance::from_parsecs(100.0).unwrap(); let pos = ICRSPosition::with_distance(
568 Angle::from_degrees(0.0),
569 Angle::from_degrees(0.0),
570 distance,
571 )
572 .unwrap();
573
574 let parallax_error_mas = 0.02;
576
577 let position_uncertainty = pos.position_uncertainty_arcsec(parallax_error_mas).unwrap();
579
580 assert!((position_uncertainty - 0.00002).abs() < 1e-10);
582
583 }
587
588 #[test]
589 fn test_distance_uncertainty_dimensional_analysis() {
590 let distance = Distance::from_parsecs(100.0).unwrap(); let pos = ICRSPosition::with_distance(
593 Angle::from_degrees(0.0),
594 Angle::from_degrees(0.0),
595 distance,
596 )
597 .unwrap();
598
599 let parallax_mas = distance.parallax_milliarcsec();
601 assert!((parallax_mas - 10.0).abs() < 1e-10);
602
603 let parallax_error_mas = 0.1;
605
606 let dist_uncertainty = pos
608 .distance_uncertainty_parsecs(parallax_error_mas)
609 .unwrap();
610
611 assert!((dist_uncertainty - 1.0).abs() < 1e-10);
613
614 }
617
618 #[test]
619 fn test_position_vs_distance_uncertainty_relationship() {
620 let distance = Distance::from_parsecs(10.0).unwrap(); let pos = ICRSPosition::with_distance(
623 Angle::from_degrees(0.0),
624 Angle::from_degrees(0.0),
625 distance,
626 )
627 .unwrap();
628
629 let parallax_error_mas = 5.0; let angular_unc_arcsec = pos.position_uncertainty_arcsec(parallax_error_mas).unwrap();
633 assert_eq!(angular_unc_arcsec, 0.005); let distance_unc_pc = pos
637 .distance_uncertainty_parsecs(parallax_error_mas)
638 .unwrap();
639 assert!((distance_unc_pc - 0.5).abs() < 1e-10);
641
642 }
646
647 #[test]
648 fn test_uncertainty_without_distance() {
649 let pos = ICRSPosition::from_degrees(0.0, 0.0).unwrap();
651
652 assert_eq!(pos.position_uncertainty_arcsec(0.1), None);
653 assert_eq!(pos.distance_uncertainty_parsecs(0.1), None);
654 }
655
656 #[test]
657 fn test_gaia_realistic_example() {
658 let distance = Distance::from_parsecs(500.0).unwrap();
661 let pos = ICRSPosition::with_distance(
662 Angle::from_degrees(120.5),
663 Angle::from_degrees(-45.2),
664 distance,
665 )
666 .unwrap();
667
668 let parallax_mas = distance.parallax_milliarcsec();
670 assert!((parallax_mas - 2.0).abs() < 1e-10);
671
672 let gaia_error_mas = 0.03;
674
675 let pos_unc = pos.position_uncertainty_arcsec(gaia_error_mas).unwrap();
677 assert!(
678 (pos_unc - 0.00003).abs() < 1e-10,
679 "Position uncertainty: {}",
680 pos_unc
681 ); let dist_unc = pos.distance_uncertainty_parsecs(gaia_error_mas).unwrap();
685 assert!((dist_unc - 7.5).abs() < 1e-10);
686
687 let fractional_error = dist_unc / distance.parsecs();
689 assert!((fractional_error - 0.015).abs() < 1e-10);
690 }
691
692 #[test]
693 fn test_from_unit_vector_north_pole() {
694 let north_pole_vec = Vector3::new(0.0, 0.0, 1.0);
695 let pos = ICRSPosition::from_unit_vector(north_pole_vec).unwrap();
696
697 assert_eq!(pos.ra().radians(), 0.0);
698 assert_eq!(pos.dec().radians(), std::f64::consts::FRAC_PI_2);
699 }
700
701 #[test]
702 fn test_from_unit_vector_south_pole() {
703 let south_pole_vec = Vector3::new(0.0, 0.0, -1.0);
704 let pos = ICRSPosition::from_unit_vector(south_pole_vec).unwrap();
705
706 assert_eq!(pos.ra().radians(), 0.0);
707 assert_eq!(pos.dec().radians(), -std::f64::consts::FRAC_PI_2);
708 }
709
710 #[test]
711 fn test_pole_roundtrip() {
712 let north_pole = ICRSPosition::from_degrees(0.0, 90.0).unwrap();
713 let unit_vec = north_pole.unit_vector();
714 let recovered = ICRSPosition::from_unit_vector(unit_vec).unwrap();
715
716 assert_eq!(recovered.ra().radians(), 0.0);
717 assert_eq!(recovered.dec().degrees(), 90.0);
718
719 let south_pole = ICRSPosition::from_degrees(0.0, -90.0).unwrap();
720 let unit_vec = south_pole.unit_vector();
721 let recovered = ICRSPosition::from_unit_vector(unit_vec).unwrap();
722
723 assert_eq!(recovered.ra().radians(), 0.0);
724 assert_eq!(recovered.dec().degrees(), -90.0);
725 }
726}