1use std::ops::Mul;
6
7use lox_approx::ApproxEq;
8use lox_bodies::{TryRotationalElements, UndefinedOriginPropertyError};
9use lox_core::glam::{DMat3, DVec3};
10use lox_core::{coords::Cartesian, f64::consts::ROTATION_RATE_EARTH};
11use lox_time::{
12 Time,
13 julian_dates::JulianDate,
14 offsets::{OffsetProvider, TryOffset},
15 time_scales::{ContinuousTimeScale, Tdb, Tt, Ut1},
16};
17use thiserror::Error;
18
19use crate::{
20 Iau, ReferenceFrame,
21 iers::{
22 Corrections, ReferenceSystem,
23 cio::CioLocator,
24 cip::CipCoords,
25 earth_rotation::{EarthRotationAngle, EquationOfTheEquinoxes},
26 polar_motion::PoleCoords,
27 precession::frame_bias,
28 },
29 rotations::to_iau::icrf_to_iau,
30};
31
32pub mod to_iau;
34pub mod to_icrf;
36
37pub trait TryRotation<Origin, Target, T>
39where
40 Origin: ReferenceFrame,
41 Target: ReferenceFrame,
42 T: ContinuousTimeScale,
43{
44 type Error: std::error::Error + Send + Sync + 'static;
46
47 fn try_rotation(
49 &self,
50 origin: Origin,
51 target: Target,
52 time: Time<T>,
53 ) -> Result<Rotation, Self::Error>;
54}
55
56#[derive(Debug, Error)]
59pub enum RotationError {
60 #[error("offset error: {0}")]
62 Offset(Box<dyn std::error::Error + Send + Sync + 'static>),
63 #[error("EOP error: {0}")]
65 Eop(Box<dyn std::error::Error + Send + Sync + 'static>),
66 #[error(transparent)]
68 UndefinedProperty(#[from] UndefinedOriginPropertyError),
69}
70
71impl RotationError {
72 pub fn offset(err: impl std::error::Error + Send + Sync + 'static) -> Self {
74 RotationError::Offset(Box::new(err))
75 }
76
77 pub fn eop(err: impl std::error::Error + Send + Sync + 'static) -> Self {
79 RotationError::Eop(Box::new(err))
80 }
81}
82
83pub trait RotationProvider<T: ContinuousTimeScale>: OffsetProvider {
85 type EopError: std::error::Error + Send + Sync + 'static;
87
88 fn corrections(
90 &self,
91 time: Time<T>,
92 sys: ReferenceSystem,
93 ) -> Result<Corrections, Self::EopError>;
94 fn pole_coords(&self, time: Time<T>) -> Result<PoleCoords, Self::EopError>;
96
97 fn icrf_to_iau<R>(&self, time: Time<T>, frame: Iau<R>) -> Result<Rotation, RotationError>
99 where
100 T: ContinuousTimeScale + Copy,
101 R: TryRotationalElements,
102 Self: TryOffset<T, Tdb>,
103 {
104 let seconds = time
105 .try_to_scale(Tdb, self)
106 .map_err(RotationError::offset)?
107 .seconds_since_j2000();
108 let angles = frame.rotational_elements(seconds);
109 let rates = frame.rotational_element_rates(seconds);
110
111 Ok(icrf_to_iau(angles, rates))
112 }
113 fn iau_to_icrf<R>(&self, time: Time<T>, frame: Iau<R>) -> Result<Rotation, RotationError>
115 where
116 T: ContinuousTimeScale + Copy,
117 R: TryRotationalElements,
118 Self: TryOffset<T, Tdb>,
119 {
120 Ok(self.icrf_to_iau(time, frame)?.transpose())
121 }
122
123 fn icrf_to_itrf(&self, time: Time<T>) -> Result<Rotation, RotationError>
125 where
126 T: ContinuousTimeScale + Copy,
127 Self: TryOffset<T, Tdb> + TryOffset<T, Tt> + TryOffset<T, Ut1>,
128 {
129 Ok(self
130 .icrf_to_cirf(time)?
131 .compose(self.cirf_to_tirf(time)?)
132 .compose(self.tirf_to_itrf(time)?))
133 }
134 fn itrf_to_icrf(&self, time: Time<T>) -> Result<Rotation, RotationError>
136 where
137 T: ContinuousTimeScale + Copy,
138 Self: TryOffset<T, Tdb> + TryOffset<T, Tt> + TryOffset<T, Ut1>,
139 {
140 Ok(self.icrf_to_itrf(time)?.transpose())
141 }
142
143 fn icrf_to_j2000(&self) -> Rotation {
145 Rotation::new(frame_bias())
146 }
147
148 fn j2000_to_icrf(&self) -> Rotation {
150 Rotation::new(frame_bias().transpose())
151 }
152
153 fn j2000_to_mod(&self, time: Time<T>, sys: ReferenceSystem) -> Result<Rotation, RotationError>
155 where
156 T: ContinuousTimeScale + Copy,
157 Self: TryOffset<T, Tt>,
158 {
159 let time = time.try_to_scale(Tt, self).map_err(RotationError::offset)?;
160 Ok(sys.precession_matrix(time).into())
161 }
162
163 fn mod_to_j2000(&self, time: Time<T>, sys: ReferenceSystem) -> Result<Rotation, RotationError>
165 where
166 T: ContinuousTimeScale + Copy,
167 Self: TryOffset<T, Tt>,
168 {
169 Ok(self.j2000_to_mod(time, sys)?.transpose())
170 }
171
172 fn icrf_to_mod(&self, time: Time<T>, sys: ReferenceSystem) -> Result<Rotation, RotationError>
174 where
175 T: ContinuousTimeScale + Copy,
176 Self: TryOffset<T, Tt>,
177 {
178 let time = time.try_to_scale(Tt, self).map_err(RotationError::offset)?;
179 Ok(sys.bias_precession_matrix(time).into())
180 }
181 fn mod_to_icrf(&self, time: Time<T>, sys: ReferenceSystem) -> Result<Rotation, RotationError>
183 where
184 T: ContinuousTimeScale + Copy,
185 Self: TryOffset<T, Tt>,
186 {
187 Ok(self.icrf_to_mod(time, sys)?.transpose())
188 }
189
190 fn mod_to_tod(&self, time: Time<T>, sys: ReferenceSystem) -> Result<Rotation, RotationError>
192 where
193 T: ContinuousTimeScale + Copy,
194 Self: TryOffset<T, Tdb>,
195 {
196 let tdb = time
197 .try_to_scale(Tdb, self)
198 .map_err(RotationError::offset)?;
199 let corr = self.corrections(time, sys).map_err(RotationError::eop)?;
200 Ok(sys.nutation_matrix(tdb, corr).into())
201 }
202 fn tod_to_mod(&self, time: Time<T>, sys: ReferenceSystem) -> Result<Rotation, RotationError>
204 where
205 T: ContinuousTimeScale + Copy,
206 Self: TryOffset<T, Tdb>,
207 {
208 Ok(self.mod_to_tod(time, sys)?.transpose())
209 }
210
211 fn tod_to_pef(&self, time: Time<T>, sys: ReferenceSystem) -> Result<Rotation, RotationError>
213 where
214 T: ContinuousTimeScale + Copy,
215 Self: TryOffset<T, Tt> + TryOffset<T, Ut1>,
216 {
217 let tt = time.try_to_scale(Tt, self).map_err(RotationError::offset)?;
218 let ut1 = time
219 .try_to_scale(Ut1, self)
220 .map_err(RotationError::offset)?;
221 let corr = self.corrections(time, sys).map_err(RotationError::eop)?;
222 Ok(
223 Rotation::new(sys.earth_rotation(tt, ut1, corr)).with_angular_velocity(DVec3::new(
224 0.0,
225 0.0,
226 ROTATION_RATE_EARTH,
227 )),
228 )
229 }
230 fn pef_to_tod(&self, time: Time<T>, sys: ReferenceSystem) -> Result<Rotation, RotationError>
232 where
233 T: ContinuousTimeScale + Copy,
234 Self: TryOffset<T, Tt> + TryOffset<T, Ut1>,
235 {
236 Ok(self.tod_to_pef(time, sys)?.transpose())
237 }
238
239 fn pef_to_itrf(&self, time: Time<T>, sys: ReferenceSystem) -> Result<Rotation, RotationError>
241 where
242 T: ContinuousTimeScale + Copy,
243 Self: TryOffset<T, Tt>,
244 {
245 let tt = time.try_to_scale(Tt, self).map_err(RotationError::offset)?;
246 let pole_coords = self.pole_coords(time).map_err(RotationError::eop)?;
247 Ok(sys.polar_motion_matrix(tt, pole_coords).into())
248 }
249
250 fn itrf_to_pef(&self, time: Time<T>, sys: ReferenceSystem) -> Result<Rotation, RotationError>
252 where
253 T: ContinuousTimeScale + Copy,
254 Self: TryOffset<T, Tt>,
255 {
256 Ok(self.pef_to_itrf(time, sys)?.transpose())
257 }
258
259 fn tod_to_teme(&self, time: Time<T>) -> Result<Rotation, RotationError>
261 where
262 T: ContinuousTimeScale + Copy,
263 Self: TryOffset<T, Tdb>,
264 {
265 let tdb = time
266 .try_to_scale(Tdb, self)
267 .map_err(RotationError::offset)?;
268
269 let eoe = EquationOfTheEquinoxes::iau1994(tdb);
270
271 Ok(Rotation::new(eoe.0.rotation_z()))
273 }
274
275 fn teme_to_tod(&self, time: Time<T>) -> Result<Rotation, RotationError>
277 where
278 T: ContinuousTimeScale + Copy,
279 Self: TryOffset<T, Tdb>,
280 {
281 Ok(self.tod_to_teme(time)?.transpose())
282 }
283
284 fn icrf_to_teme(&self, time: Time<T>) -> Result<Rotation, RotationError>
286 where
287 T: ContinuousTimeScale + Copy,
288 Self: TryOffset<T, Tt> + TryOffset<T, Tdb>,
289 {
290 let sys = ReferenceSystem::Iers1996;
291 let tt = time.try_to_scale(Tt, self).map_err(RotationError::offset)?;
292 let tdb = time
293 .try_to_scale(Tdb, self)
294 .map_err(RotationError::offset)?;
295 let corr = self.corrections(time, sys).map_err(RotationError::eop)?;
296
297 let icrf_to_mod = sys.bias_precession_matrix(tt);
298 let epsa = sys.mean_obliquity(tdb.with_scale(Tt));
299 let nut = sys.nutation(tdb);
300 let mod_to_tod = (nut + corr).nutation_matrix(epsa);
301 let eoe = EquationOfTheEquinoxes::iau1994_from_dpsi(tdb, nut.dpsi);
302 let tod_to_teme = eoe.0.rotation_z();
303
304 Ok(Rotation::new(tod_to_teme * mod_to_tod * icrf_to_mod))
305 }
306
307 fn teme_to_icrf(&self, time: Time<T>) -> Result<Rotation, RotationError>
309 where
310 T: ContinuousTimeScale + Copy,
311 Self: TryOffset<T, Tt> + TryOffset<T, Tdb>,
312 {
313 Ok(self.icrf_to_teme(time)?.transpose())
314 }
315
316 fn icrf_to_cirf(&self, time: Time<T>) -> Result<Rotation, RotationError>
318 where
319 T: ContinuousTimeScale + Copy,
320 Self: TryOffset<T, Tdb>,
321 {
322 let tdb = time
323 .try_to_scale(Tdb, self)
324 .map_err(RotationError::offset)?;
325 let mut xy = CipCoords::iau2006(tdb);
326 let s = CioLocator::iau2006(tdb, xy);
327
328 xy += self
329 .corrections(time, ReferenceSystem::Iers2010)
330 .map_err(RotationError::eop)?;
331
332 Ok(Rotation::new(xy.celestial_to_intermediate_matrix(s)))
333 }
334 fn cirf_to_icrf(&self, time: Time<T>) -> Result<Rotation, RotationError>
336 where
337 T: ContinuousTimeScale + Copy,
338 Self: TryOffset<T, Tdb>,
339 {
340 Ok(self.icrf_to_cirf(time)?.transpose())
341 }
342
343 fn cirf_to_tirf(&self, time: Time<T>) -> Result<Rotation, RotationError>
345 where
346 T: ContinuousTimeScale + Copy,
347 Self: TryOffset<T, Ut1>,
348 {
349 let time = time
350 .try_to_scale(Ut1, self)
351 .map_err(RotationError::offset)?;
352 let era = EarthRotationAngle::iau2000(time);
353 Ok(
354 Rotation::new(era.0.rotation_z()).with_angular_velocity(DVec3::new(
355 0.0,
356 0.0,
357 ROTATION_RATE_EARTH,
358 )),
359 )
360 }
361 fn tirf_to_cirf(&self, time: Time<T>) -> Result<Rotation, RotationError>
363 where
364 T: ContinuousTimeScale + Copy,
365 Self: TryOffset<T, Ut1>,
366 {
367 Ok(self.cirf_to_tirf(time)?.transpose())
368 }
369
370 fn tirf_to_itrf(&self, time: Time<T>) -> Result<Rotation, RotationError>
372 where
373 T: ContinuousTimeScale + Copy,
374 Self: TryOffset<T, Tt>,
375 {
376 let tt = time.try_to_scale(Tt, self).map_err(RotationError::offset)?;
377 let pole_coords = self.pole_coords(time).map_err(RotationError::eop)?;
378 Ok(ReferenceSystem::Iers2010
379 .polar_motion_matrix(tt, pole_coords)
380 .into())
381 }
382
383 fn itrf_to_tirf(&self, time: Time<T>) -> Result<Rotation, RotationError>
385 where
386 T: ContinuousTimeScale + Copy,
387 Self: TryOffset<T, Tt>,
388 {
389 Ok(self.tirf_to_itrf(time)?.transpose())
390 }
391}
392
393fn rotation_matrix_derivative(m: DMat3, v: DVec3) -> DMat3 {
394 let sx = DVec3::new(0.0, v.z, -v.y);
395 let sy = DVec3::new(-v.z, 0.0, v.x);
396 let sz = DVec3::new(v.y, -v.x, 0.0);
397 let s = DMat3::from_cols(sx, sy, sz);
398 -s * m
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, ApproxEq)]
403#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
404pub struct Rotation {
405 pub m: DMat3,
407 pub dm: DMat3,
409}
410
411impl Rotation {
412 pub const IDENTITY: Self = Self {
414 m: DMat3::IDENTITY,
415 dm: DMat3::ZERO,
416 };
417
418 pub fn new(m: DMat3) -> Self {
420 Self { m, dm: DMat3::ZERO }
421 }
422
423 pub fn with_derivative(mut self, dm: DMat3) -> Self {
425 self.dm = dm;
426 self
427 }
428
429 pub fn with_angular_velocity(mut self, v: DVec3) -> Self {
431 self.dm = rotation_matrix_derivative(self.m, v);
432 self
433 }
434
435 pub fn position_matrix(&self) -> DMat3 {
437 self.m
438 }
439
440 pub fn velocity_matrix(&self) -> DMat3 {
442 self.dm
443 }
444
445 pub fn compose(self, other: Self) -> Self {
447 Self {
448 m: other.m * self.m,
449 dm: other.dm * self.m + other.m * self.dm,
450 }
451 }
452
453 pub fn transpose(&self) -> Self {
455 let m = self.m.transpose();
456 let dm = self.dm.transpose();
457 Self { m, dm }
458 }
459
460 pub fn rotate_position(&self, pos: DVec3) -> DVec3 {
462 self.m * pos
463 }
464
465 pub fn rotate_velocity(&self, pos: DVec3, vel: DVec3) -> DVec3 {
467 self.dm * pos + self.m * vel
468 }
469
470 pub fn rotate_state(&self, pos: DVec3, vel: DVec3) -> (DVec3, DVec3) {
472 (self.rotate_position(pos), self.rotate_velocity(pos, vel))
473 }
474}
475
476impl Default for Rotation {
477 fn default() -> Self {
478 Self {
479 m: DMat3::IDENTITY,
480 dm: DMat3::ZERO,
481 }
482 }
483}
484
485impl Mul<DVec3> for Rotation {
486 type Output = DVec3;
487
488 fn mul(self, rhs: DVec3) -> Self::Output {
489 self.m * rhs
490 }
491}
492
493impl Mul<Cartesian> for Rotation {
494 type Output = Cartesian;
495
496 fn mul(self, rhs: Cartesian) -> Self::Output {
497 let pos = self.m * rhs.position();
498 let vel = self.dm * rhs.position() + self.m * rhs.velocity();
499 Cartesian::from_vecs(pos, vel)
500 }
501}
502
503impl From<DMat3> for Rotation {
504 fn from(matrix: DMat3) -> Self {
505 Rotation::new(matrix)
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use std::convert::Infallible;
512
513 use lox_approx::assert_approx_eq;
514 use lox_time::{deltas::TimeDelta, offsets::OffsetProvider};
515 use lox_units::AngleUnits;
516
517 use crate::iers::Iau2000Model;
518
519 use super::*;
520
521 #[derive(Debug)]
522 struct TestRotationProvider;
523
524 impl OffsetProvider for TestRotationProvider {
525 type Error = Infallible;
526
527 fn tai_to_ut1(&self, _delta: TimeDelta) -> Result<TimeDelta, Self::Error> {
528 Ok(TimeDelta::from_seconds_f64(-33.072073684954375))
529 }
530
531 fn ut1_to_tai(&self, _delta: TimeDelta) -> Result<TimeDelta, Self::Error> {
532 unreachable!()
533 }
534 }
535
536 impl<T> RotationProvider<T> for TestRotationProvider
537 where
538 T: ContinuousTimeScale,
539 {
540 type EopError = Infallible;
541
542 fn corrections(
543 &self,
544 _time: Time<T>,
545 sys: ReferenceSystem,
546 ) -> Result<Corrections, Infallible> {
547 match sys {
548 ReferenceSystem::Iers1996 => {
549 Ok(Corrections(-55.0655e-3.arcsec(), -6.3580e-3.arcsec()))
550 }
551 ReferenceSystem::Iers2003(_) => {
552 Ok(Corrections(0.1725e-3.arcsec(), -0.2650e-3.arcsec()))
553 }
554 ReferenceSystem::Iers2010 => {
555 Ok(Corrections(0.1750e-3.arcsec(), -0.2259e-3.arcsec()))
556 }
557 }
558 }
559
560 fn pole_coords(&self, _time: Time<T>) -> Result<PoleCoords, Infallible> {
561 Ok(PoleCoords {
562 xp: 0.0349282.arcsec(),
563 yp: 0.4833163.arcsec(),
564 })
565 }
566 }
567
568 #[test]
569 fn test_celestial_to_terrestrial_iers1996() {
570 let tt = Time::from_two_part_julian_date(Tt, 2454195.5, 0.500754444444444);
571 let sys = ReferenceSystem::Iers1996;
572
573 let npb_exp = DMat3::from_cols_array(&[
574 0.999998403176203,
575 -0.001639032970562,
576 -0.000712190961847,
577 0.001639000942243,
578 0.999998655799521,
579 -0.000045552846624,
580 0.000712264667137,
581 0.000044385492226,
582 0.999999745354454,
583 ])
584 .transpose();
585 let c2t_exp = DMat3::from_cols_array(&[
586 0.973104317592265,
587 0.230363826166883,
588 -0.000703332813776,
589 -0.230363798723533,
590 0.973104570754697,
591 0.000120888299841,
592 0.000712264667137,
593 0.000044385492226,
594 0.999999745354454,
595 ])
596 .transpose();
597 let c2t_pm_exp = DMat3::from_cols_array(&[
598 0.973104317712772,
599 0.230363826174782,
600 -0.000703163477127,
601 -0.230363800391868,
602 0.973104570648022,
603 0.000118545116892,
604 0.000711560100206,
605 0.000046626645796,
606 0.999999745754058,
607 ])
608 .transpose();
609
610 let npb_act = TestRotationProvider
611 .j2000_to_mod(tt.with_scale(Tdb), sys)
612 .unwrap()
613 .compose(
614 TestRotationProvider
615 .mod_to_tod(tt.with_scale(Tdb), sys)
616 .unwrap(),
617 );
618 assert_approx_eq!(npb_act.m, npb_exp, atol <= 1e-12);
619
620 let c2t_act = npb_act.compose(TestRotationProvider.tod_to_pef(tt, sys).unwrap());
621 assert_approx_eq!(c2t_act.m, c2t_exp, atol <= 1e-12);
622
623 let c2t_pm_act = c2t_act.compose(TestRotationProvider.pef_to_itrf(tt, sys).unwrap());
624 assert_approx_eq!(c2t_pm_act.m, c2t_pm_exp, atol <= 1e-12);
625 }
626
627 #[test]
628 fn test_celestial_to_terrestrial_iers2003() {
629 let tt = Time::from_two_part_julian_date(Tt, 2454195.5, 0.500754444444444);
630 let sys = ReferenceSystem::Iers2003(Iau2000Model::A);
631
632 let npb_exp = DMat3::from_cols_array(&[
633 0.999998402755640,
634 -0.001639289519579,
635 -0.000712191013215,
636 0.001639257491365,
637 0.999998655379006,
638 -0.000045552787478,
639 0.000712264729795,
640 0.000044385250265,
641 0.999999745354420,
642 ])
643 .transpose();
644 let c2t_exp = DMat3::from_cols_array(&[
645 0.973104317573209,
646 0.230363826247361,
647 -0.000703332818999,
648 -0.230363798803834,
649 0.973104570735656,
650 0.000120888549787,
651 0.000712264729795,
652 0.000044385250265,
653 0.999999745354420,
654 ])
655 .transpose();
656 let c2t_pm_exp = DMat3::from_cols_array(&[
657 0.973104317697618,
658 0.230363826238780,
659 -0.000703163482352,
660 -0.230363800455689,
661 0.973104570632883,
662 0.000118545366826,
663 0.000711560162864,
664 0.000046626403835,
665 0.999999745754024,
666 ])
667 .transpose();
668
669 let npb_act = TestRotationProvider
670 .icrf_to_mod(tt, sys)
671 .unwrap()
672 .compose(TestRotationProvider.mod_to_tod(tt, sys).unwrap());
673 assert_approx_eq!(npb_act.m, npb_exp, atol <= 1e-12);
674
675 let c2t_act = npb_act.compose(TestRotationProvider.tod_to_pef(tt, sys).unwrap());
676 assert_approx_eq!(c2t_act.m, c2t_exp, atol <= 1e-12);
677
678 let c2t_pm_act = c2t_act.compose(TestRotationProvider.pef_to_itrf(tt, sys).unwrap());
679 assert_approx_eq!(c2t_pm_act.m, c2t_pm_exp, atol <= 1e-12);
680 }
681
682 #[test]
683 fn test_celestial_to_terrestrial_iau2006() {
684 let tt = Time::from_two_part_julian_date(Tt, 2454195.5, 0.500754444444444);
685
686 let npb_exp = DMat3::from_cols_array(&[
687 0.999999746339445,
688 -0.000000005138822,
689 -0.000712264730072,
690 -0.000000026475227,
691 0.999999999014975,
692 -0.000044385242827,
693 0.000712264729599,
694 0.000044385250426,
695 0.999999745354420,
696 ])
697 .transpose();
698 let c2t_exp = DMat3::from_cols_array(&[
699 0.973104317573127,
700 0.230363826247709,
701 -0.000703332818845,
702 -0.230363798804182,
703 0.973104570735574,
704 0.000120888549586,
705 0.000712264729599,
706 0.000044385250426,
707 0.999999745354420,
708 ])
709 .transpose();
710 let c2t_pm_exp = DMat3::from_cols_array(&[
711 0.973104317697535,
712 0.230363826239128,
713 -0.000703163482198,
714 -0.230363800456037,
715 0.973104570632801,
716 0.000118545366625,
717 0.000711560162668,
718 0.000046626403995,
719 0.999999745754024,
720 ])
721 .transpose();
722
723 let npb_act = TestRotationProvider.icrf_to_cirf(tt).unwrap();
724 assert_approx_eq!(npb_act.m, npb_exp, atol <= 1e-11);
725
726 let c2t_act = npb_act.compose(TestRotationProvider.cirf_to_tirf(tt).unwrap());
727 assert_approx_eq!(c2t_act.m, c2t_exp, atol <= 1e-11);
728
729 let c2t_pm_act = c2t_act.compose(TestRotationProvider.tirf_to_itrf(tt).unwrap());
730 assert_approx_eq!(c2t_pm_act.m, c2t_pm_exp, atol <= 1e-11);
731 }
732
733 #[test]
734 fn test_tod_to_teme() {
735 let tdb = Time::from_two_part_julian_date(Tdb, 2400000.5, 41234.0);
738 let eoe: f64 = 5.357_758_254_609_257e-5; let rotation = TestRotationProvider.tod_to_teme(tdb).unwrap();
741
742 let (sin_eoe, cos_eoe) = eoe.sin_cos();
747 let expected =
748 DMat3::from_cols_array(&[cos_eoe, sin_eoe, 0.0, -sin_eoe, cos_eoe, 0.0, 0.0, 0.0, 1.0])
749 .transpose();
750
751 assert_approx_eq!(rotation.m, expected, atol <= 1e-15);
752
753 let roundtrip = rotation.compose(TestRotationProvider.teme_to_tod(tdb).unwrap());
755 assert_approx_eq!(roundtrip.m, DMat3::IDENTITY, atol <= 1e-15);
756 }
757
758 #[test]
759 fn test_teme_icrf_roundtrip() {
760 let tt = Time::from_two_part_julian_date(Tt, 2454195.5, 0.500754444444444);
763 let sys = ReferenceSystem::Iers1996;
764
765 let icrf_to_mod = TestRotationProvider.icrf_to_mod(tt, sys).unwrap();
767 let mod_to_tod = TestRotationProvider.mod_to_tod(tt, sys).unwrap();
768 let tod_to_teme = TestRotationProvider.tod_to_teme(tt).unwrap();
769
770 let icrf_to_teme = icrf_to_mod.compose(mod_to_tod).compose(tod_to_teme);
771
772 let teme_to_tod = TestRotationProvider.teme_to_tod(tt).unwrap();
774 let tod_to_mod = TestRotationProvider.tod_to_mod(tt, sys).unwrap();
775 let mod_to_icrf = TestRotationProvider.mod_to_icrf(tt, sys).unwrap();
776
777 let teme_to_icrf = teme_to_tod.compose(tod_to_mod).compose(mod_to_icrf);
778
779 let roundtrip = icrf_to_teme.compose(teme_to_icrf);
781 assert_approx_eq!(roundtrip.m, DMat3::IDENTITY, atol <= 1e-14);
782 }
783
784 #[test]
785 fn test_icrf_to_teme_fused_matches_composed() {
786 let tt = Time::from_two_part_julian_date(Tt, 2454195.5, 0.500754444444444);
787 let sys = ReferenceSystem::Iers1996;
788
789 let fused = TestRotationProvider.icrf_to_teme(tt).unwrap();
790 let composed = TestRotationProvider
791 .icrf_to_mod(tt, sys)
792 .unwrap()
793 .compose(TestRotationProvider.mod_to_tod(tt, sys).unwrap())
794 .compose(TestRotationProvider.tod_to_teme(tt).unwrap());
795
796 assert_approx_eq!(fused.m, composed.m, atol <= 1e-15);
797 assert_approx_eq!(fused.dm, composed.dm, atol <= 1e-15);
798 }
799
800 #[test]
801 fn test_angular_velocity_derivative() {
802 let omega = DVec3::new(0.1, 0.2, 0.3);
807 let rotation = Rotation::new(DMat3::IDENTITY).with_angular_velocity(omega);
808
809 assert_approx_eq!(rotation.dm, -rotation.dm.transpose(), atol <= 1e-15);
810
811 for p in [
812 DVec3::new(1.0, 0.0, 0.0),
813 DVec3::new(0.0, 1.0, 0.0),
814 DVec3::new(0.0, 0.0, 1.0),
815 DVec3::new(-2.0, 3.5, 7.0),
816 ] {
817 assert_approx_eq!(rotation.dm * p, -omega.cross(p), atol <= 1e-15);
818 }
819 }
820
821 #[test]
822 fn test_icrf_j2000_roundtrip() {
823 let fwd =
824 <TestRotationProvider as RotationProvider<Tt>>::icrf_to_j2000(&TestRotationProvider);
825 let rev =
826 <TestRotationProvider as RotationProvider<Tt>>::j2000_to_icrf(&TestRotationProvider);
827 let roundtrip = fwd.compose(rev);
828 assert_approx_eq!(roundtrip.m, DMat3::IDENTITY, atol <= 1e-15);
829 }
830
831 #[test]
832 fn test_j2000_mod_equivalence_iers1996() {
833 let tt = Time::from_two_part_julian_date(Tt, 2454195.5, 0.500754444444444);
834 let sys = ReferenceSystem::Iers1996;
835
836 let via_j2000 =
838 <TestRotationProvider as RotationProvider<Tt>>::icrf_to_j2000(&TestRotationProvider)
839 .compose(TestRotationProvider.j2000_to_mod(tt, sys).unwrap());
840 let direct = TestRotationProvider.icrf_to_mod(tt, sys).unwrap();
841
842 assert_approx_eq!(via_j2000.m, direct.m, atol <= 1e-14);
843 }
844
845 #[test]
846 fn test_j2000_mod_equivalence_iers2003() {
847 let tt = Time::from_two_part_julian_date(Tt, 2454195.5, 0.500754444444444);
848 let sys = ReferenceSystem::Iers2003(Iau2000Model::A);
849
850 let via_j2000 =
851 <TestRotationProvider as RotationProvider<Tt>>::icrf_to_j2000(&TestRotationProvider)
852 .compose(TestRotationProvider.j2000_to_mod(tt, sys).unwrap());
853 let direct = TestRotationProvider.icrf_to_mod(tt, sys).unwrap();
854
855 assert_approx_eq!(via_j2000.m, direct.m, atol <= 1e-14);
856 }
857
858 #[test]
859 fn test_j2000_mod_equivalence_iers2010() {
860 let tt = Time::from_two_part_julian_date(Tt, 2454195.5, 0.500754444444444);
861 let sys = ReferenceSystem::Iers2010;
862
863 let via_j2000 =
864 <TestRotationProvider as RotationProvider<Tt>>::icrf_to_j2000(&TestRotationProvider)
865 .compose(TestRotationProvider.j2000_to_mod(tt, sys).unwrap());
866 let direct = TestRotationProvider.icrf_to_mod(tt, sys).unwrap();
867
868 assert_approx_eq!(via_j2000.m, direct.m, atol <= 1e-14);
869 }
870
871 #[test]
872 fn test_j2000_full_chain_equivalence() {
873 let tt = Time::from_two_part_julian_date(Tt, 2454195.5, 0.500754444444444);
875 let sys = ReferenceSystem::Iers2003(Iau2000Model::A);
876
877 let via_j2000 =
878 <TestRotationProvider as RotationProvider<Tt>>::icrf_to_j2000(&TestRotationProvider)
879 .compose(TestRotationProvider.j2000_to_mod(tt, sys).unwrap())
880 .compose(TestRotationProvider.mod_to_tod(tt, sys).unwrap())
881 .compose(TestRotationProvider.tod_to_pef(tt, sys).unwrap())
882 .compose(TestRotationProvider.pef_to_itrf(tt, sys).unwrap());
883
884 let direct = TestRotationProvider
885 .icrf_to_mod(tt, sys)
886 .unwrap()
887 .compose(TestRotationProvider.mod_to_tod(tt, sys).unwrap())
888 .compose(TestRotationProvider.tod_to_pef(tt, sys).unwrap())
889 .compose(TestRotationProvider.pef_to_itrf(tt, sys).unwrap());
890
891 assert_approx_eq!(via_j2000.m, direct.m, atol <= 1e-14);
892 }
893}