nrl-tracker 0.22.5__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/METADATA +57 -10
  2. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/RECORD +86 -69
  3. pytcl/__init__.py +4 -3
  4. pytcl/assignment_algorithms/__init__.py +28 -0
  5. pytcl/assignment_algorithms/dijkstra_min_cost.py +184 -0
  6. pytcl/assignment_algorithms/gating.py +10 -10
  7. pytcl/assignment_algorithms/jpda.py +40 -40
  8. pytcl/assignment_algorithms/nd_assignment.py +379 -0
  9. pytcl/assignment_algorithms/network_flow.py +464 -0
  10. pytcl/assignment_algorithms/network_simplex.py +167 -0
  11. pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
  12. pytcl/astronomical/__init__.py +104 -3
  13. pytcl/astronomical/ephemerides.py +14 -11
  14. pytcl/astronomical/reference_frames.py +865 -56
  15. pytcl/astronomical/relativity.py +6 -5
  16. pytcl/astronomical/sgp4.py +710 -0
  17. pytcl/astronomical/special_orbits.py +532 -0
  18. pytcl/astronomical/tle.py +558 -0
  19. pytcl/atmosphere/__init__.py +43 -1
  20. pytcl/atmosphere/ionosphere.py +512 -0
  21. pytcl/atmosphere/nrlmsise00.py +809 -0
  22. pytcl/clustering/dbscan.py +2 -2
  23. pytcl/clustering/gaussian_mixture.py +3 -3
  24. pytcl/clustering/hierarchical.py +15 -15
  25. pytcl/clustering/kmeans.py +4 -4
  26. pytcl/containers/__init__.py +24 -0
  27. pytcl/containers/base.py +219 -0
  28. pytcl/containers/cluster_set.py +12 -2
  29. pytcl/containers/covertree.py +26 -29
  30. pytcl/containers/kd_tree.py +94 -29
  31. pytcl/containers/rtree.py +200 -1
  32. pytcl/containers/vptree.py +21 -28
  33. pytcl/coordinate_systems/conversions/geodetic.py +272 -5
  34. pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
  35. pytcl/coordinate_systems/projections/__init__.py +1 -1
  36. pytcl/coordinate_systems/projections/projections.py +2 -2
  37. pytcl/coordinate_systems/rotations/rotations.py +10 -6
  38. pytcl/core/__init__.py +18 -0
  39. pytcl/core/validation.py +333 -2
  40. pytcl/dynamic_estimation/__init__.py +26 -0
  41. pytcl/dynamic_estimation/gaussian_sum_filter.py +434 -0
  42. pytcl/dynamic_estimation/imm.py +14 -14
  43. pytcl/dynamic_estimation/kalman/__init__.py +30 -0
  44. pytcl/dynamic_estimation/kalman/constrained.py +382 -0
  45. pytcl/dynamic_estimation/kalman/extended.py +8 -8
  46. pytcl/dynamic_estimation/kalman/h_infinity.py +613 -0
  47. pytcl/dynamic_estimation/kalman/square_root.py +60 -573
  48. pytcl/dynamic_estimation/kalman/sr_ukf.py +302 -0
  49. pytcl/dynamic_estimation/kalman/ud_filter.py +410 -0
  50. pytcl/dynamic_estimation/kalman/unscented.py +8 -6
  51. pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
  52. pytcl/dynamic_estimation/rbpf.py +589 -0
  53. pytcl/gravity/egm.py +13 -0
  54. pytcl/gravity/spherical_harmonics.py +98 -37
  55. pytcl/gravity/tides.py +6 -6
  56. pytcl/logging_config.py +328 -0
  57. pytcl/magnetism/__init__.py +7 -0
  58. pytcl/magnetism/emm.py +10 -3
  59. pytcl/magnetism/wmm.py +260 -23
  60. pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
  61. pytcl/mathematical_functions/geometry/geometry.py +5 -5
  62. pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
  63. pytcl/mathematical_functions/signal_processing/detection.py +24 -24
  64. pytcl/mathematical_functions/signal_processing/filters.py +14 -14
  65. pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
  66. pytcl/mathematical_functions/special_functions/bessel.py +15 -3
  67. pytcl/mathematical_functions/special_functions/debye.py +136 -26
  68. pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
  69. pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
  70. pytcl/mathematical_functions/special_functions/hypergeometric.py +81 -15
  71. pytcl/mathematical_functions/transforms/fourier.py +8 -8
  72. pytcl/mathematical_functions/transforms/stft.py +12 -12
  73. pytcl/mathematical_functions/transforms/wavelets.py +9 -9
  74. pytcl/navigation/geodesy.py +246 -160
  75. pytcl/navigation/great_circle.py +101 -19
  76. pytcl/plotting/coordinates.py +7 -7
  77. pytcl/plotting/tracks.py +2 -2
  78. pytcl/static_estimation/maximum_likelihood.py +16 -14
  79. pytcl/static_estimation/robust.py +5 -5
  80. pytcl/terrain/loaders.py +5 -5
  81. pytcl/trackers/hypothesis.py +1 -1
  82. pytcl/trackers/mht.py +9 -9
  83. pytcl/trackers/multi_target.py +1 -1
  84. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/LICENSE +0 -0
  85. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/WHEEL +0 -0
  86. {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.8.0.dist-info}/top_level.txt +0 -0
@@ -21,13 +21,31 @@ References
21
21
  A&A, 2003.
