1use crate::astro::constants::earth::OMEGA_E_DOT_RAD_S;
10use crate::astro::frames::transforms::{
11 gcrs_to_itrs_matrix_with_polar_motion, mat3_vec3_mul, polar_motion_matrix, FrameTransformError,
12 PolarMotion,
13};
14use crate::astro::math::mat3::{inline_rxr, inline_tr, Mat3};
15use crate::astro::time::civil::{civil_from_j2000_seconds, j2000_seconds_from_split};
16use crate::astro::time::model::{Instant, InstantRepr, TimeScale};
17use crate::astro::time::scales::TimeScales;
18
19#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct EarthOrientation {
28 time_scales: TimeScales,
29 polar_motion: PolarMotion,
30 gcrf_to_itrf: Mat3,
31 itrf_to_gcrf: Mat3,
32 earth_rotation_vector_itrf_rad_s: [f64; 3],
33}
34
35impl EarthOrientation {
36 pub fn from_time_scales(ts: &TimeScales) -> Result<Self, FrameTransformError> {
38 Self::from_time_scales_with_polar_motion(ts, PolarMotion::ZERO)
39 }
40
41 pub fn from_time_scales_with_polar_motion(
44 ts: &TimeScales,
45 polar_motion: PolarMotion,
46 ) -> Result<Self, FrameTransformError> {
47 let gcrf_to_itrf = gcrs_to_itrs_matrix_with_polar_motion(ts, polar_motion)?;
48 let itrf_to_gcrf = inline_tr(&gcrf_to_itrf);
49 let polar = polar_motion_matrix(polar_motion)?;
50 let earth_rotation_vector_itrf_rad_s =
51 mat3_vec3_mul(&polar, &[0.0, 0.0, OMEGA_E_DOT_RAD_S])?;
52 Ok(Self {
53 time_scales: *ts,
54 polar_motion,
55 gcrf_to_itrf,
56 itrf_to_gcrf,
57 earth_rotation_vector_itrf_rad_s,
58 })
59 }
60
61 pub fn from_utc(
64 year: i32,
65 month: i32,
66 day: i32,
67 hour: i32,
68 minute: i32,
69 second: f64,
70 ) -> Result<Self, FrameTransformError> {
71 Self::from_utc_with_polar_motion(year, month, day, hour, minute, second, PolarMotion::ZERO)
72 }
73
74 pub fn from_utc_with_polar_motion(
77 year: i32,
78 month: i32,
79 day: i32,
80 hour: i32,
81 minute: i32,
82 second: f64,
83 polar_motion: PolarMotion,
84 ) -> Result<Self, FrameTransformError> {
85 let ts = TimeScales::from_utc(year, month, day, hour, minute, second)
86 .map_err(|_| invalid_input("utc", "time-scale conversion failed"))?;
87 Self::from_time_scales_with_polar_motion(&ts, polar_motion)
88 }
89
90 pub fn from_instant(epoch: Instant) -> Result<Self, FrameTransformError> {
93 Self::from_instant_with_polar_motion(epoch, PolarMotion::ZERO)
94 }
95
96 pub fn from_instant_with_polar_motion(
99 epoch: Instant,
100 polar_motion: PolarMotion,
101 ) -> Result<Self, FrameTransformError> {
102 let ts = time_scales_from_instant(epoch)?;
103 Self::from_time_scales_with_polar_motion(&ts, polar_motion)
104 }
105
106 pub fn time_scales(&self) -> TimeScales {
108 self.time_scales
109 }
110
111 pub fn polar_motion(&self) -> PolarMotion {
113 self.polar_motion
114 }
115
116 pub fn earth_rotation_vector_itrf_rad_s(&self) -> [f64; 3] {
118 self.earth_rotation_vector_itrf_rad_s
119 }
120
121 pub fn gcrf_to_itrf_matrix(&self) -> Mat3 {
123 self.gcrf_to_itrf
124 }
125
126 pub fn itrf_to_gcrf_matrix(&self) -> Mat3 {
128 self.itrf_to_gcrf
129 }
130
131 pub fn gcrf_to_itrf_rotation_rate_matrix(&self) -> Mat3 {
134 let neg_skew = neg_skew_matrix(self.earth_rotation_vector_itrf_rad_s);
135 inline_rxr(&neg_skew, &self.gcrf_to_itrf)
136 }
137
138 pub fn itrf_to_gcrf_rotation_rate_matrix(&self) -> Mat3 {
141 let skew = skew_matrix(self.earth_rotation_vector_itrf_rad_s);
142 inline_rxr(&self.itrf_to_gcrf, &skew)
143 }
144
145 pub fn gcrf_to_itrf_position_km(
147 &self,
148 position_gcrf_km: [f64; 3],
149 ) -> Result<[f64; 3], FrameTransformError> {
150 validate_vec3("position_gcrf_km", &position_gcrf_km)?;
151 mat3_vec3_mul(&self.gcrf_to_itrf, &position_gcrf_km)
152 }
153
154 pub fn itrf_to_gcrf_position_km(
156 &self,
157 position_itrf_km: [f64; 3],
158 ) -> Result<[f64; 3], FrameTransformError> {
159 validate_vec3("position_itrf_km", &position_itrf_km)?;
160 mat3_vec3_mul(&self.itrf_to_gcrf, &position_itrf_km)
161 }
162
163 pub fn gcrf_to_itrf_state_km(
168 &self,
169 position_gcrf_km: [f64; 3],
170 velocity_gcrf_km_s: [f64; 3],
171 ) -> Result<([f64; 3], [f64; 3]), FrameTransformError> {
172 validate_vec3("position_gcrf_km", &position_gcrf_km)?;
173 validate_vec3("velocity_gcrf_km_s", &velocity_gcrf_km_s)?;
174 let position_itrf_km = mat3_vec3_mul(&self.gcrf_to_itrf, &position_gcrf_km)?;
175 let rotated_velocity = mat3_vec3_mul(&self.gcrf_to_itrf, &velocity_gcrf_km_s)?;
176 let transport = cross(self.earth_rotation_vector_itrf_rad_s, position_itrf_km);
177 Ok((position_itrf_km, sub(rotated_velocity, transport)))
178 }
179
180 pub fn itrf_to_gcrf_state_km(
185 &self,
186 position_itrf_km: [f64; 3],
187 velocity_itrf_km_s: [f64; 3],
188 ) -> Result<([f64; 3], [f64; 3]), FrameTransformError> {
189 validate_vec3("position_itrf_km", &position_itrf_km)?;
190 validate_vec3("velocity_itrf_km_s", &velocity_itrf_km_s)?;
191 let position_gcrf_km = mat3_vec3_mul(&self.itrf_to_gcrf, &position_itrf_km)?;
192 let transport = cross(self.earth_rotation_vector_itrf_rad_s, position_itrf_km);
193 let velocity_rotating = add(velocity_itrf_km_s, transport);
194 let velocity_gcrf_km_s = mat3_vec3_mul(&self.itrf_to_gcrf, &velocity_rotating)?;
195 Ok((position_gcrf_km, velocity_gcrf_km_s))
196 }
197}
198
199pub trait EarthOrientationProvider: Send + Sync {
205 fn orientation_at_tdb_seconds(
207 &self,
208 epoch_tdb_seconds: f64,
209 ) -> Result<EarthOrientation, FrameTransformError>;
210}
211
212#[derive(Debug, Clone, Copy, PartialEq)]
215pub struct TdbEarthOrientationProvider {
216 polar_motion: PolarMotion,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq)]
221pub struct PolarMotionSample {
222 pub epoch_tdb_seconds: f64,
224 pub polar_motion: PolarMotion,
226}
227
228impl PolarMotionSample {
229 pub fn from_radians(
231 epoch_tdb_seconds: f64,
232 xp_rad: f64,
233 yp_rad: f64,
234 ) -> Result<Self, FrameTransformError> {
235 if !epoch_tdb_seconds.is_finite() {
236 return Err(invalid_input("epoch_tdb_seconds", "must be finite"));
237 }
238 Ok(Self {
239 epoch_tdb_seconds,
240 polar_motion: PolarMotion::from_radians(xp_rad, yp_rad)?,
241 })
242 }
243
244 pub fn from_arcseconds(
246 epoch_tdb_seconds: f64,
247 xp_arcsec: f64,
248 yp_arcsec: f64,
249 ) -> Result<Self, FrameTransformError> {
250 if !epoch_tdb_seconds.is_finite() {
251 return Err(invalid_input("epoch_tdb_seconds", "must be finite"));
252 }
253 Ok(Self {
254 epoch_tdb_seconds,
255 polar_motion: PolarMotion::from_arcseconds(xp_arcsec, yp_arcsec)?,
256 })
257 }
258}
259
260#[derive(Debug, Clone, PartialEq)]
267pub struct PolarMotionSeriesEarthOrientationProvider {
268 samples: Box<[PolarMotionSample]>,
269}
270
271impl PolarMotionSeriesEarthOrientationProvider {
272 pub fn new(samples: Vec<PolarMotionSample>) -> Result<Self, FrameTransformError> {
274 if samples.len() < 2 {
275 return Err(invalid_input(
276 "samples",
277 "must contain at least two polar-motion samples",
278 ));
279 }
280 for window in samples.windows(2) {
281 if window[0].epoch_tdb_seconds >= window[1].epoch_tdb_seconds {
282 return Err(invalid_input(
283 "samples",
284 "epochs must be strictly increasing",
285 ));
286 }
287 }
288 Ok(Self {
289 samples: samples.into_boxed_slice(),
290 })
291 }
292
293 pub fn polar_motion_at_tdb_seconds(
295 &self,
296 epoch_tdb_seconds: f64,
297 ) -> Result<PolarMotion, FrameTransformError> {
298 if !epoch_tdb_seconds.is_finite() {
299 return Err(invalid_input("epoch_tdb_seconds", "must be finite"));
300 }
301 let first = self.samples.first().expect("validated non-empty samples");
302 let last = self.samples.last().expect("validated non-empty samples");
303 if epoch_tdb_seconds < first.epoch_tdb_seconds || epoch_tdb_seconds > last.epoch_tdb_seconds
304 {
305 return Err(invalid_input(
306 "epoch_tdb_seconds",
307 "outside polar-motion series coverage",
308 ));
309 }
310
311 match self.samples.binary_search_by(|sample| {
312 sample
313 .epoch_tdb_seconds
314 .partial_cmp(&epoch_tdb_seconds)
315 .expect("validated finite epoch")
316 }) {
317 Ok(index) => Ok(self.samples[index].polar_motion),
318 Err(index) => {
319 let before = self.samples[index - 1];
320 let after = self.samples[index];
321 let span = after.epoch_tdb_seconds - before.epoch_tdb_seconds;
322 let alpha = (epoch_tdb_seconds - before.epoch_tdb_seconds) / span;
323 PolarMotion::from_radians(
324 before.polar_motion.xp_rad
325 + alpha * (after.polar_motion.xp_rad - before.polar_motion.xp_rad),
326 before.polar_motion.yp_rad
327 + alpha * (after.polar_motion.yp_rad - before.polar_motion.yp_rad),
328 )
329 }
330 }
331 }
332}
333
334impl TdbEarthOrientationProvider {
335 pub const fn new() -> Self {
337 Self {
338 polar_motion: PolarMotion::ZERO,
339 }
340 }
341
342 pub const fn with_polar_motion(polar_motion: PolarMotion) -> Self {
344 Self { polar_motion }
345 }
346
347 pub fn polar_motion(&self) -> PolarMotion {
349 self.polar_motion
350 }
351}
352
353impl Default for TdbEarthOrientationProvider {
354 fn default() -> Self {
355 Self::new()
356 }
357}
358
359impl EarthOrientationProvider for TdbEarthOrientationProvider {
360 fn orientation_at_tdb_seconds(
361 &self,
362 epoch_tdb_seconds: f64,
363 ) -> Result<EarthOrientation, FrameTransformError> {
364 let ts = time_scales_from_scale_j2000_seconds(TimeScale::Tdb, epoch_tdb_seconds)?;
365 EarthOrientation::from_time_scales_with_polar_motion(&ts, self.polar_motion)
366 }
367}
368
369impl EarthOrientationProvider for PolarMotionSeriesEarthOrientationProvider {
370 fn orientation_at_tdb_seconds(
371 &self,
372 epoch_tdb_seconds: f64,
373 ) -> Result<EarthOrientation, FrameTransformError> {
374 let ts = time_scales_from_scale_j2000_seconds(TimeScale::Tdb, epoch_tdb_seconds)?;
375 let polar_motion = self.polar_motion_at_tdb_seconds(epoch_tdb_seconds)?;
376 EarthOrientation::from_time_scales_with_polar_motion(&ts, polar_motion)
377 }
378}
379
380fn time_scales_from_instant(epoch: Instant) -> Result<TimeScales, FrameTransformError> {
381 let seconds = match epoch.repr {
382 InstantRepr::JulianDate(jd) => j2000_seconds_from_split(jd.jd_whole, jd.fraction),
383 InstantRepr::Nanos(_) => {
384 return Err(invalid_input("epoch", "must be a split Julian date"));
385 }
386 };
387 time_scales_from_scale_j2000_seconds(epoch.scale, seconds)
388}
389
390fn time_scales_from_scale_j2000_seconds(
391 scale: TimeScale,
392 epoch_j2000_s: f64,
393) -> Result<TimeScales, FrameTransformError> {
394 if !epoch_j2000_s.is_finite() {
395 return Err(invalid_input("epoch_j2000_s", "must be finite"));
396 }
397 let whole = epoch_j2000_s.floor();
398 if whole < i64::MIN as f64 || whole > i64::MAX as f64 {
399 return Err(invalid_input(
400 "epoch_j2000_s",
401 "whole seconds are out of range",
402 ));
403 }
404 let fraction = epoch_j2000_s - whole;
405 let (year, month, day, hour, minute, second) = civil_from_j2000_seconds(whole as i64);
406 TimeScales::from_scale(
407 scale,
408 year as i32,
409 month as i32,
410 day as i32,
411 hour as i32,
412 minute as i32,
413 second as f64 + fraction,
414 )
415 .map_err(|_| invalid_input("epoch_j2000_s", "time-scale conversion failed"))
416}
417
418fn invalid_input(field: &'static str, reason: &'static str) -> FrameTransformError {
419 FrameTransformError::InvalidInput { field, reason }
420}
421
422fn validate_vec3(field: &'static str, value: &[f64; 3]) -> Result<(), FrameTransformError> {
423 if value.iter().all(|component| component.is_finite()) {
424 Ok(())
425 } else {
426 Err(invalid_input(field, "components must be finite"))
427 }
428}
429
430fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
431 [
432 a[1] * b[2] - a[2] * b[1],
433 a[2] * b[0] - a[0] * b[2],
434 a[0] * b[1] - a[1] * b[0],
435 ]
436}
437
438fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
439 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
440}
441
442fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
443 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
444}
445
446fn skew_matrix(omega: [f64; 3]) -> Mat3 {
447 [
448 [0.0, -omega[2], omega[1]],
449 [omega[2], 0.0, -omega[0]],
450 [-omega[1], omega[0], 0.0],
451 ]
452}
453
454fn neg_skew_matrix(omega: [f64; 3]) -> Mat3 {
455 [
456 [0.0, omega[2], -omega[1]],
457 [-omega[2], 0.0, omega[0]],
458 [omega[1], -omega[0], 0.0],
459 ]
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 #[test]
467 fn polar_motion_series_interpolates_and_builds_orientation() {
468 let provider = PolarMotionSeriesEarthOrientationProvider::new(vec![
469 PolarMotionSample::from_arcseconds(0.0, 0.10, -0.20).expect("sample"),
470 PolarMotionSample::from_arcseconds(10.0, 0.30, -0.10).expect("sample"),
471 ])
472 .expect("series provider");
473
474 let interpolated = provider
475 .polar_motion_at_tdb_seconds(5.0)
476 .expect("interpolated pole");
477 let expected = PolarMotion::from_arcseconds(0.20, -0.15).expect("expected pole");
478 assert!((interpolated.xp_rad - expected.xp_rad).abs() <= 1.0e-21);
479 assert!((interpolated.yp_rad - expected.yp_rad).abs() <= 1.0e-21);
480
481 let orientation = provider
482 .orientation_at_tdb_seconds(5.0)
483 .expect("series-backed orientation");
484 assert!((orientation.polar_motion().xp_rad - expected.xp_rad).abs() <= 1.0e-21);
485 assert!((orientation.polar_motion().yp_rad - expected.yp_rad).abs() <= 1.0e-21);
486 }
487
488 #[test]
489 fn polar_motion_series_rejects_bad_order_and_coverage() {
490 let unordered = PolarMotionSeriesEarthOrientationProvider::new(vec![
491 PolarMotionSample::from_arcseconds(10.0, 0.10, 0.20).expect("sample"),
492 PolarMotionSample::from_arcseconds(10.0, 0.20, 0.30).expect("sample"),
493 ]);
494 assert!(unordered.is_err());
495
496 let provider = PolarMotionSeriesEarthOrientationProvider::new(vec![
497 PolarMotionSample::from_arcseconds(0.0, 0.10, 0.20).expect("sample"),
498 PolarMotionSample::from_arcseconds(10.0, 0.20, 0.30).expect("sample"),
499 ])
500 .expect("series provider");
501 assert!(provider.polar_motion_at_tdb_seconds(-1.0).is_err());
502 assert!(provider.polar_motion_at_tdb_seconds(11.0).is_err());
503 }
504}