celestial_core/cio/coordinates.rs
1//! CIP (Celestial Intermediate Pole) coordinates for the IAU 2000/2006 precession-nutation model.
2//!
3//! The Celestial Intermediate Pole defines the axis around which Earth rotates in the
4//! Celestial Intermediate Reference System (CIRS). Its position relative to the GCRS
5//! is described by two small angles X and Y, which encode the combined effects of
6//! precession and nutation.
7//!
8//! # When to use this
9//!
10//! CIP coordinates are needed when:
11//! - Converting between GCRS (geocentric celestial) and CIRS (intermediate) frames
12//! - Computing Earth Rotation Angle for sidereal time
13//! - Implementing the full IAU 2000/2006 transformation chain
14//!
15//! For most high-precision applications, you'll extract CIP coordinates from a
16//! precession-nutation matrix rather than computing them directly.
17//!
18//! # Coordinate ranges
19//!
20//! X and Y are stored in radians. Current values are on the order of 10^-7 radians
21//! (~0.02 arcseconds). The validation threshold of 0.2 radians (~11 degrees) catches
22//! obviously invalid matrices while allowing for long-term secular drift.
23
24use crate::errors::{AstroError, AstroResult};
25use crate::matrix::RotationMatrix3;
26
27/// Position of the Celestial Intermediate Pole in the GCRS.
28///
29/// X and Y are the direction cosines of the CIP unit vector projected onto
30/// the GCRS equatorial plane. They're extracted from elements [2][0] and [2][1]
31/// of the NPB (nutation-precession-bias) matrix.
32///
33/// Units are radians internally. Use [`to_arcseconds`](Self::to_arcseconds) for display.
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct CipCoordinates {
36 pub x: f64,
37 pub y: f64,
38}
39
40impl CipCoordinates {
41 /// Creates CIP coordinates from X and Y values in radians.
42 ///
43 /// No validation is performed. For coordinates extracted from a real
44 /// precession-nutation matrix, use [`from_npb_matrix`](Self::from_npb_matrix).
45 pub fn new(x: f64, y: f64) -> Self {
46 Self { x, y }
47 }
48
49 /// Extracts CIP coordinates from a nutation-precession-bias matrix.
50 ///
51 /// The NPB matrix transforms GCRS to mean-of-date coordinates. X and Y
52 /// are taken from the third row (elements [2][0] and [2][1]), which
53 /// represents the CIP direction in GCRS.
54 ///
55 /// # Errors
56 ///
57 /// Returns an error if X or Y exceeds 0.2 radians, indicating the matrix
58 /// doesn't represent a valid Earth orientation.
59 pub fn from_npb_matrix(npb_matrix: &RotationMatrix3) -> AstroResult<Self> {
60 let matrix = npb_matrix.elements();
61
62 let x = matrix[2][0];
63 let y = matrix[2][1];
64
65 if x.abs() > 0.2 || y.abs() > 0.2 {
66 return Err(AstroError::math_error(
67 "CIP coordinate extraction",
68 crate::errors::MathErrorKind::InvalidInput,
69 &format!(
70 "CIP coordinates out of reasonable range: X={:.6}, Y={:.6}",
71 x, y
72 ),
73 ));
74 }
75
76 Ok(Self { x, y })
77 }
78
79 /// Distance of the CIP from the GCRS pole, in radians.
80 ///
81 /// This is sqrt(X^2 + Y^2), useful for gauging the total pole offset.
82 pub fn magnitude(&self) -> f64 {
83 libm::sqrt(self.x * self.x + self.y * self.y)
84 }
85
86 /// Returns (X, Y) converted to degrees.
87 pub fn to_degrees(&self) -> (f64, f64) {
88 (
89 self.x * crate::constants::RAD_TO_DEG,
90 self.y * crate::constants::RAD_TO_DEG,
91 )
92 }
93
94 /// Returns (X, Y) converted to arcseconds.
95 ///
96 /// Arcseconds are the conventional unit for reporting CIP coordinates
97 /// in publications and IERS bulletins.
98 pub fn to_arcseconds(&self) -> (f64, f64) {
99 (
100 self.x * crate::constants::RAD_TO_DEG * 3600.0,
101 self.y * crate::constants::RAD_TO_DEG * 3600.0,
102 )
103 }
104}
105
106impl std::fmt::Display for CipCoordinates {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 let (x_as, y_as) = self.to_arcseconds();
109 write!(f, "CIP(X={:.3}\", Y={:.3}\")", x_as, y_as)
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::matrix::RotationMatrix3;
117
118 #[test]
119 fn test_cip_coordinates_creation() {
120 let cip = CipCoordinates::new(1e-6, -2e-6);
121 assert_eq!(cip.x, 1e-6);
122 assert_eq!(cip.y, -2e-6);
123 }
124
125 #[test]
126 fn test_cip_magnitude() {
127 let cip = CipCoordinates::new(3e-6, 4e-6);
128 assert!((cip.magnitude() - 5e-6).abs() < 1e-12);
129 }
130
131 #[test]
132 fn test_cip_display() {
133 let cip = CipCoordinates::new(1e-6, -1e-6);
134 let display = format!("{}", cip);
135 assert!(display.contains("CIP"));
136 assert!(display.contains("0.206")); // ~1e-6 radians in arcseconds
137 }
138
139 #[test]
140 fn test_cip_from_identity_matrix() {
141 // Identity matrix should give CIP coordinates of exactly zero
142 let identity = RotationMatrix3::identity();
143 let cip = CipCoordinates::from_npb_matrix(&identity).unwrap();
144
145 // For identity matrix, third column is [0, 0, 1]
146 // So X = 0, Y = 0 exactly
147 assert_eq!(cip.x, 0.0);
148 assert_eq!(cip.y, 0.0);
149 }
150
151 #[test]
152 fn test_cip_validation_error() {
153 // Create a matrix with unreasonably large CIP coordinates in third row
154 // CIP X,Y are extracted from matrix[2][0] and matrix[2][1]
155 // This should trigger the validation error (threshold is 0.2 radians ~= 11.5 degrees)
156 let invalid_matrix = RotationMatrix3::from_array([
157 [1.0, 0.0, 0.0],
158 [0.0, 1.0, 0.0],
159 [0.25, 0.05, 1.0], // X = 0.25 (too large - exceeds 0.2 rad threshold)
160 ]);
161
162 let result = CipCoordinates::from_npb_matrix(&invalid_matrix);
163
164 // Should return an error due to coordinates being out of reasonable range
165 assert!(result.is_err());
166
167 let error_message = format!("{}", result.unwrap_err());
168 assert!(error_message.contains("CIP coordinates out of reasonable range"));
169 }
170
171 #[test]
172 fn test_cip_reasonable_values() {
173 // Test that reasonable CIP coordinate values work correctly
174 let reasonable_cip = CipCoordinates::new(1e-6, 1e-6);
175
176 // These are the exact computed values for our test case
177 assert_eq!(reasonable_cip.x, 1e-6);
178 assert_eq!(reasonable_cip.y, 1e-6);
179 assert_eq!(reasonable_cip.magnitude(), libm::sqrt(2e-12_f64)); // sqrt(x² + y²)
180 }
181}