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/__init__.py +93 -0
- spherapy/__main__.py +17 -0
- spherapy/_version.py +34 -0
- spherapy/data/TLEs/25544.tle +153378 -0
- spherapy/orbit.py +874 -0
- spherapy/timespan.py +436 -0
- spherapy/updater.py +91 -0
- spherapy/util/__init__.py +0 -0
- spherapy/util/celestrak.py +87 -0
- spherapy/util/constants.py +38 -0
- spherapy/util/credentials.py +131 -0
- spherapy/util/elements_u.py +70 -0
- spherapy/util/epoch_u.py +149 -0
- spherapy/util/exceptions.py +7 -0
- spherapy/util/orbital_u.py +72 -0
- spherapy/util/spacetrack.py +272 -0
- spherapy-0.2.0.dist-info/METADATA +211 -0
- spherapy-0.2.0.dist-info/RECORD +22 -0
- spherapy-0.2.0.dist-info/WHEEL +5 -0
- spherapy-0.2.0.dist-info/entry_points.txt +3 -0
- spherapy-0.2.0.dist-info/licenses/LICENSE.md +19 -0
- spherapy-0.2.0.dist-info/top_level.txt +1 -0
spherapy/orbit.py
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
"""Class for orbital data.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- OrbitAttrDict: A TypedDict typing the instance attributes of an Orbit
|
|
5
|
+
- Orbit: A class of timestamped orbital data
|
|
6
|
+
"""
|
|
7
|
+
import datetime as dt
|
|
8
|
+
import logging
|
|
9
|
+
import pathlib
|
|
10
|
+
|
|
11
|
+
import types
|
|
12
|
+
import typing
|
|
13
|
+
from typing import Any, TypedDict, cast
|
|
14
|
+
|
|
15
|
+
from astropy import units as astropy_units
|
|
16
|
+
from astropy.time import Time as astropyTime
|
|
17
|
+
from hapsira.bodies import Earth, Mars, Moon, Sun
|
|
18
|
+
from hapsira.ephem import Ephem
|
|
19
|
+
from hapsira.twobody import Orbit as hapsiraOrbit
|
|
20
|
+
import numpy as np
|
|
21
|
+
from sgp4.api import WGS72, Satrec
|
|
22
|
+
|
|
23
|
+
# Don't need to import all of skyfield just for EarthSatellite loading.
|
|
24
|
+
# TODO: figure out the lightest import to use
|
|
25
|
+
from skyfield.api import EarthSatellite, load, wgs84
|
|
26
|
+
from skyfield.framelib import itrs
|
|
27
|
+
|
|
28
|
+
from spherapy.timespan import TimeSpan
|
|
29
|
+
from spherapy.util import epoch_u, exceptions
|
|
30
|
+
import spherapy.util.constants as consts
|
|
31
|
+
import spherapy.util.orbital_u as orbit_u
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OrbitAttrDict(TypedDict):
|
|
37
|
+
"""A TypedDict providing type annotations for the Orbit class.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
name:
|
|
41
|
+
satcat_id:
|
|
42
|
+
gen_type:
|
|
43
|
+
timespan:
|
|
44
|
+
TLE_epochs:
|
|
45
|
+
pos:
|
|
46
|
+
pos_ecef:
|
|
47
|
+
vel_ecef:
|
|
48
|
+
vel:
|
|
49
|
+
lat:
|
|
50
|
+
lon:
|
|
51
|
+
sun_pos:
|
|
52
|
+
moon_pos:
|
|
53
|
+
alt:
|
|
54
|
+
eclipse:
|
|
55
|
+
central_body:
|
|
56
|
+
period:
|
|
57
|
+
period_steps:
|
|
58
|
+
semi_major:
|
|
59
|
+
ecc:
|
|
60
|
+
inc:
|
|
61
|
+
raan:
|
|
62
|
+
argp:
|
|
63
|
+
"""
|
|
64
|
+
name: None|str
|
|
65
|
+
satcat_id: None|int
|
|
66
|
+
gen_type: None|str
|
|
67
|
+
timespan: None|TimeSpan
|
|
68
|
+
TLE_epochs: None|np.ndarray[tuple[int], np.dtype[np.datetime64]]
|
|
69
|
+
pos: None|np.ndarray[tuple[int,int], np.dtype[np.float64]]
|
|
70
|
+
pos_ecef: None|np.ndarray[tuple[int,int], np.dtype[np.float64]]
|
|
71
|
+
vel_ecef: None|np.ndarray[tuple[int,int], np.dtype[np.float64]]
|
|
72
|
+
vel: None|np.ndarray[tuple[int,int], np.dtype[np.float64]]
|
|
73
|
+
lat: None|np.ndarray[tuple[int], np.dtype[np.float64]]
|
|
74
|
+
lon: None|np.ndarray[tuple[int], np.dtype[np.float64]]
|
|
75
|
+
sun_pos: None|np.ndarray[tuple[int,int], np.dtype[np.float64]]
|
|
76
|
+
moon_pos: None|np.ndarray[tuple[int,int], np.dtype[np.float64]]
|
|
77
|
+
alt: None|np.ndarray[tuple[int], np.dtype[np.float64]]
|
|
78
|
+
eclipse: None|np.ndarray[tuple[int], np.dtype[np.bool_]]
|
|
79
|
+
central_body: None|str
|
|
80
|
+
period: None|float
|
|
81
|
+
period_steps: None|int
|
|
82
|
+
semi_major: None|np.ndarray[tuple[int], np.dtype[np.float64]]
|
|
83
|
+
ecc: None|np.ndarray[tuple[int], np.dtype[np.float64]]
|
|
84
|
+
inc: None|np.ndarray[tuple[int], np.dtype[np.float64]]
|
|
85
|
+
raan: None|np.ndarray[tuple[int], np.dtype[np.float64]]
|
|
86
|
+
argp: None|np.ndarray[tuple[int], np.dtype[np.float64]]
|
|
87
|
+
|
|
88
|
+
def _createEmptyOrbitAttrDict() -> OrbitAttrDict:
|
|
89
|
+
return OrbitAttrDict({
|
|
90
|
+
'name': None,
|
|
91
|
+
'satcat_id':None,
|
|
92
|
+
'gen_type':None,
|
|
93
|
+
'timespan':None,
|
|
94
|
+
'TLE_epochs':None,
|
|
95
|
+
'pos':None,
|
|
96
|
+
'pos_ecef':None,
|
|
97
|
+
'vel_ecef':None,
|
|
98
|
+
'vel':None,
|
|
99
|
+
'lat':None,
|
|
100
|
+
'lon':None,
|
|
101
|
+
'sun_pos':None,
|
|
102
|
+
'moon_pos':None,
|
|
103
|
+
'alt':None,
|
|
104
|
+
'eclipse':None,
|
|
105
|
+
'central_body':None,
|
|
106
|
+
'period':None,
|
|
107
|
+
'period_steps':None,
|
|
108
|
+
'semi_major':None,
|
|
109
|
+
'ecc':None,
|
|
110
|
+
'inc':None,
|
|
111
|
+
'raan':None,
|
|
112
|
+
'argp':None,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
def _validateOrbitAttrDict(obj:OrbitAttrDict, typ: type) -> bool:
|
|
116
|
+
"""Validates all fields of a TypedDict have been instantiated, and are of correct type.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
KeyError: If a field of the TypedDict is missing
|
|
120
|
+
TypeError: If a field of the TYpedDict is incorrect
|
|
121
|
+
"""
|
|
122
|
+
for attr_name, spec_attr_type in typ.__annotations__.items():
|
|
123
|
+
try:
|
|
124
|
+
attr_value = obj.get(attr_name, None)
|
|
125
|
+
except KeyError as e:
|
|
126
|
+
# Check for missing keys
|
|
127
|
+
raise KeyError('Typed Dict is missing key: %s', attr_name) from e
|
|
128
|
+
if type(spec_attr_type) is types.UnionType:
|
|
129
|
+
# check union of types
|
|
130
|
+
good_type = False
|
|
131
|
+
for sub_type in typing.get_args(spec_attr_type):
|
|
132
|
+
# choose validator
|
|
133
|
+
if sub_type in (None, types.NoneType):
|
|
134
|
+
validator = _NoneValidator
|
|
135
|
+
else:
|
|
136
|
+
validator = _genericValidator
|
|
137
|
+
if validator(attr_value, sub_type):
|
|
138
|
+
good_type = True
|
|
139
|
+
break
|
|
140
|
+
if not good_type:
|
|
141
|
+
raise TypeError(f'{attr_name} is type {type(attr_value)}, '
|
|
142
|
+
f'should be {spec_attr_type}')
|
|
143
|
+
elif not _genericValidator(attr_value, spec_attr_type):
|
|
144
|
+
# check all other types
|
|
145
|
+
raise TypeError(f'{attr_name} is type {type(attr_value)}, should be {spec_attr_type}')
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
def _NoneValidator(obj:object, check_type:type) -> bool: #noqa: ARG001
|
|
149
|
+
return type(obj) is types.NoneType
|
|
150
|
+
|
|
151
|
+
def _genericValidator(obj:object, check_type:type|types.GenericAlias) -> bool:
|
|
152
|
+
if type(check_type) is types.GenericAlias:
|
|
153
|
+
if type(obj) is check_type.__origin__:
|
|
154
|
+
return True
|
|
155
|
+
elif type(obj) is check_type:
|
|
156
|
+
return True
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
class Orbit:
|
|
160
|
+
"""Timestamped orbital data for a satellite, coordinate system depending on the central body.
|
|
161
|
+
|
|
162
|
+
Attributes:
|
|
163
|
+
timespan: Timespan over which orbit is to be simulated
|
|
164
|
+
name: Name of the satellite
|
|
165
|
+
satcat_id: NORAD satellite catelogue ID (if generated from a TLE)
|
|
166
|
+
gen_type: How the orbit was generated
|
|
167
|
+
TLE_epochs: Nx1 numpy array of TLE epoch used for propagation at each timestamp
|
|
168
|
+
units: TLE epoch
|
|
169
|
+
pos: Nx3 numpy array of cartesian coordinates of the position of the satellite
|
|
170
|
+
at each timestamp
|
|
171
|
+
units: km
|
|
172
|
+
frame: ECI
|
|
173
|
+
vel: Nx3 numpy array of cartesian velocities of the satellite at each timestamp
|
|
174
|
+
units: m/s
|
|
175
|
+
frame: ECI
|
|
176
|
+
pos_ecef: Nx3 numpy array of cartesian coordinates of the position of the satellite
|
|
177
|
+
at each timestamp
|
|
178
|
+
units: km
|
|
179
|
+
frame: ECEF
|
|
180
|
+
vel_ecef: Nx3 numpy array of cartesian velocities of the satellite at each timestamp
|
|
181
|
+
units: m/s
|
|
182
|
+
frame: ECEF
|
|
183
|
+
lat: Nx1 numpy array of central body latitudes of the satellite at each timestamp
|
|
184
|
+
units: degrees
|
|
185
|
+
lon: Nx1 numpy array of central body longitudes of the satellite at each timestamp
|
|
186
|
+
units: degrees
|
|
187
|
+
sun_pos: Nx3 numpy array of cartesian coordinates of the position of the Sun
|
|
188
|
+
at each timestamp
|
|
189
|
+
units: km
|
|
190
|
+
frame: ECI
|
|
191
|
+
moon_pos: Nx3 numpy array of cartesian coordinates of the position of the Moon
|
|
192
|
+
at each timestamp
|
|
193
|
+
units: km
|
|
194
|
+
frame: ECI
|
|
195
|
+
alt: Nx1 numpy array of altitudes above central body at each timestamp
|
|
196
|
+
units: km
|
|
197
|
+
eclipse: Nx1 numpy array of flag indicating if satellite is eclipsed at each timestamp
|
|
198
|
+
units: km
|
|
199
|
+
central_body: body the satellite is orbiting
|
|
200
|
+
period: orbital period in secs
|
|
201
|
+
period_steps: number of TimeSpan timestamps required to complete an orbit
|
|
202
|
+
semi_major: Nx1 numpy array of orbit semi-major axis calculated at that timestep
|
|
203
|
+
units: km
|
|
204
|
+
will be constant if no orbital maneauvers
|
|
205
|
+
ecc: Nx1 numpy array of orbit eccentricity calculated at that timestep
|
|
206
|
+
units: unitless
|
|
207
|
+
will be constant if no orbital maneauvers
|
|
208
|
+
inc: Nx1 numpy array of orbit inclination calculated at that timestep
|
|
209
|
+
units: degree
|
|
210
|
+
will be constant if no orbital maneauvers
|
|
211
|
+
raan: Nx1 numpy array of orbit RAAN calculated at that timestep
|
|
212
|
+
units: degree
|
|
213
|
+
will be constant if no orbital maneauvers
|
|
214
|
+
argp: Nx1 numpy array of orbit Arg Perigee calculated at that timestep
|
|
215
|
+
units: degree
|
|
216
|
+
will be constant if no orbital maneauvers
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(self, data:OrbitAttrDict, calc_astrobodies:bool=False):
|
|
220
|
+
"""The constructor should never be called directly.
|
|
221
|
+
|
|
222
|
+
Use one of:
|
|
223
|
+
Orbit.fromTLE()
|
|
224
|
+
Orbit.fromListOfPositions()
|
|
225
|
+
Orbit.fromPropagatedOrbitalParam()
|
|
226
|
+
Orbit.fromAnalyticalOrbitalParam()
|
|
227
|
+
"""
|
|
228
|
+
# Should always be called from a class method, spit error if not.
|
|
229
|
+
if not _validateOrbitAttrDict(data, OrbitAttrDict):
|
|
230
|
+
logger.error("Orbit() should not be called directly, "
|
|
231
|
+
"use one of the fromXX constructors.")
|
|
232
|
+
raise ValueError("Orbit() should not be called directly, "
|
|
233
|
+
"use one of the fromXX constructors")
|
|
234
|
+
|
|
235
|
+
self.name = data['name']
|
|
236
|
+
self.satcat_id = data['satcat_id']
|
|
237
|
+
self.gen_type = data['gen_type']
|
|
238
|
+
|
|
239
|
+
#time data
|
|
240
|
+
self.timespan = data['timespan']
|
|
241
|
+
self.TLE_epochs = data['TLE_epochs']
|
|
242
|
+
|
|
243
|
+
#pos data
|
|
244
|
+
self.pos = data['pos']
|
|
245
|
+
self.pos_ecef = data['pos_ecef']
|
|
246
|
+
self.vel_ecef = data['vel_ecef']
|
|
247
|
+
self.vel = data['vel']
|
|
248
|
+
self.lat = data['lat']
|
|
249
|
+
self.lon = data['lon']
|
|
250
|
+
self.alt = data['alt']
|
|
251
|
+
|
|
252
|
+
# These should be None from the constructor
|
|
253
|
+
self.sun_pos = data['sun_pos']
|
|
254
|
+
self.moon_pos = data['moon_pos']
|
|
255
|
+
self.eclipse = data['eclipse']
|
|
256
|
+
|
|
257
|
+
#orbit_data
|
|
258
|
+
self.central_body = data['central_body']
|
|
259
|
+
self.period = data['period']
|
|
260
|
+
self.period_steps = data['period_steps']
|
|
261
|
+
self.semi_major = data['semi_major']
|
|
262
|
+
self.ecc = data['ecc']
|
|
263
|
+
self.inc = data['inc']
|
|
264
|
+
self.raan = data['raan']
|
|
265
|
+
self.argp = data['argp']
|
|
266
|
+
|
|
267
|
+
# Check required fields are not empty
|
|
268
|
+
if self.timespan is None:
|
|
269
|
+
raise AttributeError('Error in creating Orbit object, no Timespan assigned')
|
|
270
|
+
if self.pos is None:
|
|
271
|
+
raise AttributeError('Error in creating Orbit object, no position data assigned')
|
|
272
|
+
if self.vel is None:
|
|
273
|
+
raise AttributeError('Error in creating Orbit object, no velocity data assigned')
|
|
274
|
+
if self.name is None:
|
|
275
|
+
raise AttributeError('Error in creating Orbit object, no name assigned')
|
|
276
|
+
if self.gen_type is None:
|
|
277
|
+
raise AttributeError('Error in creating Orbit object, no generator type assigned')
|
|
278
|
+
|
|
279
|
+
if calc_astrobodies:
|
|
280
|
+
logger.info('Creating ephemeris for Sun using timespan')
|
|
281
|
+
# Timescale for sun position calculation should use TDB, not UTC
|
|
282
|
+
# The resultant difference is likely very small
|
|
283
|
+
ephem_sun = Ephem.from_body(Sun,
|
|
284
|
+
astropyTime(self.timespan.asAstropy(scale='tdb')),
|
|
285
|
+
attractor=Earth)
|
|
286
|
+
sun_pos = ephem_sun.rv()[0]
|
|
287
|
+
self.sun_pos = np.asarray(sun_pos.to(astropy_units.km))
|
|
288
|
+
|
|
289
|
+
logger.info('Creating ephemeris for Moon using timespan')
|
|
290
|
+
ephem_moon = Ephem.from_body(Moon,
|
|
291
|
+
astropyTime(self.timespan.asAstropy(scale='tdb')),
|
|
292
|
+
attractor=Earth)
|
|
293
|
+
moon_pos = ephem_moon.rv()[0]
|
|
294
|
+
self.moon_pos = np.asarray(moon_pos.to(astropy_units.km))
|
|
295
|
+
self.eclipse = self._calcEclipse(self.pos, self.sun_pos)
|
|
296
|
+
|
|
297
|
+
#pos_list
|
|
298
|
+
@classmethod
|
|
299
|
+
def fromListOfPositions(cls, timespan:TimeSpan,
|
|
300
|
+
positions:np.ndarray[tuple[int, int], np.dtype[np.float64]],
|
|
301
|
+
astrobodies:bool=False) -> 'Orbit':
|
|
302
|
+
"""Create an orbit from a list of positions.
|
|
303
|
+
|
|
304
|
+
Creat an obit by explicitly specifying the position of the
|
|
305
|
+
satellite at each point in time. Useful for simplified test cases; but
|
|
306
|
+
may lead to unphysical orbits.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
timespan: Timespan over which orbit is to be simulated
|
|
310
|
+
positions: Nx3 numpy array of cartesian coordinates of the position of the satellite
|
|
311
|
+
at each timestamp
|
|
312
|
+
units: km
|
|
313
|
+
frame: ECI
|
|
314
|
+
astrobodies: [Optional] Flag to calculate Sun and Moon positions at timestamps
|
|
315
|
+
Default is False
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
satplot.Orbit
|
|
319
|
+
"""
|
|
320
|
+
attr_dct = _createEmptyOrbitAttrDict()
|
|
321
|
+
if len(positions) != len(timespan):
|
|
322
|
+
raise ValueError(f"Number of supplied positions does not match timespan length: "
|
|
323
|
+
f"{len(positions)} =/= {len(timespan)}")
|
|
324
|
+
|
|
325
|
+
attr_dct['timespan'] = timespan
|
|
326
|
+
attr_dct['pos'] = positions
|
|
327
|
+
# Assume linear motion between each position at each timestep;
|
|
328
|
+
# Then assume it stops at the last timestep.
|
|
329
|
+
vel = positions[1:] - positions[:-1]
|
|
330
|
+
attr_dct['vel'] = np.concatenate((vel, np.array([[0, 0, 0]])))
|
|
331
|
+
attr_dct['name'] = 'Sat from position list'
|
|
332
|
+
attr_dct['gen_type'] = 'position list'
|
|
333
|
+
|
|
334
|
+
return cls(attr_dct, calc_astrobodies=astrobodies)
|
|
335
|
+
|
|
336
|
+
#TLE
|
|
337
|
+
@classmethod
|
|
338
|
+
def fromTLE(cls, timespan:TimeSpan,
|
|
339
|
+
tle_path:pathlib.Path,
|
|
340
|
+
astrobodies:bool=True,
|
|
341
|
+
unsafe:bool=False) -> 'Orbit':
|
|
342
|
+
"""Create an orbit from an existing TLE or a list of historical TLEs.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
timespan : TimeSpan over which orbit is to be simulated
|
|
346
|
+
tle_path : path to file containing TLEs for a satellite
|
|
347
|
+
astrobodies: [Optional] Flag to calculate Sun and Moon positions at timestamps
|
|
348
|
+
Default is False
|
|
349
|
+
unsafe: [Optional] Flag to ignore TLE usage more than 14 days either side of timestamps
|
|
350
|
+
Optional
|
|
351
|
+
Default is False
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
-------
|
|
355
|
+
satplot.Orbit
|
|
356
|
+
"""
|
|
357
|
+
# load all TLEs and create skyfield earth sats
|
|
358
|
+
skyfld_earth_sats = load.tle_file(f'{tle_path}')
|
|
359
|
+
|
|
360
|
+
# generate list of epochs for each TLE
|
|
361
|
+
epochs = np.asarray([a.epoch.utc_datetime() for a in skyfld_earth_sats])
|
|
362
|
+
|
|
363
|
+
# ensure no duplicate skyfld sats
|
|
364
|
+
_, unq_idxs = np.unique(epochs, return_index=True)
|
|
365
|
+
unq_skyfld_earth_sats = list(np.asarray(skyfld_earth_sats)[unq_idxs])
|
|
366
|
+
|
|
367
|
+
skyfld_earth_sats = unq_skyfld_earth_sats
|
|
368
|
+
tle_epoch_dates = [sat.epoch.utc_datetime() for sat in skyfld_earth_sats]
|
|
369
|
+
|
|
370
|
+
# check timespan is valid for provided TLEs
|
|
371
|
+
if timespan.start < tle_epoch_dates[0] - dt.timedelta(days=14):
|
|
372
|
+
logger.error("Timespan begins before provided TLEs (+14 days)")
|
|
373
|
+
if not unsafe:
|
|
374
|
+
raise exceptions.OutOfRangeError("Timespan begins before provided TLEs (+14 days)")
|
|
375
|
+
elif timespan.start > tle_epoch_dates[-1] + dt.timedelta(days=14):
|
|
376
|
+
logger.error("Timespan begins after provided TLEs (+14 days)")
|
|
377
|
+
if not unsafe:
|
|
378
|
+
raise exceptions.OutOfRangeError("Timespan begins after provided TLEs (+14 days)")
|
|
379
|
+
elif timespan.end > tle_epoch_dates[-1] + dt.timedelta(days=14):
|
|
380
|
+
logger.error("Timespan ends after provided TLEs (+14 days)")
|
|
381
|
+
if not unsafe:
|
|
382
|
+
raise exceptions.OutOfRangeError("Timespan ends after provided TLEs (+14 days)")
|
|
383
|
+
|
|
384
|
+
attr_dct = _createEmptyOrbitAttrDict()
|
|
385
|
+
|
|
386
|
+
_timespan_arr = timespan.asDatetime()
|
|
387
|
+
_timespan_arr = cast("np.ndarray[tuple[int], np.dtype[np.datetime64]]", _timespan_arr)
|
|
388
|
+
closest_tle_epochs = epoch_u.findClosestDatetimeIndices(_timespan_arr,
|
|
389
|
+
np.asarray(tle_epoch_dates))
|
|
390
|
+
|
|
391
|
+
d = np.hstack((1, np.diff(closest_tle_epochs)))
|
|
392
|
+
|
|
393
|
+
# timestep idx where TLE epoch changes, append len(timespan)
|
|
394
|
+
# to not require separate loop iteration to handle last element
|
|
395
|
+
timespan_epoch_trans_idxs = np.hstack((np.where(d!=0)[0],len(timespan)))
|
|
396
|
+
# tle_dates idx where TLE epoch changes in timespan
|
|
397
|
+
timespan_epoch_trans_tle_idxs = closest_tle_epochs[np.where(d!=0)[0]]
|
|
398
|
+
|
|
399
|
+
tle_epoch_idxs = []
|
|
400
|
+
sub_timespans = []
|
|
401
|
+
for ii, trans_start_idx in enumerate(timespan_epoch_trans_idxs[:-1]):
|
|
402
|
+
tle_epoch_idxs.append(timespan_epoch_trans_tle_idxs[ii])
|
|
403
|
+
sub_timespans.append(timespan[trans_start_idx,timespan_epoch_trans_idxs[ii+1]])
|
|
404
|
+
|
|
405
|
+
skyfld_ts = load.timescale(builtin=True)
|
|
406
|
+
|
|
407
|
+
# Calculate data for first run
|
|
408
|
+
sub_timespan = sub_timespans[0]
|
|
409
|
+
# help out static type analysis (won't be None), no runtime effect
|
|
410
|
+
sub_timespan = cast("np.ndarray[tuple[int], np.dtype[np.datetime64]]",sub_timespan)
|
|
411
|
+
sub_skyfld_earthsat = skyfld_earth_sats[tle_epoch_idxs[0]]
|
|
412
|
+
tle_epoch = tle_epoch_dates[tle_epoch_idxs[0]]
|
|
413
|
+
sat_rec = sub_skyfld_earthsat.at(skyfld_ts.utc(sub_timespan))
|
|
414
|
+
pos = sat_rec.position.km.T
|
|
415
|
+
vel = sat_rec.velocity.km_per_s.T * 1000
|
|
416
|
+
ecef = sat_rec.frame_xyz_and_velocity(itrs)
|
|
417
|
+
pos_ecef = ecef[0].km.T
|
|
418
|
+
vel_ecef = ecef[1].km_per_s.T * 1000
|
|
419
|
+
|
|
420
|
+
skyfld_lat, skyfld_lon = wgs84.latlon_of(sat_rec)
|
|
421
|
+
lat = skyfld_lat.degrees
|
|
422
|
+
lon = skyfld_lon.degrees
|
|
423
|
+
ecc = np.tile(sub_skyfld_earthsat.model.ecco,len(sub_timespan))
|
|
424
|
+
inc = np.tile(sub_skyfld_earthsat.model.inclo,len(sub_timespan))
|
|
425
|
+
semi_major = np.tile(sub_skyfld_earthsat.model.a * consts.R_EARTH,len(sub_timespan))
|
|
426
|
+
raan = np.tile(sub_skyfld_earthsat.model.nodeo,len(sub_timespan))
|
|
427
|
+
argp = np.tile(sub_skyfld_earthsat.model.argpo,len(sub_timespan))
|
|
428
|
+
TLE_epochs = np.tile(tle_epoch,len(sub_timespan)) #noqa: N806
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# fill in data for any other sub timespans
|
|
432
|
+
for ii in range(1,len(sub_timespans)):
|
|
433
|
+
sub_timespan = sub_timespans[ii]
|
|
434
|
+
# help out static type analysis (won't be None), no runtime effect
|
|
435
|
+
sub_timespan = cast("np.ndarray[tuple[int], np.dtype[np.datetime64]]",sub_timespan)
|
|
436
|
+
sub_skyfld_earthsat = skyfld_earth_sats[tle_epoch_idxs[ii]]
|
|
437
|
+
tle_epoch = tle_epoch_dates[tle_epoch_idxs[ii]]
|
|
438
|
+
sat_rec = sub_skyfld_earthsat.at(skyfld_ts.utc(sub_timespan))
|
|
439
|
+
pos = np.vstack((pos, sat_rec.position.km.T))
|
|
440
|
+
vel = np.vstack((vel, sat_rec.velocity.km_per_s.T * 1000))
|
|
441
|
+
ecef = sat_rec.frame_xyz_and_velocity(itrs)
|
|
442
|
+
pos_ecef = np.vstack((pos_ecef, ecef[0].km.T))
|
|
443
|
+
vel_ecef = np.vstack((vel_ecef, ecef[1].km_per_s.T * 1000))
|
|
444
|
+
|
|
445
|
+
skyfld_lat, skyfld_lon = wgs84.latlon_of(sat_rec)
|
|
446
|
+
lat = np.concatenate((lat, skyfld_lat.degrees))
|
|
447
|
+
lon = np.concatenate((lon, skyfld_lon.degrees))
|
|
448
|
+
ecc = np.concatenate((ecc, np.tile(sub_skyfld_earthsat.model.ecco, len(sub_timespan))))
|
|
449
|
+
inc = np.concatenate((inc,
|
|
450
|
+
np.tile(sub_skyfld_earthsat.model.inclo, len(sub_timespan))))
|
|
451
|
+
semi_major = np.concatenate((semi_major,
|
|
452
|
+
np.tile(sub_skyfld_earthsat.model.a * consts.R_EARTH, len(sub_timespan)))) #noqa:E501
|
|
453
|
+
raan = np.concatenate((raan,
|
|
454
|
+
np.tile(sub_skyfld_earthsat.model.nodeo, len(sub_timespan))))
|
|
455
|
+
argp = np.concatenate((argp,
|
|
456
|
+
np.tile(sub_skyfld_earthsat.model.argpo, len(sub_timespan))))
|
|
457
|
+
TLE_epochs = np.concatenate((TLE_epochs, np.tile(tle_epoch, len(sub_timespan)))) #noqa: N806
|
|
458
|
+
|
|
459
|
+
attr_dct['timespan'] = timespan
|
|
460
|
+
attr_dct['satcat_id'] = int(skyfld_earth_sats[0].target_name.split('#')[-1].split(' ')[0])
|
|
461
|
+
attr_dct['gen_type'] = 'propagated from TLE'
|
|
462
|
+
attr_dct['central_body'] = 'Earth'
|
|
463
|
+
attr_dct['pos'] = pos
|
|
464
|
+
attr_dct['alt'] = np.linalg.norm(pos, axis=1) - consts.R_EARTH
|
|
465
|
+
attr_dct['pos_ecef'] = pos_ecef
|
|
466
|
+
attr_dct['vel_ecef'] = vel_ecef
|
|
467
|
+
attr_dct['vel'] = vel
|
|
468
|
+
attr_dct['lat'] = lat
|
|
469
|
+
attr_dct['lon'] = lon
|
|
470
|
+
attr_dct['ecc'] = ecc
|
|
471
|
+
attr_dct['inc'] = inc
|
|
472
|
+
attr_dct['semi_major'] = semi_major
|
|
473
|
+
attr_dct['raan'] = raan
|
|
474
|
+
attr_dct['argp'] = argp
|
|
475
|
+
attr_dct['TLE_epochs'] = TLE_epochs
|
|
476
|
+
|
|
477
|
+
period = float(2 * np.pi / sub_skyfld_earthsat.model.no_kozai * 60)
|
|
478
|
+
attr_dct['period'] = period
|
|
479
|
+
|
|
480
|
+
if timespan.time_step is not None:
|
|
481
|
+
attr_dct['period_steps'] = int(period / timespan.time_step.total_seconds())
|
|
482
|
+
else:
|
|
483
|
+
attr_dct['period_steps'] = None
|
|
484
|
+
attr_dct['name'] = sub_skyfld_earthsat.name
|
|
485
|
+
|
|
486
|
+
return cls(attr_dct, calc_astrobodies=astrobodies)
|
|
487
|
+
|
|
488
|
+
#fake TLE
|
|
489
|
+
@classmethod
|
|
490
|
+
def fromPropagatedOrbitalParam(cls, timespan:TimeSpan, #noqa: PLR0913
|
|
491
|
+
a:float=6978,
|
|
492
|
+
ecc:float=0,
|
|
493
|
+
inc:float=0,
|
|
494
|
+
raan:float=0,
|
|
495
|
+
argp:float=0,
|
|
496
|
+
mean_nu:float=0,
|
|
497
|
+
name:str='Fake TLE',
|
|
498
|
+
astrobodies:bool=True,
|
|
499
|
+
unsafe:bool=False) -> 'Orbit':
|
|
500
|
+
"""Create an orbit from orbital parameters, propagated using sgp4.
|
|
501
|
+
|
|
502
|
+
Orbits created using this class method will respect gravity corrections such as J4,
|
|
503
|
+
allowing for semi-analytical sun-synchronous orbits.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
timespan: Timespan over which orbit is to be simulated
|
|
507
|
+
a: [Optional] semi-major axis of the orbit in km
|
|
508
|
+
Default is 6978 ~ 600km above the earth.
|
|
509
|
+
ecc: [Optional] eccentricty, dimensionless number 0 < ecc < 1
|
|
510
|
+
Default is 0, which is a circular orbit
|
|
511
|
+
inc: [Optional] inclination of orbit in degrees
|
|
512
|
+
Default is 0, which represents an orbit around the Earth's equator
|
|
513
|
+
raan: [Optional] right-ascension of the ascending node
|
|
514
|
+
Default is 0
|
|
515
|
+
argp: [Optional] argument of the perigee in degrees
|
|
516
|
+
Default is 0, which represents an orbit with its semimajor axis in
|
|
517
|
+
the plane of the Earth's equator
|
|
518
|
+
mean_nu: [Optional] mean anomaly in degrees
|
|
519
|
+
Default is 0, which represents an orbit that is beginning at periapsis
|
|
520
|
+
name: [Optional] string giving the name of the orbit
|
|
521
|
+
Default is 'Fake TLE'
|
|
522
|
+
astrobodies: [Optional] Flag to calculate Sun and Moon positions at timestamps
|
|
523
|
+
Default is False
|
|
524
|
+
unsafe: [Optional] Flag to ignore semi-major axis inside Earth's radius
|
|
525
|
+
Default is False
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
satplot.Orbit
|
|
529
|
+
"""
|
|
530
|
+
if a < consts.R_EARTH + consts.EARTH_MIN_ALT:
|
|
531
|
+
logger.error("Semimajor axis, %s, is too close to Earth", a)
|
|
532
|
+
if not unsafe:
|
|
533
|
+
raise exceptions.OutOfRangeError(f"Semimajor axis, {a}, is too close to Earth")
|
|
534
|
+
|
|
535
|
+
if ecc > 1 or ecc < 0:
|
|
536
|
+
logger.error("Eccentricity, %s, is non circular or eliptical", ecc)
|
|
537
|
+
raise exceptions.OutOfRangeError(f"Eccentricity, {ecc}, is non circular or eliptical")
|
|
538
|
+
|
|
539
|
+
if inc > 180 or inc < -180: #noqa: PLR2004
|
|
540
|
+
logger.error("Inclination, %s, is out of range, should be -180 < inc < 180", inc)
|
|
541
|
+
raise exceptions.OutOfRangeError(f"Inclination, {inc}, is out of range, "
|
|
542
|
+
f"should be -180 < inc < 180")
|
|
543
|
+
|
|
544
|
+
if raan > 360 or raan < 0: #noqa: PLR2004
|
|
545
|
+
logger.error("RAAN, %s, is out of range, should be 0 < inc < 360", raan)
|
|
546
|
+
raise exceptions.OutOfRangeError(f"RAAN, {raan}, is out of range, "
|
|
547
|
+
f"should be 0 < inc < 360")
|
|
548
|
+
|
|
549
|
+
if argp > 360 or argp < 0: #noqa: PLR2004
|
|
550
|
+
logger.error("Argument of periapsis, %s, is out of range, "
|
|
551
|
+
"should be 0 < argp < 360", inc)
|
|
552
|
+
raise exceptions.OutOfRangeError(f"Argument of periapsis, {inc}, is out of range, "
|
|
553
|
+
f"should be 0 < argp < 360")
|
|
554
|
+
|
|
555
|
+
if mean_nu > 360 or mean_nu < 0: #noqa: PLR2004
|
|
556
|
+
logger.error("Mean anomaly, %s, is out of range, should be 0 < mean_nu < 360", mean_nu)
|
|
557
|
+
raise exceptions.OutOfRangeError(f"Mean anomaly, {mean_nu}, is out of range, "
|
|
558
|
+
f"should be 0 < mean_nu < 360")
|
|
559
|
+
|
|
560
|
+
attr_dct = _createEmptyOrbitAttrDict()
|
|
561
|
+
|
|
562
|
+
skyfld_ts = load.timescale(builtin=True)
|
|
563
|
+
|
|
564
|
+
t0_epoch = epoch_u.datetime2sgp4epoch(timespan.start)
|
|
565
|
+
satrec = Satrec()
|
|
566
|
+
mean_motion = orbit_u.calcMeanMotion(a * 1e3)
|
|
567
|
+
|
|
568
|
+
satrec.sgp4init(WGS72, # gravity model
|
|
569
|
+
'i', # mode
|
|
570
|
+
1, # satnum
|
|
571
|
+
t0_epoch, # mode
|
|
572
|
+
0, # bstar [/earth radii]
|
|
573
|
+
0, # ndot [revs/day]
|
|
574
|
+
0, # nddot [revs/day^3]
|
|
575
|
+
ecc, # ecc
|
|
576
|
+
argp, # arg perigee [radians]
|
|
577
|
+
inc, # inclination [radians]
|
|
578
|
+
mean_nu, # mean anomaly [radians]
|
|
579
|
+
mean_motion * 60, # mean motion [rad/min]
|
|
580
|
+
raan) # raan [radians]
|
|
581
|
+
|
|
582
|
+
skyfld_earthsat = EarthSatellite.from_satrec(satrec, skyfld_ts)
|
|
583
|
+
|
|
584
|
+
pos = np.empty((len(timespan), 3))
|
|
585
|
+
vel = np.empty((len(timespan), 3))
|
|
586
|
+
|
|
587
|
+
_timespan_arr = timespan.asDatetime()
|
|
588
|
+
_timespan_arr = cast("np.ndarray[tuple[int], np.dtype[np.datetime64]]", _timespan_arr)
|
|
589
|
+
for ii, timestep in enumerate(_timespan_arr):
|
|
590
|
+
pos[ii, :] = skyfld_earthsat.at(skyfld_ts.utc(timestep)).position.km
|
|
591
|
+
vel[ii, :] = skyfld_earthsat.at(skyfld_ts.utc(timestep)).velocity.km_per_s * 1000
|
|
592
|
+
|
|
593
|
+
# TODO: calculate ecef and lat,lon values.
|
|
594
|
+
# TODO: satrec doesn't have a frame_xyz_and_velocity in this case
|
|
595
|
+
|
|
596
|
+
period = float(2 * np.pi / skyfld_earthsat.model.no_kozai * 60)
|
|
597
|
+
if timespan.time_step is not None:
|
|
598
|
+
period_steps = int(period / timespan.time_step.total_seconds())
|
|
599
|
+
else:
|
|
600
|
+
period_steps = None
|
|
601
|
+
|
|
602
|
+
attr_dct['name'] = name
|
|
603
|
+
attr_dct['timespan'] = timespan
|
|
604
|
+
attr_dct['gen_type'] = 'propagated from orbital param'
|
|
605
|
+
attr_dct['central_body'] = 'Earth'
|
|
606
|
+
attr_dct['pos'] = pos
|
|
607
|
+
attr_dct['alt'] = np.linalg.norm(pos, axis=1) - consts.R_EARTH
|
|
608
|
+
attr_dct['vel'] = vel
|
|
609
|
+
attr_dct['ecc'] = np.full(len(timespan), ecc)
|
|
610
|
+
attr_dct['inc'] = np.full(len(timespan), inc)
|
|
611
|
+
attr_dct['semi_major'] = np.full(len(timespan), a)
|
|
612
|
+
attr_dct['raan'] = np.full(len(timespan), raan)
|
|
613
|
+
attr_dct['argp'] = np.full(len(timespan), argp)
|
|
614
|
+
attr_dct['TLE_epochs'] = np.full(len(timespan),timespan.start)
|
|
615
|
+
attr_dct['period'] = period
|
|
616
|
+
attr_dct['period_steps'] = period_steps
|
|
617
|
+
|
|
618
|
+
return cls(attr_dct, calc_astrobodies=astrobodies)
|
|
619
|
+
|
|
620
|
+
#analytical
|
|
621
|
+
@classmethod
|
|
622
|
+
def fromAnalyticalOrbitalParam(cls, timespan:TimeSpan, #noqa: C901, PLR0912, PLR0913
|
|
623
|
+
body:str='Earth',
|
|
624
|
+
a:float=6978,
|
|
625
|
+
ecc:float=0,
|
|
626
|
+
inc:float=0,
|
|
627
|
+
raan:float=0,
|
|
628
|
+
argp:float=0,
|
|
629
|
+
mean_nu:float=0,
|
|
630
|
+
name:str='Analytical',
|
|
631
|
+
astrobodies:bool=True,
|
|
632
|
+
unsafe:bool=False) -> 'Orbit':
|
|
633
|
+
"""Create an analytical orbit defined by orbital parameters.
|
|
634
|
+
|
|
635
|
+
Orbits created using this class method will NOT respect gravity corrections such as J4,
|
|
636
|
+
and as such sun-synchronous orbit are not possible.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
timespan: Timespan over which orbit is to be simulated
|
|
640
|
+
body: [Optional] string indicating around what body the satellite orbits,
|
|
641
|
+
Default is 'Earth'
|
|
642
|
+
Options are ['Earth','Sun','Mars','Moon']
|
|
643
|
+
a: [Optional] semi-major axis of the orbit in km
|
|
644
|
+
Default is 6978 ~ 600km above the earth.
|
|
645
|
+
ecc: [Optional] eccentricty, dimensionless number 0 < ecc < 1
|
|
646
|
+
Default is 0, which is a circular orbit
|
|
647
|
+
inc: [Optional] inclination of orbit in degrees
|
|
648
|
+
Default is 0, which represents an orbit around the Earth's equator
|
|
649
|
+
raan: [Optional] right-ascension of the ascending node
|
|
650
|
+
Default is 0
|
|
651
|
+
argp: [Optional] argument of the perigee in degrees
|
|
652
|
+
Default is 0, which represents an orbit with its semimajor axis in
|
|
653
|
+
the plane of the Earth's equator
|
|
654
|
+
mean_nu: [Optional] mean anomaly in degrees
|
|
655
|
+
Default is 0, which represents an orbit that is beginning at periapsis
|
|
656
|
+
name: [Optional] string giving the name of the orbit
|
|
657
|
+
Default is 'Analytical'
|
|
658
|
+
astrobodies: [Optional] Flag to calculate Sun and Moon positions at timestamps
|
|
659
|
+
Default is False
|
|
660
|
+
unsafe: [Optional] Flag to ignore semi-major axis inside Earth's radius
|
|
661
|
+
Default is False
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
satplot.Orbit
|
|
665
|
+
"""
|
|
666
|
+
if body.upper() == 'EARTH':
|
|
667
|
+
central_body = Earth
|
|
668
|
+
min_a = consts.R_EARTH + consts.EARTH_MIN_ALT
|
|
669
|
+
elif body.upper() == 'SUN':
|
|
670
|
+
central_body = Sun
|
|
671
|
+
min_a = consts.R_SUN + consts.SUN_MIN_ALT
|
|
672
|
+
elif body.upper() == 'MARS':
|
|
673
|
+
central_body = Mars
|
|
674
|
+
min_a = consts.R_MARS + consts.MARS_MIN_ALT
|
|
675
|
+
elif body.upper() == 'MOON':
|
|
676
|
+
central_body = Moon
|
|
677
|
+
min_a = consts.R_MOON + consts.MOON_MIN_ALT
|
|
678
|
+
else:
|
|
679
|
+
logger.error("Invalid central body %s. Valid options are Earth, Sun, Mars, Moon", body)
|
|
680
|
+
raise ValueError(f"Invalid central body {body}.")
|
|
681
|
+
|
|
682
|
+
if a < min_a:
|
|
683
|
+
logger.error("Semimajor axis, %s, is too close to the central body, {body.upper()}", a)
|
|
684
|
+
if not unsafe:
|
|
685
|
+
raise exceptions.OutOfRangeError(f"Semimajor axis, {a}, is too close "
|
|
686
|
+
f"to the central body, {body.upper()}")
|
|
687
|
+
|
|
688
|
+
if ecc > 1 or ecc < 0:
|
|
689
|
+
logger.error("Eccentricity, %s, is non circular or eliptical", ecc)
|
|
690
|
+
raise exceptions.OutOfRangeError(f"Eccentricity, {ecc}, is non circular or eliptical")
|
|
691
|
+
|
|
692
|
+
if inc > 180 or inc < -180: #noqa: PLR2004
|
|
693
|
+
logger.error("Inclination, %s, is out of range, should be -180 < inc < 180", inc)
|
|
694
|
+
raise exceptions.OutOfRangeError(f"Inclination, {inc}, is out of range, "
|
|
695
|
+
f"should be -180 < inc < 180")
|
|
696
|
+
|
|
697
|
+
if raan > 360 or raan < 0: #noqa: PLR2004
|
|
698
|
+
logger.error("RAAN, %s, is out of range, should be 0 < inc < 360", inc)
|
|
699
|
+
raise exceptions.OutOfRangeError(f"RAAN, {inc}, is out of range, "
|
|
700
|
+
f"should be 0 < inc < 360")
|
|
701
|
+
|
|
702
|
+
if argp > 360 or argp < 0: #noqa: PLR2004
|
|
703
|
+
logger.error("Argument of periapsis, %s, is out of range, "
|
|
704
|
+
"should be 0 < argp < 360", raan)
|
|
705
|
+
raise exceptions.OutOfRangeError(f"Argument of periapsis, {raan}, is out of "
|
|
706
|
+
f"range, should be 0 < argp < 360")
|
|
707
|
+
|
|
708
|
+
if mean_nu > 360 or mean_nu < 0: #noqa: PLR2004
|
|
709
|
+
logger.error("Mean anomaly, %s, is out of range, should be 0 < mean_nu < 360", mean_nu)
|
|
710
|
+
raise exceptions.OutOfRangeError(f"Mean anomaly, {mean_nu}, is out of range, "
|
|
711
|
+
f"should be 0 < mean_nu < 360")
|
|
712
|
+
|
|
713
|
+
attr_dct = _createEmptyOrbitAttrDict()
|
|
714
|
+
|
|
715
|
+
logger.info("Creating analytical orbit")
|
|
716
|
+
orb = hapsiraOrbit.from_classical(central_body,
|
|
717
|
+
a * astropy_units.one * astropy_units.km,
|
|
718
|
+
ecc * astropy_units.one,
|
|
719
|
+
inc * astropy_units.one * astropy_units.deg,
|
|
720
|
+
raan * astropy_units.one * astropy_units.deg,
|
|
721
|
+
argp * astropy_units.one * astropy_units.deg,
|
|
722
|
+
mean_nu * astropy_units.one * astropy_units.deg)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
logger.info("Creating ephemeris for orbit, using timespan")
|
|
726
|
+
ephem = Ephem.from_orbit(orb, timespan.asAstropy())
|
|
727
|
+
|
|
728
|
+
pos = np.asarray(ephem.rv()[0], dtype=np.float64)
|
|
729
|
+
vel = np.asarray(ephem.rv()[1], dtype=np.float64) * 1000
|
|
730
|
+
|
|
731
|
+
period = float(orb.period.unit.in_units('s') * orb.period.value)
|
|
732
|
+
if timespan.time_step is not None:
|
|
733
|
+
period_steps = int(period / timespan.time_step.total_seconds())
|
|
734
|
+
else:
|
|
735
|
+
period_steps = None
|
|
736
|
+
|
|
737
|
+
attr_dct['name'] = name
|
|
738
|
+
attr_dct['timespan'] = timespan
|
|
739
|
+
attr_dct['gen_type'] = 'analytical orbit'
|
|
740
|
+
attr_dct['central_body'] = body
|
|
741
|
+
attr_dct['pos'] = pos
|
|
742
|
+
# TODO: altitude should be sourced from central body
|
|
743
|
+
attr_dct['alt'] = np.linalg.norm(pos, axis=1) - consts.R_EARTH
|
|
744
|
+
attr_dct['vel'] = vel
|
|
745
|
+
attr_dct['ecc'] = ecc * np.ones(len(timespan))
|
|
746
|
+
attr_dct['inc'] = inc * np.ones(len(timespan))
|
|
747
|
+
attr_dct['semi_major'] = a * np.ones(len(timespan))
|
|
748
|
+
attr_dct['raan'] = raan * np.ones(len(timespan))
|
|
749
|
+
attr_dct['argp'] = argp * np.ones(len(timespan))
|
|
750
|
+
attr_dct['period'] = period
|
|
751
|
+
attr_dct['period_steps'] = period_steps
|
|
752
|
+
|
|
753
|
+
return cls(attr_dct, calc_astrobodies=astrobodies)
|
|
754
|
+
|
|
755
|
+
@classmethod
|
|
756
|
+
def fromDummyConstantPosition(cls, timespan:TimeSpan,
|
|
757
|
+
pos:tuple[float,float,float]|np.ndarray[tuple[int],np.dtype[np.float64]],
|
|
758
|
+
sun_pos:tuple[float,float,float]|np.ndarray[tuple[int],np.dtype[np.float64]]|None=None,
|
|
759
|
+
moon_pos:tuple[float,float,float]|np.ndarray[tuple[int],np.dtype[np.float64]]|None=None) -> 'Orbit': #noqa: E501
|
|
760
|
+
"""Creates an static orbit for testing.
|
|
761
|
+
|
|
762
|
+
Satellite position is defined by pos, while sun and moon positions are optional,
|
|
763
|
+
but can also be specified.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
timespan: Timespan over which orbit is to be simulated
|
|
767
|
+
pos: Nx3 numpy array of cartesian coordinates of the position of the satellite
|
|
768
|
+
at each timestamp
|
|
769
|
+
units: km
|
|
770
|
+
frame: ECI
|
|
771
|
+
sun_pos: [Optional] Nx3 numpy array of cartesian coordinates of the position of the Sun
|
|
772
|
+
at each timestamp
|
|
773
|
+
units: km
|
|
774
|
+
frame: ECI
|
|
775
|
+
moon_pos: [Optional] Nx3 numpy array of cartesian coordinates of the position of the
|
|
776
|
+
Moon at each timestamp
|
|
777
|
+
units: km
|
|
778
|
+
frame: ECI
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
satplot.Orbit
|
|
782
|
+
"""
|
|
783
|
+
attr_dct = _createEmptyOrbitAttrDict()
|
|
784
|
+
attr_dct['timespan'] = timespan
|
|
785
|
+
attr_dct['name'] = 'dummy_const_pos'
|
|
786
|
+
attr_dct['pos'] = np.full((len(timespan), 3), pos)
|
|
787
|
+
attr_dct['vel'] = np.full((len(timespan), 3), 0)
|
|
788
|
+
attr_dct['gen_type'] = 'constant position for testing'
|
|
789
|
+
obj = cls(attr_dct, calc_astrobodies=False)
|
|
790
|
+
if sun_pos is not None:
|
|
791
|
+
obj.sun_pos = np.full((len(timespan), 3), sun_pos)
|
|
792
|
+
if moon_pos is not None:
|
|
793
|
+
obj.moon_pos = np.full((len(timespan), 3), moon_pos)
|
|
794
|
+
return obj
|
|
795
|
+
|
|
796
|
+
def getPosition(self, search_time:dt.datetime|astropyTime) \
|
|
797
|
+
-> np.ndarray[tuple[int],np.dtype[np.float64]]:
|
|
798
|
+
"""Return the position at the specified or closest time.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
search_time: the timestamp to search for
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
position
|
|
805
|
+
|
|
806
|
+
Raises:
|
|
807
|
+
ValueError: orbit has no pos data
|
|
808
|
+
"""
|
|
809
|
+
if self.pos is None:
|
|
810
|
+
raise ValueError("Orbit has no pos data")
|
|
811
|
+
return self._getAttributeClosestTime(self.pos, search_time)
|
|
812
|
+
|
|
813
|
+
def getVelocity(self, search_time:dt.datetime|astropyTime) \
|
|
814
|
+
-> np.ndarray[tuple[int], np.dtype[np.float64]]:
|
|
815
|
+
"""Return the velocity at the specified or closest time.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
search_time: the timestamp to search for
|
|
819
|
+
|
|
820
|
+
Returns:
|
|
821
|
+
velocity
|
|
822
|
+
|
|
823
|
+
Raises:
|
|
824
|
+
ValueError: orbit has no vel data
|
|
825
|
+
"""
|
|
826
|
+
if self.vel is None:
|
|
827
|
+
raise ValueError("Orbit has no vel data")
|
|
828
|
+
return self._getAttributeClosestTime(self.vel, search_time)
|
|
829
|
+
|
|
830
|
+
def _getAttributeClosestTime(self, attr:np.ndarray[Any, np.dtype[np.float64]],
|
|
831
|
+
search_time:dt.datetime|astropyTime)\
|
|
832
|
+
-> np.ndarray[tuple[int], np.dtype[np.float64]]:
|
|
833
|
+
|
|
834
|
+
if self.timespan is None:
|
|
835
|
+
raise ValueError("Orbit has no timespan")
|
|
836
|
+
|
|
837
|
+
if isinstance(search_time, dt.datetime):
|
|
838
|
+
_, closest_idx = self.timespan.getClosest(search_time)
|
|
839
|
+
elif isinstance(search_time, astropyTime):
|
|
840
|
+
search_dt = search_time.to_datetime()
|
|
841
|
+
_, closest_idx = self.timespan.getClosest(search_dt)
|
|
842
|
+
else:
|
|
843
|
+
logger.error("%s cannot be used to index %s", search_time, attr)
|
|
844
|
+
raise TypeError(f"{search_time} cannot be used to index {attr}")
|
|
845
|
+
|
|
846
|
+
return attr[closest_idx, :]
|
|
847
|
+
|
|
848
|
+
def _calcEclipse(self, pos:np.ndarray[tuple[int,int],np.dtype[np.float64]],
|
|
849
|
+
sun:np.ndarray[tuple[int,int],np.dtype[np.float64]])\
|
|
850
|
+
-> np.ndarray[tuple[int],np.dtype[np.bool_]]:
|
|
851
|
+
earth_ang_size = np.arctan(consts.R_EARTH/consts.AU)
|
|
852
|
+
neg_sun_norm = np.linalg.norm(-sun,axis=1)
|
|
853
|
+
neg_sun_unit = -sun/neg_sun_norm[:,None]
|
|
854
|
+
pos_neg_sun = pos-sun
|
|
855
|
+
pos_neg_sun_norm = np.linalg.norm(pos_neg_sun,axis=1)
|
|
856
|
+
pos_neg_sun_unit = pos_neg_sun/pos_neg_sun_norm[:,None]
|
|
857
|
+
earth_sat_ang_sep = np.arccos(np.sum(neg_sun_unit*pos_neg_sun_unit, axis=1))
|
|
858
|
+
sunlit_angsep_truth = earth_sat_ang_sep > earth_ang_size
|
|
859
|
+
sunlit_dist_truth = pos_neg_sun_norm < neg_sun_norm
|
|
860
|
+
sunlit = np.logical_or(sunlit_angsep_truth, sunlit_dist_truth)
|
|
861
|
+
|
|
862
|
+
return np.logical_not(sunlit)
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
# def _findClosestEpochIndices(target:list[float], values:float) -> list[int]:
|
|
867
|
+
# right_idx = np.searchsorted(target, values)
|
|
868
|
+
# left_idx = right_idx - 1
|
|
869
|
+
# # replace any idx greater than len of target with last idx in target
|
|
870
|
+
# right_idx[np.where(right_idx==len(target))] = len(target) - 1
|
|
871
|
+
# stacked_idx = np.hstack((left_idx.reshape(-1,1),right_idx.reshape(-1,1)))
|
|
872
|
+
# target_vals_at_idxs = target[stacked_idx]
|
|
873
|
+
# closest_idx_columns = np.argmin(np.abs(target_vals_at_idxs - values.reshape(-1,1)),axis=1)
|
|
874
|
+
# return stacked_idx[range(len(stacked_idx)),closest_idx_columns]
|