1use std::f64::consts::PI;
38
39use super::{
40 gcrf_to_itrf_state, gcrf_to_itrf_state_approx, itrf_to_gcrf_state, itrf_to_gcrf_state_approx,
41};
42use super::{
43 qcirs2gcrs, qitrf2gcrf, qitrf2gcrf_approx, qitrf2tirs, qteme2itrf, qtirs2cirs, Error, Result,
44};
45use crate::frames::Frame;
46use crate::mathtypes::{Quaternion, Vector3};
47use crate::TimeLike;
48
49const ASEC2RAD: f64 = PI / 180.0 / 3600.0;
50
51const FRAME_BIAS_DALPHA0_AS: f64 = -0.014600;
70const FRAME_BIAS_XI0_AS: f64 = -0.016617;
71const FRAME_BIAS_ETA0_AS: f64 = -0.006819;
72
73fn qeme2000_to_gcrf() -> Quaternion {
79 let dalpha0 = FRAME_BIAS_DALPHA0_AS * ASEC2RAD;
80 let xi0 = FRAME_BIAS_XI0_AS * ASEC2RAD;
81 let eta0 = FRAME_BIAS_ETA0_AS * ASEC2RAD;
82 Quaternion::rotz(dalpha0) * Quaternion::roty(xi0) * Quaternion::rotx(-eta0)
83}
84
85fn is_earth_rotating(f: Frame) -> bool {
91 match f {
92 Frame::ITRF | Frame::TIRS => true,
93 Frame::CIRS
94 | Frame::GCRF
95 | Frame::TEME
96 | Frame::EME2000
97 | Frame::ICRF
98 | Frame::LVLH
99 | Frame::RTN
100 | Frame::NTW => false,
101 }
102}
103
104fn is_orbit_dependent(f: Frame) -> bool {
108 match f {
109 Frame::LVLH | Frame::RTN | Frame::NTW => true,
110 Frame::ITRF
111 | Frame::TIRS
112 | Frame::CIRS
113 | Frame::GCRF
114 | Frame::TEME
115 | Frame::EME2000
116 | Frame::ICRF => false,
117 }
118}
119
120fn frame_order(f: Frame) -> u8 {
126 match f {
127 Frame::ITRF => 0,
128 Frame::TIRS => 1,
129 Frame::CIRS => 2,
130 Frame::GCRF => 3,
131 Frame::TEME => 4,
132 Frame::EME2000 => 5,
133 Frame::ICRF => 6,
134 Frame::LVLH => 7,
135 Frame::RTN => 8,
136 Frame::NTW => 9,
137 }
138}
139
140fn canonicalise(from: Frame, to: Frame) -> (Frame, Frame, bool) {
142 if frame_order(from) <= frame_order(to) {
143 (from, to, false)
144 } else {
145 (to, from, true)
146 }
147}
148
149pub fn rotation<T: TimeLike>(from: Frame, to: Frame, t: &T) -> Result<Quaternion> {
169 if from == to {
170 return Ok(Quaternion::identity());
171 }
172 let (a, b, reversed) = canonicalise(from, to);
173 let q = canonical_rotation(a, b, t)?;
174 Ok(if reversed { q.conjugate() } else { q })
175}
176
177pub fn rotation_with_state<T: TimeLike>(
201 from: Frame,
202 to: Frame,
203 t: &T,
204 pos_gcrf: &Vector3,
205 vel_gcrf: &Vector3,
206) -> Result<Quaternion> {
207 if from == to {
208 return Ok(Quaternion::identity());
209 }
210 let is_orbit_frame = |f: Frame| -> bool {
213 match f {
214 Frame::LVLH | Frame::RTN | Frame::NTW => true,
215 Frame::ITRF
216 | Frame::TIRS
217 | Frame::CIRS
218 | Frame::GCRF
219 | Frame::TEME
220 | Frame::EME2000
221 | Frame::ICRF => false,
222 }
223 };
224 if !is_orbit_frame(from) && !is_orbit_frame(to) {
229 return rotation(from, to, t);
230 }
231 let to_gcrf_q = |f: Frame| -> Result<Quaternion> {
233 if is_orbit_frame(f) {
234 Ok(Quaternion::from_rotation_matrix(&super::to_gcrf(
235 f, pos_gcrf, vel_gcrf,
236 )?))
237 } else {
238 rotation(f, Frame::GCRF, t)
239 }
240 };
241 let q_from = to_gcrf_q(from)?;
243 let q_to = to_gcrf_q(to)?;
244 Ok(q_to.conjugate() * q_from)
245}
246
247pub fn rotation_approx<T: TimeLike>(from: Frame, to: Frame, t: &T) -> Result<Quaternion> {
257 if from == to {
258 return Ok(Quaternion::identity());
259 }
260 reject_for_approx(from)?;
261 reject_for_approx(to)?;
262 let (a, b, reversed) = canonicalise(from, to);
263 let q = canonical_rotation_approx(a, b, t)?;
264 Ok(if reversed { q.conjugate() } else { q })
265}
266
267pub fn transform_state<T: TimeLike>(
281 from: Frame,
282 to: Frame,
283 t: &T,
284 pos: &Vector3,
285 vel: &Vector3,
286) -> Result<(Vector3, Vector3)> {
287 if from == to {
288 return Ok((*pos, *vel));
289 }
290 state_dispatch(from, to, t, pos, vel, false)
291}
292
293pub fn transform_state_approx<T: TimeLike>(
300 from: Frame,
301 to: Frame,
302 t: &T,
303 pos: &Vector3,
304 vel: &Vector3,
305) -> Result<(Vector3, Vector3)> {
306 if from == to {
307 return Ok((*pos, *vel));
308 }
309 reject_for_approx(from)?;
310 reject_for_approx(to)?;
311 state_dispatch(from, to, t, pos, vel, true)
312}
313
314fn reject_for_approx(frame: Frame) -> Result<()> {
319 match frame {
320 Frame::TIRS | Frame::CIRS => Err(Error::ApproxNotSupportedForFrame { frame }),
321 _ => Ok(()),
322 }
323}
324
325fn canonical_rotation<T: TimeLike>(from: Frame, to: Frame, t: &T) -> Result<Quaternion> {
328 use Frame::*;
329 let q = match (from, to) {
330 (ITRF, TIRS) => qitrf2tirs(t),
332 (TIRS, CIRS) => qtirs2cirs(t),
333 (CIRS, GCRF) => qcirs2gcrs(t),
334 (ITRF, TEME) => qteme2itrf(t).conjugate(),
335 (GCRF, EME2000) => qeme2000_to_gcrf().conjugate(),
336 (GCRF, ICRF) => Quaternion::identity(),
337
338 (ITRF, GCRF) => qitrf2gcrf(t),
340
341 (ITRF, CIRS) => qtirs2cirs(t) * qitrf2tirs(t),
343 (TIRS, GCRF) => qcirs2gcrs(t) * qtirs2cirs(t),
344 (TIRS, TEME) => (qitrf2tirs(t) * qteme2itrf(t)).conjugate(),
348 (TIRS, ICRF) => qcirs2gcrs(t) * qtirs2cirs(t),
349 (CIRS, ICRF) => qcirs2gcrs(t),
350 (EME2000, ICRF) => qeme2000_to_gcrf(),
351
352 (CIRS, TEME) => (qtirs2cirs(t) * qitrf2tirs(t) * qteme2itrf(t)).conjugate(),
356 (ITRF, EME2000) => qeme2000_to_gcrf().conjugate() * qitrf2gcrf(t),
357 (ITRF, ICRF) => qitrf2gcrf(t),
358 (TIRS, EME2000) => qeme2000_to_gcrf().conjugate() * qcirs2gcrs(t) * qtirs2cirs(t),
359 (CIRS, EME2000) => qeme2000_to_gcrf().conjugate() * qcirs2gcrs(t),
360 (GCRF, TEME) => (qitrf2gcrf(t) * qteme2itrf(t)).conjugate(),
366
367 (TEME, EME2000) => qeme2000_to_gcrf().conjugate() * qitrf2gcrf(t) * qteme2itrf(t),
369 (TEME, ICRF) => qitrf2gcrf(t) * qteme2itrf(t),
370
371 (LVLH | RTN | NTW, _) | (_, LVLH | RTN | NTW) => {
373 return Err(Error::OrbitFrameRequiresState { from, to });
374 }
375
376 _ => unreachable!("non-canonical pair reached canonical_rotation: ({from}, {to})"),
379 };
380 Ok(q)
381}
382
383fn canonical_rotation_approx<T: TimeLike>(from: Frame, to: Frame, t: &T) -> Result<Quaternion> {
386 use Frame::*;
387 let q = match (from, to) {
388 (ITRF, GCRF) => qitrf2gcrf_approx(t),
389 (ITRF, TEME) => qteme2itrf(t).conjugate(),
390 (ITRF, EME2000) => qeme2000_to_gcrf().conjugate() * qitrf2gcrf_approx(t),
391 (ITRF, ICRF) => qitrf2gcrf_approx(t),
392
393 (GCRF, EME2000) => qeme2000_to_gcrf().conjugate(),
394 (GCRF, ICRF) => Quaternion::identity(),
395 (GCRF, TEME) => (qitrf2gcrf_approx(t) * qteme2itrf(t)).conjugate(),
397
398 (EME2000, ICRF) => qeme2000_to_gcrf(),
399 (TEME, EME2000) => qeme2000_to_gcrf().conjugate() * qitrf2gcrf_approx(t) * qteme2itrf(t),
400 (TEME, ICRF) => qitrf2gcrf_approx(t) * qteme2itrf(t),
401
402 (LVLH | RTN | NTW, _) | (_, LVLH | RTN | NTW) => {
405 return Err(Error::OrbitFrameRequiresState { from, to });
406 }
407
408 _ => unreachable!("non-canonical pair reached canonical_rotation_approx: ({from}, {to})"),
409 };
410 Ok(q)
411}
412
413fn state_dispatch<T: TimeLike>(
433 from: Frame,
434 to: Frame,
435 t: &T,
436 pos: &Vector3,
437 vel: &Vector3,
438 approx: bool,
439) -> Result<(Vector3, Vector3)> {
440 use Frame::*;
441
442 if is_orbit_dependent(from) || is_orbit_dependent(to) {
445 return Err(Error::OrbitFrameRequiresState { from, to });
446 }
447
448 let is_rotating = is_earth_rotating;
449
450 if !is_rotating(from) && !is_rotating(to) {
452 let q = if approx {
453 rotation_approx(from, to, t)?
454 } else {
455 rotation(from, to, t)?
456 };
457 return Ok((q * *pos, q * *vel));
458 }
459
460 if is_rotating(from) && is_rotating(to) {
463 let q = rotation(from, to, t)?;
467 return Ok((q * *pos, q * *vel));
468 }
469
470 if is_rotating(from) {
472 let (p_itrf, v_itrf) = if from == ITRF {
475 (*pos, *vel)
476 } else {
477 let q = rotation(TIRS, ITRF, t)?;
479 (q * *pos, q * *vel)
480 };
481 let (p_gcrf, v_gcrf) = if approx {
483 itrf_to_gcrf_state_approx(&p_itrf, &v_itrf, t)
484 } else {
485 itrf_to_gcrf_state(&p_itrf, &v_itrf, t)
486 };
487 let q = if approx {
489 rotation_approx(GCRF, to, t)?
490 } else {
491 rotation(GCRF, to, t)?
492 };
493 return Ok((q * p_gcrf, q * v_gcrf));
494 }
495 let q = if approx {
498 rotation_approx(from, GCRF, t)?
499 } else {
500 rotation(from, GCRF, t)?
501 };
502 let p_gcrf = q * *pos;
503 let v_gcrf = q * *vel;
504 let (p_itrf, v_itrf) = if approx {
506 gcrf_to_itrf_state_approx(&p_gcrf, &v_gcrf, t)
507 } else {
508 gcrf_to_itrf_state(&p_gcrf, &v_gcrf, t)
509 };
510 if to == ITRF {
512 Ok((p_itrf, v_itrf))
513 } else {
514 let q_itrf_to_tirs = rotation(ITRF, TIRS, t)?;
516 Ok((q_itrf_to_tirs * p_itrf, q_itrf_to_tirs * v_itrf))
517 }
518}
519
520#[cfg(test)]
523mod tests {
524 use super::super::qgcrf2itrf;
525 use super::*;
526 use crate::Instant;
527
528 fn t() -> Instant {
529 Instant::from_datetime(2026, 5, 22, 12, 0, 0.0).unwrap()
530 }
531
532 #[test]
533 fn identity_pairs() {
534 let tm = t();
535 for f in [
536 Frame::ITRF,
537 Frame::TIRS,
538 Frame::CIRS,
539 Frame::GCRF,
540 Frame::TEME,
541 Frame::EME2000,
542 Frame::ICRF,
543 ] {
544 let q = rotation(f, f, &tm).unwrap();
545 assert!((q.w - 1.0).abs() < 1e-15, "{f}: w={}", q.w);
546 }
547 }
548
549 #[test]
550 fn matches_qitrf2gcrf() {
551 let tm = t();
552 let q_dispatch = rotation(Frame::ITRF, Frame::GCRF, &tm).unwrap();
553 let q_direct = qitrf2gcrf(&tm);
554 let v = numeris::vector![1000.0, 2000.0, 3000.0];
555 let v_dispatch = q_dispatch * v;
556 let v_direct = q_direct * v;
557 assert!(
558 (v_dispatch - v_direct).norm() < 1e-9,
559 "dispatch={v_dispatch:?} direct={v_direct:?}"
560 );
561 }
562
563 #[test]
564 fn matches_qgcrf2itrf() {
565 let tm = t();
566 let q_dispatch = rotation(Frame::GCRF, Frame::ITRF, &tm).unwrap();
567 let q_direct = qgcrf2itrf(&tm);
568 let v = numeris::vector![1000.0, 2000.0, 3000.0];
569 let v_dispatch = q_dispatch * v;
570 let v_direct = q_direct * v;
571 assert!((v_dispatch - v_direct).norm() < 1e-9);
572 }
573
574 #[test]
575 fn matches_qitrf2tirs_direct_path() {
576 let tm = t();
578 let q_dispatch = rotation(Frame::ITRF, Frame::TIRS, &tm).unwrap();
579 let q_direct = qitrf2tirs(&tm);
580 let v = numeris::vector![1000.0, 2000.0, 3000.0];
581 assert!((q_dispatch * v - q_direct * v).norm() < 1e-12);
582 }
583
584 #[test]
585 fn matches_qteme2itrf() {
586 let tm = t();
587 let q_dispatch = rotation(Frame::TEME, Frame::ITRF, &tm).unwrap();
588 let q_direct = qteme2itrf(&tm);
589 let v = numeris::vector![1000.0, 2000.0, 3000.0];
590 assert!((q_dispatch * v - q_direct * v).norm() < 1e-12);
591 }
592
593 #[test]
599 fn dispatch_teme_pairs_have_correct_direction() {
600 use super::super::qteme2gcrf;
601 let tm = t();
602 let v = numeris::vector![7000e3_f64, 1000e3, 2000e3];
603
604 let q_teme_to_gcrf_ref = qteme2gcrf(&tm);
610
611 let q_dispatch = rotation_approx(Frame::TEME, Frame::GCRF, &tm).unwrap();
614 let lhs = q_dispatch * v;
615 let rhs = q_teme_to_gcrf_ref * v;
616 assert!(
617 (lhs - rhs).norm() / v.norm() < 1e-12,
618 "rotation_approx(TEME,GCRF) direction mismatch: dispatch={lhs:?} ref={rhs:?}"
619 );
620
621 let q_dispatch = rotation_approx(Frame::GCRF, Frame::TEME, &tm).unwrap();
623 let lhs = q_dispatch * v;
624 let rhs = q_teme_to_gcrf_ref.conjugate() * v;
625 assert!(
626 (lhs - rhs).norm() / v.norm() < 1e-12,
627 "rotation_approx(GCRF,TEME) direction mismatch: dispatch={lhs:?} ref={rhs:?}"
628 );
629
630 let q_dispatch = rotation(Frame::TEME, Frame::GCRF, &tm).unwrap();
635 let lhs = q_dispatch * v;
636 let rhs = q_teme_to_gcrf_ref * v;
637 assert!(
638 (lhs - rhs).norm() < 100.0,
639 "rotation(TEME,GCRF) direction mismatch (>100 m): \
640 dispatch={lhs:?} approx_ref={rhs:?}"
641 );
642
643 let q_dispatch = rotation(Frame::GCRF, Frame::TEME, &tm).unwrap();
645 let lhs = q_dispatch * v;
646 let rhs = q_teme_to_gcrf_ref.conjugate() * v;
647 assert!(
648 (lhs - rhs).norm() < 100.0,
649 "rotation(GCRF,TEME) direction mismatch (>100 m): \
650 dispatch={lhs:?} approx_ref={rhs:?}"
651 );
652
653 let v_teme = v;
661 let lhs = rotation(Frame::TEME, Frame::TIRS, &tm).unwrap() * v_teme;
662 let rhs = qitrf2tirs(&tm) * (qteme2itrf(&tm) * v_teme);
663 assert!(
664 (lhs - rhs).norm() / v.norm() < 1e-12,
665 "rotation(TEME,TIRS) direction mismatch: dispatch={lhs:?} ref={rhs:?}"
666 );
667
668 let lhs = rotation(Frame::TEME, Frame::CIRS, &tm).unwrap() * v_teme;
669 let rhs = qtirs2cirs(&tm) * (qitrf2tirs(&tm) * (qteme2itrf(&tm) * v_teme));
670 assert!(
671 (lhs - rhs).norm() / v.norm() < 1e-12,
672 "rotation(TEME,CIRS) direction mismatch: dispatch={lhs:?} ref={rhs:?}"
673 );
674 }
675
676 #[test]
677 fn roundtrip_all_pairs() {
678 let tm = t();
679 let v = numeris::vector![6378.0, 2000.0, 3000.0];
680 let frames = [
681 Frame::ITRF,
682 Frame::TIRS,
683 Frame::CIRS,
684 Frame::GCRF,
685 Frame::TEME,
686 Frame::EME2000,
687 Frame::ICRF,
688 ];
689 for &a in &frames {
690 for &b in &frames {
691 let q_ab = rotation(a, b, &tm).unwrap();
692 let q_ba = rotation(b, a, &tm).unwrap();
693 let v_round = q_ba * (q_ab * v);
694 let err = (v_round - v).norm() / v.norm();
695 assert!(err < 1e-12, "({a} → {b} → {a}) error {err}");
696 }
697 }
698 }
699
700 #[test]
701 fn approx_rejects_intermediate_frames() {
702 let tm = t();
703 for f in [Frame::TIRS, Frame::CIRS] {
704 let err = rotation_approx(f, Frame::GCRF, &tm).unwrap_err();
705 assert!(matches!(err, Error::ApproxNotSupportedForFrame { frame } if frame == f));
706 let err = rotation_approx(Frame::GCRF, f, &tm).unwrap_err();
707 assert!(matches!(err, Error::ApproxNotSupportedForFrame { frame } if frame == f));
708 }
709 }
710
711 #[test]
712 fn approx_matches_qitrf2gcrf_approx() {
713 let tm = t();
714 let q_dispatch = rotation_approx(Frame::ITRF, Frame::GCRF, &tm).unwrap();
715 let q_direct = qitrf2gcrf_approx(&tm);
716 let v = numeris::vector![1000.0, 2000.0, 3000.0];
717 assert!((q_dispatch * v - q_direct * v).norm() < 1e-9);
718 }
719
720 #[test]
721 fn orbit_frames_rejected() {
722 let tm = t();
723 for of in [Frame::LVLH, Frame::RTN, Frame::NTW] {
724 assert!(matches!(
725 rotation(of, Frame::GCRF, &tm),
726 Err(Error::OrbitFrameRequiresState { .. })
727 ));
728 assert!(matches!(
729 rotation(Frame::GCRF, of, &tm),
730 Err(Error::OrbitFrameRequiresState { .. })
731 ));
732 }
733 }
734
735 #[test]
736 fn rotation_with_state_unifies_both_front_doors() {
737 let tm = t();
738 let pos = numeris::vector![7000.0e3, 0.0, 0.0];
739 let vel = numeris::vector![0.0, 7.5e3, 1.0e3];
740 let v = numeris::vector![1000.0, -2000.0, 3000.0];
741
742 let q_earth = rotation_with_state(Frame::ITRF, Frame::TEME, &tm, &pos, &vel).unwrap();
744 let q_ref = rotation(Frame::ITRF, Frame::TEME, &tm).unwrap();
745 assert!((q_earth * v - q_ref * v).norm() < 1e-6);
746
747 let q_rtn = rotation_with_state(Frame::RTN, Frame::GCRF, &tm, &pos, &vel).unwrap();
749 let dcm = crate::frametransform::to_gcrf(Frame::RTN, &pos, &vel).unwrap();
750 assert!((q_rtn * v - dcm * v).norm() < 1e-9);
751
752 let direct = rotation_with_state(Frame::TEME, Frame::RTN, &tm, &pos, &vel).unwrap();
755 let via_gcrf = {
756 let q_teme_gcrf = rotation(Frame::TEME, Frame::GCRF, &tm).unwrap();
757 let q_gcrf_rtn = rotation_with_state(Frame::GCRF, Frame::RTN, &tm, &pos, &vel).unwrap();
758 q_gcrf_rtn * q_teme_gcrf
759 };
760 assert!((direct * v - via_gcrf * v).norm() < 1e-6);
761
762 let back = rotation_with_state(Frame::RTN, Frame::TEME, &tm, &pos, &vel).unwrap();
764 assert!((back * (direct * v) - v).norm() < 1e-6);
765 }
766
767 #[test]
768 fn icrf_eme2000_constant_bias() {
769 let t1 = Instant::from_datetime(2000, 1, 1, 0, 0, 0.0).unwrap();
771 let t2 = Instant::from_datetime(2026, 5, 22, 0, 0, 0.0).unwrap();
772 let q1 = rotation(Frame::ICRF, Frame::EME2000, &t1).unwrap();
773 let q2 = rotation(Frame::ICRF, Frame::EME2000, &t2).unwrap();
774 assert!((q1.w - q2.w).abs() < 1e-15);
775 }
776
777 #[test]
778 fn eme2000_bias_matches_iers_2010() {
779 let t = Instant::from_datetime(2000, 1, 1, 12, 0, 0.0).unwrap();
783 let q = rotation(Frame::EME2000, Frame::GCRF, &t).unwrap();
784 let e1 = numeris::vector![1.0_f64, 0.0, 0.0];
785 let e2 = numeris::vector![0.0_f64, 1.0, 0.0];
786 let e3 = numeris::vector![0.0_f64, 0.0, 1.0];
787 let c0 = q * e1;
792 let c1 = q * e2;
793 let c2 = q * e3;
794 assert!((c0[0] - 1.0).abs() < 1e-14);
796 assert!((c0[1] - (-7.07827974e-8)).abs() < 1e-15);
797 assert!((c0[2] - 8.05614894e-8).abs() < 1e-15);
798 assert!((c1[0] - 7.07827948e-8).abs() < 1e-15);
800 assert!((c1[1] - 1.0).abs() < 1e-14);
801 assert!((c1[2] - 3.30594449e-8).abs() < 1e-15);
802 assert!((c2[0] - (-8.05614917e-8)).abs() < 1e-15);
804 assert!((c2[1] - (-3.30594392e-8)).abs() < 1e-15);
805 assert!((c2[2] - 1.0).abs() < 1e-14);
806 }
807
808 #[test]
809 fn transform_state_itrf_to_gcrf_matches_direct() {
810 let tm = t();
811 let p_itrf = numeris::vector![6378137.0, 0.0, 0.0];
812 let v_itrf = numeris::vector![0.0, 0.0, 0.0];
813 let (p_dispatch, v_dispatch) =
814 transform_state(Frame::ITRF, Frame::GCRF, &tm, &p_itrf, &v_itrf).unwrap();
815 let (p_direct, v_direct) = itrf_to_gcrf_state(&p_itrf, &v_itrf, &tm);
816 assert!((p_dispatch - p_direct).norm() < 1e-9);
817 assert!((v_dispatch - v_direct).norm() < 1e-12);
818 }
819
820 #[test]
821 fn transform_state_roundtrip_itrf_gcrf() {
822 let tm = t();
823 let p = numeris::vector![6378137.0, 0.0, 0.0];
824 let v = numeris::vector![0.0, 7600.0, 0.0];
825 let (p2, v2) = transform_state(Frame::ITRF, Frame::GCRF, &tm, &p, &v).unwrap();
826 let (p3, v3) = transform_state(Frame::GCRF, Frame::ITRF, &tm, &p2, &v2).unwrap();
827 assert!((p3 - p).norm() / p.norm() < 1e-10);
828 assert!((v3 - v).norm() / v.norm() < 1e-10);
829 }
830
831 #[test]
832 fn transform_state_all_non_orbit_pairs_roundtrip() {
833 let tm = t();
836 let p = numeris::vector![7000000.0, 0.0, 0.0];
837 let v = numeris::vector![0.0, 7600.0, 0.0];
838 let frames = [
839 Frame::ITRF,
840 Frame::TIRS,
841 Frame::CIRS,
842 Frame::GCRF,
843 Frame::TEME,
844 Frame::EME2000,
845 Frame::ICRF,
846 ];
847 for &a in &frames {
848 for &b in &frames {
849 let (pa, va) = transform_state(a, b, &tm, &p, &v).unwrap();
850 let (pr, vr) = transform_state(b, a, &tm, &pa, &va).unwrap();
851 let pos_err = (pr - p).norm() / p.norm();
852 let vel_err = (vr - v).norm() / v.norm();
853 assert!(pos_err < 1e-10, "({a}↔{b}) pos roundtrip err {pos_err}");
854 assert!(vel_err < 1e-10, "({a}↔{b}) vel roundtrip err {vel_err}");
855 }
856 }
857 }
858
859 #[test]
860 fn transform_state_inertial_pair_no_sweep() {
861 let tm = t();
864 let p = numeris::vector![7000000.0, 0.0, 0.0];
865 let v = numeris::vector![0.0, 7600.0, 0.0];
866 let (_, v_teme) = transform_state(Frame::GCRF, Frame::TEME, &tm, &p, &v).unwrap();
867 assert!(
868 (v_teme.norm() - v.norm()).abs() < 1e-9,
869 "inertial pair preserved |v|: |v|={}, |v_teme|={}",
870 v.norm(),
871 v_teme.norm()
872 );
873 }
874
875 #[test]
876 fn transform_state_tirs_via_itrf_chain() {
877 let tm = t();
880 let p_tirs = numeris::vector![7000000.0, 0.0, 0.0];
881 let v_tirs = numeris::vector![0.0, 0.0, 0.0];
882 let (p_a, v_a) = transform_state(Frame::TIRS, Frame::GCRF, &tm, &p_tirs, &v_tirs).unwrap();
883 let q_tirs_to_itrf = rotation(Frame::TIRS, Frame::ITRF, &tm).unwrap();
885 let p_itrf = q_tirs_to_itrf * p_tirs;
886 let v_itrf = q_tirs_to_itrf * v_tirs;
887 let (p_b, v_b) = itrf_to_gcrf_state(&p_itrf, &v_itrf, &tm);
888 assert!((p_a - p_b).norm() < 1e-9);
889 assert!((v_a - v_b).norm() < 1e-12);
890 }
891
892 #[test]
893 fn transform_state_itrf_tirs_no_sweep() {
894 let tm = t();
897 let p = numeris::vector![7000000.0, 0.0, 0.0];
898 let v = numeris::vector![0.0, 7600.0, 0.0];
899 let (_, v_tirs) = transform_state(Frame::ITRF, Frame::TIRS, &tm, &p, &v).unwrap();
900 assert!((v_tirs.norm() - v.norm()).abs() < 1e-9);
901 }
902
903 #[test]
904 fn transform_state_approx_rejects_intermediates() {
905 let tm = t();
906 let p = numeris::vector![7000000.0, 0.0, 0.0];
907 let v = numeris::vector![0.0, 7600.0, 0.0];
908 for f in [Frame::TIRS, Frame::CIRS] {
909 assert!(matches!(
910 transform_state_approx(f, Frame::GCRF, &tm, &p, &v),
911 Err(Error::ApproxNotSupportedForFrame { .. })
912 ));
913 }
914 }
915
916 #[test]
917 fn transform_state_orbit_frames_rejected() {
918 let tm = t();
919 let p = numeris::vector![7000000.0, 0.0, 0.0];
920 let v = numeris::vector![0.0, 7600.0, 0.0];
921 for of in [Frame::LVLH, Frame::RTN, Frame::NTW] {
922 assert!(matches!(
923 transform_state(of, Frame::GCRF, &tm, &p, &v),
924 Err(Error::OrbitFrameRequiresState { .. })
925 ));
926 }
927 }
928}