22
22
  """
23
23
 
24
- from typing import Tuple
24
+ import logging
25
+ from functools import lru_cache
26
+ from typing import Any, Optional, Tuple
25
27
 
26
28
  import numpy as np
27
29
  from numpy.typing import NDArray
28
30
 
29
31
  from pytcl.astronomical.time_systems import JD_J2000
30
32
 
33
+ # Module logger
34
+ _logger = logging.getLogger("pytcl.astronomical.reference_frames")
35
+
36
+ # Cache configuration
37
+ _CACHE_JD_DECIMALS = 6 # ~86ms precision for JD quantization
38
+ _CACHE_MAXSIZE = 128 # Max cached epochs
39
+
40
+
41
+ def _quantize_jd(jd: float) -> float:
42
+ """Quantize Julian date for cache key compatibility.
43
+
44
+ Rounds to _CACHE_JD_DECIMALS decimal places (~86ms precision).
45
+ This enables cache hits for nearly identical epochs.
46
+ """
47
+ return round(jd, _CACHE_JD_DECIMALS)
48
+
31
49
 
32
50
  def julian_centuries_j2000(jd: float) -> float:
33
51
  """
@@ -78,6 +96,39 @@ def precession_angles_iau76(T: float) -> Tuple[float, float, float]:
78
96
  )
79
97
 
80
98
 
99
+ @lru_cache(maxsize=_CACHE_MAXSIZE)
100
+ def _precession_matrix_cached(
101
+ jd_quantized: float,
102
+ ) -> tuple[tuple[np.ndarray[Any, Any], ...], ...]:
103
+ """Cached precession matrix computation (internal).
104
+
105
+ Returns tuple of tuples for hashability.
106
+ """
107
+ T = julian_centuries_j2000(jd_quantized)
108
+ zeta, theta, z = precession_angles_iau76(T)
109
+
110
+ cos_zeta = np.cos(zeta)
111
+ sin_zeta = np.sin(zeta)
112
+ cos_theta = np.cos(theta)
113
+ sin_theta = np.sin(theta)
114
+ cos_z = np.cos(z)
115
+ sin_z = np.sin(z)
116
+
117
+ return (
118
+ (
119
+ cos_zeta * cos_theta * cos_z - sin_zeta * sin_z,
120
+ -sin_zeta * cos_theta * cos_z - cos_zeta * sin_z,
121
+ -sin_theta * cos_z,
122
+ ),
123
+ (
124
+ cos_zeta * cos_theta * sin_z + sin_zeta * cos_z,
125
+ -sin_zeta * cos_theta * sin_z + cos_zeta * cos_z,
126
+ -sin_theta * sin_z,
127
+ ),
128
+ (cos_zeta * sin_theta, -sin_zeta * sin_theta, cos_theta),
129
+ )
130
+
131
+
81
132
  def precession_matrix_iau76(jd: float) -> NDArray[np.floating]:
82
133
  """
83
134
  Compute IAU 1976 precession matrix from J2000 to date.
@@ -92,34 +143,15 @@ def precession_matrix_iau76(jd: float) -> NDArray[np.floating]:
92
143
  P : ndarray
93
144
  Precession rotation matrix (3x3).
94
145
  Transforms from J2000 (GCRF) to mean of date.
95
- """
96
- T = julian_centuries_j2000(jd)
97
- zeta, theta, z = precession_angles_iau76(T)
98
-
99
- cos_zeta = np.cos(zeta)
100
- sin_zeta = np.sin(zeta)
101
- cos_theta = np.cos(theta)
102
- sin_theta = np.sin(theta)
103
- cos_z = np.cos(z)
104
- sin_z = np.sin(z)
105
-
106
- P = np.array(
107
- [
108
- [
109
- cos_zeta * cos_theta * cos_z - sin_zeta * sin_z,
110
- -sin_zeta * cos_theta * cos_z - cos_zeta * sin_z,
111
- -sin_theta * cos_z,
112
- ],
113
- [
114
- cos_zeta * cos_theta * sin_z + sin_zeta * cos_z,
115
- -sin_zeta * cos_theta * sin_z + cos_zeta * cos_z,
116
- -sin_theta * sin_z,
117
- ],
118
- [cos_zeta * sin_theta, -sin_zeta * sin_theta, cos_theta],
119
- ]
120
- )
121
146
 
