spherapy 0.2.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.
spherapy/timespan.py ADDED
@@ -0,0 +1,436 @@
1
+ """Class for series of timestamps.
2
+
3
+ This module provides:
4
+ - TimeSpan: a series of timestamps to be used by an orbit object
5
+ """
6
+ import datetime as dt
7
+ import logging
8
+
9
+ from typing import cast
10
+ from typing_extensions import Self
11
+
12
+ from astropy.time import Time as astropyTime
13
+ from dateutil.relativedelta import relativedelta
14
+ import numpy as np
15
+ from skyfield.api import load
16
+ import skyfield.timelib
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class TimeSpan:
22
+ """A series of timestamps.
23
+
24
+ Attributes:
25
+ start: The first timestamp
26
+ end: The last timestamp
27
+ time_step: The difference between timestamps in seconds
28
+ Can be None if irregular steps
29
+ time_period: The difference between end and start in seconds
30
+
31
+ """
32
+ def __init__(self, t0:dt.datetime, timestep:str='1S', timeperiod:str='10S'):
33
+ """Creates a series of timestamps in UTC.
34
+
35
+ Difference between each timestamp = timestep
36
+ Total duration = greatest integer number of timesteps less than timeperiod
37
+ If timeperiod is an integer multiple of timestep,
38
+ then TimeSpan[-1] - TimeSpan[0] = timeperiod
39
+ If timeperiod is NOT an integer multiple of timestep,
40
+ then TimeSpan[-1] = int(timeperiod/timestep) (Note: int cast, not round)
41
+
42
+ Does not account for Leap seconds, similar to all Posix compliant UTC based time
43
+ representations. see: https://numpy.org/doc/stable/reference/arrays.datetime.html#datetime64-shortcomings
44
+ for equivalent shortcomings.
45
+ Always contains at least two timestamps
46
+
47
+ Parameters
48
+ t0: datetime defining the start of the TimeSpan.
49
+ If timezone naive, assumed to be in UTC
50
+ If timezone aware, will be converted to UTC
51
+ timestep: String describing the time step of the time span.
52
+ The string is constructed as an integer or float, followed by a time unit:
53
+ (d)ays, (H)ours, (M)inutes, (S)econds, (mS) milliseconds, (uS) microseconds
54
+ (the default is '1S')
55
+ timeperiod: String describing the time period of the time span.
56
+ The string is constructed as an integer or float, followed by a time unit:
57
+ (d)ays, (H)ours, (M)inutes, (S)econds, (mS) milliseconds, (uS) microseconds
58
+ (the default is '1d')
59
+
60
+ Raises:
61
+ ------
62
+ ValueError
63
+ """
64
+ # convert and/or add timezone info
65
+ if t0.tzinfo is not None:
66
+ t0 = t0.astimezone(dt.timezone.utc)
67
+ else:
68
+ t0 = t0.replace(tzinfo=dt.timezone.utc)
69
+
70
+ self.start:dt.datetime = t0
71
+ self.time_step:None|dt.timedelta = self._parseTimestep(timestep)
72
+ self.end:dt.datetime = self._parseTimeperiod(t0, timeperiod)
73
+ self.time_period = self.end - self.start
74
+
75
+ logger.info("Creating TimeSpan between %s and %s with %s seconds timestep",
76
+ self.start, self.end, self.time_step.total_seconds())
77
+
78
+
79
+ if self.start >= self.end:
80
+ logger.error("Timeperiod: %s results in an end date earlier than the start date",
81
+ timeperiod)
82
+ raise ValueError(f"Invalid timeperiod value: {timeperiod}")
83
+
84
+ if self.time_period < self.time_step:
85
+ logger.error("Timeperiod: %s is smaller than the timestep %s.", timeperiod, timestep)
86
+ raise ValueError(f"Invalid timeperiod value, too small: {timeperiod}")
87
+
88
+ # Calculate step numbers and correct for non integer steps.
89
+ n_down = int(np.floor(self.time_period / self.time_step))
90
+ if n_down != self.time_period / self.time_step:
91
+ logger.info("Rounding to previous integer number of timesteps")
92
+ self.end = n_down * self.time_step + self.start
93
+ self.time_period = self.end - self.start
94
+
95
+ logger.info("Adjusting TimeSpan to %s -> %s with %s seconds timestep",
96
+ self.start, self.end, self.time_step.total_seconds())
97
+
98
+
99
+ self._timearr = np.arange(self.start.replace(tzinfo=None),
100
+ self.end.replace(tzinfo=None)+self.time_step,
101
+ self.time_step).astype(dt.datetime)
102
+ self._timearr = np.vectorize(lambda x: x.replace(tzinfo=dt.timezone.utc))(self._timearr)
103
+ self._skyfield_timespan = load.timescale()
104
+
105
+
106
+ def __hash__(self) -> int:
107
+ """Returns a hash of the timespan."""
108
+ return hash((self.start, self.end, self.time_step))
109
+
110
+ # Make it callable and return the data for that entry
111
+ def __call__(self) -> np.ndarray[tuple[int], np.dtype[np.datetime64]]:
112
+ """Returns the internal _timearr when the TimeSpan is called."""
113
+ return self._timearr
114
+
115
+ def __getitem__(self, idx:None|int|np.integer|tuple|list|np.ndarray|slice=None) \
116
+ -> None|dt.datetime|np.ndarray[tuple[int], np.dtype[np.datetime64]]: # noqa: PLR0911
117
+ """Returns an index or slice of the TimeSpan as an array of datetime objects."""
118
+ if idx is None:
119
+ return self._timearr
120
+ if isinstance(idx, int|np.integer):
121
+ return self._timearr[idx]
122
+ if isinstance(idx, tuple):
123
+ if len(idx) == 2: #noqa: PLR2004
124
+ return self._timearr[idx[0]:idx[1]]
125
+ return self._timearr[idx[0]:idx[1]:idx[2]]
126
+ if isinstance(idx, list):
127
+ return self._timearr[[idx]]
128
+ if isinstance(idx, np.ndarray):
129
+ return self._timearr[idx]
130
+ if isinstance(idx,slice):
131
+ return self._timearr[idx]
132
+ raise TypeError('index is unknown type')
133
+
134
+ def __eq__(self, other:object) -> bool:
135
+ """Checks if other TimeSpan is equal to self."""
136
+ if not isinstance(other, TimeSpan):
137
+ return NotImplemented
138
+
139
+ if len(self) != len(other):
140
+ return False
141
+
142
+ return bool(np.all(self.asDatetime() == other.asDatetime()))
143
+
144
+ def __add__(self, other:Self) -> 'TimeSpan':
145
+ """Apeends other TimeSpan to this one, does not order timesteps."""
146
+ self_copy = TimeSpan(self.start)
147
+
148
+ self_copy.start = self.start
149
+ self_copy.time_step = None
150
+
151
+ self_copy.end = other.end
152
+ self_copy.time_period = other.end - self_copy.start
153
+
154
+ self_copy._timearr = np.hstack((self._timearr, other._timearr))
155
+
156
+ return self_copy
157
+
158
+ def asAstropy(self, idx:None|int=None, scale:str='utc') -> astropyTime:
159
+ """Return ndarray of TimeSpan as astropy.time objects.
160
+
161
+ Args:
162
+ idx: timestamp index to return inside an astropyTime object
163
+ if no index supplied, returns all timestamps
164
+ scale: astropy time scale, can be one of
165
+ ('tai', 'tcb', 'tcg', 'tdb', 'tt', 'ut1', 'utc')
166
+
167
+ Returns:
168
+ -------
169
+ ndarray
170
+ """
171
+ if idx is None:
172
+ return astropyTime(self._timearr, scale=scale)
173
+ return astropyTime(self._timearr[idx], scale=scale)
174
+
175
+ def asDatetime(self, idx:None|int=None) \
176
+ -> dt.datetime|np.ndarray[tuple[int], np.dtype[np.datetime64]]:
177
+ """Return ndarray of TimeSpan as datetime objects.
178
+
179
+ Args:
180
+ idx: timestamp index to return as a datetime
181
+ If no index supplied, returns whole array
182
+
183
+ Returns:
184
+ -------
185
+ ndarray
186
+ """
187
+ if idx is None:
188
+ return self._timearr
189
+ return self._timearr[idx]
190
+
191
+ def asSkyfield(self, idx:int) -> skyfield.timelib.Time:
192
+ """Return TimeSpan element as Skyfield Time object.
193
+
194
+ Args:
195
+ idx: timestamp index to return as skyfield time object
196
+
197
+ Returns:
198
+ -------
199
+ Skyfield Time
200
+ """
201
+ datetime = self._timearr[idx]
202
+ datetime = datetime.replace(tzinfo=dt.timezone.utc)
203
+ return self._skyfield_timespan.from_datetime(datetime)
204
+
205
+ def asText(self, idx:int) -> str:
206
+ """Returns a text representation of a particular timestamp.
207
+
208
+ Timestamp will be formatted as YYYY-mm-dd HH:MM:SS
209
+
210
+ Args:
211
+ idx: timestamp index to format
212
+
213
+ Returns:
214
+ str:
215
+ """
216
+ return self._timearr[idx].strftime("%Y-%m-%d %H:%M:%S")
217
+
218
+ def secondsSinceStart(self) -> np.ndarray[tuple[int], np.dtype[np.float64]]:
219
+ """Return ndarray with the seconds of all timesteps since the beginning.
220
+
221
+ Returns:
222
+ array of seconds since start for each timestamp
223
+ """
224
+ diff = self._timearr - self.start
225
+ days = np.vectorize(lambda x: x.days)(diff)
226
+ secs = np.vectorize(lambda x: x.seconds)(diff)
227
+ usecs = np.vectorize(lambda x: x.microseconds)(diff)
228
+ return days * 86400 + secs + usecs * 1e-6
229
+
230
+ def getClosest(self, t_search:dt.datetime) -> tuple[dt.datetime, int]:
231
+ """Find the closest time in a TimeSpan.
232
+
233
+ Parameters
234
+ ----------
235
+ t_search : datetime to search for in TimeSpan
236
+ If timezone naive, assumed to be in UTC
237
+ If timezone aware, will be converted to UTCtime to find
238
+
239
+ Returns:
240
+ -------
241
+ datetime, int
242
+ Closest datetime in TimeSpan, index of closest date in TimeSpan
243
+ """
244
+ # convert and/or add timezone info
245
+ if t_search.tzinfo is not None:
246
+ t_search = t_search.astimezone(dt.timezone.utc)
247
+ else:
248
+ t_search = t_search.replace(tzinfo=dt.timezone.utc)
249
+ diff = self._timearr - t_search
250
+ out = np.abs(np.vectorize(lambda x: x.total_seconds())(diff))
251
+ res_index = int(np.argmin(out))
252
+ res_datetime = self.asDatetime(res_index)
253
+
254
+ # res_datetime must be a dt.datetime since res_index is an int, so cast
255
+ res_datetime = cast("dt.datetime", res_datetime)
256
+
257
+ return res_datetime, res_index
258
+
259
+ def areTimesWithin(self, t_search:dt.datetime|np.ndarray[tuple[int], np.dtype[np.datetime64]])\
260
+ -> np.ndarray[tuple[int],np.dtype[np.bool_]]:
261
+ """Find if the provided times are within the timespan.
262
+
263
+ Args:
264
+ t_search: times to check if within timespan
265
+ If timezone naive, assumed to be in UTC
266
+ If timezone aware, will be converted to UTCtime to find
267
+
268
+ Returns:
269
+ ndarray of bools, True if within timespan
270
+ """
271
+ if isinstance(t_search, dt.datetime):
272
+ if t_search.tzinfo is not None:
273
+ t_search = t_search.astimezone(dt.timezone.utc)
274
+ else:
275
+ t_search = t_search.replace(tzinfo=dt.timezone.utc)
276
+
277
+ elif isinstance(t_search, np.ndarray):
278
+ if t_search[0].tzinfo is not None:
279
+ t_search = np.vectorize(lambda x:x.astimezone(dt.timezone.utc))(t_search)
280
+ else:
281
+ t_search = np.vectorize(lambda x:x.replace(tzinfo=dt.timezone.utc))(t_search)
282
+
283
+ else:
284
+ raise TypeError('t_search is of an unrecognised type,'
285
+ ' should be a datetime object or ndarray')
286
+
287
+ return np.logical_and(t_search>np.asarray(self.start), t_search<np.asarray(self.end))
288
+
289
+ def getFractionalIndices(self, t_search:dt.datetime|np.ndarray[tuple[int],
290
+ np.dtype[np.datetime64]])\
291
+ -> np.ndarray[tuple[int],np.dtype[np.float64]]:
292
+ """Find the fractional indices of timespan at wich t_search should be inserted.
293
+
294
+ Find the indices in the original timespan at which each value of t_search should be
295
+ inserted to maintain the sorted order.
296
+ The integer part of the index indicates the value immediately prior to the value in
297
+ t_search, while the fractional part represents the point between the two adjacent indices
298
+ in the timespan at which the t_search value falls.
299
+ For example (using integers rather than datetime objects:
300
+ timespan = [0, 1, 5, 6, 10]
301
+ t_search = [0.5, 2, 3, 8]
302
+ timespan.getFractionalIndices(t_search) = [0.5, 1.25, 1.5, 3.5]
303
+ All values of t_search must be within the timespan, otherwise the output is undefined.
304
+
305
+ Args:
306
+ t_search: times to locate within timespan
307
+ If timezone naive, assumed to be in UTC
308
+ If timezone aware, will be converted to UTCtime to find
309
+
310
+ Returns:
311
+ ndarray of fractional indices
312
+ """
313
+ if isinstance(t_search, dt.datetime):
314
+ if t_search.tzinfo is not None:
315
+ t_search = t_search.astimezone(dt.timezone.utc)
316
+ else:
317
+ t_search = t_search.replace(tzinfo=dt.timezone.utc)
318
+ t_search = np.asarray(t_search)
319
+ elif isinstance(t_search, np.ndarray):
320
+ if t_search[0].tzinfo is not None:
321
+ t_search = np.vectorize(lambda x:x.astimezone(dt.timezone.utc))(t_search)
322
+ else:
323
+ t_search = np.vectorize(lambda x:x.replace(tzinfo=dt.timezone.utc))(t_search)
324
+ else:
325
+ raise TypeError('t_search is of an unrecognised type, '
326
+ 'should be a datetime object or ndarray')
327
+
328
+ post_idxs = np.searchsorted(self._timearr, t_search) # type: ignore [arg-type]
329
+ pre_idxs = post_idxs-1
330
+ return (t_search - self._timearr[pre_idxs])\
331
+ /(self._timearr[post_idxs]-self._timearr[pre_idxs]) + pre_idxs
332
+
333
+
334
+ def _parseTimeperiod(self, t0:dt.datetime, timeperiod:str) -> dt.datetime:
335
+ last_idx = 0
336
+ for idx, letter in enumerate(timeperiod):
337
+ last_idx = idx
338
+ if not (letter.isdigit() or letter == '.'):
339
+ break
340
+
341
+ val = float(timeperiod[0:last_idx])
342
+ unit = timeperiod[last_idx:]
343
+ values = np.zeros(6)
344
+ if unit == 'd':
345
+ values[5] = val
346
+ elif unit == 'H':
347
+ values[4] = val
348
+ elif unit == 'M':
349
+ values[3] = val
350
+ elif unit == 'S':
351
+ values[2] = val
352
+ elif unit == 'mS':
353
+ values[0] = 1000 * val
354
+ elif unit == 'uS':
355
+ values[0] = val
356
+ else:
357
+ logger.error("Invalid timeperiod unit: Valid units are (y)ears, (m)onths, (W)eeks,"
358
+ " (d)ays, (H)ours, (M)inutes, (S)econds, (mS) milliseconds,"
359
+ " (uS) microseconds")
360
+ raise ValueError(f"Invalid timeperiod unit:{unit}")
361
+
362
+ return t0 + relativedelta(days=values[5], hours=values[4], minutes=values[3],
363
+ seconds=values[2], microseconds=values[0])
364
+
365
+ def _parseTimestep(self, timestep:str) -> dt.timedelta:
366
+ last_idx = 0
367
+ for idx, letter in enumerate(timestep):
368
+ last_idx = idx
369
+ if not (letter.isdigit() or letter == '.'):
370
+ break
371
+
372
+ val = float(timestep[0:last_idx])
373
+ unit = timestep[last_idx:]
374
+ values = np.zeros(6)
375
+ if unit == 'd':
376
+ values[5] = val
377
+ elif unit == 'H':
378
+ values[4] = val
379
+ elif unit == 'M':
380
+ values[3] = val
381
+ elif unit == 'S':
382
+ values[2] = val
383
+ elif unit == 'mS':
384
+ values[1] = val
385
+ elif unit == 'uS':
386
+ values[0] = val
387
+ else:
388
+ logger.error("Invalid timestep unit: Valid units are (d)ays, (H)ours," \
389
+ " (M)inutes, (S)econds, (mS) milliseconds, (uS) microseconds")
390
+ raise ValueError(f"Invalid timestep unit:{unit}")
391
+
392
+ return dt.timedelta(days=values[5], seconds=values[2],
393
+ microseconds=values[0], milliseconds=values[1],
394
+ minutes=values[3], hours=values[4])
395
+
396
+ def __len__(self) -> int:
397
+ """Returns the length of the TimeSpan."""
398
+ return len(self._timearr)
399
+
400
+
401
+ def cherryPickFromIndices(self, idxs:int|tuple|slice):
402
+ """Adjust TimeSpan to only contain the indices specified by idxs.
403
+
404
+ Args:
405
+ idxs: numpy style indexing of TimeSpan
406
+ """
407
+ self._timearr = self._timearr[idxs]
408
+ self.start = self._timearr[0]
409
+ self.end = self._timearr[-1]
410
+ self.time_step = None
411
+ self.time_period = self.end - self.start
412
+
413
+ @classmethod
414
+ def fromDatetime(cls, dt_arr:np.ndarray[tuple[int], np.dtype[np.datetime64]],
415
+ timezone:dt.timezone=dt.timezone.utc) -> 'TimeSpan':
416
+ """Create a TimeSpan from an array of datetime objects.
417
+
418
+ Args:
419
+ dt_arr: 1D array of datetime objects
420
+ timezone: timezone to apply to each element of datetime array.
421
+ Default: dt.timezone.utc
422
+ """
423
+ for ii in range(len(dt_arr)):
424
+ dt_arr[ii] = dt_arr[ii].replace(tzinfo=timezone)
425
+ start = dt_arr[0]
426
+ end = dt_arr[-1]
427
+ tperiod = (end-start).total_seconds()
428
+ tstep = tperiod/len(dt_arr)
429
+ t = cls(start,f'{tstep}S',f'{tperiod}S')
430
+ t._timearr = dt_arr.copy()
431
+ t.start = start
432
+ t.end = end
433
+ t.time_step = None
434
+ t.time_period = t.end-t.start
435
+
436
+ return t
spherapy/updater.py ADDED
@@ -0,0 +1,91 @@
1
+ """Functions to update TLE of a satcat ID, and fetch where the file is stored."""
2
+ import datetime as dt
3
+ import pathlib
4
+
5
+ import spherapy
6
+ from spherapy.util import epoch_u, spacetrack
7
+ import spherapy.util.spacetrack as celestrak
8
+
9
+
10
+ def updateTLEs(sat_id_list: list[int]) -> list[int]:
11
+ """Fetch most recent TLE for provided list of satcat IDs.
12
+
13
+ Fetch most recent TLE for provided list of satcat IDs,
14
+ If credentials are stored, use Spacetrack as TLE source, this will fetch all historical
15
+ TLEs for that satcat ID (from most recent stored TLE up to current)
16
+ If no credentials are stored, use Celestrak as TLE source, this will fetch only the most
17
+ recent TLE
18
+
19
+ Args:
20
+ sat_id_list: list of satcat ids to fetch
21
+
22
+ Returns:
23
+ list of satcat ids successfully fetched
24
+ """
25
+ if spacetrack.doCredentialsExist():
26
+ modified_list = spacetrack.updateTLEs(sat_id_list)
27
+ else:
28
+ modified_list = celestrak.updateTLEs(sat_id_list)
29
+
30
+ return modified_list
31
+
32
+ def getTLEFilePaths(sat_id_list:list[int], use_packaged:bool=False) -> list[pathlib.Path]:
33
+ """Fetch list of paths to TLE files.
34
+
35
+ Fetch paths to the file storing the list of TLEs for each provided satcat ID
36
+ If credentials are stored, return path containing all historical TLEs (fetched from Spacetrack)
37
+ If no credentials are stored, return path to .temptle containing only most recent TLE
38
+ (from Celestrak)
39
+ You can force the use of TLE data packaged with spherapy by setting use_packaged=True
40
+ This data will be out of date, and should only be used in examples.
41
+
42
+ Args:
43
+ sat_id_list: list of satcat ids to find paths
44
+ use_packaged: use the default TLEs packaged with spherapy, these will be out of date.
45
+
46
+ Returns:
47
+ list of paths
48
+ """
49
+ if use_packaged and spherapy.packaged_TLEs is None:
50
+ raise ValueError('There are no TLEs packaged with spherapy')
51
+ if use_packaged and spherapy.packaged_TLEs is not None:
52
+ try:
53
+ path_list = []
54
+ attempted_sat_id = 0
55
+ for sat_id in sat_id_list:
56
+ attempted_sat_id = sat_id
57
+ path_list.append(spherapy.packaged_TLEs[sat_id])
58
+ except KeyError as e:
59
+ raise KeyError(f'TLE for {attempted_sat_id} is not packaged with spherapy.') from e
60
+ else:
61
+ return path_list
62
+ elif spacetrack.doCredentialsExist():
63
+ return [ spacetrack.getTLEFilePath(sat_id) for sat_id in sat_id_list ]
64
+ return [ celestrak.getTLEFilePath(sat_id) for sat_id in sat_id_list ]
65
+
66
+ def getStoredEpochLimits(sat_id_list:list[int], use_packaged:bool=False) \
67
+ -> dict[int, None|tuple[dt.datetime, dt.datetime|None]]:
68
+ """Returns limiting epochs for the stored TLEs for each sat in sat_id_list.
69
+
70
+ [description]
71
+
72
+ Args:
73
+ sat_id_list (list[int]): [description]
74
+ use_packaged (bool): [description] (default: `False`)
75
+
76
+ Returns:
77
+ list[tuple[dt.datetime, dt.datetime]]: [description]
78
+ """
79
+ terminating_epochs = {}
80
+ if use_packaged and spherapy.packaged_TLEs is not None:
81
+ for sat_id in sat_id_list:
82
+ packaged_path = spherapy.packaged_TLEs[sat_id]
83
+ terminating_epochs[sat_id] = epoch_u.getStoredEpochs(packaged_path)
84
+ elif spacetrack.doCredentialsExist():
85
+ for sat_id in sat_id_list:
86
+ terminating_epochs[sat_id] = spacetrack.getStoredEpochs(sat_id)
87
+ else:
88
+ for sat_id in sat_id_list:
89
+ terminating_epochs[sat_id] = celestrak.getStoredEpochs(sat_id)
90
+
91
+ return terminating_epochs
File without changes
@@ -0,0 +1,87 @@
1
+ """Functions to fetch TLEs from Celestrak.
2
+
3
+ Attributes:
4
+ MAX_RETRIES: number of times to try and reach Celestrak.
5
+ TIMEOUT: timeout for connection and request servicing by Celestrak.
6
+ """
7
+
8
+ import datetime as dt
9
+ import logging
10
+ import pathlib
11
+
12
+ import requests
13
+
14
+ import spherapy
15
+ from spherapy.util import epoch_u
16
+
17
+ MAX_RETRIES=3
18
+ TIMEOUT=10
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ def updateTLEs(sat_id_list:list[int]) -> list[int]:
23
+ """Fetch most recent TLE for satcat IDs from celestrak.
24
+
25
+ Fetch most recent TLE for provided list of satcat IDs, and store in file.
26
+ Will try MAX_RETRIES before raising a ValueError
27
+
28
+ Args:
29
+ sat_id_list: list of satcat ids to fetch
30
+
31
+ Returns:
32
+ list: list of satcat ids successfully fetched
33
+
34
+ Raises:
35
+ TimeoutError
36
+ """
37
+ logger.info("Using CELESTRAK to update TLEs")
38
+ modified_list = []
39
+ for sat_id in sat_id_list:
40
+ url = f'https://celestrak.org/NORAD/elements/gp.php?CATNR={sat_id}'
41
+ retry_num = 0
42
+ fetch_successful = False
43
+ while retry_num < MAX_RETRIES:
44
+ retry_num += 1
45
+ r = requests.get(url, timeout=TIMEOUT)
46
+ if r.status_code == requests.codes.success:
47
+ fetch_successful = True
48
+ tle_file = getTLEFilePath(sat_id)
49
+ dat_list = r.text.split('\r\n')
50
+ with tle_file.open('w') as fp:
51
+ fp.write(f'0 {dat_list[0].rstrip()}\n')
52
+ fp.write(f'{dat_list[1].rstrip()}\n')
53
+ fp.write(f'{dat_list[2].rstrip()}')
54
+ modified_list.append(sat_id)
55
+ break
56
+
57
+ if retry_num == MAX_RETRIES and fetch_successful:
58
+ logger.error('Could not fetch celestrak information for sat_id: %s', sat_id)
59
+ raise TimeoutError(f'Could not fetch celestrak information for sat_id: {sat_id}')
60
+
61
+ return modified_list
62
+
63
+ def getTLEFilePath(sat_id:int) -> pathlib.Path:
64
+ """Gives path to file where celestrak TLE is stored.
65
+
66
+ Celestrak TLEs are stored in {satcadID}.temptle
67
+
68
+ Args:
69
+ sat_id: satcat ID
70
+
71
+ Returns:
72
+ path to file
73
+ """
74
+ return spherapy.tle_dir.joinpath(f'{sat_id}.temptle')
75
+
76
+ def getStoredEpochs(sat_id:int) -> None|tuple[dt.datetime, dt.datetime|None]:
77
+ """Return the start and end epoch for {sat_id}.temptle .
78
+
79
+ Args:
80
+ sat_id: satcat id to check
81
+
82
+ Returns:
83
+ (first epoch datetime, last epoch datetime)
84
+ None if no spacetrack tle stored for sat_id
85
+ """
86
+ tle_path = getTLEFilePath(sat_id)
87
+ return epoch_u.getStoredEpochs(tle_path)
@@ -0,0 +1,38 @@
1
+ """Namespace of useful orbital constants."""
2
+
3
+ import astropy.constants as astroconst
4
+
5
+ # ######### BODY CONSTANTS ##########
6
+ M_EARTH = astroconst.M_earth.value # Mass of earth (Kg)
7
+ M_SUN = astroconst.M_sun.value # Mass of the sun (Kg)
8
+ M_MOON = 7.342e22 # Mass of the sun (Kg)
9
+ M_MARS = 6.4171e23 # Mass of the sun (Kg)
10
+ R_SUN = astroconst.R_sun.value / 1000 # Radius of sun (695700 km)
11
+ R_EARTH = astroconst.R_earth.value / 1000 # Radius of Earth (6378.1 km)
12
+ R_MOON = 1738 # Radius of the Moon (km)
13
+ R_MARS = 3390 # Radius of Mars (km)
14
+ SUN_MIN_ALT = 0 # Minimum orbital altitude of the sun (km)
15
+ EARTH_MIN_ALT = 350 # Minimum orbital altitude of the earth (km)
16
+ MOON_MIN_ALT = 10 # Minimum orbital altitude of the moon (km)
17
+ MARS_MIN_ALT = 200 # Minimum orbital altitude of mars (km)
18
+ G = astroconst.G.value # Gravitational constant (SI)
19
+ GM_EARTH = astroconst.GM_earth.value # Earth standard gravitational parameter (SI)
20
+ GM_SUN = astroconst.GM_sun.value # Sun standard gravitational parameter (SI)
21
+ GM_MOON = 4.9048695e12 # Moon standard gravitational parameter (SI)
22
+ GM_MARS = 4.282837e13 # Mars standard gravitational parameter (SI)
23
+ AU = astroconst.au.value / 1000 # Earth-Sun avg distance (1.49597871e8 km)
24
+ J2 = 1082.6267e-6 # Earth J2 perturbations (SI)
25
+ W_EARTH = 7.29211510e-5 # Rotation rate of Earth rads/sec
26
+ # ######### TEMP CONSTANTS ##########
27
+ SB_SIGMA = astroconst.sigma_sb.value # the Stefan-Boltzmann constant (5.67037442e-8 SI)
28
+ T_EARTH = 250 # Temperature of the Earth (K)
29
+ T_SUN = 5780 # Temperature of the Sun (K)
30
+ T_SPACE = 4 # Temperature of space (K)
31
+ SOL_CONST = 1391 # Max. flux of the sun at the earth (W/m^2)
32
+ L_SUN = 3.828e26 # Solar luminosity in watts
33
+
34
+ # ######### TIME CONSTANTS ##########
35
+
36
+ DAY_IN_MINS = 24 * 60
37
+ HALF_HOUR = 30
38
+ SECS_PER_DAY = 24 * 60 * 60