nrl-tracker 0.22.5__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.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/METADATA +57 -10
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/RECORD +84 -69
- pytcl/__init__.py +4 -3
- pytcl/assignment_algorithms/__init__.py +28 -0
- 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 +104 -3
- pytcl/astronomical/ephemerides.py +14 -11
- pytcl/astronomical/reference_frames.py +865 -56
- pytcl/astronomical/relativity.py +6 -5
- pytcl/astronomical/sgp4.py +710 -0
- pytcl/astronomical/special_orbits.py +532 -0
- pytcl/astronomical/tle.py +558 -0
- pytcl/atmosphere/__init__.py +43 -1
- 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 +24 -0
- pytcl/containers/base.py +219 -0
- pytcl/containers/cluster_set.py +12 -2
- pytcl/containers/covertree.py +26 -29
- pytcl/containers/kd_tree.py +94 -29
- 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 +1 -1
- 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 +14 -14
- pytcl/dynamic_estimation/kalman/__init__.py +30 -0
- pytcl/dynamic_estimation/kalman/constrained.py +382 -0
- pytcl/dynamic_estimation/kalman/extended.py +8 -8
- 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 +8 -6
- pytcl/dynamic_estimation/particle_filters/bootstrap.py +15 -15
- pytcl/dynamic_estimation/rbpf.py +589 -0
- 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 +7 -0
- 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/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/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/geodesy.py +246 -160
- pytcl/navigation/great_circle.py +101 -19
- 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/hypothesis.py +1 -1
- pytcl/trackers/mht.py +9 -9
- pytcl/trackers/multi_target.py +1 -1
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/LICENSE +0 -0
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/WHEEL +0 -0
- {nrl_tracker-0.22.5.dist-info → nrl_tracker-1.7.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Two-Line Element (TLE) set parsing and data structures.
|
|
3
|
+
|
|
4
|
+
This module provides functions for parsing NORAD Two-Line Element sets,
|
|
5
|
+
which are the standard format for distributing satellite orbital data.
|
|
6
|
+
|
|
7
|
+
TLE Format
|
|
8
|
+
----------
|
|
9
|
+
A TLE consists of two 69-character lines containing orbital elements
|
|
10
|
+
in a specialized format used by NORAD/USSPACECOM.
|
|
11
|
+
|
|
12
|
+
Line 1 format:
|
|
13
|
+
1 NNNNNC NNNNNAAA NNNNN.NNNNNNNN +.NNNNNNNN +NNNNN-N +NNNNN-N N NNNNN
|
|
14
|
+
|
|
15
|
+
Column Description
|
|
16
|
+
01 Line number (1)
|
|
17
|
+
03-07 Satellite catalog number
|
|
18
|
+
08 Classification (U=unclassified)
|
|
19
|
+
10-11 International designator (last two digits of launch year)
|
|
20
|
+
12-14 International designator (launch number of the year)
|
|
21
|
+
15-17 International designator (piece of the launch)
|
|
22
|
+
19-20 Epoch year (last two digits)
|
|
23
|
+
21-32 Epoch (day of the year and fractional portion of the day)
|
|
24
|
+
34-43 First derivative of mean motion (ballistic coefficient)
|
|
25
|
+
45-52 Second derivative of mean motion (decimal point assumed)
|
|
26
|
+
54-61 BSTAR drag term (decimal point assumed)
|
|
27
|
+
63 Ephemeris type
|
|
28
|
+
65-68 Element set number
|
|
29
|
+
69 Checksum (modulo 10)
|
|
30
|
+
|
|
31
|
+
Line 2 format:
|
|
32
|
+
2 NNNNN NNN.NNNN NNN.NNNN NNNNNNN NNN.NNNN NNN.NNNN NN.NNNNNNNNNNNNNN
|
|
33
|
+
|
|
34
|
+
Column Description
|
|
35
|
+
01 Line number (2)
|
|
36
|
+
03-07 Satellite catalog number
|
|
37
|
+
09-16 Inclination (degrees)
|
|
38
|
+
18-25 Right ascension of ascending node (degrees)
|
|
39
|
+
27-33 Eccentricity (decimal point assumed)
|
|
40
|
+
35-42 Argument of perigee (degrees)
|
|
41
|
+
44-51 Mean anomaly (degrees)
|
|
42
|
+
53-63 Mean motion (revolutions per day)
|
|
43
|
+
64-68 Revolution number at epoch
|
|
44
|
+
69 Checksum (modulo 10)
|
|
45
|
+
|
|
46
|
+
References
|
|
47
|
+
----------
|
|
48
|
+
.. [1] Vallado, D. A., "Fundamentals of Astrodynamics and Applications,"
|
|
49
|
+
4th ed., Microcosm Press, 2013, Appendix C.
|
|
50
|
+
.. [2] Hoots, F. R. and Roehrich, R. L., "Spacetrack Report No. 3:
|
|
51
|
+
Models for Propagation of NORAD Element Sets," 1980.
|
|
52
|
+
.. [3] CelesTrak, https://celestrak.org/NORAD/documentation/tle-fmt.php
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from datetime import datetime, timezone
|
|
56
|
+
from typing import NamedTuple
|
|
57
|
+
|
|
58
|
+
import numpy as np
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TLE(NamedTuple):
|
|
62
|
+
"""Two-Line Element set data structure.
|
|
63
|
+
|
|
64
|
+
Contains all orbital elements and metadata from a NORAD TLE.
|
|
65
|
+
|
|
66
|
+
Attributes
|
|
67
|
+
----------
|
|
68
|
+
name : str
|
|
69
|
+
Satellite name (from line 0, if present).
|
|
70
|
+
catalog_number : int
|
|
71
|
+
NORAD catalog number.
|
|
72
|
+
classification : str
|
|
73
|
+
Classification ('U' = unclassified).
|
|
74
|
+
int_designator : str
|
|
75
|
+
International designator (e.g., '98067A').
|
|
76
|
+
epoch_year : int
|
|
77
|
+
Epoch year (4-digit).
|
|
78
|
+
epoch_day : float
|
|
79
|
+
Epoch day of year (fractional).
|
|
80
|
+
ndot : float
|
|
81
|
+
First derivative of mean motion (rev/day^2).
|
|
82
|
+
nddot : float
|
|
83
|
+
Second derivative of mean motion (rev/day^3).
|
|
84
|
+
bstar : float
|
|
85
|
+
BSTAR drag coefficient (1/Earth radii).
|
|
86
|
+
ephemeris_type : int
|
|
87
|
+
Ephemeris type (usually 0 for SGP4).
|
|
88
|
+
element_set_number : int
|
|
89
|
+
Element set number.
|
|
90
|
+
inclination : float
|
|
91
|
+
Inclination (radians).
|
|
92
|
+
raan : float
|
|
93
|
+
Right ascension of ascending node (radians).
|
|
94
|
+
eccentricity : float
|
|
95
|
+
Eccentricity (dimensionless).
|
|
96
|
+
arg_perigee : float
|
|
97
|
+
Argument of perigee (radians).
|
|
98
|
+
mean_anomaly : float
|
|
99
|
+
Mean anomaly (radians).
|
|
100
|
+
mean_motion : float
|
|
101
|
+
Mean motion (radians/minute).
|
|
102
|
+
revolution_number : int
|
|
103
|
+
Revolution number at epoch.
|
|
104
|
+
line1 : str
|
|
105
|
+
Original TLE line 1.
|
|
106
|
+
line2 : str
|
|
107
|
+
Original TLE line 2.
|
|
108
|
+
|
|
109
|
+
Notes
|
|
110
|
+
-----
|
|
111
|
+
Angular quantities are stored in radians for consistency with the
|
|
112
|
+
rest of the pytcl library. Mean motion is in radians/minute as
|
|
113
|
+
required by SGP4/SDP4.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
name: str
|
|
117
|
+
catalog_number: int
|
|
118
|
+
classification: str
|
|
119
|
+
int_designator: str
|
|
120
|
+
epoch_year: int
|
|
121
|
+
epoch_day: float
|
|
122
|
+
ndot: float
|
|
123
|
+
nddot: float
|
|
124
|
+
bstar: float
|
|
125
|
+
ephemeris_type: int
|
|
126
|
+
element_set_number: int
|
|
127
|
+
inclination: float
|
|
128
|
+
raan: float
|
|
129
|
+
eccentricity: float
|
|
130
|
+
arg_perigee: float
|
|
131
|
+
mean_anomaly: float
|
|
132
|
+
mean_motion: float
|
|
133
|
+
revolution_number: int
|
|
134
|
+
line1: str
|
|
135
|
+
line2: str
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_decimal_with_exponent(s: str) -> float:
|
|
139
|
+
"""Parse TLE decimal format with implicit decimal point and exponent.
|
|
140
|
+
|
|
141
|
+
TLE format uses: +NNNNN-N meaning 0.NNNNN * 10^-N
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
s : str
|
|
146
|
+
String in TLE decimal format (e.g., '+12345-4' or ' 12345-4').
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
float
|
|
151
|
+
Parsed value.
|
|
152
|
+
"""
|
|
153
|
+
s = s.strip()
|
|
154
|
+
if not s or s == "00000-0" or s == "+00000-0" or s == " 00000-0":
|
|
155
|
+
return 0.0
|
|
156
|
+
|
|
157
|
+
# Handle sign
|
|
158
|
+
if s[0] == "-":
|
|
159
|
+
sign = -1
|
|
160
|
+
s = s[1:]
|
|
161
|
+
elif s[0] == "+" or s[0] == " ":
|
|
162
|
+
sign = 1
|
|
163
|
+
s = s[1:]
|
|
164
|
+
else:
|
|
165
|
+
sign = 1
|
|
166
|
+
|
|
167
|
+
# Find exponent marker (- or +)
|
|
168
|
+
for i in range(len(s) - 1, 0, -1):
|
|
169
|
+
if s[i] == "-" or s[i] == "+":
|
|
170
|
+
mantissa = float("0." + s[:i])
|
|
171
|
+
exponent = int(s[i:])
|
|
172
|
+
return sign * mantissa * (10**exponent)
|
|
173
|
+
|
|
174
|
+
# No exponent found, just parse as decimal
|
|
175
|
+
return sign * float("0." + s)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _verify_checksum(line: str) -> bool:
|
|
179
|
+
"""Verify TLE line checksum.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
line : str
|
|
184
|
+
TLE line (69 characters).
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
bool
|
|
189
|
+
True if checksum is valid.
|
|
190
|
+
"""
|
|
191
|
+
if len(line) < 69:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
checksum = 0
|
|
195
|
+
for c in line[:68]:
|
|
196
|
+
if c.isdigit():
|
|
197
|
+
checksum += int(c)
|
|
198
|
+
elif c == "-":
|
|
199
|
+
checksum += 1
|
|
200
|
+
|
|
201
|
+
expected = int(line[68])
|
|
202
|
+
return (checksum % 10) == expected
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _epoch_year_to_full_year(year_2digit: int) -> int:
|
|
206
|
+
"""Convert 2-digit epoch year to 4-digit year.
|
|
207
|
+
|
|
208
|
+
Uses the convention that years < 57 are in 2000s, otherwise 1900s.
|
|
209
|
+
This matches the NORAD convention (Sputnik launched in 1957).
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
year_2digit : int
|
|
214
|
+
Two-digit year (0-99).
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
int
|
|
219
|
+
Four-digit year.
|
|
220
|
+
"""
|
|
221
|
+
if year_2digit < 57:
|
|
222
|
+
return 2000 + year_2digit
|
|
223
|
+
else:
|
|
224
|
+
return 1900 + year_2digit
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def parse_tle(
|
|
228
|
+
line1: str,
|
|
229
|
+
line2: str,
|
|
230
|
+
name: str = "",
|
|
231
|
+
verify_checksum: bool = True,
|
|
232
|
+
) -> TLE:
|
|
233
|
+
"""Parse a Two-Line Element set.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
line1 : str
|
|
238
|
+
First line of the TLE (69 characters).
|
|
239
|
+
line2 : str
|
|
240
|
+
Second line of the TLE (69 characters).
|
|
241
|
+
name : str, optional
|
|
242
|
+
Satellite name. Default empty.
|
|
243
|
+
verify_checksum : bool, optional
|
|
244
|
+
Whether to verify line checksums. Default True.
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
tle : TLE
|
|
249
|
+
Parsed TLE data structure.
|
|
250
|
+
|
|
251
|
+
Raises
|
|
252
|
+
------
|
|
253
|
+
ValueError
|
|
254
|
+
If TLE format is invalid or checksum fails.
|
|
255
|
+
|
|
256
|
+
Examples
|
|
257
|
+
--------
|
|
258
|
+
>>> line1 = "1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9993"
|
|
259
|
+
>>> line2 = "2 25544 51.6400 247.4627 0006703 130.5360 325.0288 15.49815350479001"
|
|
260
|
+
>>> tle = parse_tle(line1, line2, name="ISS (ZARYA)")
|
|
261
|
+
>>> print(f"Inclination: {np.degrees(tle.inclination):.4f} deg")
|
|
262
|
+
"""
|
|
263
|
+
# Validate line lengths
|
|
264
|
+
line1 = line1.rstrip()
|
|
265
|
+
line2 = line2.rstrip()
|
|
266
|
+
|
|
267
|
+
if len(line1) < 69:
|
|
268
|
+
raise ValueError(f"Line 1 too short: {len(line1)} characters (need 69)")
|
|
269
|
+
if len(line2) < 69:
|
|
270
|
+
raise ValueError(f"Line 2 too short: {len(line2)} characters (need 69)")
|
|
271
|
+
|
|
272
|
+
# Verify line numbers
|
|
273
|
+
if line1[0] != "1":
|
|
274
|
+
raise ValueError(f"Line 1 should start with '1', got '{line1[0]}'")
|
|
275
|
+
if line2[0] != "2":
|
|
276
|
+
raise ValueError(f"Line 2 should start with '2', got '{line2[0]}'")
|
|
277
|
+
|
|
278
|
+
# Verify checksums
|
|
279
|
+
if verify_checksum:
|
|
280
|
+
if not _verify_checksum(line1):
|
|
281
|
+
raise ValueError("Line 1 checksum failed")
|
|
282
|
+
if not _verify_checksum(line2):
|
|
283
|
+
raise ValueError("Line 2 checksum failed")
|
|
284
|
+
|
|
285
|
+
# Parse line 1
|
|
286
|
+
catalog_number = int(line1[2:7])
|
|
287
|
+
classification = line1[7]
|
|
288
|
+
int_designator = line1[9:17].strip()
|
|
289
|
+
|
|
290
|
+
epoch_year_2digit = int(line1[18:20])
|
|
291
|
+
epoch_year = _epoch_year_to_full_year(epoch_year_2digit)
|
|
292
|
+
epoch_day = float(line1[20:32])
|
|
293
|
+
|
|
294
|
+
# First derivative of mean motion (revs/day^2, divided by 2)
|
|
295
|
+
ndot_str = line1[33:43].strip()
|
|
296
|
+
ndot = float(ndot_str) * 2 # Multiply by 2 (stored as ndot/2)
|
|
297
|
+
|
|
298
|
+
# Second derivative of mean motion (revs/day^3, divided by 6)
|
|
299
|
+
nddot = _parse_decimal_with_exponent(line1[44:52]) * 6 # Multiply by 6
|
|
300
|
+
|
|
301
|
+
# BSTAR drag coefficient
|
|
302
|
+
bstar = _parse_decimal_with_exponent(line1[53:61])
|
|
303
|
+
|
|
304
|
+
ephemeris_type = int(line1[62])
|
|
305
|
+
element_set_number = int(line1[64:68].strip() or "0")
|
|
306
|
+
|
|
307
|
+
# Parse line 2
|
|
308
|
+
catalog_number_2 = int(line2[2:7])
|
|
309
|
+
if catalog_number_2 != catalog_number:
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"Catalog number mismatch: {catalog_number} vs {catalog_number_2}"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Angles in degrees
|
|
315
|
+
inclination_deg = float(line2[8:16])
|
|
316
|
+
raan_deg = float(line2[17:25])
|
|
317
|
+
|
|
318
|
+
# Eccentricity (decimal point assumed)
|
|
319
|
+
eccentricity = float("0." + line2[26:33])
|
|
320
|
+
|
|
321
|
+
arg_perigee_deg = float(line2[34:42])
|
|
322
|
+
mean_anomaly_deg = float(line2[43:51])
|
|
323
|
+
|
|
324
|
+
# Mean motion (revs/day) -> radians/minute
|
|
325
|
+
mean_motion_revs_day = float(line2[52:63])
|
|
326
|
+
mean_motion = mean_motion_revs_day * 2 * np.pi / 1440.0 # rad/min
|
|
327
|
+
|
|
328
|
+
revolution_number = int(line2[63:68].strip() or "0")
|
|
329
|
+
|
|
330
|
+
# Convert angles to radians
|
|
331
|
+
deg_to_rad = np.pi / 180.0
|
|
332
|
+
|
|
333
|
+
return TLE(
|
|
334
|
+
name=name,
|
|
335
|
+
catalog_number=catalog_number,
|
|
336
|
+
classification=classification,
|
|
337
|
+
int_designator=int_designator,
|
|
338
|
+
epoch_year=epoch_year,
|
|
339
|
+
epoch_day=epoch_day,
|
|
340
|
+
ndot=ndot,
|
|
341
|
+
nddot=nddot,
|
|
342
|
+
bstar=bstar,
|
|
343
|
+
ephemeris_type=ephemeris_type,
|
|
344
|
+
element_set_number=element_set_number,
|
|
345
|
+
inclination=inclination_deg * deg_to_rad,
|
|
346
|
+
raan=raan_deg * deg_to_rad,
|
|
347
|
+
eccentricity=eccentricity,
|
|
348
|
+
arg_perigee=arg_perigee_deg * deg_to_rad,
|
|
349
|
+
mean_anomaly=mean_anomaly_deg * deg_to_rad,
|
|
350
|
+
mean_motion=mean_motion,
|
|
351
|
+
revolution_number=revolution_number,
|
|
352
|
+
line1=line1,
|
|
353
|
+
line2=line2,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def parse_tle_3line(lines: str) -> TLE:
|
|
358
|
+
"""Parse a three-line TLE (with satellite name).
|
|
359
|
+
|
|
360
|
+
Parameters
|
|
361
|
+
----------
|
|
362
|
+
lines : str
|
|
363
|
+
Three-line TLE string (name, line1, line2).
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
tle : TLE
|
|
368
|
+
Parsed TLE data structure.
|
|
369
|
+
|
|
370
|
+
Examples
|
|
371
|
+
--------
|
|
372
|
+
>>> tle_text = '''ISS (ZARYA)
|
|
373
|
+
... 1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9993
|
|
374
|
+
... 2 25544 51.6400 247.4627 0006703 130.5360 325.0288 15.49815350479001'''
|
|
375
|
+
>>> tle = parse_tle_3line(tle_text)
|
|
376
|
+
>>> print(tle.name)
|
|
377
|
+
ISS (ZARYA)
|
|
378
|
+
"""
|
|
379
|
+
line_list = lines.strip().split("\n")
|
|
380
|
+
|
|
381
|
+
if len(line_list) < 2:
|
|
382
|
+
raise ValueError("TLE must have at least 2 lines")
|
|
383
|
+
|
|
384
|
+
if len(line_list) == 2:
|
|
385
|
+
return parse_tle(line_list[0], line_list[1])
|
|
386
|
+
else:
|
|
387
|
+
name = line_list[0].strip()
|
|
388
|
+
return parse_tle(line_list[1], line_list[2], name=name)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def tle_epoch_to_jd(tle: TLE) -> float:
|
|
392
|
+
"""Convert TLE epoch to Julian date.
|
|
393
|
+
|
|
394
|
+
Parameters
|
|
395
|
+
----------
|
|
396
|
+
tle : TLE
|
|
397
|
+
Parsed TLE.
|
|
398
|
+
|
|
399
|
+
Returns
|
|
400
|
+
-------
|
|
401
|
+
jd : float
|
|
402
|
+
Julian date of TLE epoch.
|
|
403
|
+
|
|
404
|
+
Examples
|
|
405
|
+
--------
|
|
406
|
+
>>> tle = parse_tle(line1, line2)
|
|
407
|
+
>>> jd = tle_epoch_to_jd(tle)
|
|
408
|
+
"""
|
|
409
|
+
# Start of year
|
|
410
|
+
year = tle.epoch_year
|
|
411
|
+
|
|
412
|
+
# Julian date at midnight Jan 1 of epoch year
|
|
413
|
+
# Using algorithm from time_systems module
|
|
414
|
+
a = (14 - 1) // 12
|
|
415
|
+
y = year + 4800 - a
|
|
416
|
+
m = 1 + 12 * a - 3
|
|
417
|
+
|
|
418
|
+
jd_jan1 = 1 + (153 * m + 2) // 5 + 365 * y + y // 4 - y // 100 + y // 400 - 32045
|
|
419
|
+
jd_jan1 = float(jd_jan1) - 0.5 # Midnight
|
|
420
|
+
|
|
421
|
+
# Add day of year (1-based, so subtract 1)
|
|
422
|
+
jd = jd_jan1 + (tle.epoch_day - 1.0)
|
|
423
|
+
|
|
424
|
+
return jd
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def tle_epoch_to_datetime(tle: TLE) -> datetime:
|
|
428
|
+
"""Convert TLE epoch to Python datetime.
|
|
429
|
+
|
|
430
|
+
Parameters
|
|
431
|
+
----------
|
|
432
|
+
tle : TLE
|
|
433
|
+
Parsed TLE.
|
|
434
|
+
|
|
435
|
+
Returns
|
|
436
|
+
-------
|
|
437
|
+
dt : datetime
|
|
438
|
+
TLE epoch as UTC datetime.
|
|
439
|
+
"""
|
|
440
|
+
year = tle.epoch_year
|
|
441
|
+
day_of_year = tle.epoch_day
|
|
442
|
+
|
|
443
|
+
# Integer day and fractional part
|
|
444
|
+
day_int = int(day_of_year)
|
|
445
|
+
day_frac = day_of_year - day_int
|
|
446
|
+
|
|
447
|
+
# Convert to datetime
|
|
448
|
+
dt = datetime(year, 1, 1, tzinfo=timezone.utc) + __import__("datetime").timedelta(
|
|
449
|
+
days=day_int - 1 + day_frac
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
return dt
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def format_tle(tle: TLE, include_name: bool = True) -> str:
|
|
456
|
+
"""Format TLE data structure back to TLE string.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
tle : TLE
|
|
461
|
+
TLE data structure.
|
|
462
|
+
include_name : bool, optional
|
|
463
|
+
Whether to include satellite name as line 0. Default True.
|
|
464
|
+
|
|
465
|
+
Returns
|
|
466
|
+
-------
|
|
467
|
+
str
|
|
468
|
+
Formatted TLE string.
|
|
469
|
+
"""
|
|
470
|
+
lines = []
|
|
471
|
+
|
|
472
|
+
if include_name and tle.name:
|
|
473
|
+
lines.append(tle.name)
|
|
474
|
+
|
|
475
|
+
lines.append(tle.line1)
|
|
476
|
+
lines.append(tle.line2)
|
|
477
|
+
|
|
478
|
+
return "\n".join(lines)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def is_deep_space(tle: TLE) -> bool:
|
|
482
|
+
"""Determine if TLE requires deep-space (SDP4) propagation.
|
|
483
|
+
|
|
484
|
+
Satellites with orbital period >= 225 minutes use SDP4 instead
|
|
485
|
+
of SGP4 due to lunar-solar perturbations.
|
|
486
|
+
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
489
|
+
tle : TLE
|
|
490
|
+
Parsed TLE.
|
|
491
|
+
|
|
492
|
+
Returns
|
|
493
|
+
-------
|
|
494
|
+
bool
|
|
495
|
+
True if deep-space propagation (SDP4) is required.
|
|
496
|
+
"""
|
|
497
|
+
# Mean motion in rad/min, period = 2*pi / n
|
|
498
|
+
period_minutes = 2 * np.pi / tle.mean_motion
|
|
499
|
+
return period_minutes >= 225.0
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def semi_major_axis_from_mean_motion(n: float, mu: float = 398600.4418) -> float:
|
|
503
|
+
"""Compute semi-major axis from mean motion.
|
|
504
|
+
|
|
505
|
+
Uses the relationship n = sqrt(mu / a^3) where n is in rad/s.
|
|
506
|
+
|
|
507
|
+
Parameters
|
|
508
|
+
----------
|
|
509
|
+
n : float
|
|
510
|
+
Mean motion (radians/minute).
|
|
511
|
+
mu : float, optional
|
|
512
|
+
Gravitational parameter (km^3/s^2). Default is Earth.
|
|
513
|
+
|
|
514
|
+
Returns
|
|
515
|
+
-------
|
|
516
|
+
a : float
|
|
517
|
+
Semi-major axis (km).
|
|
518
|
+
"""
|
|
519
|
+
# Convert to rad/s
|
|
520
|
+
n_rad_s = n / 60.0
|
|
521
|
+
|
|
522
|
+
# a = (mu / n^2)^(1/3)
|
|
523
|
+
return (mu / (n_rad_s**2)) ** (1.0 / 3.0)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def orbital_period_from_tle(tle: TLE) -> float:
|
|
527
|
+
"""Compute orbital period from TLE mean motion.
|
|
528
|
+
|
|
529
|
+
Parameters
|
|
530
|
+
----------
|
|
531
|
+
tle : TLE
|
|
532
|
+
Parsed TLE.
|
|
533
|
+
|
|
534
|
+
Returns
|
|
535
|
+
-------
|
|
536
|
+
period : float
|
|
537
|
+
Orbital period (seconds).
|
|
538
|
+
"""
|
|
539
|
+
# Mean motion in rad/min, period = 2*pi / n (in minutes)
|
|
540
|
+
period_minutes = 2 * np.pi / tle.mean_motion
|
|
541
|
+
return period_minutes * 60.0 # Convert to seconds
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
__all__ = [
|
|
545
|
+
# Types
|
|
546
|
+
"TLE",
|
|
547
|
+
# Parsing
|
|
548
|
+
"parse_tle",
|
|
549
|
+
"parse_tle_3line",
|
|
550
|
+
# Conversion
|
|
551
|
+
"tle_epoch_to_jd",
|
|
552
|
+
"tle_epoch_to_datetime",
|
|
553
|
+
"format_tle",
|
|
554
|
+
# Utilities
|
|
555
|
+
"is_deep_space",
|
|
556
|
+
"semi_major_axis_from_mean_motion",
|
|
557
|
+
"orbital_period_from_tle",
|
|
558
|
+
]
|
pytcl/atmosphere/__init__.py
CHANGED
|
@@ -3,8 +3,26 @@ Atmospheric models for tracking applications.
|
|
|
3
3
|
|
|
4
4
|
This module provides standard atmosphere models used for computing
|
|
5
5
|
temperature, pressure, density, and other properties at various altitudes.
|
|
6
|
+
|
|
7
|
+
Submodules
|
|
8
|
+
----------
|
|
9
|
+
models : Standard atmosphere models (US76, ISA)
|
|
10
|
+
ionosphere : Ionospheric models for GPS/GNSS corrections
|
|
6
11
|
"""
|
|
7
12
|
|
|
13
|
+
from pytcl.atmosphere.ionosphere import (
|
|
14
|
+
DEFAULT_KLOBUCHAR,
|
|
15
|
+
F_L1,
|
|
16
|
+
F_L2,
|
|
17
|
+
IonosphereState,
|
|
18
|
+
KlobucharCoefficients,
|
|
19
|
+
dual_frequency_tec,
|
|
20
|
+
ionospheric_delay_from_tec,
|
|
21
|
+
klobuchar_delay,
|
|
22
|
+
magnetic_latitude,
|
|
23
|
+
scintillation_index,
|
|
24
|
+
simple_iri,
|
|
25
|
+
)
|
|
8
26
|
from pytcl.atmosphere.models import G0 # Constants
|
|
9
27
|
from pytcl.atmosphere.models import (
|
|
10
28
|
GAMMA,
|
|
@@ -19,19 +37,43 @@ from pytcl.atmosphere.models import (
|
|
|
19
37
|
true_airspeed_from_mach,
|
|
20
38
|
us_standard_atmosphere_1976,
|
|
21
39
|
)
|
|
40
|
+
from pytcl.atmosphere.nrlmsise00 import (
|
|
41
|
+
NRLMSISE00,
|
|
42
|
+
F107Index,
|
|
43
|
+
NRLMSISE00Output,
|
|
44
|
+
nrlmsise00,
|
|
45
|
+
)
|
|
22
46
|
|
|
23
47
|
__all__ = [
|
|
48
|
+
# Atmosphere state and models
|
|
24
49
|
"AtmosphereState",
|
|
25
50
|
"us_standard_atmosphere_1976",
|
|
26
51
|
"isa_atmosphere",
|
|
27
52
|
"altitude_from_pressure",
|
|
28
53
|
"mach_number",
|
|
29
54
|
"true_airspeed_from_mach",
|
|
30
|
-
#
|
|
55
|
+
# NRLMSISE-00 High-Fidelity Model
|
|
56
|
+
"NRLMSISE00",
|
|
57
|
+
"NRLMSISE00Output",
|
|
58
|
+
"F107Index",
|
|
59
|
+
"nrlmsise00",
|
|
60
|
+
# Atmosphere constants
|
|
31
61
|
"T0",
|
|
32
62
|
"P0",
|
|
33
63
|
"RHO0",
|
|
34
64
|
"G0",
|
|
35
65
|
"R",
|
|
36
66
|
"GAMMA",
|
|
67
|
+
# Ionosphere
|
|
68
|
+
"IonosphereState",
|
|
69
|
+
"KlobucharCoefficients",
|
|
70
|
+
"DEFAULT_KLOBUCHAR",
|
|
71
|
+
"klobuchar_delay",
|
|
72
|
+
"dual_frequency_tec",
|
|
73
|
+
"ionospheric_delay_from_tec",
|
|
74
|
+
"simple_iri",
|
|
75
|
+
"magnetic_latitude",
|
|
76
|
+
"scintillation_index",
|
|
77
|
+
"F_L1",
|
|
78
|
+
"F_L2",
|
|
37
79
|
]
|