122
- return P
147
+ Notes
148
+ -----
149
+ Results are cached for repeated queries at the same epoch.
150
+ Cache key is quantized to ~86ms precision.
151
+ """
152
+ jd_q = _quantize_jd(jd)
153
+ cached = _precession_matrix_cached(jd_q)
154
+ return np.array(cached)
123
155
 
124
156
 
125
157
  def nutation_angles_iau80(jd: float) -> Tuple[float, float]:
@@ -203,6 +235,40 @@ def mean_obliquity_iau80(jd: float) -> float:
203
235
  return eps0_arcsec * np.pi / (180 * 3600)
204
236
 
205
237
 
238
+ @lru_cache(maxsize=_CACHE_MAXSIZE)
239
+ def _nutation_matrix_cached(
240
+ jd_quantized: float,
241
+ ) -> tuple[tuple[np.ndarray[Any, Any], ...], ...]:
242
+ """Cached nutation matrix computation (internal).
243
+
244
+ Returns tuple of tuples for hashability.
245
+ """
246
+ dpsi, deps = nutation_angles_iau80(jd_quantized)
247
+ eps0 = mean_obliquity_iau80(jd_quantized)
248
+ eps = eps0 + deps
249
+
250
+ cos_eps0 = np.cos(eps0)
251
+ sin_eps0 = np.sin(eps0)
252
+ cos_eps = np.cos(eps)
253
+ sin_eps = np.sin(eps)
254
+ cos_dpsi = np.cos(dpsi)
255
+ sin_dpsi = np.sin(dpsi)
256
+
257
+ return (
258
+ (cos_dpsi, -sin_dpsi * cos_eps0, -sin_dpsi * sin_eps0),
259
+ (
260
+ sin_dpsi * cos_eps,
261
+ cos_dpsi * cos_eps0 * cos_eps + sin_eps0 * sin_eps,
262
+ cos_dpsi * sin_eps0 * cos_eps - cos_eps0 * sin_eps,
263
+ ),
264
+ (
265
+ sin_dpsi * sin_eps,
266
+ cos_dpsi * cos_eps0 * sin_eps - sin_eps0 * cos_eps,
267
+ cos_dpsi * sin_eps0 * sin_eps + cos_eps0 * cos_eps,
268
+ ),
269
+ )
270
+
271
+
206
272
  def nutation_matrix(jd: float) -> NDArray[np.floating]:
207
273
  """
208
274
  Compute nutation matrix.
@@ -217,35 +283,15 @@ def nutation_matrix(jd: float) -> NDArray[np.floating]:
217
283
  N : ndarray
218
284
  Nutation rotation matrix (3x3).
219
285
  Transforms from mean of date to true of date.
220
- """
221
- dpsi, deps = nutation_angles_iau80(jd)
222
- eps0 = mean_obliquity_iau80(jd)
223
- eps = eps0 + deps
224
-
225
- cos_eps0 = np.cos(eps0)
226
- sin_eps0 = np.sin(eps0)
227
- cos_eps = np.cos(eps)
228
- sin_eps = np.sin(eps)
229
- cos_dpsi = np.cos(dpsi)
230
- sin_dpsi = np.sin(dpsi)
231
-
232
- N = np.array(
233
- [
234
- [cos_dpsi, -sin_dpsi * cos_eps0, -sin_dpsi * sin_eps0],
235
- [
236
- sin_dpsi * cos_eps,
237
- cos_dpsi * cos_eps0 * cos_eps + sin_eps0 * sin_eps,
238
- cos_dpsi * sin_eps0 * cos_eps - cos_eps0 * sin_eps,
239
- ],
240
- [
241
- sin_dpsi * sin_eps,
242
- cos_dpsi * cos_eps0 * sin_eps - sin_eps0 * cos_eps,
243
- cos_dpsi * sin_eps0 * sin_eps + cos_eps0 * cos_eps,
244
- ],
245
- ]
246
- )
247
286
 
248
- return N
287
+ Notes
288
+ -----
289
+ Results are cached for repeated queries at the same epoch.
290
+ Cache key is quantized to ~86ms precision.
291
+ """
292
+ jd_q = _quantize_jd(jd)
293
+ cached = _nutation_matrix_cached(jd_q)
294
+ return np.array(cached)
249
295
 
250
296
 
251
297
  def earth_rotation_angle(jd_ut1: float) -> float:
