tabsim 0.0.1__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.
- tabsim/__init__.py +0 -0
- tabsim/config.py +904 -0
- tabsim/dask/__init__.py +0 -0
- tabsim/dask/coordinates.py +469 -0
- tabsim/dask/extras.py +60 -0
- tabsim/dask/interferometry.py +572 -0
- tabsim/dask/observation.py +969 -0
- tabsim/data/EDA2.itrf.txt +256 -0
- tabsim/data/MWA.itrf.txt +128 -0
- tabsim/data/MeerKAT.itrf.txt +64 -0
- tabsim/data/__init__.py +0 -0
- tabsim/data/aux_data/calibrator.skymodel +2 -0
- tabsim/data/aux_data/norad_satellite.rfimodel +61711 -0
- tabsim/data/aux_data/satellite.ole +3 -0
- tabsim/data/aux_data/satellite.rfimodel +3 -0
- tabsim/data/aux_data/stationary.loc +4 -0
- tabsim/data/aux_data/stationary.rfimodel +4 -0
- tabsim/data/config_files/sim_config_base.yaml +172 -0
- tabsim/data/config_files/tab_config_base.yaml +86 -0
- tabsim/jax/__init__.py +0 -0
- tabsim/jax/coordinates.py +1082 -0
- tabsim/jax/interferometry.py +667 -0
- tabsim/jax/utils.py +90 -0
- tabsim/plot.py +323 -0
- tabsim/scripts/__init__.py +1 -0
- tabsim/scripts/ast_region.py +56 -0
- tabsim/scripts/flag_data.py +195 -0
- tabsim/scripts/sim_vis.py +136 -0
- tabsim/scripts/tle_sat_region.py +191 -0
- tabsim/sky.py +137 -0
- tabsim/tle.py +753 -0
- tabsim/tools.py +111 -0
- tabsim/write.py +715 -0
- tabsim-0.0.1.dist-info/METADATA +991 -0
- tabsim-0.0.1.dist-info/RECORD +39 -0
- tabsim-0.0.1.dist-info/WHEEL +5 -0
- tabsim-0.0.1.dist-info/entry_points.txt +5 -0
- tabsim-0.0.1.dist-info/licenses/LICENSE +674 -0
- tabsim-0.0.1.dist-info/top_level.txt +1 -0
tabsim/tle.py
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
from astropy.time import Time
|
|
2
|
+
from astropy.coordinates import EarthLocation
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from skyfield.api import load, wgs84, EarthSatellite
|
|
5
|
+
from skyfield.positionlib import position_of_radec
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from numpy.typing import ArrayLike
|
|
9
|
+
|
|
10
|
+
from spacetrack import SpaceTrackClient
|
|
11
|
+
import spacetrack.operators as op
|
|
12
|
+
|
|
13
|
+
from tqdm import tqdm
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import ast
|
|
17
|
+
import string
|
|
18
|
+
import random
|
|
19
|
+
|
|
20
|
+
from glob import glob
|
|
21
|
+
|
|
22
|
+
from importlib.resources import files
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_tle_dir(tle_dir: str | None):
|
|
26
|
+
|
|
27
|
+
if tle_dir:
|
|
28
|
+
tle_dir = os.path.abspath(tle_dir)
|
|
29
|
+
else:
|
|
30
|
+
tle_dir = files("tabsim.data").joinpath("aux_data/tles").__str__()
|
|
31
|
+
|
|
32
|
+
os.makedirs(tle_dir, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
return tle_dir
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_space_track_client(username, password):
|
|
38
|
+
return SpaceTrackClient(identity=username, password=password)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def fetch_tle_data(
|
|
42
|
+
st_client: SpaceTrackClient,
|
|
43
|
+
norad_ids: list[int],
|
|
44
|
+
epoch_jd: float,
|
|
45
|
+
window_days: float = 1.0,
|
|
46
|
+
limit: int = 2000,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Fetch TLE data for given NORAD IDs around a specific epoch.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
st_client : SpaceTrackClient
|
|
54
|
+
SpaceTrackClient instance
|
|
55
|
+
norad_ids : list[int]
|
|
56
|
+
List of NORAD IDs
|
|
57
|
+
epoch_jd : float
|
|
58
|
+
Julian date for the epoch
|
|
59
|
+
window_days : int
|
|
60
|
+
Window size in days around the epoch
|
|
61
|
+
limit : int
|
|
62
|
+
Maximum number of results to return
|
|
63
|
+
|
|
64
|
+
Returns :
|
|
65
|
+
pandas.DataFrame containing TLE data
|
|
66
|
+
"""
|
|
67
|
+
start_time = Time(epoch_jd - window_days, format="jd", scale="ut1").datetime
|
|
68
|
+
end_time = Time(epoch_jd + window_days, format="jd", scale="ut1").datetime
|
|
69
|
+
date_range = op.inclusive_range(start_time, end_time)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
raw_data = st_client.tle(
|
|
73
|
+
norad_cat_id=norad_ids, epoch=date_range, limit=limit, format="json"
|
|
74
|
+
)
|
|
75
|
+
return pd.DataFrame(ast.literal_eval(raw_data))
|
|
76
|
+
except Exception as e:
|
|
77
|
+
print(f"Error fetching TLE data: {str(e)}")
|
|
78
|
+
raise
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
|
82
|
+
return "".join(random.choice(chars) for _ in range(size))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_tles_by_id(
|
|
86
|
+
username: str,
|
|
87
|
+
password: str,
|
|
88
|
+
norad_ids: list[int],
|
|
89
|
+
epoch_jd: float,
|
|
90
|
+
window_days: float = 1.0,
|
|
91
|
+
limit: int = 2000,
|
|
92
|
+
tle_dir: str | None = None,
|
|
93
|
+
) -> pd.DataFrame:
|
|
94
|
+
|
|
95
|
+
tle_dir = make_tle_dir(tle_dir)
|
|
96
|
+
|
|
97
|
+
norad_ids = list(np.array(list(set(norad_ids))).astype(int))
|
|
98
|
+
n_ids_start = len(norad_ids)
|
|
99
|
+
epoch_str = Time(epoch_jd, format="jd", scale="ut1").strftime("%Y-%m-%d")
|
|
100
|
+
|
|
101
|
+
tles_local = pd.DataFrame()
|
|
102
|
+
if tle_dir:
|
|
103
|
+
tle_dir = os.path.abspath(tle_dir)
|
|
104
|
+
tle_paths = glob(os.path.join(tle_dir, f"{epoch_str}-*.json"))
|
|
105
|
+
local_ids = []
|
|
106
|
+
if len(tle_paths) > 0:
|
|
107
|
+
tles_local = pd.concat([pd.read_json(tle_path) for tle_path in tle_paths])
|
|
108
|
+
tles_local = tles_local[tles_local["NORAD_CAT_ID"].isin(norad_ids)]
|
|
109
|
+
local_ids = tles_local["NORAD_CAT_ID"].unique()
|
|
110
|
+
norad_ids = list(set(norad_ids) - set(local_ids))
|
|
111
|
+
print(f"Local TLEs loaded : {len(local_ids)}")
|
|
112
|
+
else:
|
|
113
|
+
local_ids = []
|
|
114
|
+
|
|
115
|
+
max_ids = 500
|
|
116
|
+
n_ids = len(norad_ids)
|
|
117
|
+
|
|
118
|
+
n_req = n_ids // max_ids + 1 if n_ids % max_ids > 0 else n_ids // max_ids
|
|
119
|
+
|
|
120
|
+
remote_ids = []
|
|
121
|
+
tles = pd.DataFrame()
|
|
122
|
+
if len(norad_ids) > 0:
|
|
123
|
+
client = get_space_track_client(username, password)
|
|
124
|
+
tles = [0] * n_req
|
|
125
|
+
for i in range(n_req):
|
|
126
|
+
tles[i] = fetch_tle_data(client, norad_ids, epoch_jd, window_days, limit)
|
|
127
|
+
if sum([len(tle) for tle in tles]) > 0:
|
|
128
|
+
tles = pd.concat(tles)
|
|
129
|
+
tles["Fetch_Timestamp"] = Time.now().fits
|
|
130
|
+
remote_ids = tles["NORAD_CAT_ID"].unique()
|
|
131
|
+
else:
|
|
132
|
+
tles = pd.DataFrame()
|
|
133
|
+
|
|
134
|
+
print(f"Remote TLEs loaded : {len(remote_ids)}")
|
|
135
|
+
print(f"TLEs not found : {n_ids_start - len(remote_ids) - len(local_ids)}")
|
|
136
|
+
|
|
137
|
+
save_name = id_generator()
|
|
138
|
+
|
|
139
|
+
if tle_dir and len(tles) > 0:
|
|
140
|
+
save_path = os.path.join(tle_dir, f"{epoch_str}-{save_name}.json")
|
|
141
|
+
tles.to_json(save_path)
|
|
142
|
+
print(f"Saving remotely obtained TLEs to {save_path}")
|
|
143
|
+
elif len(tles) > 0:
|
|
144
|
+
save_path = os.path.join("./", f"{epoch_str}-{save_name}.json")
|
|
145
|
+
tles.to_json(save_path)
|
|
146
|
+
print(f"Saving remotely obtained TLEs to {save_path}")
|
|
147
|
+
|
|
148
|
+
if tle_dir:
|
|
149
|
+
tles = pd.concat([tles_local, tles])
|
|
150
|
+
|
|
151
|
+
if len(tles) > 0:
|
|
152
|
+
tles.reset_index(drop=True, inplace=True)
|
|
153
|
+
tles["EPOCH_JD"] = tles["EPOCH"].apply(
|
|
154
|
+
lambda x: Time(spacetrack_time_to_isot(x)).jd
|
|
155
|
+
)
|
|
156
|
+
tles = type_cast_tles(tles)
|
|
157
|
+
tles = get_closest_times(tles, epoch_jd)
|
|
158
|
+
|
|
159
|
+
return tles
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_tles_by_name(
|
|
163
|
+
username: str,
|
|
164
|
+
password: str,
|
|
165
|
+
names: list[str],
|
|
166
|
+
epoch_jd: float,
|
|
167
|
+
window_days: float = 1.0,
|
|
168
|
+
limit: int = 10000,
|
|
169
|
+
tle_dir: str | None = None,
|
|
170
|
+
) -> pd.DataFrame:
|
|
171
|
+
|
|
172
|
+
tle_dir = make_tle_dir(tle_dir)
|
|
173
|
+
|
|
174
|
+
# Calculate the date threshold
|
|
175
|
+
epoch_str = Time(epoch_jd, format="jd", scale="ut1").strftime("%Y-%m-%d")
|
|
176
|
+
start_time = Time(epoch_jd - window_days, format="jd", scale="ut1").datetime
|
|
177
|
+
end_time = Time(epoch_jd + window_days, format="jd", scale="ut1").datetime
|
|
178
|
+
drange = op.inclusive_range(start_time, end_time)
|
|
179
|
+
|
|
180
|
+
names_op = [op.like(name.upper()) for name in names]
|
|
181
|
+
|
|
182
|
+
st = SpaceTrackClient(identity=username, password=password)
|
|
183
|
+
|
|
184
|
+
local_ids = 0
|
|
185
|
+
remote_ids = 0
|
|
186
|
+
tles = [0] * len(names)
|
|
187
|
+
for i, name in enumerate(names):
|
|
188
|
+
tle_path = os.path.join(tle_dir, f"{epoch_str}-{name}.json")
|
|
189
|
+
if os.path.isfile(tle_path):
|
|
190
|
+
tle = pd.read_json(tle_path)
|
|
191
|
+
tles[i] = tle
|
|
192
|
+
local_ids += len(tle["NORAD_CAT_ID"].unique())
|
|
193
|
+
else:
|
|
194
|
+
tle = pd.DataFrame(
|
|
195
|
+
ast.literal_eval(
|
|
196
|
+
st.tle(
|
|
197
|
+
object_name=names_op[i],
|
|
198
|
+
epoch=drange,
|
|
199
|
+
limit=limit,
|
|
200
|
+
format="json",
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
tle["Fetch_Timestamp"] = Time.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
205
|
+
tles[i] = tle
|
|
206
|
+
if len(tle) > 0:
|
|
207
|
+
remote_ids += len(tle["NORAD_CAT_ID"].unique())
|
|
208
|
+
tles[i].to_json(tle_path)
|
|
209
|
+
|
|
210
|
+
print(f"Local TLEs loaded : {local_ids}")
|
|
211
|
+
print(f"Remote TLEs loaded : {remote_ids}")
|
|
212
|
+
|
|
213
|
+
tles = pd.concat(tles)
|
|
214
|
+
if len(tles) > 0:
|
|
215
|
+
tles.reset_index(drop=True, inplace=True)
|
|
216
|
+
tles["EPOCH_JD"] = tles["EPOCH"].apply(
|
|
217
|
+
lambda x: Time(spacetrack_time_to_isot(x)).jd
|
|
218
|
+
)
|
|
219
|
+
tles = type_cast_tles(tles)
|
|
220
|
+
tles = get_closest_times(tles, epoch_jd)
|
|
221
|
+
|
|
222
|
+
return tles
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def spacetrack_time_to_isot(spacetrack_time: str) -> str:
|
|
226
|
+
"""Convert times returned by a SpaceTrack API call to ISOT.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
spacetrack_time : str
|
|
231
|
+
SpaceTrack formatted time.
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
str
|
|
236
|
+
ISOT formatted time.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
dt = datetime.strptime(spacetrack_time, "%Y-%m-%d %H:%M:%S")
|
|
240
|
+
isot = dt.strftime("%Y-%m-%dT%H:%M:%S.000")
|
|
241
|
+
|
|
242
|
+
return isot
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_closest_times(
|
|
246
|
+
df: pd.DataFrame,
|
|
247
|
+
target_time_jd: float,
|
|
248
|
+
id_col: str = "NORAD_CAT_ID",
|
|
249
|
+
time_jd_col: str = "EPOCH_JD",
|
|
250
|
+
) -> pd.DataFrame:
|
|
251
|
+
"""
|
|
252
|
+
For each unique item in the DataFrame, find the instance with time closest to target_time.
|
|
253
|
+
|
|
254
|
+
Parameters:
|
|
255
|
+
-----------
|
|
256
|
+
df : pandas.DataFrame
|
|
257
|
+
DataFrame containing items and their time instances
|
|
258
|
+
target_time : datetime or timestamp
|
|
259
|
+
The reference time to compare against
|
|
260
|
+
id_col : str, default="NORAD_CAT_ID"
|
|
261
|
+
Name of the column containing norad_ids.
|
|
262
|
+
time_jd_col : str, default="EPOCH_JD"
|
|
263
|
+
Name of the column containing time values in Julian date.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
--------
|
|
267
|
+
pandas.DataFrame
|
|
268
|
+
DataFrame containing one row per unique item, with the instance closest to target_time
|
|
269
|
+
"""
|
|
270
|
+
# Calculate absolute time difference for each row
|
|
271
|
+
df = df.copy()
|
|
272
|
+
df["time_diff"] = df[time_jd_col] - target_time_jd
|
|
273
|
+
df["time_diff_abs"] = np.abs(df[time_jd_col] - target_time_jd)
|
|
274
|
+
|
|
275
|
+
# Group by item and get the row with minimum time difference
|
|
276
|
+
closest_instances = df.loc[df.groupby(id_col)["time_diff_abs"].idxmin()]
|
|
277
|
+
|
|
278
|
+
return closest_instances
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_visible_satellite_tles(
|
|
282
|
+
username: str,
|
|
283
|
+
password: str,
|
|
284
|
+
times: ArrayLike,
|
|
285
|
+
observer_lat: float,
|
|
286
|
+
observer_lon: float,
|
|
287
|
+
observer_elevation: float,
|
|
288
|
+
target_ra: float,
|
|
289
|
+
target_dec: float,
|
|
290
|
+
max_angular_separation: float,
|
|
291
|
+
min_elevation: float,
|
|
292
|
+
names: ArrayLike = [],
|
|
293
|
+
norad_ids: ArrayLike = [],
|
|
294
|
+
tle_dir: str | None = None,
|
|
295
|
+
) -> tuple:
|
|
296
|
+
"""Get the TLEs corresponding to satellites that satisfy the conditions given.
|
|
297
|
+
|
|
298
|
+
Parameters
|
|
299
|
+
----------
|
|
300
|
+
username : str
|
|
301
|
+
SpaceTrack username
|
|
302
|
+
password : str
|
|
303
|
+
SpaceTrack password.
|
|
304
|
+
times : ArrayLike
|
|
305
|
+
Times to condsider in Astropy.time.Time format.
|
|
306
|
+
observer_lat : float
|
|
307
|
+
Observer latitude in degrees.
|
|
308
|
+
observer_lon : float
|
|
309
|
+
Observer longitude in degrees.
|
|
310
|
+
observer_elevation : float
|
|
311
|
+
Observer elevation in metres above sea level.
|
|
312
|
+
target_ra : float
|
|
313
|
+
Right ascension of the target direction.
|
|
314
|
+
target_dec : float
|
|
315
|
+
Declination of the target direction.
|
|
316
|
+
max_angular_separation : float
|
|
317
|
+
Maximum angular separation, in degrees, to accept a satellite pass.
|
|
318
|
+
min_elevation : float
|
|
319
|
+
Minimum elevation, in degrees, above the horizon to accept the satellite pass.
|
|
320
|
+
norad_ids : ArrayLike
|
|
321
|
+
NORAD IDs to consider.
|
|
322
|
+
names: list
|
|
323
|
+
Satellite names to consider. An approximate search is done.
|
|
324
|
+
tle_dir: str
|
|
325
|
+
Directory path where TLEs should be / are cached.
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
tuple
|
|
330
|
+
- NORAD IDs that pass the criteria.
|
|
331
|
+
- TLEs for the satellites corresponding to the returned NORAD IDs.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
tle_dir = make_tle_dir(tle_dir)
|
|
335
|
+
|
|
336
|
+
tles = pd.DataFrame()
|
|
337
|
+
if len(norad_ids) > 0:
|
|
338
|
+
tles = get_tles_by_id(
|
|
339
|
+
username, password, norad_ids, np.mean(times.jd), tle_dir=tle_dir
|
|
340
|
+
)
|
|
341
|
+
if len(names) > 0:
|
|
342
|
+
tles = pd.concat(
|
|
343
|
+
[
|
|
344
|
+
tles,
|
|
345
|
+
get_tles_by_name(
|
|
346
|
+
username, password, names, np.mean(times.jd), tle_dir=tle_dir
|
|
347
|
+
),
|
|
348
|
+
]
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if len(tles) > 0:
|
|
352
|
+
windows = check_satellite_visibilibities(
|
|
353
|
+
tles["NORAD_CAT_ID"].values,
|
|
354
|
+
tles["TLE_LINE1"].values,
|
|
355
|
+
tles["TLE_LINE2"].values,
|
|
356
|
+
times,
|
|
357
|
+
observer_lat,
|
|
358
|
+
observer_lon,
|
|
359
|
+
observer_elevation,
|
|
360
|
+
target_ra,
|
|
361
|
+
target_dec,
|
|
362
|
+
max_angular_separation,
|
|
363
|
+
min_elevation,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if len(windows) > 0:
|
|
367
|
+
tles_ = tles[tles["NORAD_CAT_ID"].isin(windows["norad_id"])][
|
|
368
|
+
["NORAD_CAT_ID", "TLE_LINE1", "TLE_LINE2"]
|
|
369
|
+
].values
|
|
370
|
+
return tles_[:, 0], tles_[:, 1:]
|
|
371
|
+
else:
|
|
372
|
+
return [], None
|
|
373
|
+
else:
|
|
374
|
+
return [], None
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def type_cast_tles(tles: pd.DataFrame) -> pd.DataFrame:
|
|
378
|
+
|
|
379
|
+
numeric_cols = [
|
|
380
|
+
"NORAD_CAT_ID",
|
|
381
|
+
"EPOCH_MICROSECONDS",
|
|
382
|
+
"MEAN_MOTION",
|
|
383
|
+
"ECCENTRICITY",
|
|
384
|
+
"INCLINATION",
|
|
385
|
+
"RA_OF_ASC_NODE",
|
|
386
|
+
"ARG_OF_PERICENTER",
|
|
387
|
+
"MEAN_ANOMALY",
|
|
388
|
+
"EPHEMERIS_TYPE",
|
|
389
|
+
"ELEMENT_SET_NO",
|
|
390
|
+
"REV_AT_EPOCH",
|
|
391
|
+
"BSTAR",
|
|
392
|
+
"MEAN_MOTION_DOT",
|
|
393
|
+
"MEAN_MOTION_DDOT",
|
|
394
|
+
"FILE",
|
|
395
|
+
"OBJECT_NUMBER",
|
|
396
|
+
"SEMIMAJOR_AXIS",
|
|
397
|
+
"PERIOD",
|
|
398
|
+
"APOGEE",
|
|
399
|
+
"PERIGEE",
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
for col in numeric_cols:
|
|
403
|
+
tles[col] = pd.to_numeric(tles[col])
|
|
404
|
+
tles["DECAYED"] = pd.to_numeric(tles["DECAYED"]).astype(bool)
|
|
405
|
+
|
|
406
|
+
return tles
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def make_window(
|
|
410
|
+
times: ArrayLike, alt: ArrayLike, angular_sep: ArrayLike, idx: ArrayLike
|
|
411
|
+
) -> dict:
|
|
412
|
+
"""Make a dictionary containing the start and end times of a satellite pass including some stats.
|
|
413
|
+
|
|
414
|
+
Parameters
|
|
415
|
+
----------
|
|
416
|
+
times : ArrayLike[Time]
|
|
417
|
+
Times of the satellite pass.
|
|
418
|
+
alt : ArrayLike
|
|
419
|
+
Altitude of the satellite during pass.
|
|
420
|
+
angular_sep : ArrayLike
|
|
421
|
+
Angular separation of the satellite during pass from target.
|
|
422
|
+
idx: ArrayLike
|
|
423
|
+
Index locations of the window.
|
|
424
|
+
Returns
|
|
425
|
+
-------
|
|
426
|
+
dict
|
|
427
|
+
Dictionary of stats.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
window = {
|
|
431
|
+
"start_time": times[idx][0].datetime.strftime(
|
|
432
|
+
f"%Y-%m-%d-%H:%M:%S.%f {times.scale.upper()}"
|
|
433
|
+
),
|
|
434
|
+
"end_time": times[idx][-1].datetime.strftime(
|
|
435
|
+
f"%Y-%m-%d-%H:%M:%S.%f {times.scale.upper()}"
|
|
436
|
+
),
|
|
437
|
+
"visible_period": (times[idx][-1] - times[idx][0]).sec,
|
|
438
|
+
"min_ang_sep": np.min(angular_sep[idx]),
|
|
439
|
+
"max_elevation": np.max(alt[idx]),
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return window
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def check_visibility(
|
|
446
|
+
tle_line1: str,
|
|
447
|
+
tle_line2: str,
|
|
448
|
+
times: list[Time],
|
|
449
|
+
observer_lat: float,
|
|
450
|
+
observer_lon: float,
|
|
451
|
+
observer_elevation: float,
|
|
452
|
+
target_ra: float,
|
|
453
|
+
target_dec: float,
|
|
454
|
+
max_ang_sep: float,
|
|
455
|
+
min_elev: float,
|
|
456
|
+
) -> list:
|
|
457
|
+
"""Calculate visibility windows for a satellite when observing a celestial target.
|
|
458
|
+
|
|
459
|
+
This function determines time windows when a satellite will pass a celestial
|
|
460
|
+
target based on the satellite's orbital parameters (TLE), observer location,
|
|
461
|
+
target coordinates, and visibility constraints.
|
|
462
|
+
|
|
463
|
+
Parameters
|
|
464
|
+
----------
|
|
465
|
+
tle_line1 : str
|
|
466
|
+
First line of the satellite's Two-Line Element set (TLE).
|
|
467
|
+
tle_line2 : str
|
|
468
|
+
Second line of the satellite's Two-Line Element set (TLE).
|
|
469
|
+
times : list[Time]
|
|
470
|
+
Array of observation times as Astropy Time objects.
|
|
471
|
+
observer_lat : float
|
|
472
|
+
Observer's latitude in degrees.
|
|
473
|
+
observer_lon : float
|
|
474
|
+
Observer's longitude in degrees.
|
|
475
|
+
observer_elevation : float
|
|
476
|
+
Observer's elevation above sea level in meters.
|
|
477
|
+
target_ra : float
|
|
478
|
+
Right Ascension of the target in degrees.
|
|
479
|
+
target_dec : float
|
|
480
|
+
Declination of the target in degrees.
|
|
481
|
+
max_ang_sep : float
|
|
482
|
+
Maximum allowed angular separation between satellite and target in degrees.
|
|
483
|
+
min_elev : float
|
|
484
|
+
Minimum required elevation of the satellite above horizon in degrees.
|
|
485
|
+
|
|
486
|
+
Returns
|
|
487
|
+
-------
|
|
488
|
+
list
|
|
489
|
+
List of visibility windows, where each window is a dictionary containing:
|
|
490
|
+
- 'start_time': Start time of the visibility window
|
|
491
|
+
- 'end_time': End time of the visibility window
|
|
492
|
+
- 'max_elevation': Maximum elevation during the window
|
|
493
|
+
- 'min_angular_separation': Minimum angular separation during the window
|
|
494
|
+
|
|
495
|
+
Notes
|
|
496
|
+
-----
|
|
497
|
+
The function uses the WGS84 Earth model and converts the satellite's position
|
|
498
|
+
to topocentric coordinates for elevation calculations. Visibility windows are
|
|
499
|
+
determined based on both elevation constraints and angular separation from the
|
|
500
|
+
target.
|
|
501
|
+
|
|
502
|
+
The function requires the Skyfield library for satellite calculations and
|
|
503
|
+
assumes the existence of a `make_window` helper function to format the output
|
|
504
|
+
windows.
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
ts = load.timescale()
|
|
508
|
+
sf_times = ts.ut1_jd(times.jd)
|
|
509
|
+
|
|
510
|
+
# Set up observer location
|
|
511
|
+
observer_location = wgs84.latlon(observer_lat, observer_lon, observer_elevation)
|
|
512
|
+
|
|
513
|
+
# Create satellite object
|
|
514
|
+
satellite = EarthSatellite(tle_line1, tle_line2, ts=ts)
|
|
515
|
+
|
|
516
|
+
# Create celestial target position
|
|
517
|
+
target = position_of_radec(
|
|
518
|
+
ra_hours=target_ra / 15, dec_degrees=target_dec
|
|
519
|
+
) # Convert RA to hours
|
|
520
|
+
|
|
521
|
+
satellite_position = satellite.at(sf_times)
|
|
522
|
+
|
|
523
|
+
topocentric = satellite_position - observer_location.at(sf_times)
|
|
524
|
+
alt, az, distance = topocentric.altaz()
|
|
525
|
+
|
|
526
|
+
angular_sep = topocentric.separation_from(target).degrees
|
|
527
|
+
|
|
528
|
+
vis_idx = np.where((alt.degrees > min_elev) & (angular_sep < max_ang_sep))[0]
|
|
529
|
+
break_idx = np.where(np.diff(vis_idx) > 1)[0]
|
|
530
|
+
if len(break_idx) > 0 or len(vis_idx) > 0:
|
|
531
|
+
break_idx = np.concatenate([[0], break_idx, [len(times)]])
|
|
532
|
+
windows = [
|
|
533
|
+
make_window(
|
|
534
|
+
times,
|
|
535
|
+
alt.degrees,
|
|
536
|
+
angular_sep,
|
|
537
|
+
vis_idx[break_idx[i] : break_idx[i + 1]],
|
|
538
|
+
)
|
|
539
|
+
for i in range(len(break_idx) - 1)
|
|
540
|
+
]
|
|
541
|
+
else:
|
|
542
|
+
windows = []
|
|
543
|
+
|
|
544
|
+
# print(windows)
|
|
545
|
+
|
|
546
|
+
return windows
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def check_satellite_visibilibities(
|
|
550
|
+
norad_ids: list[int],
|
|
551
|
+
tles_line1: list[str],
|
|
552
|
+
tles_line2: list[str],
|
|
553
|
+
times: list[Time],
|
|
554
|
+
observer_lat: float,
|
|
555
|
+
observer_lon: float,
|
|
556
|
+
observer_elevation: float,
|
|
557
|
+
target_ra: float,
|
|
558
|
+
target_dec: float,
|
|
559
|
+
max_ang_sep: float,
|
|
560
|
+
min_elev: float,
|
|
561
|
+
) -> dict:
|
|
562
|
+
"""Calculate visibility windows for a satellite when observing a celestial target.
|
|
563
|
+
|
|
564
|
+
This function determines time windows when a satellite will pass a celestial
|
|
565
|
+
target based on the satellite's orbital parameters (TLE), observer location,
|
|
566
|
+
target coordinates, and visibility constraints.
|
|
567
|
+
|
|
568
|
+
Parameters
|
|
569
|
+
----------
|
|
570
|
+
norad_ids: list[int]
|
|
571
|
+
NORAD IDs to calculate for.
|
|
572
|
+
tles_line1 : list[str]
|
|
573
|
+
First line of the satellites' Two-Line Element set (TLE).
|
|
574
|
+
tles_line2 : list[str]
|
|
575
|
+
Second line of the satellites' Two-Line Element set (TLE).
|
|
576
|
+
times : list[Time]
|
|
577
|
+
Array of observation times as Astropy Time objects.
|
|
578
|
+
observer_lat : float
|
|
579
|
+
Observer's latitude in degrees.
|
|
580
|
+
observer_lon : float
|
|
581
|
+
Observer's longitude in degrees.
|
|
582
|
+
observer_elevation : float
|
|
583
|
+
Observer's elevation above sea level in meters.
|
|
584
|
+
target_ra : float
|
|
585
|
+
Right Ascension of the target in degrees.
|
|
586
|
+
target_dec : float
|
|
587
|
+
Declination of the target in degrees.
|
|
588
|
+
max_ang_sep : float
|
|
589
|
+
Maximum allowed angular separation between satellite and target in degrees.
|
|
590
|
+
min_elev : float
|
|
591
|
+
Minimum required elevation of the satellite above horizon in degrees.
|
|
592
|
+
|
|
593
|
+
Returns
|
|
594
|
+
-------
|
|
595
|
+
dict
|
|
596
|
+
Dict of list of visibility windows for each NORAD ID, where each window is a dictionary containing:
|
|
597
|
+
- 'start_time': Start time of the visibility window
|
|
598
|
+
- 'end_time': End time of the visibility window
|
|
599
|
+
- 'max_elevation': Maximum elevation during the window
|
|
600
|
+
- 'min_angular_separation': Minimum angular separation during the window
|
|
601
|
+
|
|
602
|
+
Notes
|
|
603
|
+
-----
|
|
604
|
+
The function uses the WGS84 Earth model and converts the satellite's position
|
|
605
|
+
to topocentric coordinates for elevation calculations. Visibility windows are
|
|
606
|
+
determined based on both elevation constraints and angular separation from the
|
|
607
|
+
target.
|
|
608
|
+
|
|
609
|
+
The function requires the Skyfield library for satellite calculations and
|
|
610
|
+
assumes the existence of a `make_window` helper function to format the output
|
|
611
|
+
windows.
|
|
612
|
+
"""
|
|
613
|
+
|
|
614
|
+
print()
|
|
615
|
+
print(
|
|
616
|
+
f"Searching which satellites satisfy max_ang_sep: {max_ang_sep:.0f} and min_elev: {min_elev:.0f}"
|
|
617
|
+
)
|
|
618
|
+
all_windows = []
|
|
619
|
+
for i in tqdm(range(len(norad_ids))):
|
|
620
|
+
windows = check_visibility(
|
|
621
|
+
tles_line1[i],
|
|
622
|
+
tles_line2[i],
|
|
623
|
+
times,
|
|
624
|
+
observer_lat,
|
|
625
|
+
observer_lon,
|
|
626
|
+
observer_elevation,
|
|
627
|
+
target_ra,
|
|
628
|
+
target_dec,
|
|
629
|
+
max_ang_sep,
|
|
630
|
+
min_elev,
|
|
631
|
+
)
|
|
632
|
+
if len(windows) > 0:
|
|
633
|
+
all_windows += [{"norad_id": norad_ids[i], **window} for window in windows]
|
|
634
|
+
|
|
635
|
+
print(f"Found {len(all_windows)} matching satellites")
|
|
636
|
+
return pd.DataFrame(all_windows)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def get_satellite_positions(tles: list, times_jd: list) -> ArrayLike:
|
|
640
|
+
"""Calculate the ICRS positions of satellites by propagating their TLEs over the given times.
|
|
641
|
+
|
|
642
|
+
Parameters
|
|
643
|
+
----------
|
|
644
|
+
tles : Array (n_sat, 2)
|
|
645
|
+
TLEs usind to propagate positions.
|
|
646
|
+
times : Array (n_time,)
|
|
647
|
+
Times to calculate positions at in Julian date.
|
|
648
|
+
|
|
649
|
+
Returns
|
|
650
|
+
-------
|
|
651
|
+
Array (n_sat, n_time, 3)
|
|
652
|
+
Satellite positions over time
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
ts = load.timescale()
|
|
656
|
+
sf_times = ts.ut1_jd(times_jd)
|
|
657
|
+
|
|
658
|
+
sat_pos = np.array(
|
|
659
|
+
[
|
|
660
|
+
EarthSatellite(tle_line1, tle_line2, ts=ts).at(sf_times).position.km.T * 1e3
|
|
661
|
+
for tle_line1, tle_line2 in tles
|
|
662
|
+
]
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
return sat_pos
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def ant_pos(ant_itrf: ArrayLike, times_jd: ArrayLike) -> ArrayLike:
|
|
669
|
+
|
|
670
|
+
ts = load.timescale()
|
|
671
|
+
t = ts.ut1_jd(times_jd)
|
|
672
|
+
|
|
673
|
+
location = EarthLocation(x=ant_itrf[0], y=ant_itrf[1], z=ant_itrf[2], unit="m")
|
|
674
|
+
observer = wgs84.latlon(
|
|
675
|
+
location.lat.degree, location.lon.degree, location.height.value
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
return (observer.at(t).position.km * 1e3).T
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def ants_pos(ants_itrf: ArrayLike, times_jd: ArrayLike) -> ArrayLike:
|
|
682
|
+
|
|
683
|
+
return np.transpose(
|
|
684
|
+
np.array([ant_pos(ant_itrf, times_jd) for ant_itrf in ants_itrf]),
|
|
685
|
+
axes=(1, 0, 2),
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def sat_distance(tle: list[str], times_jd: ArrayLike, obs_itrf: ArrayLike) -> ArrayLike:
|
|
690
|
+
|
|
691
|
+
ts = load.timescale()
|
|
692
|
+
|
|
693
|
+
t = ts.ut1_jd(times_jd)
|
|
694
|
+
|
|
695
|
+
satellite = EarthSatellite(tle[0], tle[1], ts=ts)
|
|
696
|
+
|
|
697
|
+
location = EarthLocation(x=obs_itrf[0], y=obs_itrf[1], z=obs_itrf[2], unit="m")
|
|
698
|
+
|
|
699
|
+
observer = wgs84.latlon(location.lat.degree, location.lon.degree, location.height)
|
|
700
|
+
|
|
701
|
+
topo = (satellite - observer).at(t)
|
|
702
|
+
|
|
703
|
+
return topo.distance().m
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def sathub_time_to_isot(sathub_time: str) -> str:
|
|
707
|
+
"""Convert the epoch time return by a call to the SatChecker from SatHub to isot.
|
|
708
|
+
|
|
709
|
+
Parameters
|
|
710
|
+
----------
|
|
711
|
+
sathub_time : str
|
|
712
|
+
Time format returned by the SatChecker api.
|
|
713
|
+
|
|
714
|
+
Returns
|
|
715
|
+
-------
|
|
716
|
+
str
|
|
717
|
+
Time format in isot, easily ingested by astropy.time.Time.
|
|
718
|
+
"""
|
|
719
|
+
|
|
720
|
+
dt = datetime.strptime(sathub_time, "%Y-%m-%d %H:%M:%S UTC")
|
|
721
|
+
isot = dt.strftime("%Y-%m-%dT%H:%M:%S.000")
|
|
722
|
+
|
|
723
|
+
return isot
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def get_sat_pos_tle(
|
|
727
|
+
tle_line1: str, tle_line2: str, sat_name: str, times_jd: float
|
|
728
|
+
) -> ArrayLike:
|
|
729
|
+
"""Calculate the satellite position in GCRS (ECI) frame at the given Julian dates.
|
|
730
|
+
|
|
731
|
+
Parameters
|
|
732
|
+
----------
|
|
733
|
+
tle_line1 : str
|
|
734
|
+
First line of the TLE.
|
|
735
|
+
tle_line2 : str
|
|
736
|
+
Second line fo the TLE
|
|
737
|
+
sat_name : str
|
|
738
|
+
Satellite name. This is often given in the line above the TLE.
|
|
739
|
+
times_jd : float
|
|
740
|
+
Julian dates at which to evaluate the satellite position.
|
|
741
|
+
|
|
742
|
+
Returns
|
|
743
|
+
-------
|
|
744
|
+
ArrayLike
|
|
745
|
+
Satellite positions in metres in the GCRS (ECI) frame.
|
|
746
|
+
"""
|
|
747
|
+
|
|
748
|
+
ts = load.timescale()
|
|
749
|
+
sat = EarthSatellite(tle_line1, tle_line2, sat_name, ts)
|
|
750
|
+
t_s = ts.ut1_jd(times_jd)
|
|
751
|
+
sat_pos = sat.at(t_s).position.m
|
|
752
|
+
|
|
753
|
+
return sat_pos
|