nrl-tracker 1.4.0__py3-none-any.whl → 1.6.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.
@@ -23,7 +23,7 @@ References
23
23
 
24
24
  import logging
25
25
  from functools import lru_cache
26
- from typing import Tuple
26
+ from typing import Optional, Tuple
27
27
 
28
28
  import numpy as np
29
29
  from numpy.typing import NDArray
@@ -689,6 +689,619 @@ def equatorial_to_ecliptic(
689
689
  return R @ r_eq
690
690
 
691
691
 
692
+ # =============================================================================
693
+ # TEME Frame Transformations
694
+ # =============================================================================
695
+
696
+
697
+ def teme_to_pef(
698
+ r_teme: NDArray[np.floating],
699
+ jd_ut1: float,
700
+ ) -> NDArray[np.floating]:
701
+ """
702
+ Transform position from TEME to PEF (Pseudo Earth-Fixed).
703
+
704
+ TEME is the True Equator, Mean Equinox frame used by SGP4.
705
+ This transformation applies only the GMST rotation.
706
+
707
+ Parameters
708
+ ----------
709
+ r_teme : ndarray
710
+ Position in TEME frame (km), shape (3,).
711
+ jd_ut1 : float
712
+ Julian date in UT1.
713
+
714
+ Returns
715
+ -------
716
+ r_pef : ndarray
717
+ Position in PEF frame (km), shape (3,).
718
+ """
719
+ gmst = gmst_iau82(jd_ut1)
720
+ R = sidereal_rotation_matrix(gmst)
721
+ return R @ r_teme
722
+
723
+
724
+ def pef_to_teme(
725
+ r_pef: NDArray[np.floating],
726
+ jd_ut1: float,
727
+ ) -> NDArray[np.floating]:
728
+ """
729
+ Transform position from PEF to TEME.
730
+
731
+ Parameters
732
+ ----------
733
+ r_pef : ndarray
734
+ Position in PEF frame (km), shape (3,).
735
+ jd_ut1 : float
736
+ Julian date in UT1.
737
+
738
+ Returns
739
+ -------
740
+ r_teme : ndarray
741
+ Position in TEME frame (km), shape (3,).
742
+ """
743
+ gmst = gmst_iau82(jd_ut1)
744
+ R = sidereal_rotation_matrix(gmst)
745
+ return R.T @ r_pef
746
+
747
+
748
+ def teme_to_itrf(
749
+ r_teme: NDArray[np.floating],
750
+ jd_ut1: float,
751
+ xp: float = 0.0,
752
+ yp: float = 0.0,
753
+ ) -> NDArray[np.floating]:
754
+ """
755
+ Transform position from TEME to ITRF (Earth-fixed).
756
+
757
+ TEME is the True Equator, Mean Equinox frame used by SGP4/SDP4.
758
+ This is the frame in which TLE-propagated positions are expressed.
759
+
760
+ Parameters
761
+ ----------
762
+ r_teme : ndarray
763
+ Position in TEME frame (km), shape (3,).
764
+ jd_ut1 : float
765
+ Julian date in UT1.
766
+ xp : float, optional
767
+ Polar motion x (radians). Default 0.
768
+ yp : float, optional
769
+ Polar motion y (radians). Default 0.
770
+
771
+ Returns
772
+ -------
773
+ r_itrf : ndarray
774
+ Position in ITRF frame (km), shape (3,).
775
+
776
+ Notes
777
+ -----
778
+ TEME is a quasi-inertial frame that uses the mean equinox instead
779
+ of the true equinox. The transformation sequence is:
780
+
781
+ TEME -> PEF (via GMST rotation) -> ITRF (via polar motion)
782
+
783
+ Examples
784
+ --------
785
+ >>> from pytcl.astronomical.sgp4 import sgp4_propagate
786
+ >>> from pytcl.astronomical.tle import parse_tle
787
+ >>> tle = parse_tle(line1, line2)
788
+ >>> state = sgp4_propagate(tle, 0.0)
789
+ >>> r_itrf = teme_to_itrf(state.r, jd_ut1)
790
+ """
791
+ r_pef = teme_to_pef(r_teme, jd_ut1)
792
+ W = polar_motion_matrix(xp, yp)
793
+ return W @ r_pef
794
+
795
+
796
+ def itrf_to_teme(
797
+ r_itrf: NDArray[np.floating],
798
+ jd_ut1: float,
799
+ xp: float = 0.0,
800
+ yp: float = 0.0,
801
+ ) -> NDArray[np.floating]:
802
+ """
803
+ Transform position from ITRF to TEME.
804
+
805
+ Parameters
806
+ ----------
807
+ r_itrf : ndarray
808
+ Position in ITRF frame (km), shape (3,).
809
+ jd_ut1 : float
810
+ Julian date in UT1.
811
+ xp : float, optional
812
+ Polar motion x (radians). Default 0.
813
+ yp : float, optional
814
+ Polar motion y (radians). Default 0.
815
+
816
+ Returns
817
+ -------
818
+ r_teme : ndarray
819
+ Position in TEME frame (km), shape (3,).
820
+ """
821
+ W = polar_motion_matrix(xp, yp)
822
+ r_pef = W.T @ r_itrf
823
+ return pef_to_teme(r_pef, jd_ut1)
824
+
825
+
826
+ def teme_to_gcrf(
827
+ r_teme: NDArray[np.floating],
828
+ jd_tt: float,
829
+ ) -> NDArray[np.floating]:
830
+ """
831
+ Transform position from TEME to GCRF (inertial).
832
+
833
+ This transformation accounts for the difference between
834
+ the mean and true equinox (equation of equinoxes) and then
835
+ applies precession and nutation to go from TOD to GCRF.
836
+
837
+ Parameters
838
+ ----------
839
+ r_teme : ndarray
840
+ Position in TEME frame (km), shape (3,).
841
+ jd_tt : float
842
+ Julian date in TT (Terrestrial Time).
843
+
844
+ Returns
845
+ -------
846
+ r_gcrf : ndarray
847
+ Position in GCRF frame (km), shape (3,).
848
+
849
+ Notes
850
+ -----
851
+ The transformation sequence is:
852
+
853
+ TEME -> TOD (via equation of equinoxes)
854
+ TOD -> MOD (via nutation, inverse)
855
+ MOD -> GCRF (via precession, inverse)
856
+
857
+ Examples
858
+ --------
859
+ >>> state = sgp4_propagate(tle, 60.0)
860
+ >>> r_gcrf = teme_to_gcrf(state.r, jd_tt)
861
+ """
862
+ eq_eq = equation_of_equinoxes(jd_tt)
863
+
864
+ # TEME to TOD: rotate by equation of equinoxes
865
+ cos_eq = np.cos(-eq_eq)
866
+ sin_eq = np.sin(-eq_eq)
867
+
868
+ R_eq = np.array([[cos_eq, -sin_eq, 0], [sin_eq, cos_eq, 0], [0, 0, 1]])
869
+
870
+ r_tod = R_eq @ r_teme
871
+
872
+ # TOD to MOD (inverse nutation)
873
+ N = nutation_matrix(jd_tt)
874
+ r_mod = N.T @ r_tod
875
+
876
+ # MOD to GCRF (inverse precession)
877
+ P = precession_matrix_iau76(jd_tt)
878
+ return P.T @ r_mod
879
+
880
+
881
+ def gcrf_to_teme(
882
+ r_gcrf: NDArray[np.floating],
883
+ jd_tt: float,
884
+ ) -> NDArray[np.floating]:
885
+ """
886
+ Transform position from GCRF to TEME.
887
+
888
+ Parameters
889
+ ----------
890
+ r_gcrf : ndarray
891
+ Position in GCRF frame (km), shape (3,).
892
+ jd_tt : float
893
+ Julian date in TT.
894
+
895
+ Returns
896
+ -------
897
+ r_teme : ndarray
898
+ Position in TEME frame (km), shape (3,).
899
+ """
900
+ # GCRF to MOD (precession)
901
+ P = precession_matrix_iau76(jd_tt)
902
+ r_mod = P @ r_gcrf
903
+
904
+ # MOD to TOD (nutation)
905
+ N = nutation_matrix(jd_tt)
906
+ r_tod = N @ r_mod
907
+
908
+ # TOD to TEME: rotate by equation of equinoxes
909
+ eq_eq = equation_of_equinoxes(jd_tt)
910
+ cos_eq = np.cos(eq_eq)
911
+ sin_eq = np.sin(eq_eq)
912
+
913
+ R_eq = np.array([[cos_eq, -sin_eq, 0], [sin_eq, cos_eq, 0], [0, 0, 1]])
914
+
915
+ return R_eq @ r_tod
916
+
917
+
918
+ def teme_to_itrf_with_velocity(
919
+ r_teme: NDArray[np.floating],
920
+ v_teme: NDArray[np.floating],
921
+ jd_ut1: float,
922
+ xp: float = 0.0,
923
+ yp: float = 0.0,
924
+ ) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
925
+ """
926
+ Transform position and velocity from TEME to ITRF.
927
+
928
+ This properly accounts for the velocity transformation including
929
+ the Earth's rotation rate.
930
+
931
+ Parameters
932
+ ----------
933
+ r_teme : ndarray
934
+ Position in TEME frame (km), shape (3,).
935
+ v_teme : ndarray
936
+ Velocity in TEME frame (km/s), shape (3,).
937
+ jd_ut1 : float
938
+ Julian date in UT1.
939
+ xp : float, optional
940
+ Polar motion x (radians). Default 0.
941
+ yp : float, optional
942
+ Polar motion y (radians). Default 0.
943
+
944
+ Returns
945
+ -------
946
+ r_itrf : ndarray
947
+ Position in ITRF frame (km), shape (3,).
948
+ v_itrf : ndarray
949
+ Velocity in ITRF frame (km/s), shape (3,).
950
+ """
951
+ omega_earth = 7.29211514670698e-5 # rad/s
952
+
953
+ gmst = gmst_iau82(jd_ut1)
954
+ R = sidereal_rotation_matrix(gmst)
955
+ W = polar_motion_matrix(xp, yp)
956
+
957
+ # Position transformation
958
+ r_pef = R @ r_teme
959
+ r_itrf = W @ r_pef
960
+
961
+ # Velocity includes Earth rotation effect
962
+ omega_vec = np.array([0.0, 0.0, omega_earth])
963
+ v_pef = R @ v_teme - np.cross(omega_vec, r_pef)
964
+ v_itrf = W @ v_pef
965
+
966
+ return r_itrf, v_itrf
967
+
968
+
969
+ def itrf_to_teme_with_velocity(
970
+ r_itrf: NDArray[np.floating],
971
+ v_itrf: NDArray[np.floating],
972
+ jd_ut1: float,
973
+ xp: float = 0.0,
974
+ yp: float = 0.0,
975
+ ) -> Tuple[NDArray[np.floating], NDArray[np.floating]]:
976
+ """
977
+ Transform position and velocity from ITRF to TEME.
978
+
979
+ Parameters
980
+ ----------
981
+ r_itrf : ndarray
982
+ Position in ITRF frame (km), shape (3,).
983
+ v_itrf : ndarray
984
+ Velocity in ITRF frame (km/s), shape (3,).
985
+ jd_ut1 : float
986
+ Julian date in UT1.
987
+ xp : float, optional
988
+ Polar motion x (radians). Default 0.
989
+ yp : float, optional
990
+ Polar motion y (radians). Default 0.
991
+
992
+ Returns
993
+ -------
994
+ r_teme : ndarray
995
+ Position in TEME frame (km), shape (3,).
996
+ v_teme : ndarray
997
+ Velocity in TEME frame (km/s), shape (3,).
998
+ """
999
+ omega_earth = 7.29211514670698e-5 # rad/s
1000
+
1001
+ gmst = gmst_iau82(jd_ut1)
1002
+ R = sidereal_rotation_matrix(gmst)
1003
+ W = polar_motion_matrix(xp, yp)
1004
+
1005
+ # Position transformation
1006
+ r_pef = W.T @ r_itrf
1007
+ r_teme = R.T @ r_pef
1008
+
1009
+ # Velocity includes Earth rotation effect
1010
+ omega_vec = np.array([0.0, 0.0, omega_earth])
1011
+ v_pef = W.T @ v_itrf
1012
+ v_teme = R.T @ (v_pef + np.cross(omega_vec, r_pef))
1013
+
1014
+ return r_teme, v_teme
1015
+
1016
+
1017
+ # =============================================================================
1018
+ # TOD/MOD Frame Transformations (Legacy Conventions)
1019
+ # =============================================================================
1020
+
1021
+
1022
+ def gcrf_to_mod(
1023
+ r_gcrf: NDArray[np.floating],
1024
+ jd_tt: float,
1025
+ ) -> NDArray[np.floating]:
1026
+ """
1027
+ Transform position from GCRF to MOD (Mean of Date).
1028
+
1029
+ MOD is the mean equator and mean equinox of date frame.
1030
+ This applies only the precession transformation.
1031
+
1032
+ Parameters
1033
+ ----------
1034
+ r_gcrf : ndarray
1035
+ Position in GCRF frame (km), shape (3,).
1036
+ jd_tt : float
1037
+ Julian date in TT (Terrestrial Time).
1038
+
1039
+ Returns
1040
+ -------
1041
+ r_mod : ndarray
1042
+ Position in MOD frame (km), shape (3,).
1043
+
1044
+ Notes
1045
+ -----
1046
+ MOD is a legacy frame convention. For most modern applications,
1047
+ GCRF (J2000) is preferred. MOD was historically used in older
1048
+ software and publications.
1049
+
1050
+ The transformation is simply the precession matrix:
1051
+ r_mod = P @ r_gcrf
1052
+
1053
+ See Also
1054
+ --------
1055
+ mod_to_gcrf : Inverse transformation.
1056
+ gcrf_to_tod : Includes nutation for true of date.
1057
+ """
1058
+ P = precession_matrix_iau76(jd_tt)
1059
+ return P @ r_gcrf
1060
+
1061
+
1062
+ def mod_to_gcrf(
1063
+ r_mod: NDArray[np.floating],
1064
+ jd_tt: float,
1065
+ ) -> NDArray[np.floating]:
1066
+ """
1067
+ Transform position from MOD (Mean of Date) to GCRF.
1068
+
1069
+ Parameters
1070
+ ----------
1071
+ r_mod : ndarray
1072
+ Position in MOD frame (km), shape (3,).
1073
+ jd_tt : float
1074
+ Julian date in TT.
1075
+
1076
+ Returns
1077
+ -------
1078
+ r_gcrf : ndarray
1079
+ Position in GCRF frame (km), shape (3,).
1080
+
1081
+ See Also
1082
+ --------
1083
+ gcrf_to_mod : Forward transformation.
1084
+ """
1085
+ P = precession_matrix_iau76(jd_tt)
1086
+ return P.T @ r_mod
1087
+
1088
+
1089
+ def gcrf_to_tod(
1090
+ r_gcrf: NDArray[np.floating],
1091
+ jd_tt: float,
1092
+ ) -> NDArray[np.floating]:
1093
+ """
1094
+ Transform position from GCRF to TOD (True of Date).
1095
+
1096
+ TOD is the true equator and true equinox of date frame.
1097
+ This applies both precession and nutation transformations.
1098
+
1099
+ Parameters
1100
+ ----------
1101
+ r_gcrf : ndarray
1102
+ Position in GCRF frame (km), shape (3,).
1103
+ jd_tt : float
1104
+ Julian date in TT (Terrestrial Time).
1105
+
1106
+ Returns
1107
+ -------
1108
+ r_tod : ndarray
1109
+ Position in TOD frame (km), shape (3,).
1110
+
1111
+ Notes
1112
+ -----
1113
+ TOD is a legacy frame convention. The transformation is:
1114
+ r_mod = P @ r_gcrf
1115
+ r_tod = N @ r_mod
1116
+
1117
+ where P is the precession matrix and N is the nutation matrix.
1118
+
1119
+ See Also
1120
+ --------
1121
+ tod_to_gcrf : Inverse transformation.
1122
+ gcrf_to_mod : Mean of date (without nutation).
1123
+ """
1124
+ P = precession_matrix_iau76(jd_tt)
1125
+ N = nutation_matrix(jd_tt)
1126
+ return N @ (P @ r_gcrf)
1127
+
1128
+
1129
+ def tod_to_gcrf(
1130
+ r_tod: NDArray[np.floating],
1131
+ jd_tt: float,
1132
+ ) -> NDArray[np.floating]:
1133
+ """
1134
+ Transform position from TOD (True of Date) to GCRF.
1135
+
1136
+ Parameters
1137
+ ----------
1138
+ r_tod : ndarray
1139
+ Position in TOD frame (km), shape (3,).
1140
+ jd_tt : float
1141
+ Julian date in TT.
1142
+
1143
+ Returns
1144
+ -------
1145
+ r_gcrf : ndarray
1146
+ Position in GCRF frame (km), shape (3,).
1147
+
1148
+ See Also
1149
+ --------
1150
+ gcrf_to_tod : Forward transformation.
1151
+ """
1152
+ P = precession_matrix_iau76(jd_tt)
1153
+ N = nutation_matrix(jd_tt)
1154
+ return P.T @ (N.T @ r_tod)
1155
+
1156
+
1157
+ def mod_to_tod(
1158
+ r_mod: NDArray[np.floating],
1159
+ jd_tt: float,
1160
+ ) -> NDArray[np.floating]:
1161
+ """
1162
+ Transform position from MOD (Mean of Date) to TOD (True of Date).
1163
+
1164
+ This applies only the nutation transformation.
1165
+
1166
+ Parameters
1167
+ ----------
1168
+ r_mod : ndarray
1169
+ Position in MOD frame (km), shape (3,).
1170
+ jd_tt : float
1171
+ Julian date in TT.
1172
+
1173
+ Returns
1174
+ -------
1175
+ r_tod : ndarray
1176
+ Position in TOD frame (km), shape (3,).
1177
+
1178
+ Notes
1179
+ -----
1180
+ The transformation is simply the nutation matrix:
1181
+ r_tod = N @ r_mod
1182
+
1183
+ See Also
1184
+ --------
1185
+ tod_to_mod : Inverse transformation.
1186
+ """
1187
+ N = nutation_matrix(jd_tt)
1188
+ return N @ r_mod
1189
+
1190
+
1191
+ def tod_to_mod(
1192
+ r_tod: NDArray[np.floating],
1193
+ jd_tt: float,
1194
+ ) -> NDArray[np.floating]:
1195
+ """
1196
+ Transform position from TOD (True of Date) to MOD (Mean of Date).
1197
+
1198
+ Parameters
1199
+ ----------
1200
+ r_tod : ndarray
1201
+ Position in TOD frame (km), shape (3,).
1202
+ jd_tt : float
1203
+ Julian date in TT.
1204
+
1205
+ Returns
1206
+ -------
1207
+ r_mod : ndarray
1208
+ Position in MOD frame (km), shape (3,).
1209
+
1210
+ See Also
1211
+ --------
1212
+ mod_to_tod : Forward transformation.
1213
+ """
1214
+ N = nutation_matrix(jd_tt)
1215
+ return N.T @ r_tod
1216
+
1217
+
1218
+ def tod_to_itrf(
1219
+ r_tod: NDArray[np.floating],
1220
+ jd_ut1: float,
1221
+ jd_tt: Optional[float] = None,
1222
+ xp: float = 0.0,
1223
+ yp: float = 0.0,
1224
+ ) -> NDArray[np.floating]:
1225
+ """
1226
+ Transform position from TOD (True of Date) to ITRF.
1227
+
1228
+ Parameters
1229
+ ----------
1230
+ r_tod : ndarray
1231
+ Position in TOD frame (km), shape (3,).
1232
+ jd_ut1 : float
1233
+ Julian date in UT1.
1234
+ jd_tt : float, optional
1235
+ Julian date in TT. If not provided, assumed equal to jd_ut1.
1236
+ xp : float, optional
1237
+ Polar motion x (radians). Default 0.
1238
+ yp : float, optional
1239
+ Polar motion y (radians). Default 0.
1240
+
1241
+ Returns
1242
+ -------
1243
+ r_itrf : ndarray
1244
+ Position in ITRF frame (km), shape (3,).
1245
+
1246
+ Notes
1247
+ -----
1248
+ The transformation applies the sidereal rotation (using GAST)
1249
+ and polar motion:
1250
+ r_pef = R(GAST) @ r_tod
1251
+ r_itrf = W @ r_pef
1252
+
1253
+ See Also
1254
+ --------
1255
+ itrf_to_tod : Inverse transformation.
1256
+ """
1257
+ if jd_tt is None:
1258
+ jd_tt = jd_ut1
1259
+ gast = gast_iau82(jd_ut1, jd_tt)
1260
+ R = sidereal_rotation_matrix(gast)
1261
+ W = polar_motion_matrix(xp, yp)
1262
+ return W @ (R @ r_tod)
1263
+
1264
+
1265
+ def itrf_to_tod(
1266
+ r_itrf: NDArray[np.floating],
1267
+ jd_ut1: float,
1268
+ jd_tt: Optional[float] = None,
1269
+ xp: float = 0.0,
1270
+ yp: float = 0.0,
1271
+ ) -> NDArray[np.floating]:
1272
+ """
1273
+ Transform position from ITRF to TOD (True of Date).
1274
+
1275
+ Parameters
1276
+ ----------
1277
+ r_itrf : ndarray
1278
+ Position in ITRF frame (km), shape (3,).
1279
+ jd_ut1 : float
1280
+ Julian date in UT1.
1281
+ jd_tt : float, optional
1282
+ Julian date in TT. If not provided, assumed equal to jd_ut1.
1283
+ xp : float, optional
1284
+ Polar motion x (radians). Default 0.
1285
+ yp : float, optional
1286
+ Polar motion y (radians). Default 0.
1287
+
1288
+ Returns
1289
+ -------
1290
+ r_tod : ndarray
1291
+ Position in TOD frame (km), shape (3,).
1292
+
1293
+ See Also
1294
+ --------
1295
+ tod_to_itrf : Forward transformation.
1296
+ """
1297
+ if jd_tt is None:
1298
+ jd_tt = jd_ut1
1299
+ gast = gast_iau82(jd_ut1, jd_tt)
1300
+ R = sidereal_rotation_matrix(gast)
1301
+ W = polar_motion_matrix(xp, yp)
1302
+ return R.T @ (W.T @ r_itrf)
1303
+
1304
+
692
1305
  def clear_transformation_cache() -> None:
693
1306
  """Clear cached transformation matrices.
694
1307
 
@@ -743,6 +1356,24 @@ __all__ = [
743
1356
  # Ecliptic/equatorial
744
1357
  "ecliptic_to_equatorial",
745
1358
  "equatorial_to_ecliptic",
1359
+ # TEME transformations (for SGP4/SDP4)
1360
+ "teme_to_pef",
1361
+ "pef_to_teme",
1362
+ "teme_to_itrf",
1363
+ "itrf_to_teme",
1364
+ "teme_to_gcrf",
1365
+ "gcrf_to_teme",
1366
+ "teme_to_itrf_with_velocity",
1367
+ "itrf_to_teme_with_velocity",
1368
+ # TOD/MOD transformations (legacy conventions)
1369
+ "gcrf_to_mod",
1370
+ "mod_to_gcrf",
1371
+ "gcrf_to_tod",
1372
+ "tod_to_gcrf",
1373
+ "mod_to_tod",
1374
+ "tod_to_mod",
1375
+ "tod_to_itrf",
1376
+ "itrf_to_tod",
746
1377
  # Cache management
747
1378
  "clear_transformation_cache",
748
1379
  "get_cache_info",