@@ -647,6 +693,746 @@ def equatorial_to_ecliptic(
647
693
  return R @ r_eq
648
694
 
649
695
 
696
+ # =============================================================================
697
+ # TEME Frame Transformations
698
+ # =============================================================================
699
+
700
+
701
+ def teme_to_pef(
702
+ r_teme: NDArray[np.floating],
703
+ jd_ut1: float,
704
+ ) -> NDArray[np.floating]:
705
+ """
706
+ Transform position from TEME to PEF (Pseudo Earth-Fixed).
707
+
708
+ TEME is the True Equator, Mean Equinox frame used by SGP4.
709
+ This transformation applies only the GMST rotation.
710
+
711
+ Parameters
712
+ ----------
713
+ r_teme : ndarray
714
+ Position in TEME frame (km), shape (3,).
715
+ jd_ut1 : float
716
+ Julian date in UT1.
717
+
718
+ Returns
719
+ -------
720
+ r_pef : ndarray
721
+ Position in PEF frame (km), shape (3,).
722
+ """
723
+ gmst = gmst_iau82(jd_ut1)
724
+ R = sidereal_rotation_matrix(gmst)
725
+ return R @ r_teme
726
+
727
+
728
+ def pef_to_teme(
729
+ r_pef: NDArray[np.floating],
730
+ jd_ut1: float,
731
+ ) -> NDArray[np.floating]:
732
+ """
733
+ Transform position from PEF to TEME.
734
+
735
+ Parameters
736
+ ----------
737
+ r_pef : ndarray
738
+ Position in PEF frame (km), shape (3,).
739
+ jd_ut1 : float
740
+ Julian date in UT1.
741
+
742
+ Returns
743
+ -------
744
+ r_teme : ndarray
745
+ Position in TEME frame (km), shape (3,).
746
+ """
747
+ gmst = gmst_iau82(jd_ut1)
748
+ R = sidereal_rotation_matrix(gmst)
749
+ return R.T @ r_pef
750
+
751
+
752
+ def teme_to_itrf(
753
+ r_teme: NDArray[np.floating],
754
+ jd_ut1: float,
755
+ xp: float = 0.0,
756
+ yp: float = 0.0,
757
+ ) -> NDArray[np.floating]:
758
+ """
759
+ Transform position from TEME to ITRF (Earth-fixed).
760
+
761
+ TEME is the True Equator, Mean Equinox frame used by SGP4/SDP4.
762
+ This is the frame in which TLE-propagated positions are expressed.
763
+
764
+ Parameters
765
+ ----------
766
+ r_teme : ndarray
767
+ Position in TEME frame (km), shape (3,).
768
+ jd_ut1 : float
769
+ Julian date in UT1.
770
+ xp : float, optional
771
+ Polar motion x (radians). Default 0.
772
+ yp : float, optional
773
+ Polar motion y (radians). Default 0.
774
+
775
+ Returns
776
+ -------
777
+ r_itrf : ndarray
778
+ Position in ITRF frame (km), shape (3,).
779
+
780
+ Notes
781
+ -----
782
+ TEME is a quasi-inertial frame that uses the mean equinox instead
783
+ of the true equinox. The transformation sequence is:
784
+
785
+ TEME -> PEF (via GMST rotation) -> ITRF (via polar motion)
786
+
787
+ Examples
788
+ --------
789
+ >>> from pytcl.astronomical.sgp4 import sgp4_propagate
790
+ >>> from pytcl.astronomical.tle import parse_tle
791
+ >>> tle = parse_tle(line1, line2)
792
+ >>> state = sgp4_propagate(tle, 0.0)
793
+ >>> r_itrf = teme_to_itrf(state.r, jd_ut1)
794
+ """
795
+ r_pef = teme_to_pef(r_teme, jd_ut1)
796
+ W = polar_motion_matrix(xp, yp)
797
+ return W @ r_pef
798
+
799
+
800
+ def itrf_to_teme(
801
+ r_itrf: NDArray[np.floating],
802
+ jd_ut1: float,
803
+ xp: float = 0.0,
804
+ yp: float = 0.0,
805
+ ) -> NDArray[np.floating]:
806
+ """
807
+ Transform position from ITRF to TEME.
808
+
809
+ Parameters
810
+ ----------
811
+ r_itrf : ndarray
812
+ Position in ITRF frame (km), shape (3,).
813
+ jd_ut1 : float
814
+ Julian date in UT1.
815
+ xp : float, optional
816
+ Polar motion x (radians). Default 0.
817
+ yp : float, optional
818
+ Polar motion y (radians). Default 0.
819
+
820
+ Returns
821
+ -------
822
+ r_teme : ndarray
823
+ Position in TEME frame (km), shape (3,).
824
+ """
825
+ W = polar_motion_matrix(xp, yp)
826
+ r_pef = W.T @ r_itrf
827
+ return pef_to_teme(r_pef, jd_ut1)
828
+
829
+
830
+ def teme_to_gcrf(
831
+ r_teme: NDArray[np.floating],
832
+ jd_tt: float,
833
+ ) -> NDArray[np.floating]:
834
+ """
835
+ Transform position from TEME to GCRF (inertial).
836
+
837
+ This transformation accounts for the difference between
838
+ the mean and true equinox (equation of equinoxes) and then
839
+ applies precession and nutation to go from TOD to GCRF.
840
+
841
+ Parameters
842
+ ----------
843
+ r_teme : ndarray
844
+ Position in TEME frame (km), shape (3,).
845
+ jd_tt : float
846
+ Julian date in TT (Terrestrial Time).
847
+
848
+ Returns
849
+ -------
850
+ r_gcrf : ndarray
851
+ Position in GCRF frame (km), shape (3,).
852
+
853
+ Notes
854
+ -----
855
+ The transformation sequence is:
856
+
857
+ TEME -> TOD (via equation of equinoxes)
858
+ TOD -> MOD (via nutation, inverse)
859
+ MOD -> GCRF (via precession, inverse)
860
+
861
+ Examples
862
+ --------
863
+ >>> state = sgp4_propagate(tle, 60.0)
864
+ >>> r_gcrf = teme_to_gcrf(state.r, jd_tt)
865
+ """
866
+ eq_eq = equation_of_equinoxes(jd_tt)
867
+
868
+ # TEME to TOD: rotate by equation of equinoxes
869
+ cos_eq = np.cos(-eq_eq)
870
+ sin_eq = np.sin(-eq_eq)
871
+
872
+ R_eq = np.array([[cos_eq, -sin_eq, 0], [sin_eq, cos_eq, 0], [0, 0, 1]])
873
+
874
+ r_tod = R_eq @ r_teme
875
+
876
+ # TOD to MOD (inverse nutation)
877
+ N = nutation_matrix(jd_tt)
878
+ r_mod = N.T @ r_tod
879
+
880
+ # MOD to GCRF (inverse precession)
881
+ P = precession_matrix_iau76(jd_tt)
882
+ return P.T @ r_mod
883
+
884
+
885
+ def gcrf_to_teme(
886
+ r_gcrf: NDArray[np.floating],
887
+ jd_tt: float,
888
+ ) -> NDArray[np.floating]:
889
+ """
890
+ Transform position from GCRF to TEME.
891
+
892
+ Parameters
893
+ ----------
894
+ r_gcrf : ndarray
895
+ Position in GCRF frame (km), shape (3,).
896
+ jd_tt : float
897
+ Julian date in TT.
898
+
899
+ Returns
900
+ -------
901
+ r_teme : ndarray
902
+ Position in TEME frame (km), shape (3,).
903
+ """
904
+ # GCRF to MOD (precession)
905
+ P = precession_matrix_iau76(jd_tt)
906
+ r_mod = P @ r_gcrf
907
+
908
+ # MOD to TOD (nutation)
909
+ N = nutation_matrix(jd_tt)
910
+ r_tod = N @ r_mod
911
+
912
+ # TOD to TEME: rotate by equation of equinoxes
913
+ eq_eq = equation_of_equinoxes(jd_tt)
914
+ cos_eq = np.cos(eq_eq)
915
+ sin_eq = np.sin(eq_eq)
916
+
917
+ R_eq = np.array([[cos_eq, -sin_eq, 0], [sin_eq, cos_eq, 0], [0, 0, 1]])
918
+
919
+ return R_eq @ r_tod
920
+
921
+
922
+ def teme_to_itrf_with_velocity(
923
+ r_teme: NDArray[np.floating],
924
+ v_teme: NDArray[np.floating],
925
+ jd_ut1: float,
926
+ xp: float = 0.0,
927
+ yp: float = 0.0,
928
+ ) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
929
+ """
930
+ Transform position and velocity from TEME to ITRF.
931
+
932
+ This properly accounts for the velocity transformation including
933
+ the Earth's rotation rate.
934
+
935
+ Parameters
936
+ ----------
937
+ r_teme : ndarray
938
+ Position in TEME frame (km), shape (3,).
939
+ v_teme : ndarray
940
+ Velocity in TEME frame (km/s), shape (3,).
941
+ jd_ut1 : float
942
+ Julian date in UT1.
943
+ xp : float, optional
944
+ Polar motion x (radians). Default 0.
945
+ yp : float, optional
946
+ Polar motion y (radians). Default 0.
947
+
948
+ Returns
949
+ -------
950
+ r_itrf : ndarray
951
+ Position in ITRF frame (km), shape (3,).
952
+ v_itrf : ndarray
953
+ Velocity in ITRF frame (km/s), shape (3,).
954
+ """
955
+ omega_earth = 7.29211514670698e-5 # rad/s
956
+
957
+ gmst = gmst_iau82(jd_ut1)
958
+ R = sidereal_rotation_matrix(gmst)
959
+ W = polar_motion_matrix(xp, yp)
960
+
961
+ # Position transformation
962
+ r_pef = R @ r_teme
963
+ r_itrf = W @ r_pef
964
+
965
+ # Velocity includes Earth rotation effect
966
+ omega_vec = np.array([0.0, 0.0, omega_earth])
967
+ v_pef = R @ v_teme - np.cross(omega_vec, r_pef)
968
+ v_itrf = W @ v_pef
969
+
970
+ return r_itrf, v_itrf
971
+
972
+
973
+ def itrf_to_teme_with_velocity(
974
+ r_itrf: NDArray[np.floating],
975
+ v_itrf: NDArray[np.floating],
976
+ jd_ut1: float,
977
+ xp: float = 0.0,
978
+ yp: float = 0.0,
979
+ ) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
980
+ """
981
+ Transform position and velocity from ITRF to TEME.
982
+
983
+ Parameters
984
+ ----------
985
+ r_itrf : ndarray
986
+ Position in ITRF frame (km), shape (3,).
987
+ v_itrf : ndarray
988
+ Velocity in ITRF frame (km/s), shape (3,).
989
+ jd_ut1 : float
990
+ Julian date in UT1.
991
+ xp : float, optional
992
+ Polar motion x (radians). Default 0.
993
+ yp : float, optional
994
+ Polar motion y (radians). Default 0.
995
+
996
+ Returns
997
+ -------
998
+ r_teme : ndarray
999
+ Position in TEME frame (km), shape (3,).
1000
+ v_teme : ndarray
1001
+ Velocity in TEME frame (km/s), shape (3,).
1002
+ """
1003
+ omega_earth = 7.29211514670698e-5 # rad/s
1004
+
1005
+ gmst = gmst_iau82(jd_ut1)
1006
+ R = sidereal_rotation_matrix(gmst)
1007
+ W = polar_motion_matrix(xp, yp)
1008
+
1009
+ # Position transformation
1010
+ r_pef = W.T @ r_itrf
1011
+ r_teme = R.T @ r_pef
1012
+
1013
+ # Velocity includes Earth rotation effect
1014
+ omega_vec = np.array([0.0, 0.0, omega_earth])
1015
+ v_pef = W.T @ v_itrf
1016
+ v_teme = R.T @ (v_pef + np.cross(omega_vec, r_pef))
1017
+
1018
+ return r_teme, v_teme
1019
+
1020
+
1021
+ # =============================================================================
1022
+ # TOD/MOD Frame Transformations (Legacy Conventions)
1023
+ # =============================================================================
1024
+
1025
+
1026
+ def gcrf_to_mod(
1027
+ r_gcrf: NDArray[np.floating],
1028
+ jd_tt: float,
1029
+ ) -> NDArray[np.floating]:
1030
+ """
1031
+ Transform position from GCRF to MOD (Mean of Date).
1032
+
1033
+ MOD is the mean equator and mean equinox of date frame.
1034
+ This applies only the precession transformation.
1035
+
1036
+ Parameters
1037
+ ----------
1038
+ r_gcrf : ndarray
1039
+ Position in GCRF frame (km), shape (3,).
1040
+ jd_tt : float
1041
+ Julian date in TT (Terrestrial Time).
1042
+
1043
+ Returns
1044
+ -------
1045
+ r_mod : ndarray
1046
+ Position in MOD frame (km), shape (3,).
1047
+
1048
+ Notes
1049
+ -----
1050
+ MOD is a legacy frame convention. For most modern applications,
1051
+ GCRF (J2000) is preferred. MOD was historically used in older
1052
+ software and publications.
1053
+
1054
+ The transformation is simply the precession matrix:
1055
+ r_mod = P @ r_gcrf
1056
+
1057
+ See Also
1058
+ --------
1059
+ mod_to_gcrf : Inverse transformation.
1060
+ gcrf_to_tod : Includes nutation for true of date.
1061
+ """
1062
+ P = precession_matrix_iau76(jd_tt)
1063
+ return P @ r_gcrf
1064
+
1065
+
1066
+ def mod_to_gcrf(
1067
+ r_mod: NDArray[np.floating],
1068
+ jd_tt: float,
1069
+ ) -> NDArray[np.floating]:
1070
+ """
1071
+ Transform position from MOD (Mean of Date) to GCRF.
1072
+
1073
+ Parameters
1074
+ ----------
1075
+ r_mod : ndarray
1076
+ Position in MOD frame (km), shape (3,).
1077
+ jd_tt : float
1078
+ Julian date in TT.
1079
+
1080
+ Returns
1081
+ -------
1082
+ r_gcrf : ndarray
1083
+ Position in GCRF frame (km), shape (3,).
1084
+
1085
+ See Also
1086
+ --------
1087
+ gcrf_to_mod : Forward transformation.
1088
+ """
1089
+ P = precession_matrix_iau76(jd_tt)
1090
+ return P.T @ r_mod
1091
+
1092
+
1093
+ def gcrf_to_tod(
1094
+ r_gcrf: NDArray[np.floating],
1095
+ jd_tt: float,
1096
+ ) -> NDArray[np.floating]:
1097
+ """
1098
+ Transform position from GCRF to TOD (True of Date).
1099
+
1100
+ TOD is the true equator and true equinox of date frame.
1101
+ This applies both precession and nutation transformations.
1102
+
1103
+ Parameters
1104
+ ----------
1105
+ r_gcrf : ndarray
1106
+ Position in GCRF frame (km), shape (3,).
1107
+ jd_tt : float
1108
+ Julian date in TT (Terrestrial Time).
1109
+
1110
+ Returns
1111
+ -------
1112
+ r_tod : ndarray
1113
+ Position in TOD frame (km), shape (3,).
1114
+
1115
+ Notes
1116
+ -----
1117
+ TOD is a legacy frame convention. The transformation is:
1118
+ r_mod = P @ r_gcrf
1119
+ r_tod = N @ r_mod
1120
+
1121
+ where P is the precession matrix and N is the nutation matrix.
1122
+
1123
+ See Also
1124
+ --------
1125
+ tod_to_gcrf : Inverse transformation.
1126
+ gcrf_to_mod : Mean of date (without nutation).
1127
+ """
1128
+ P = precession_matrix_iau76(jd_tt)
1129
+ N = nutation_matrix(jd_tt)
1130
+ return N @ (P @ r_gcrf)
1131
+
1132
+
1133
+ def tod_to_gcrf(
1134
+ r_tod: NDArray[np.floating],
1135
+ jd_tt: float,
1136
+ ) -> NDArray[np.floating]:
1137
+ """
1138
+ Transform position from TOD (True of Date) to GCRF.
1139
+
1140
+ Parameters
1141
+ ----------
1142
+ r_tod : ndarray
1143
+ Position in TOD frame (km), shape (3,).
1144
+ jd_tt : float
1145
+ Julian date in TT.
1146
+
1147
+ Returns
1148
+ -------
1149
+ r_gcrf : ndarray
1150
+ Position in GCRF frame (km), shape (3,).
1151
+
1152
+ See Also
1153
+ --------
1154
+ gcrf_to_tod : Forward transformation.
1155
+ """
1156
+ P = precession_matrix_iau76(jd_tt)
1157
+ N = nutation_matrix(jd_tt)
1158
+ return P.T @ (N.T @ r_tod)
1159
+
1160
+
1161
+ def mod_to_tod(
1162
+ r_mod: NDArray[np.floating],
1163
+ jd_tt: float,
1164
+ ) -> NDArray[np.floating]:
1165
+ """
1166
+ Transform position from MOD (Mean of Date) to TOD (True of Date).
1167
+
1168
+ This applies only the nutation transformation.
1169
+
1170
+ Parameters
1171
+ ----------
1172
+ r_mod : ndarray
1173
+ Position in MOD frame (km), shape (3,).
1174
+ jd_tt : float
1175
+ Julian date in TT.
1176
+
1177
+ Returns
1178
+ -------
1179
+ r_tod : ndarray
1180
+ Position in TOD frame (km), shape (3,).
1181
+
1182
+ Notes
1183
+ -----
1184
+ The transformation is simply the nutation matrix:
1185
+ r_tod = N @ r_mod
1186
+
1187
+ See Also
1188
+ --------
1189
+ tod_to_mod : Inverse transformation.
1190
+ """
1191
+ N = nutation_matrix(jd_tt)
1192
+ return N @ r_mod
1193
+
1194
+
1195
+ def tod_to_mod(
1196
+ r_tod: NDArray[np.floating],
1197
+ jd_tt: float,
1198
+ ) -> NDArray[np.floating]:
1199
+ """
1200
+ Transform position from TOD (True of Date) to MOD (Mean of Date).
1201
+
1202
+ Parameters
1203
+ ----------
1204
+ r_tod : ndarray
1205
+ Position in TOD frame (km), shape (3,).
1206
+ jd_tt : float
1207
+ Julian date in TT.
1208
+
1209
+ Returns
1210
+ -------
1211
+ r_mod : ndarray
1212
+ Position in MOD frame (km), shape (3,).
1213
+
1214
+ See Also
1215
+ --------
1216
+ mod_to_tod : Forward transformation.
1217
+ """
1218
+ N = nutation_matrix(jd_tt)
1219
+ return N.T @ r_tod
1220
+
1221
+
1222
+ def tod_to_itrf(
1223
+ r_tod: NDArray[np.floating],
1224
+ jd_ut1: float,
1225
+ jd_tt: Optional[float] = None,
1226
+ xp: float = 0.0,
1227
+ yp: float = 0.0,
1228
+ ) -> NDArray[np.floating]:
1229
+ """
1230
+ Transform position from TOD (True of Date) to ITRF.
1231
+
1232
+ Parameters
1233
+ ----------
1234
+ r_tod : ndarray
1235
+ Position in TOD frame (km), shape (3,).
1236
+ jd_ut1 : float
1237
+ Julian date in UT1.
1238
+ jd_tt : float, optional
1239
+ Julian date in TT. If not provided, assumed equal to jd_ut1.
1240
+ xp : float, optional
1241
+ Polar motion x (radians). Default 0.
1242
+ yp : float, optional
1243
+ Polar motion y (radians). Default 0.
1244
+
1245
+ Returns
1246
+ -------
1247
+ r_itrf : ndarray
1248
+ Position in ITRF frame (km), shape (3,).
1249
+
1250
+ Notes
1251
+ -----
1252
+ The transformation applies the sidereal rotation (using GAST)
1253
+ and polar motion:
1254
+ r_pef = R(GAST) @ r_tod
1255
+ r_itrf = W @ r_pef
1256
+
1257
+ See Also
1258
+ --------
1259
+ itrf_to_tod : Inverse transformation.
1260
+ """
1261
+ if jd_tt is None:
1262
+ jd_tt = jd_ut1
1263
+ gast = gast_iau82(jd_ut1, jd_tt)
1264
+ R = sidereal_rotation_matrix(gast)
1265
+ W = polar_motion_matrix(xp, yp)
1266
+ return W @ (R @ r_tod)
1267
+
1268
+
1269
+ def itrf_to_tod(
1270
+ r_itrf: NDArray[np.floating],
1271
+ jd_ut1: float,
1272
+ jd_tt: Optional[float] = None,
1273
+ xp: float = 0.0,
1274
+ yp: float = 0.0,
1275
+ ) -> NDArray[np.floating]:
1276
+ """
1277
+ Transform position from ITRF to TOD (True of Date).
1278
+
1279
+ Parameters
1280
+ ----------
1281
+ r_itrf : ndarray
1282
+ Position in ITRF frame (km), shape (3,).
1283
+ jd_ut1 : float
1284
+ Julian date in UT1.
1285
+ jd_tt : float, optional
1286
+ Julian date in TT. If not provided, assumed equal to jd_ut1.
1287
+ xp : float, optional
1288
+ Polar motion x (radians). Default 0.
1289
+ yp : float, optional
1290
+ Polar motion y (radians). Default 0.
1291
+
1292
+ Returns
1293
+ -------
1294
+ r_tod : ndarray
1295
+ Position in TOD frame (km), shape (3,).
1296
+
1297
+ See Also
1298
+ --------
1299
+ tod_to_itrf : Forward transformation.
1300
+ """
1301
+ if jd_tt is None:
1302
+ jd_tt = jd_ut1
1303
+ gast = gast_iau82(jd_ut1, jd_tt)
1304
+ R = sidereal_rotation_matrix(gast)
1305
+ W = polar_motion_matrix(xp, yp)
1306
+ return R.T @ (W.T @ r_itrf)
1307
+
1308
+
1309
+ def gcrf_to_pef(
1310
+ r_gcrf: NDArray[np.floating],
1311
+ jd_ut1: float,
1312
+ jd_tt: float,
1313
+ ) -> NDArray[np.floating]:
1314
+ """
1315
+ Transform position from GCRF (inertial) to PEF (Earth-fixed, rotation only).
1316
+
1317
+ PEF (Pseudo-Earth Fixed) is an intermediate reference frame between
1318
+ GCRF and ITRF. It includes precession, nutation, and Earth rotation,
1319
+ but excludes polar motion.
1320
+
1321
+ Parameters
1322
+ ----------
1323
+ r_gcrf : ndarray
1324
+ Position in GCRF (km), shape (3,).
1325
+ jd_ut1 : float
1326
+ Julian date in UT1.
1327
+ jd_tt : float
1328
+ Julian date in TT.
1329
+
1330
+ Returns
1331
+ -------
1332
+ r_pef : ndarray
1333
+ Position in PEF (km), shape (3,).
1334
+
1335
+ Notes
1336
+ -----
1337
+ The transformation chain is: GCRF -> MOD -> TOD -> PEF
1338
+ - Precession: GCRF -> MOD
1339
+ - Nutation: MOD -> TOD
1340
+ - Sidereal rotation: TOD -> PEF
1341
+
1342
+ See Also
1343
+ --------
1344
+ pef_to_gcrf : Inverse transformation
1345
+ gcrf_to_itrf : Includes polar motion
1346
+
1347
+ References
1348
+ ----------
1349
+ .. [1] Vallado et al., "Fundamentals of Astrodynamics and Applications", 4th ed.
1350
+ """
1351
+ # Precession: GCRF -> MOD
1352
+ P = precession_matrix_iau76(jd_tt)
1353
+ r_mod = P @ r_gcrf
1354
+
1355
+ # Nutation: MOD -> TOD
1356
+ N = nutation_matrix(jd_tt)
1357
+ r_tod = N @ r_mod
1358
+
1359
+ # Sidereal rotation: TOD -> PEF
1360
+ gast = gast_iau82(jd_ut1, jd_tt)
1361
+ R = sidereal_rotation_matrix(gast)
1362
+ r_pef = R @ r_tod
1363
+
1364
+ return r_pef
1365
+
1366
+
1367
+ def pef_to_gcrf(
1368
+ r_pef: NDArray[np.floating],
1369
+ jd_ut1: float,
1370
+ jd_tt: float,
1371
+ ) -> NDArray[np.floating]:
1372
+ """
1373
+ Transform position from PEF (Earth-fixed, rotation only) to GCRF (inertial).
1374
+
1375
+ Inverse of gcrf_to_pef.
1376
+
1377
+ Parameters
1378
+ ----------
1379
+ r_pef : ndarray
1380
+ Position in PEF (km), shape (3,).
1381
+ jd_ut1 : float
1382
+ Julian date in UT1.
1383
+ jd_tt : float
1384
+ Julian date in TT.
1385
+
1386
+ Returns
1387
+ -------
1388
+ r_gcrf : ndarray
1389
+ Position in GCRF (km), shape (3,).
1390
+
1391
+ See Also
1392
+ --------
1393
+ gcrf_to_pef : Forward transformation
1394
+ """
1395
+ # Compute rotation matrices
1396
+ P = precession_matrix_iau76(jd_tt)
1397
+ N = nutation_matrix(jd_tt)
1398
+ gast = gast_iau82(jd_ut1, jd_tt)
1399
+ R = sidereal_rotation_matrix(gast)
1400
+
1401
+ # Inverse transformation: GCRF = P.T * N.T * R.T * PEF
1402
+ r_tod = R.T @ r_pef
1403
+ r_mod = N.T @ r_tod
1404
+ r_gcrf = P.T @ r_mod
1405
+
1406
+ return r_gcrf
1407
+
1408
+
1409
+ def clear_transformation_cache() -> None:
1410
+ """Clear cached transformation matrices.
1411
+
1412
+ Call this function to clear all cached precession and nutation
1413
+ matrices. Useful when memory is constrained or after processing
1414
+ a batch of observations at different epochs.
1415
+ """
1416
+ _precession_matrix_cached.cache_clear()
1417
+ _nutation_matrix_cached.cache_clear()
1418
+ _logger.debug("Transformation matrix cache cleared")
1419
+
1420
+
1421
+ def get_cache_info() -> dict[str, Any]:
1422
+ """Get cache statistics for transformation matrices.
1423
+
1424
+ Returns
1425
+ -------
1426
+ dict
1427
+ Dictionary with 'precession' and 'nutation' keys, each containing
1428
+ CacheInfo namedtuple with hits, misses, maxsize, currsize.
1429
+ """
1430
+ return {
1431
+ "precession": _precession_matrix_cached.cache_info(),
1432
+ "nutation": _nutation_matrix_cached.cache_info(),
1433
+ }
1434
+
1435
+
650
1436
  __all__ = [
651
1437
  # Time utilities
652
1438
  "julian_centuries_j2000",
@@ -669,9 +1455,32 @@ __all__ = [
669
1455
  # Full transformations
670
1456
  "gcrf_to_itrf",
671
1457
  "itrf_to_gcrf",
1458
+ "gcrf_to_pef",
1459
+ "pef_to_gcrf",
672
1460
  "eci_to_ecef",
673
1461
  "ecef_to_eci",
674
1462
  # Ecliptic/equatorial
675
1463
  "ecliptic_to_equatorial",
676
1464
  "equatorial_to_ecliptic",
1465
+ # TEME transformations (for SGP4/SDP4)
1466
+ "teme_to_pef",
1467
+ "pef_to_teme",
1468
+ "teme_to_itrf",
1469
+ "itrf_to_teme",
1470
+ "teme_to_gcrf",
1471
+ "gcrf_to_teme",
1472
+ "teme_to_itrf_with_velocity",
1473
+ "itrf_to_teme_with_velocity",
1474
+ # TOD/MOD transformations (legacy conventions)
1475
+ "gcrf_to_mod",
1476
+ "mod_to_gcrf",
1477
+ "gcrf_to_tod",
1478
+ "tod_to_gcrf",
1479
+ "mod_to_tod",
1480
+ "tod_to_mod",
1481
+ "tod_to_itrf",
1482
+ "itrf_to_tod",
1483
+ # Cache management
1484
+ "clear_transformation_cache",
1485
+ "get_cache_info",
677
1486
  ]