nrl-tracker 0.21.4__py3-none-any.whl → 1.7.5__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.
- {nrl_tracker-0.21.4.dist-info → nrl_tracker-1.7.5.dist-info}/METADATA +57 -10
- nrl_tracker-1.7.5.dist-info/RECORD +165 -0
- pytcl/__init__.py +4 -3
- pytcl/assignment_algorithms/__init__.py +28 -0
- pytcl/assignment_algorithms/data_association.py +2 -7
- pytcl/assignment_algorithms/gating.py +10 -10
- pytcl/assignment_algorithms/jpda.py +40 -40
- pytcl/assignment_algorithms/nd_assignment.py +379 -0
- pytcl/assignment_algorithms/network_flow.py +371 -0
- pytcl/assignment_algorithms/three_dimensional/assignment.py +3 -3
- pytcl/astronomical/__init__.py +162 -8
- pytcl/astronomical/ephemerides.py +533 -0
- pytcl/astronomical/reference_frames.py +865 -56
- pytcl/astronomical/relativity.py +473 -0
- pytcl/astronomical/sgp4.py +710 -0
- pytcl/astronomical/special_orbits.py +532 -0
- pytcl/astronomical/tle.py +558 -0
- pytcl/atmosphere/__init__.py +45 -3
- pytcl/atmosphere/ionosphere.py +512 -0
- pytcl/atmosphere/nrlmsise00.py +809 -0
- pytcl/clustering/dbscan.py +2 -2
- pytcl/clustering/gaussian_mixture.py +3 -3
- pytcl/clustering/hierarchical.py +15 -15
- pytcl/clustering/kmeans.py +4 -4
- pytcl/containers/__init__.py +28 -21
- pytcl/containers/base.py +219 -0
- pytcl/containers/cluster_set.py +2 -1
- pytcl/containers/covertree.py +26 -29
- pytcl/containers/kd_tree.py +94 -29
- pytcl/containers/measurement_set.py +1 -9
- pytcl/containers/rtree.py +200 -1
- pytcl/containers/vptree.py +21 -28
- pytcl/coordinate_systems/conversions/geodetic.py +272 -5
- pytcl/coordinate_systems/jacobians/jacobians.py +2 -2
- pytcl/coordinate_systems/projections/__init__.py +4 -2
- pytcl/coordinate_systems/projections/projections.py +2 -2
- pytcl/coordinate_systems/rotations/rotations.py +10 -6
- pytcl/core/__init__.py +18 -0
- pytcl/core/validation.py +333 -2
- pytcl/dynamic_estimation/__init__.py +26 -0
- pytcl/dynamic_estimation/gaussian_sum_filter.py +434 -0
- pytcl/dynamic_estimation/imm.py +15 -18
- pytcl/dynamic_estimation/kalman/__init__.py +30 -0
- pytcl/dynamic_estimation/kalman/constrained.py +382 -0
- pytcl/dynamic_estimation/kalman/extended.py +9 -12
- pytcl/dynamic_estimation/kalman/h_infinity.py +613 -0
- pytcl/dynamic_estimation/kalman/square_root.py +60 -573
- pytcl/dynamic_estimation/kalman/sr_ukf.py +302 -0
- pytcl/dynamic_estimation/kalman/ud_filter.py +410 -0
- pytcl/dynamic_estimation/kalman/unscented.py +9 -10
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
- pytcl/dynamic_estimation/rbpf.py +589 -0
- pytcl/dynamic_estimation/smoothers.py +1 -5
- pytcl/dynamic_models/discrete_time/__init__.py +1 -5
- pytcl/dynamic_models/process_noise/__init__.py +1 -5
- pytcl/gravity/egm.py +13 -0
- pytcl/gravity/spherical_harmonics.py +98 -37
- pytcl/gravity/tides.py +6 -6
- pytcl/logging_config.py +328 -0
- pytcl/magnetism/__init__.py +10 -14
- pytcl/magnetism/emm.py +10 -3
- pytcl/magnetism/wmm.py +260 -23
- pytcl/mathematical_functions/combinatorics/combinatorics.py +5 -5
- pytcl/mathematical_functions/geometry/geometry.py +5 -5
- pytcl/mathematical_functions/interpolation/__init__.py +2 -2
- pytcl/mathematical_functions/numerical_integration/quadrature.py +6 -6
- pytcl/mathematical_functions/signal_processing/detection.py +24 -24
- pytcl/mathematical_functions/signal_processing/filters.py +14 -14
- pytcl/mathematical_functions/signal_processing/matched_filter.py +12 -12
- pytcl/mathematical_functions/special_functions/__init__.py +2 -2
- pytcl/mathematical_functions/special_functions/bessel.py +15 -3
- pytcl/mathematical_functions/special_functions/debye.py +136 -26
- pytcl/mathematical_functions/special_functions/error_functions.py +3 -1
- pytcl/mathematical_functions/special_functions/gamma_functions.py +4 -4
- pytcl/mathematical_functions/special_functions/hypergeometric.py +81 -15
- pytcl/mathematical_functions/transforms/fourier.py +8 -8
- pytcl/mathematical_functions/transforms/stft.py +12 -12
- pytcl/mathematical_functions/transforms/wavelets.py +9 -9
- pytcl/navigation/__init__.py +14 -10
- pytcl/navigation/geodesy.py +246 -160
- pytcl/navigation/great_circle.py +101 -19
- pytcl/navigation/ins.py +1 -5
- pytcl/plotting/coordinates.py +7 -7
- pytcl/plotting/tracks.py +2 -2
- pytcl/static_estimation/maximum_likelihood.py +16 -14
- pytcl/static_estimation/robust.py +5 -5
- pytcl/terrain/loaders.py +5 -5
- pytcl/trackers/__init__.py +3 -14
- pytcl/trackers/hypothesis.py +1 -1
- pytcl/trackers/mht.py +9 -9
- pytcl/trackers/multi_target.py +2 -5
- nrl_tracker-0.21.4.dist-info/RECORD +0 -148
- {nrl_tracker-0.21.4.dist-info → nrl_tracker-1.7.5.dist-info}/LICENSE +0 -0
- {nrl_tracker-0.21.4.dist-info → nrl_tracker-1.7.5.dist-info}/WHEEL +0 -0
- {nrl_tracker-0.21.4.dist-info → nrl_tracker-1.7.5.dist-info}/top_level.txt +0 -0
|
@@ -21,13 +21,31 @@ References
|
|
|
21
21
|
A&A, 2003.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
]
|