satelles 0.0.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.
- satelles/__init__.py +26 -0
- satelles/py.typed +0 -0
- satelles/satellite.py +177 -0
- satelles/tle.py +559 -0
- satelles-0.0.0.dist-info/METADATA +12 -0
- satelles-0.0.0.dist-info/RECORD +8 -0
- satelles-0.0.0.dist-info/WHEEL +4 -0
- satelles-0.0.0.dist-info/licenses/LICENSE +21 -0
satelles/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# **************************************************************************************
|
|
2
|
+
|
|
3
|
+
# @package satelles
|
|
4
|
+
# @license MIT License Copyright (c) 2025 Michael J. Roberts
|
|
5
|
+
|
|
6
|
+
# **************************************************************************************
|
|
7
|
+
|
|
8
|
+
from .satellite import Satellite
|
|
9
|
+
from .tle import TLE
|
|
10
|
+
|
|
11
|
+
# **************************************************************************************
|
|
12
|
+
|
|
13
|
+
__version__ = "0.0.0"
|
|
14
|
+
|
|
15
|
+
# **************************************************************************************
|
|
16
|
+
|
|
17
|
+
__license__ = "MIT"
|
|
18
|
+
|
|
19
|
+
# **************************************************************************************
|
|
20
|
+
|
|
21
|
+
__all__: list[str] = [
|
|
22
|
+
"Satellite",
|
|
23
|
+
"TLE",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# **************************************************************************************
|
satelles/py.typed
ADDED
|
File without changes
|
satelles/satellite.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# **************************************************************************************
|
|
2
|
+
|
|
3
|
+
# @package satelles
|
|
4
|
+
# @license MIT License Copyright (c) 2025 Michael J. Roberts
|
|
5
|
+
|
|
6
|
+
# **************************************************************************************
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator
|
|
11
|
+
|
|
12
|
+
# **************************************************************************************
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ID(BaseModel):
|
|
16
|
+
id: Annotated[
|
|
17
|
+
int,
|
|
18
|
+
Field(
|
|
19
|
+
ge=0,
|
|
20
|
+
description="The satellite catalog number, e.g., NORAD ID",
|
|
21
|
+
),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
name: Annotated[
|
|
25
|
+
str,
|
|
26
|
+
Field(
|
|
27
|
+
description="The designated name of the satellite",
|
|
28
|
+
),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
classification: Annotated[
|
|
32
|
+
str,
|
|
33
|
+
Field(
|
|
34
|
+
description="The classification of the satellite, e.g., 'U' for unclassified, 'C' for classified, 'S' for secret",
|
|
35
|
+
),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
designator: Annotated[
|
|
39
|
+
str,
|
|
40
|
+
Field(
|
|
41
|
+
description="The international designator of the satellite",
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
year: Annotated[
|
|
46
|
+
int,
|
|
47
|
+
Field(
|
|
48
|
+
ge=1900,
|
|
49
|
+
le=2100,
|
|
50
|
+
description="The Epoch year of the TLE (full four-digit year)",
|
|
51
|
+
),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
day: Annotated[
|
|
55
|
+
float,
|
|
56
|
+
Field(
|
|
57
|
+
ge=1,
|
|
58
|
+
le=367,
|
|
59
|
+
description="Epoch day of the year with fractional portion included, e.g., 123.456789",
|
|
60
|
+
),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
jd: Annotated[
|
|
64
|
+
float,
|
|
65
|
+
Field(
|
|
66
|
+
description="The Julian date of the Epoch",
|
|
67
|
+
),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
ephemeris: Annotated[
|
|
71
|
+
int,
|
|
72
|
+
Field(
|
|
73
|
+
description="Ephemeris type (always zero; only used in undistributed TLE data)",
|
|
74
|
+
),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
set: Annotated[
|
|
78
|
+
int,
|
|
79
|
+
Field(
|
|
80
|
+
ge=0,
|
|
81
|
+
description="The element set number, incremented when a new TLE is generated for this object",
|
|
82
|
+
),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
@field_validator("classification")
|
|
86
|
+
def validate_classification(cls, value: str) -> str:
|
|
87
|
+
mapping = {"U": "Unclassified", "C": "Classified", "S": "Secret"}
|
|
88
|
+
if value not in mapping.keys():
|
|
89
|
+
raise ValueError(f"Classification must be one of {list(mapping.keys())}")
|
|
90
|
+
return mapping[value]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# **************************************************************************************
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class OrbitalElements(BaseModel):
|
|
97
|
+
drag: Annotated[
|
|
98
|
+
float,
|
|
99
|
+
Field(
|
|
100
|
+
description="The B*, the drag term, or radiation pressure coefficient (decimal point assumed)",
|
|
101
|
+
),
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
raan: Annotated[
|
|
105
|
+
float,
|
|
106
|
+
Field(
|
|
107
|
+
description="Right Ascension of the ascending node (in degrees)",
|
|
108
|
+
),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
inclination: Annotated[
|
|
112
|
+
float,
|
|
113
|
+
Field(
|
|
114
|
+
description="The orbital inclination of the satellite (in degrees)",
|
|
115
|
+
),
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
eccentricity: Annotated[
|
|
119
|
+
float,
|
|
120
|
+
Field(
|
|
121
|
+
description="The orbital eccentricity of the satellite (dimensionless)",
|
|
122
|
+
),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
argument_of_perigee: Annotated[
|
|
126
|
+
float,
|
|
127
|
+
Field(
|
|
128
|
+
description="The argument of perigee of the satellite (in degrees)",
|
|
129
|
+
),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
mean_anomaly: Annotated[
|
|
133
|
+
float,
|
|
134
|
+
Field(
|
|
135
|
+
description="The mean anomaly of the satellite (in degrees)",
|
|
136
|
+
),
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
mean_motion: Annotated[
|
|
140
|
+
float,
|
|
141
|
+
Field(
|
|
142
|
+
gt=0,
|
|
143
|
+
description="The mean motion (revolutions per day) of the satellite",
|
|
144
|
+
),
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
first_derivative_of_mean_motion: Annotated[
|
|
148
|
+
float,
|
|
149
|
+
Field(
|
|
150
|
+
description="The first derivative of mean motion (decimal point assumed) of the satellite 'the ballistic coefficient'",
|
|
151
|
+
),
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
second_derivative_of_mean_motion: Annotated[
|
|
155
|
+
float,
|
|
156
|
+
Field(
|
|
157
|
+
description="Second derivative of mean motion (decimal point assumed) of the satellite",
|
|
158
|
+
),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
number_of_revolutions: Annotated[
|
|
162
|
+
int,
|
|
163
|
+
Field(
|
|
164
|
+
ge=0,
|
|
165
|
+
description="The number of complete revolutions the satellite has made around the Earth at the Epoch time",
|
|
166
|
+
),
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# **************************************************************************************
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Satellite(ID, OrbitalElements):
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# **************************************************************************************
|
satelles/tle.py
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
# **************************************************************************************
|
|
2
|
+
|
|
3
|
+
# @package satelles
|
|
4
|
+
# @license MIT License Copyright (c) 2025 Michael J. Roberts
|
|
5
|
+
|
|
6
|
+
# **************************************************************************************
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
|
+
from typing import Any, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from .satellite import Satellite
|
|
13
|
+
|
|
14
|
+
# **************************************************************************************
|
|
15
|
+
|
|
16
|
+
line1_regex = re.compile(
|
|
17
|
+
r"^1\s+"
|
|
18
|
+
r"(?P<id>[0-9A-Z]{5})"
|
|
19
|
+
r"(?P<classification>[A-Z])\s+"
|
|
20
|
+
r"(?P<designator>\d{2}\d{3}[A-Z]{1,3})\s+"
|
|
21
|
+
r"(?P<year>\d{2})(?P<day>\d{3}\.\d{8})\s+"
|
|
22
|
+
r"(?P<first_derivative_of_mean_motion>[+-]?\.\d{8})\s+"
|
|
23
|
+
r"(?P<second_derivative_of_mean_motion>[+-]?\d{5}[+-]\d)\s+"
|
|
24
|
+
r"(?P<drag>[+-]?\d{5}[+-]\d)\s+"
|
|
25
|
+
r"(?P<ephemeris>\d)\s+"
|
|
26
|
+
r"(?P<set>\d+)$"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# **************************************************************************************
|
|
30
|
+
|
|
31
|
+
line2_regex = re.compile(
|
|
32
|
+
r"^2\s+"
|
|
33
|
+
r"(?P<id>[0-9A-Z]{5})\s+"
|
|
34
|
+
r"(?P<inclination>\d+\.\d+)\s+"
|
|
35
|
+
r"(?P<raan>\d+\.\d+)\s+"
|
|
36
|
+
r"(?P<eccentricity>\d{7})\s+"
|
|
37
|
+
r"(?P<argument_of_perigee>\d+\.\d+)\s+"
|
|
38
|
+
r"(?P<mean_anomaly>\d+\.\d+)\s+"
|
|
39
|
+
r"(?P<mean_motion>\d+\.\d+)\s+"
|
|
40
|
+
r"(?P<number_of_revolutions>\d+)$"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# **************************************************************************************
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_scientific_notation(value: str) -> float:
|
|
47
|
+
"""
|
|
48
|
+
Parse a string in the form ±DDDDD±D, where the first part is the mantissa (with an
|
|
49
|
+
implicit decimal) and the second part is the exponent.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
value: The value to parse.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The value as a float.
|
|
56
|
+
"""
|
|
57
|
+
pattern = re.compile(r"^([+-]?)(\d{5})([+-]\d)$")
|
|
58
|
+
|
|
59
|
+
match = pattern.match(value)
|
|
60
|
+
|
|
61
|
+
if not match:
|
|
62
|
+
return 0.0
|
|
63
|
+
|
|
64
|
+
sign = -1 if match.group(1) == "-" else 1
|
|
65
|
+
|
|
66
|
+
mantissa = int(match.group(2)) / 1e5
|
|
67
|
+
|
|
68
|
+
exponent = int(match.group(3))
|
|
69
|
+
|
|
70
|
+
return sign * mantissa * (10**exponent)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# **************************************************************************************
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_id_field(value: Any) -> Optional[int]:
|
|
77
|
+
"""
|
|
78
|
+
Parse a string as an integer, with support for base-36 encoding.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
value: The value to parse.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The value as an integer, or None if the value is not a valid integer
|
|
85
|
+
"""
|
|
86
|
+
if not isinstance(value, str):
|
|
87
|
+
return None
|
|
88
|
+
try:
|
|
89
|
+
if value and value[0].isalpha():
|
|
90
|
+
return int(value, 36)
|
|
91
|
+
else:
|
|
92
|
+
return int(value, 10)
|
|
93
|
+
except ValueError:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# **************************************************************************************
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_classification_field(value: Any) -> Optional[str]:
|
|
101
|
+
"""
|
|
102
|
+
Parse a classification field, which is a single character representing the
|
|
103
|
+
classification of the satellite.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
value: The value to parse.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The classification of the satellite as a string, or None if the value is not
|
|
110
|
+
valid, e.g., either one of "U", "C", or "S".
|
|
111
|
+
"""
|
|
112
|
+
if not isinstance(value, str):
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
v = value.strip().upper()
|
|
116
|
+
|
|
117
|
+
if len(v) != 1:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
if v not in {"U", "C", "S"}:
|
|
121
|
+
raise ValueError("Invalid classification field. Must be one of: U, C, S")
|
|
122
|
+
|
|
123
|
+
return v
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# **************************************************************************************
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_designator_field(value: Any) -> Optional[str]:
|
|
130
|
+
"""
|
|
131
|
+
Parse a string as a designator field.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
value: The value to parse.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The value as a string, or None if the value is not a valid designator.
|
|
138
|
+
"""
|
|
139
|
+
if not isinstance(value, str):
|
|
140
|
+
return None
|
|
141
|
+
return value.strip()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# **************************************************************************************
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def parse_epoch_field(value: Any) -> Optional[Tuple[int, float, float]]:
|
|
148
|
+
if not isinstance(value, str):
|
|
149
|
+
return None
|
|
150
|
+
try:
|
|
151
|
+
# The first two digits represent the year (with a cutoff at 57):
|
|
152
|
+
year = int(value[:2])
|
|
153
|
+
if year < 57:
|
|
154
|
+
year += 2000
|
|
155
|
+
else:
|
|
156
|
+
year += 1900
|
|
157
|
+
day = float(value[2:])
|
|
158
|
+
except ValueError:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# Compute the date corresponding to the day of year (note: day 1 is January 1):
|
|
162
|
+
dt = datetime(year, 1, 1, tzinfo=timezone.utc) + timedelta(days=day - 1)
|
|
163
|
+
|
|
164
|
+
# Convert Unix timestamp to Julian date:
|
|
165
|
+
jd = dt.timestamp() / 86400 + 2440587.5
|
|
166
|
+
|
|
167
|
+
return (year, day, jd)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# **************************************************************************************
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def parse_bstar_drag_term_field(value: Any) -> Optional[float]:
|
|
174
|
+
if not isinstance(value, str):
|
|
175
|
+
return None
|
|
176
|
+
return parse_scientific_notation(value)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# **************************************************************************************
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def parse_ephemeris_type_field(value: Any) -> Optional[int]:
|
|
183
|
+
if not isinstance(value, str):
|
|
184
|
+
return None
|
|
185
|
+
try:
|
|
186
|
+
return int(value)
|
|
187
|
+
except ValueError:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# **************************************************************************************
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def parse_set_number_field(value: Any) -> Optional[int]:
|
|
195
|
+
if not isinstance(value, str):
|
|
196
|
+
return None
|
|
197
|
+
try:
|
|
198
|
+
return int(value)
|
|
199
|
+
except ValueError:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# **************************************************************************************
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def parse_raan_field(value: Any) -> Optional[float]:
|
|
207
|
+
if not isinstance(value, str):
|
|
208
|
+
return None
|
|
209
|
+
try:
|
|
210
|
+
return float(value)
|
|
211
|
+
except ValueError:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# **************************************************************************************
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def parse_inclination_field(value: Any) -> Optional[float]:
|
|
219
|
+
if not isinstance(value, str):
|
|
220
|
+
return None
|
|
221
|
+
try:
|
|
222
|
+
return float(value)
|
|
223
|
+
except ValueError:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# **************************************************************************************
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def parse_eccentricity_field(value: Any) -> Optional[float]:
|
|
231
|
+
if not isinstance(value, str):
|
|
232
|
+
return None
|
|
233
|
+
try:
|
|
234
|
+
# The eccentricity field has an implicit leading zero.
|
|
235
|
+
return float("0." + value.strip())
|
|
236
|
+
except ValueError:
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# **************************************************************************************
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def parse_argument_of_perigee_field(value: Any) -> Optional[float]:
|
|
244
|
+
if not isinstance(value, str):
|
|
245
|
+
return None
|
|
246
|
+
try:
|
|
247
|
+
return float(value)
|
|
248
|
+
except ValueError:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# **************************************************************************************
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def parse_mean_anomaly_field(value: Any) -> Optional[float]:
|
|
256
|
+
if not isinstance(value, str):
|
|
257
|
+
return None
|
|
258
|
+
try:
|
|
259
|
+
return float(value)
|
|
260
|
+
except ValueError:
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# **************************************************************************************s
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def parse_mean_motion_field(value: Any) -> Optional[float]:
|
|
268
|
+
if not isinstance(value, str):
|
|
269
|
+
return None
|
|
270
|
+
try:
|
|
271
|
+
return float(value)
|
|
272
|
+
except ValueError:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# **************************************************************************************
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def parse_first_derivative_of_mean_motion_field(value: Any) -> Optional[float]:
|
|
280
|
+
if not isinstance(value, str):
|
|
281
|
+
return None
|
|
282
|
+
try:
|
|
283
|
+
return float(value)
|
|
284
|
+
except ValueError:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# **************************************************************************************
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def parse_second_derivative_of_mean_motion_field(value: Any) -> Optional[float]:
|
|
292
|
+
if not isinstance(value, str):
|
|
293
|
+
return None
|
|
294
|
+
return parse_scientific_notation(value)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# **************************************************************************************
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def parse_number_of_revolutions_field(value: Any) -> Optional[int]:
|
|
301
|
+
if not isinstance(value, str):
|
|
302
|
+
return None
|
|
303
|
+
try:
|
|
304
|
+
return int(value)
|
|
305
|
+
except ValueError:
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# **************************************************************************************
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def parse_tle(tle: str) -> Satellite:
|
|
313
|
+
"""
|
|
314
|
+
Parse a TLE string and return a Satellite instance if successful, otherwise raise a
|
|
315
|
+
ValueError.
|
|
316
|
+
|
|
317
|
+
The TLE string can be in one of two formats:
|
|
318
|
+
- Three-line set (name, line1, line2)
|
|
319
|
+
- Two-line set (line1, line2), in which case the satellite name is set to an empty string.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
tle: The TLE string to parse.
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
ValueError: If the TLE string is not in a valid format or if any of the fields are invalid.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
A Satellite instance if parsing is successful.
|
|
329
|
+
"""
|
|
330
|
+
# Split the TLE into lines and remove any empty lines:
|
|
331
|
+
lines = [line.strip() for line in tle.splitlines() if line.strip()]
|
|
332
|
+
|
|
333
|
+
if len(lines) == 2:
|
|
334
|
+
name = ""
|
|
335
|
+
line1, line2 = lines
|
|
336
|
+
elif len(lines) >= 3:
|
|
337
|
+
name = lines[0]
|
|
338
|
+
line1, line2 = lines[1], lines[2]
|
|
339
|
+
else:
|
|
340
|
+
raise ValueError("Invalid TLE format")
|
|
341
|
+
|
|
342
|
+
# Match the TLE to the line 1 regular expression:
|
|
343
|
+
m1 = line1_regex.match(line1)
|
|
344
|
+
|
|
345
|
+
# Match the TLE to the line 2 regular expression:
|
|
346
|
+
m2 = line2_regex.match(line2)
|
|
347
|
+
|
|
348
|
+
if not m1 or not m2:
|
|
349
|
+
raise ValueError("Invalid TLE format")
|
|
350
|
+
|
|
351
|
+
id_field = m1.group("id")
|
|
352
|
+
classification_field = m1.group("classification")
|
|
353
|
+
designator_field = m1.group("designator")
|
|
354
|
+
year_field = m1.group("year")
|
|
355
|
+
day_field = m1.group("day")
|
|
356
|
+
fdmm_field = m1.group("first_derivative_of_mean_motion")
|
|
357
|
+
sdmm_field = m1.group("second_derivative_of_mean_motion")
|
|
358
|
+
drag_field = m1.group("drag")
|
|
359
|
+
ephemeris_field = m1.group("ephemeris")
|
|
360
|
+
set_field = m1.group("set")
|
|
361
|
+
inclination_field = m2.group("inclination")
|
|
362
|
+
raan_field = m2.group("raan")
|
|
363
|
+
eccentricity_field = m2.group("eccentricity")
|
|
364
|
+
argument_of_perigee_field = m2.group("argument_of_perigee")
|
|
365
|
+
mean_anomaly_field = m2.group("mean_anomaly")
|
|
366
|
+
mean_motion_field = m2.group("mean_motion")
|
|
367
|
+
number_of_revolutions_field = m2.group("number_of_revolutions")
|
|
368
|
+
|
|
369
|
+
id = parse_id_field(id_field)
|
|
370
|
+
|
|
371
|
+
if id is None:
|
|
372
|
+
raise ValueError("Invalid ID field")
|
|
373
|
+
|
|
374
|
+
classification = parse_classification_field(classification_field)
|
|
375
|
+
|
|
376
|
+
if classification is None:
|
|
377
|
+
raise ValueError("Invalid classification field")
|
|
378
|
+
|
|
379
|
+
designator = parse_designator_field(designator_field)
|
|
380
|
+
|
|
381
|
+
if designator is None:
|
|
382
|
+
raise ValueError("Invalid designator field")
|
|
383
|
+
|
|
384
|
+
epoch = parse_epoch_field(f"{year_field}{day_field}")
|
|
385
|
+
if epoch is None:
|
|
386
|
+
raise ValueError("Invalid epoch field")
|
|
387
|
+
|
|
388
|
+
year, day, jd = epoch
|
|
389
|
+
|
|
390
|
+
fdmm = parse_first_derivative_of_mean_motion_field(fdmm_field)
|
|
391
|
+
if fdmm is None:
|
|
392
|
+
raise ValueError("Invalid first derivative of mean motion field")
|
|
393
|
+
|
|
394
|
+
sdmm = parse_second_derivative_of_mean_motion_field(sdmm_field)
|
|
395
|
+
if sdmm is None:
|
|
396
|
+
raise ValueError("Invalid second derivative of mean motion field")
|
|
397
|
+
|
|
398
|
+
drag = parse_bstar_drag_term_field(drag_field)
|
|
399
|
+
if drag is None:
|
|
400
|
+
raise ValueError("Invalid b* drag field")
|
|
401
|
+
|
|
402
|
+
ephemeris = parse_ephemeris_type_field(ephemeris_field)
|
|
403
|
+
if ephemeris is None:
|
|
404
|
+
raise ValueError("Invalid ephemeris type field")
|
|
405
|
+
|
|
406
|
+
set_number = parse_set_number_field(set_field)
|
|
407
|
+
if set_number is None:
|
|
408
|
+
raise ValueError("Invalid set number field")
|
|
409
|
+
|
|
410
|
+
inclination = parse_inclination_field(inclination_field)
|
|
411
|
+
if inclination is None:
|
|
412
|
+
raise ValueError("Invalid inclination field")
|
|
413
|
+
|
|
414
|
+
raan = parse_raan_field(raan_field)
|
|
415
|
+
if raan is None:
|
|
416
|
+
raise ValueError("Invalid raan field")
|
|
417
|
+
|
|
418
|
+
eccentricity = parse_eccentricity_field(eccentricity_field)
|
|
419
|
+
if eccentricity is None:
|
|
420
|
+
raise ValueError("Invalid eccentricity field")
|
|
421
|
+
|
|
422
|
+
argument_of_perigee = parse_argument_of_perigee_field(argument_of_perigee_field)
|
|
423
|
+
if argument_of_perigee is None:
|
|
424
|
+
raise ValueError("Invalid argument of perigee field")
|
|
425
|
+
|
|
426
|
+
mean_anomaly = parse_mean_anomaly_field(mean_anomaly_field)
|
|
427
|
+
if mean_anomaly is None:
|
|
428
|
+
raise ValueError("Invalid mean anomaly field")
|
|
429
|
+
|
|
430
|
+
mean_motion = parse_mean_motion_field(mean_motion_field)
|
|
431
|
+
if mean_motion is None:
|
|
432
|
+
raise ValueError("Invalid mean motion field")
|
|
433
|
+
|
|
434
|
+
number_of_revolutions = parse_number_of_revolutions_field(
|
|
435
|
+
number_of_revolutions_field
|
|
436
|
+
)
|
|
437
|
+
if number_of_revolutions is None:
|
|
438
|
+
raise ValueError("Invalid number of revolutions field")
|
|
439
|
+
|
|
440
|
+
# Assemble the satellite using the parsed fields and return it:
|
|
441
|
+
return Satellite(
|
|
442
|
+
id=id,
|
|
443
|
+
name=name,
|
|
444
|
+
classification=classification,
|
|
445
|
+
designator=designator,
|
|
446
|
+
year=year,
|
|
447
|
+
day=day,
|
|
448
|
+
jd=jd,
|
|
449
|
+
ephemeris=ephemeris,
|
|
450
|
+
set=set_number,
|
|
451
|
+
drag=drag,
|
|
452
|
+
raan=raan,
|
|
453
|
+
inclination=inclination,
|
|
454
|
+
eccentricity=eccentricity,
|
|
455
|
+
argument_of_perigee=argument_of_perigee,
|
|
456
|
+
mean_anomaly=mean_anomaly,
|
|
457
|
+
mean_motion=mean_motion,
|
|
458
|
+
first_derivative_of_mean_motion=fdmm,
|
|
459
|
+
second_derivative_of_mean_motion=sdmm,
|
|
460
|
+
number_of_revolutions=number_of_revolutions,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# **************************************************************************************
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class TLE:
|
|
468
|
+
_satellite: Satellite
|
|
469
|
+
|
|
470
|
+
def __init__(self, tle_string: str) -> None:
|
|
471
|
+
# Parse the TLE string and create a Satellite instance:
|
|
472
|
+
try:
|
|
473
|
+
satellite = parse_tle(tle_string)
|
|
474
|
+
except ValueError as e:
|
|
475
|
+
raise ValueError(f"Invalid TLE string: {e}")
|
|
476
|
+
|
|
477
|
+
self._satellite = satellite
|
|
478
|
+
|
|
479
|
+
def __repr__(self) -> str:
|
|
480
|
+
return f"<TLE(name={self._satellite.name!r}, id={self._satellite.id})>"
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def name(self) -> str:
|
|
484
|
+
return self._satellite.name
|
|
485
|
+
|
|
486
|
+
@property
|
|
487
|
+
def id(self) -> int:
|
|
488
|
+
return self._satellite.id
|
|
489
|
+
|
|
490
|
+
@property
|
|
491
|
+
def classification(self) -> str:
|
|
492
|
+
return self._satellite.classification
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def designator(self) -> str:
|
|
496
|
+
return self._satellite.designator
|
|
497
|
+
|
|
498
|
+
@property
|
|
499
|
+
def year(self) -> int:
|
|
500
|
+
return self._satellite.year
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def day(self) -> float:
|
|
504
|
+
return self._satellite.day
|
|
505
|
+
|
|
506
|
+
@property
|
|
507
|
+
def julian_date(self) -> float:
|
|
508
|
+
return self._satellite.jd
|
|
509
|
+
|
|
510
|
+
@property
|
|
511
|
+
def ephemeris(self) -> int:
|
|
512
|
+
return self._satellite.ephemeris
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def set(self) -> int:
|
|
516
|
+
return self._satellite.set
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def b_star_drag(self) -> float:
|
|
520
|
+
return self._satellite.drag
|
|
521
|
+
|
|
522
|
+
@property
|
|
523
|
+
def right_ascension_of_the_ascending_node(self) -> float:
|
|
524
|
+
return self._satellite.raan
|
|
525
|
+
|
|
526
|
+
@property
|
|
527
|
+
def inclination(self) -> float:
|
|
528
|
+
return self._satellite.inclination
|
|
529
|
+
|
|
530
|
+
@property
|
|
531
|
+
def eccentricity(self) -> float:
|
|
532
|
+
return self._satellite.eccentricity
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
def argument_of_perigee(self) -> float:
|
|
536
|
+
return self._satellite.argument_of_perigee
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def mean_anomaly(self) -> float:
|
|
540
|
+
return self._satellite.mean_anomaly
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def mean_motion(self) -> float:
|
|
544
|
+
return self._satellite.mean_motion
|
|
545
|
+
|
|
546
|
+
@property
|
|
547
|
+
def first_derivative_of_mean_motion(self) -> float:
|
|
548
|
+
return self._satellite.first_derivative_of_mean_motion
|
|
549
|
+
|
|
550
|
+
@property
|
|
551
|
+
def second_derivative_of_mean_motion(self) -> float:
|
|
552
|
+
return self._satellite.second_derivative_of_mean_motion
|
|
553
|
+
|
|
554
|
+
@property
|
|
555
|
+
def number_of_revolutions(self) -> int:
|
|
556
|
+
return self._satellite.number_of_revolutions
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# **************************************************************************************
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: satelles
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: michealroberts <michael@observerly.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Requires-Dist: pydantic>=2.10.6
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# satelles
|
|
12
|
+
A hypermodern python package starter template with astral uv ruff and mypy
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
satelles/__init__.py,sha256=uJBz6mxm3IQ8VZnZoOdKKWCA2UUKf3M8BTYlF_Uxb2M,788
|
|
2
|
+
satelles/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
satelles/satellite.py,sha256=2NiOPDP23aEmAUHLDnG05rBqR2E4xXY5OXwU5pSxZhU,4446
|
|
4
|
+
satelles/tle.py,sha256=aOdcqRJC75CcSEHY3cpVaLGaorm7_HdfqKrlC9mJOPc,15629
|
|
5
|
+
satelles-0.0.0.dist-info/METADATA,sha256=af0UZJT3-OKTzrNghN69SG2g_MT9ghVm0sqgaUxm2ws,346
|
|
6
|
+
satelles-0.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
satelles-0.0.0.dist-info/licenses/LICENSE,sha256=jHblKj3g6JEPKRCGlMqRHrCdZIyN_3NFu0aHrQht3FU,1075
|
|
8
|
+
satelles-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Michael J. Roberts
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|