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
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
import jax.numpy as jnp
|
|
2
|
+
from jax import jit, vmap, jacrev, config, Array
|
|
3
|
+
from tabsim.jax.utils import jit_with_doc
|
|
4
|
+
|
|
5
|
+
config.update("jax_enable_x64", True)
|
|
6
|
+
|
|
7
|
+
# Constants
|
|
8
|
+
G = 6.67408e-11 # Gravitational constant in m^3/kg/s^2
|
|
9
|
+
M_e = 5.9722e24 # Mass of the Earth in kilograms
|
|
10
|
+
R_e = 6.371e6 # Average radius of the Earth in metres
|
|
11
|
+
T_s = 86164.0905 # Sidereal day in seconds
|
|
12
|
+
Omega_e = 2 * jnp.pi / T_s # Earth rotation rate in rad/s
|
|
13
|
+
C = 299792458.0 # Speed of light in m/s
|
|
14
|
+
DAY_SECS = 24 * 3600.0 # Seconds in a day
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def secs_to_days(seconds):
|
|
18
|
+
|
|
19
|
+
return seconds / DAY_SECS
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def days_to_secs(days):
|
|
23
|
+
|
|
24
|
+
return days * DAY_SECS
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@jit_with_doc
|
|
28
|
+
def radec_to_lmn(ra: Array, dec: Array, phase_centre: Array) -> Array:
|
|
29
|
+
"""
|
|
30
|
+
Convert right-ascension and declination positions of a set of sources to
|
|
31
|
+
direction cosines.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
ra : ndarray (n_src,)
|
|
36
|
+
Right-ascension in degrees.
|
|
37
|
+
dec : ndarray (n_src,)
|
|
38
|
+
Declination in degrees.
|
|
39
|
+
phase_centre : ndarray (2,)
|
|
40
|
+
The ra and dec coordinates of the phase centre in degrees.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
lmn : ndarray (n_src, 3)
|
|
45
|
+
The direction cosines, (l,m,n), coordinates of each source.
|
|
46
|
+
"""
|
|
47
|
+
ra = jnp.asarray(ra)
|
|
48
|
+
dec = jnp.asarray(dec)
|
|
49
|
+
phase_centre = jnp.asarray(phase_centre)
|
|
50
|
+
ra, dec = jnp.deg2rad(jnp.array([ra, dec]))
|
|
51
|
+
phase_centre = jnp.deg2rad(phase_centre)
|
|
52
|
+
|
|
53
|
+
delta_ra = ra - phase_centre[0]
|
|
54
|
+
dec_0 = phase_centre[1]
|
|
55
|
+
|
|
56
|
+
l = jnp.cos(dec) * jnp.sin(delta_ra)
|
|
57
|
+
m = jnp.sin(dec) * jnp.cos(dec_0) - jnp.cos(dec) * jnp.sin(dec_0) * jnp.cos(
|
|
58
|
+
delta_ra
|
|
59
|
+
)
|
|
60
|
+
n = jnp.sqrt(1 - l**2 - m**2)
|
|
61
|
+
|
|
62
|
+
return jnp.array([l, m, n]).T
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@jit_with_doc
|
|
66
|
+
def radec_to_XYZ(ra: Array, dec: Array) -> Array:
|
|
67
|
+
"""
|
|
68
|
+
Convert Right ascension and Declination to unit vector in ECI coordinates.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
ra : ndarray (n_src,)
|
|
73
|
+
Right-ascension in degrees.
|
|
74
|
+
dec : ndarray (n_src,)
|
|
75
|
+
Declination in degrees.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
xyz: ndarray (n_src, 3) or (3,)
|
|
80
|
+
The ECI coordinate unit vector of each source.
|
|
81
|
+
"""
|
|
82
|
+
ra = jnp.asarray(ra)
|
|
83
|
+
dec = jnp.asarray(dec)
|
|
84
|
+
ra, dec = jnp.deg2rad(jnp.array([ra, dec]))
|
|
85
|
+
x = jnp.cos(ra) * jnp.cos(dec)
|
|
86
|
+
y = jnp.sin(ra) * jnp.cos(dec)
|
|
87
|
+
z = jnp.sin(dec)
|
|
88
|
+
|
|
89
|
+
return jnp.array([x, y, z]).T
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# @jit_with_doc
|
|
93
|
+
# def radec_to_altaz(ra: float, dec: float, latitude: float, longitude: float, times: Array) -> Array:
|
|
94
|
+
# """
|
|
95
|
+
# !! Do not use this function - Needs to be checked !!
|
|
96
|
+
# Convert Right ascension and Declination to unit vector in ECI coordinates.
|
|
97
|
+
|
|
98
|
+
# Parameters
|
|
99
|
+
# ----------
|
|
100
|
+
# ra : float
|
|
101
|
+
# Right-ascension in degrees.
|
|
102
|
+
# dec : float
|
|
103
|
+
# Declination in degrees.
|
|
104
|
+
# latitude: float
|
|
105
|
+
# The latitude of the observer in degrees.
|
|
106
|
+
# times: ndarray (n_time,)
|
|
107
|
+
# The time of each position in seconds.
|
|
108
|
+
|
|
109
|
+
# Returns
|
|
110
|
+
# -------
|
|
111
|
+
# altaz: ndarray (n_time, 2)
|
|
112
|
+
# The altiude and azimuth of the source at each time.
|
|
113
|
+
# """
|
|
114
|
+
# ra, dec = jnp.deg2rad(jnp.asarray([ra, dec]))
|
|
115
|
+
# lat, lon = jnp.deg2rad(jnp.asarray([latitude, longitude]))
|
|
116
|
+
# times = jnp.asarray(times)
|
|
117
|
+
# gmst = 2.0 * jnp.pi * times / T_s
|
|
118
|
+
# ha = (gmst + lon - ra) % 2 * jnp.pi
|
|
119
|
+
|
|
120
|
+
# alt = jnp.arcsin(
|
|
121
|
+
# jnp.sin(dec) * jnp.sin(lat) + jnp.cos(dec) * jnp.cos(lat) * jnp.cos(ha)
|
|
122
|
+
# )
|
|
123
|
+
|
|
124
|
+
# az = jnp.arctan2(
|
|
125
|
+
# -jnp.sin(ha), jnp.cos(ha) * jnp.sin(lat) - jnp.tan(dec) * jnp.cos(lat)
|
|
126
|
+
# )
|
|
127
|
+
|
|
128
|
+
# # az = jnp.arccos(
|
|
129
|
+
# # (jnp.sin(dec) - jnp.sin(alt) * jnp.sin(lat)) / (jnp.cos(alt) * jnp.cos(lat))
|
|
130
|
+
# # )
|
|
131
|
+
# # az = jnp.where(ha < 0, 2 * jnp.pi - az, az)
|
|
132
|
+
|
|
133
|
+
# # az = jnp.arcsin(
|
|
134
|
+
# # jnp.sin(dec) * jnp.sin(lat) + jnp.cos(dec) * jnp.cos(lat) * jnp.cos(ha)
|
|
135
|
+
# # )
|
|
136
|
+
# # az = jnp.where(ha < jnp.pi, 2 * jnp.pi - az, az)
|
|
137
|
+
|
|
138
|
+
# return jnp.rad2deg(jnp.array([alt, az]).T)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@jit_with_doc
|
|
142
|
+
def ENU_to_GEO(geo_ref: Array, enu: Array) -> Array:
|
|
143
|
+
"""
|
|
144
|
+
Convert a set of points in ENU co-ordinates to geographic coordinates i.e.
|
|
145
|
+
(latitude, longitude, elevation).
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
geo_ref: ndarray (3,)
|
|
150
|
+
The latitude, longitude and elevation, (lat,lon,elevation), of the
|
|
151
|
+
reference position i.e. ENU = (0,0,0).
|
|
152
|
+
enu: ndarray (n_ants, 3)
|
|
153
|
+
The ENU coordinates of each antenna. (East, North, Up).
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
geo_ants: ndarray (n_ant, 3)
|
|
158
|
+
The geographic coordinates, (lat,lon,elevation), of each antenna.
|
|
159
|
+
"""
|
|
160
|
+
geo_ref = jnp.asarray(geo_ref)
|
|
161
|
+
enu = jnp.asarray(enu)
|
|
162
|
+
R_e = earth_radius(geo_ref[0])
|
|
163
|
+
d_lon = jnp.rad2deg(
|
|
164
|
+
jnp.arcsin(enu[:, 1] / (R_e * jnp.cos(jnp.deg2rad(geo_ref[0]))))
|
|
165
|
+
)
|
|
166
|
+
d_lat = jnp.rad2deg(jnp.arcsin(enu[:, 0] / R_e))
|
|
167
|
+
geo_ants = jnp.array(geo_ref)[None, :] + jnp.array([d_lat, d_lon, enu[:, -1]]).T
|
|
168
|
+
|
|
169
|
+
return geo_ants
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@jit_with_doc
|
|
173
|
+
def GEO_to_XYZ(geo: Array, times: Array) -> Array:
|
|
174
|
+
"""
|
|
175
|
+
Convert geographic coordinates to an Earth Centred Inertial (ECI)
|
|
176
|
+
coordinate frame. This is different to ECEF as ECI remains fixed with the
|
|
177
|
+
celestial sphere whereas ECEF coordinates rotate w.r.t. the celestial
|
|
178
|
+
sphere. (0,0,0) is the Earth's centre of mass, +z points to the North Pole
|
|
179
|
+
and +x is in the plane of the Equator passing through the Meridian at t = 0
|
|
180
|
+
and +y is also in the plane of the Equator and passes through 90 degrees
|
|
181
|
+
East at t = 0. ECEF and ECI are aligned when t % T_s = 0.
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
geo: ndarray (n_time, 3)
|
|
186
|
+
The geographic coordinates, (lat,lon,elevation), at each point in time.
|
|
187
|
+
times: ndarray (n_time,)
|
|
188
|
+
The time of each position in seconds.
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
xyz: ndarray (n_time, 3)
|
|
193
|
+
The ECI coordinates at each time, (lat,lon,elevation), of each antenna.
|
|
194
|
+
"""
|
|
195
|
+
times = jnp.asarray(times)
|
|
196
|
+
geo = jnp.asarray(geo) * jnp.ones((len(times), 3))
|
|
197
|
+
lat, lon, elevation = geo.T
|
|
198
|
+
R_e = earth_radius(lat)
|
|
199
|
+
r = R_e + elevation
|
|
200
|
+
lat = jnp.deg2rad(lat)
|
|
201
|
+
lon = jnp.deg2rad(lon)
|
|
202
|
+
omega = 2.0 * jnp.pi / T_s
|
|
203
|
+
|
|
204
|
+
x = r * jnp.cos(lon + (omega * times)) * jnp.cos(lat)
|
|
205
|
+
y = r * jnp.sin(lon + (omega * times)) * jnp.cos(lat)
|
|
206
|
+
z = r * jnp.sin(lat)
|
|
207
|
+
|
|
208
|
+
return jnp.array([x, y, z]).T
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@jit_with_doc
|
|
212
|
+
def GEO_to_XYZ_vmap0(geo: Array, times: Array) -> Array:
|
|
213
|
+
"""
|
|
214
|
+
Convert geographic coordinates to an Earth Centred Inertial (ECI)
|
|
215
|
+
coordinate frame. This is different to ECEF as ECI remains fixed with the
|
|
216
|
+
celestial sphere whereas ECEF coordinates rotate w.r.t. the celestial
|
|
217
|
+
sphere. (0,0,0) is the Earth's centre of mass, +z points to the North Pole
|
|
218
|
+
and +x is in the plane of the Equator passing through the Meridian at t = 0
|
|
219
|
+
and +y is also in the plane of the Equator and passes through 90 degrees
|
|
220
|
+
East at t = 0. ECEF and ECI are aligned when t % T_s = 0.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
geo: ndarray (n_src, n_time, 3)
|
|
225
|
+
The geographic coordinates, (lat,lon,elevation), at each point in time.
|
|
226
|
+
times: ndarray (n_time,)
|
|
227
|
+
The time of each position in seconds.
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
xyz: ndarray (n_src, n_time, 3)
|
|
232
|
+
The ECI coordinates at each time, (lat,lon,elevation), of each antenna.
|
|
233
|
+
"""
|
|
234
|
+
return vmap(GEO_to_XYZ, in_axes=(0, None), out_axes=0)(geo, times)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@jit_with_doc
|
|
238
|
+
def GEO_to_XYZ_vmap1(geo: Array, times: Array) -> Array:
|
|
239
|
+
"""
|
|
240
|
+
Convert geographic coordinates to an Earth Centred Inertial (ECI)
|
|
241
|
+
coordinate frame. This is different to ECEF as ECI remains fixed with the
|
|
242
|
+
celestial sphere whereas ECEF coordinates rotate w.r.t. the celestial
|
|
243
|
+
sphere. (0,0,0) is the Earth's centre of mass, +z points to the North Pole
|
|
244
|
+
and +x is in the plane of the Equator passing through the Meridian at t = 0
|
|
245
|
+
and +y is also in the plane of the Equator and passes through 90 degrees
|
|
246
|
+
East at t = 0. ECEF and ECI are aligned when t % T_s = 0.
|
|
247
|
+
|
|
248
|
+
Parameters
|
|
249
|
+
----------
|
|
250
|
+
geo: ndarray (n_time, n_ant, 3)
|
|
251
|
+
The geographic coordinates, (lat,lon,elevation), at each point in time.
|
|
252
|
+
times: ndarray (n_time,)
|
|
253
|
+
The time of each position in seconds.
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
-------
|
|
257
|
+
xyz: ndarray (n_time, n_ant, 3)
|
|
258
|
+
The ECI coordinates at each time, (lat,lon,elevation), of each antenna.
|
|
259
|
+
"""
|
|
260
|
+
return vmap(GEO_to_XYZ, in_axes=(1, None), out_axes=1)(geo, times)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@jit_with_doc
|
|
264
|
+
def alt_az_of_source(lsa: Array, lat: float, ra: float, dec: float) -> Array:
|
|
265
|
+
"""Calculate the altitude and azimuth of a given source direction over time.
|
|
266
|
+
Taken from https://astronomy.stackexchange.com/questions/14492/need-simple-equation-for-rise-transit-and-set-time
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
lst : Array (n_time,)
|
|
271
|
+
Local sidereal angle in degrees.
|
|
272
|
+
lat : float
|
|
273
|
+
Latitude of the observer in degrees.
|
|
274
|
+
ra : float
|
|
275
|
+
Right ascension of the source in degrees.
|
|
276
|
+
dec : float
|
|
277
|
+
DEclination of the source in degrees.
|
|
278
|
+
|
|
279
|
+
Returns
|
|
280
|
+
-------
|
|
281
|
+
Array (n_time, 2)
|
|
282
|
+
Altitude and azimuth of the source in degrees relative to the observer.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
h0 = jnp.atleast_1d(lsa) - ra
|
|
286
|
+
ones = jnp.ones_like(h0)
|
|
287
|
+
lat, h0, dec = jnp.deg2rad(jnp.array([lat * ones, h0, dec * ones]))
|
|
288
|
+
|
|
289
|
+
a = jnp.cos(lat) * jnp.sin(dec) - jnp.cos(dec) * jnp.sin(lat) * jnp.cos(h0)
|
|
290
|
+
b = jnp.cos(dec) * jnp.sin(h0)
|
|
291
|
+
c = jnp.sin(dec) * jnp.sin(lat) + jnp.cos(dec) * jnp.cos(lat) * jnp.cos(h0)
|
|
292
|
+
|
|
293
|
+
alt = jnp.rad2deg(jnp.arctan2(c, jnp.sqrt(a**2 + b**2)))
|
|
294
|
+
az = jnp.rad2deg(jnp.arctan2(b, a))
|
|
295
|
+
|
|
296
|
+
return jnp.array([alt, az]).T
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@jit_with_doc
|
|
300
|
+
def rise_and_set_of_source(lat: float, ra: float, dec: float) -> Array:
|
|
301
|
+
|
|
302
|
+
lat, dec = jnp.deg2rad(jnp.array([lat, dec]))
|
|
303
|
+
|
|
304
|
+
a = jnp.rad2deg(jnp.arccos(-jnp.tan(dec) * jnp.tan(lat)))
|
|
305
|
+
|
|
306
|
+
return jnp.array([ra - a, ra + a])
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@jit_with_doc
|
|
310
|
+
def lst_deg2sec(lst: Array) -> Array:
|
|
311
|
+
"""Convert a sidereal time in degrees to seconds.
|
|
312
|
+
|
|
313
|
+
Parameters
|
|
314
|
+
----------
|
|
315
|
+
lst : Array
|
|
316
|
+
Sidereal time in degrees to convert.
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
Array
|
|
321
|
+
Sidereal time in seconds.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
return lst / 360 * T_s
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@jit_with_doc
|
|
328
|
+
def lst_sec2deg(lst: Array) -> Array:
|
|
329
|
+
"""Convert a sidereal time from seconds to degrees.
|
|
330
|
+
|
|
331
|
+
Parameters
|
|
332
|
+
----------
|
|
333
|
+
lst : Array
|
|
334
|
+
Sidereal time in seconds to convert.
|
|
335
|
+
|
|
336
|
+
Returns
|
|
337
|
+
-------
|
|
338
|
+
Array
|
|
339
|
+
Sidereal time in degrees.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
return lst / T_s * 360
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def gmsa_from_jd(jd: float) -> float:
|
|
346
|
+
"""Get the Greenwich Mean Sidereal Angle in degrees from the Julian Day (UT1).
|
|
347
|
+
Calculated using https://aa.usno.navy.mil/faq/GAST
|
|
348
|
+
|
|
349
|
+
Parameters
|
|
350
|
+
----------
|
|
351
|
+
jd : float
|
|
352
|
+
Julian Day (UT1).
|
|
353
|
+
|
|
354
|
+
Returns
|
|
355
|
+
-------
|
|
356
|
+
float
|
|
357
|
+
Greenwich Mean Sidereal Angle in degrees.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
gmst_hours = 18.697375 + 24.065709824279 * (jd - 2451545.0)
|
|
361
|
+
|
|
362
|
+
gmsa = gmst_hours * 15
|
|
363
|
+
|
|
364
|
+
return gmsa
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def jd_from_gmst(gmst: float) -> float:
|
|
368
|
+
"""Get the Julian Date (UT1) from the Greenwich Mean Sidereal Time in seconds.
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
gmst : float
|
|
373
|
+
Greenwich Mean Sidereal Time in seconds.
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
float
|
|
378
|
+
Julian Date (UT1).
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
gmst_hours = gmst / 3600
|
|
382
|
+
|
|
383
|
+
jd = (gmst_hours - 18.697375) / 24.065709824279 + 2451545.0
|
|
384
|
+
|
|
385
|
+
return jd
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def jd_to_mjd(jd):
|
|
389
|
+
|
|
390
|
+
mjd = jd - 2400000.5
|
|
391
|
+
|
|
392
|
+
return mjd
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def mjd_to_jd(mjd):
|
|
396
|
+
|
|
397
|
+
jd = mjd + 2400000.5
|
|
398
|
+
|
|
399
|
+
return jd
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def time_above_horizon(lat: float, dec: float) -> float:
|
|
403
|
+
"""
|
|
404
|
+
The number of degrees an object is above the horizon in a given day.
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
lat, dec = jnp.deg2rad(jnp.array([lat, dec]))
|
|
408
|
+
|
|
409
|
+
if jnp.cos(dec) == 0 or jnp.cos(lat) == 0:
|
|
410
|
+
return jnp.inf
|
|
411
|
+
|
|
412
|
+
H = 2 * jnp.rad2deg(jnp.arccos(-jnp.tan(lat) * jnp.tan(dec)))
|
|
413
|
+
|
|
414
|
+
return H
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@jit_with_doc
|
|
418
|
+
def transit_altitude(lat: float, dec: float) -> float:
|
|
419
|
+
"""Calculate the altitude of a source at transit given an observer's latitude.
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
lat : float
|
|
424
|
+
Latitude of the observer in degrees.
|
|
425
|
+
dec : float
|
|
426
|
+
Declination of the source in degrees.
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
float
|
|
431
|
+
Altitude of the source at transit in degrees.
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
alt = 90 - jnp.abs(dec - lat)
|
|
435
|
+
|
|
436
|
+
return alt
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@jit_with_doc
|
|
440
|
+
def earth_radius(lat: float) -> float:
|
|
441
|
+
"""Calculate the earth radius according to the ellipsoidal model at a given latitude.
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
lat : float
|
|
446
|
+
Latitude of the location in degrees.
|
|
447
|
+
|
|
448
|
+
Returns
|
|
449
|
+
-------
|
|
450
|
+
float
|
|
451
|
+
Earth radius in metres at the given latitude.
|
|
452
|
+
"""
|
|
453
|
+
a = 6378137.0 # equitorial radius
|
|
454
|
+
b = 6356752.3 # polar radius
|
|
455
|
+
lat = jnp.deg2rad(lat)
|
|
456
|
+
cos = jnp.cos(lat)
|
|
457
|
+
sin = jnp.sin(lat)
|
|
458
|
+
r = jnp.sqrt(
|
|
459
|
+
((a**2 * cos) ** 2 + (b**2 * sin) ** 2) / ((a * cos) ** 2 + (b * sin) ** 2)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
return r
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@jit_with_doc
|
|
466
|
+
def enu_to_itrf(enu: Array, lat: float, lon: float, el: float) -> Array:
|
|
467
|
+
"""
|
|
468
|
+
Calculate ITRF coordinates from ENU coordinates of antennas given the
|
|
469
|
+
latitude and longitude of the antenna array centre.
|
|
470
|
+
|
|
471
|
+
Paramters
|
|
472
|
+
---------
|
|
473
|
+
enu: Array (n_ant, 3)
|
|
474
|
+
The East, North, Up coordinates of each antenna.
|
|
475
|
+
lat: float
|
|
476
|
+
The latitude of the observer/telescope.
|
|
477
|
+
lon: float
|
|
478
|
+
The longitude of the observer/telescope.
|
|
479
|
+
el: float
|
|
480
|
+
The elevation of the observer/telescope.
|
|
481
|
+
|
|
482
|
+
Returns
|
|
483
|
+
-------
|
|
484
|
+
itrf: Array (n_ant, 3)
|
|
485
|
+
The ITRF coordinates of the antennas.
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
enu = jnp.atleast_2d(enu)
|
|
489
|
+
R = earth_radius(lat) + el
|
|
490
|
+
lat, lon = jnp.deg2rad(jnp.array([lat, lon]))
|
|
491
|
+
|
|
492
|
+
r0 = R * jnp.array(
|
|
493
|
+
[jnp.cos(lat) * jnp.cos(lon), jnp.cos(lat) * jnp.sin(lon), jnp.sin(lat)]
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
R = jnp.array(
|
|
497
|
+
[
|
|
498
|
+
[-jnp.sin(lon), jnp.cos(lon), 0],
|
|
499
|
+
[-jnp.cos(lon) * jnp.sin(lat), -jnp.sin(lon) * jnp.sin(lat), jnp.cos(lat)],
|
|
500
|
+
[jnp.cos(lat) * jnp.cos(lon), jnp.cos(lat) * jnp.sin(lon), jnp.sin(lat)],
|
|
501
|
+
]
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return r0[None, :] + jnp.dot(enu, R)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def enu_to_xyz_local(enu, lat):
|
|
508
|
+
"""
|
|
509
|
+
https://web.njit.edu/~gary/728/Lecture6.html
|
|
510
|
+
"""
|
|
511
|
+
enu = jnp.atleast_2d(enu)
|
|
512
|
+
lat = jnp.deg2rad(lat)
|
|
513
|
+
|
|
514
|
+
R = jnp.array(
|
|
515
|
+
[
|
|
516
|
+
[0, -jnp.sin(lat), jnp.cos(lat)],
|
|
517
|
+
[1, 0, 0],
|
|
518
|
+
[0, jnp.cos(lat), jnp.sin(lat)],
|
|
519
|
+
]
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
return jnp.dot(enu, R.T)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
@jit_with_doc
|
|
526
|
+
def itrf_to_geo(itrf: Array) -> Array:
|
|
527
|
+
"""Convert ITRF coordinates to geodetic coordinates.
|
|
528
|
+
|
|
529
|
+
Parameters
|
|
530
|
+
----------
|
|
531
|
+
itrf : Array (n_ant, 3)
|
|
532
|
+
ITRF coordinates.
|
|
533
|
+
|
|
534
|
+
Returns
|
|
535
|
+
-------
|
|
536
|
+
Array (n_ant, 3)
|
|
537
|
+
Geodetic coordinates (latitude, longitude, elevation)
|
|
538
|
+
"""
|
|
539
|
+
x, y, z = jnp.atleast_2d(itrf).T
|
|
540
|
+
xy = jnp.sqrt(x**2 + y**2)
|
|
541
|
+
|
|
542
|
+
lat = jnp.rad2deg(jnp.arcsin(z / xy))
|
|
543
|
+
lon = jnp.rad2deg(jnp.arctan2(y, x))
|
|
544
|
+
el = jnp.linalg.norm(itrf, axis=-1) - earth_radius(lat)
|
|
545
|
+
|
|
546
|
+
return jnp.array([lat, lon, el]).T
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@jit_with_doc
|
|
550
|
+
def itrf_to_xyz(itrf: Array, gsa: Array) -> Array:
|
|
551
|
+
"""Transform coordinates from the ITRF (ECEF) frame to an ECI frame that aligns with the celestial sphere.
|
|
552
|
+
|
|
553
|
+
Parameters
|
|
554
|
+
----------
|
|
555
|
+
itrf : Array (n_ant, 3)
|
|
556
|
+
ITRF coordinates in metres.
|
|
557
|
+
gsa : Array (n_time,)
|
|
558
|
+
Greenwich sidereal angle in degrees.
|
|
559
|
+
|
|
560
|
+
Returns
|
|
561
|
+
-------
|
|
562
|
+
Array (n_time, n_ant, 3)
|
|
563
|
+
ECI coordinates in metres.
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
itrf = jnp.atleast_2d(itrf)
|
|
567
|
+
gsa = jnp.atleast_1d(gsa)
|
|
568
|
+
ecef_to_eci = lambda ecef, gsa: jnp.einsum("ij,aj->ai", Rotz(gsa), ecef)
|
|
569
|
+
xyz = vmap(ecef_to_eci, in_axes=(None, 0))(itrf, gsa)
|
|
570
|
+
|
|
571
|
+
return xyz
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@jit_with_doc
|
|
575
|
+
def xyz_to_itrf(xyz: Array, gsa: Array) -> Array:
|
|
576
|
+
"""Transform coordinates from the ECI frame to the ITRF (ECEF) frame that is fixed with the Earth.
|
|
577
|
+
|
|
578
|
+
Parameters
|
|
579
|
+
----------
|
|
580
|
+
xyz : Array (n_time, 3)
|
|
581
|
+
ECI coordinates in metres.
|
|
582
|
+
gsa : Array (n_time,)
|
|
583
|
+
Greenwich sidereal time in degrees.
|
|
584
|
+
|
|
585
|
+
Returns
|
|
586
|
+
-------
|
|
587
|
+
Array (n_time, 3)
|
|
588
|
+
ITRF (ECEF) coordinates in metres.
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
xyz = jnp.atleast_2d(xyz)
|
|
592
|
+
gsa = jnp.atleast_1d(gsa)
|
|
593
|
+
eci_to_ecef = lambda eci, gsa: Rotz(-gsa) @ eci
|
|
594
|
+
itrf = vmap(eci_to_ecef, in_axes=(0, 0))(xyz, gsa)
|
|
595
|
+
|
|
596
|
+
return itrf
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@jit_with_doc
|
|
600
|
+
def itrf_to_uvw(itrf: Array, h0: Array, dec: float) -> Array:
|
|
601
|
+
"""
|
|
602
|
+
Calculate uvw coordinates from ITRF/ECEF coordinates,
|
|
603
|
+
source hour angle and declination. Use the Greenwich hour
|
|
604
|
+
angle when using true ITRF coordinates such as those produced
|
|
605
|
+
with 'enu_to_itrf' or provided in an MS file. Use local hour angle when using local 'xyz'
|
|
606
|
+
coordinates as defined in most radio interferometry textbooks
|
|
607
|
+
or those produced with 'enu_to_xyz_local'.
|
|
608
|
+
|
|
609
|
+
Parameters
|
|
610
|
+
----------
|
|
611
|
+
ITRF: Array (n_ant, 3)
|
|
612
|
+
Antenna positions in the ITRF frame in units of metres.
|
|
613
|
+
h0: Array (n_time,)
|
|
614
|
+
The hour angle of the target in decimal degrees.
|
|
615
|
+
dec: float
|
|
616
|
+
The declination of the target in decimal degrees.
|
|
617
|
+
|
|
618
|
+
Returns
|
|
619
|
+
-------
|
|
620
|
+
uvw: Array (n_time, n_ant, 3)
|
|
621
|
+
The uvw coordinates of the antennas for a given observer
|
|
622
|
+
location, time and target (ra,dec).
|
|
623
|
+
"""
|
|
624
|
+
|
|
625
|
+
itrf = jnp.atleast_2d(itrf)
|
|
626
|
+
itrf = itrf - itrf[0, None, :]
|
|
627
|
+
|
|
628
|
+
h0 = jnp.deg2rad(jnp.atleast_1d(h0))
|
|
629
|
+
dec = jnp.deg2rad(jnp.asarray(dec))
|
|
630
|
+
ones = jnp.ones_like(h0)
|
|
631
|
+
|
|
632
|
+
R = jnp.array(
|
|
633
|
+
[
|
|
634
|
+
[jnp.sin(h0), jnp.cos(h0), jnp.zeros_like(h0)],
|
|
635
|
+
[
|
|
636
|
+
-jnp.sin(dec) * jnp.cos(h0),
|
|
637
|
+
jnp.sin(dec) * jnp.sin(h0),
|
|
638
|
+
jnp.cos(dec) * ones,
|
|
639
|
+
],
|
|
640
|
+
[
|
|
641
|
+
jnp.cos(dec) * jnp.cos(h0),
|
|
642
|
+
-jnp.cos(dec) * jnp.sin(h0),
|
|
643
|
+
jnp.sin(dec) * ones,
|
|
644
|
+
],
|
|
645
|
+
]
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
uvw = jnp.einsum("ijt,aj->tai", R, itrf)
|
|
649
|
+
|
|
650
|
+
return uvw
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@jit_with_doc
|
|
654
|
+
def calculate_fringe_frequency(
|
|
655
|
+
times_mjd: Array,
|
|
656
|
+
freq: float,
|
|
657
|
+
rfi_xyz: Array,
|
|
658
|
+
ants_itrf: Array,
|
|
659
|
+
ants_u: Array,
|
|
660
|
+
dec: float,
|
|
661
|
+
) -> Array:
|
|
662
|
+
"""Calculate the fringe frequency of an RFI source.
|
|
663
|
+
|
|
664
|
+
Parameters
|
|
665
|
+
----------
|
|
666
|
+
times_mjd : Array (n_time,)
|
|
667
|
+
Times are which the RFI and antenna positions are given in Modified Julian Date.
|
|
668
|
+
freq : float
|
|
669
|
+
Observational frequency in Hz.
|
|
670
|
+
rfi_xyz : Array (n_time, 3)
|
|
671
|
+
Position of the RFI source in the ECI frame in metres.
|
|
672
|
+
ants_itrf : Array (n_ant, 3)
|
|
673
|
+
Antenna positions in the ITRF (ECEF) frame in metres.
|
|
674
|
+
ants_u : Array (n_time, n_ant)
|
|
675
|
+
U component of the antennas in UVW frame in metres.
|
|
676
|
+
dec : float
|
|
677
|
+
Phase centre declination in degrees.
|
|
678
|
+
|
|
679
|
+
Returns
|
|
680
|
+
-------
|
|
681
|
+
Array (n_time, n_bl)
|
|
682
|
+
Fringe frequencies on each baseline.
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
lam = C / freq
|
|
686
|
+
# Should change this to astropy.time.Time
|
|
687
|
+
gsa = gmsa_from_jd(mjd_to_jd(times_mjd))
|
|
688
|
+
times = (times_mjd - times_mjd[0]) * 24 * 3600
|
|
689
|
+
|
|
690
|
+
r_ecef = xyz_to_itrf(rfi_xyz, gsa)
|
|
691
|
+
s_ecef = r_ecef - jnp.mean(ants_itrf, axis=0)
|
|
692
|
+
s_hat_ecef = s_ecef / jnp.linalg.norm(s_ecef, axis=-1, keepdims=True)
|
|
693
|
+
s_hat_dot = jnp.gradient(s_hat_ecef, times, axis=0)
|
|
694
|
+
|
|
695
|
+
a1, a2 = jnp.triu_indices(len(ants_itrf), 1)
|
|
696
|
+
bl_ecef = ants_itrf[a1] - ants_itrf[a2]
|
|
697
|
+
bl_u = ants_u[:, a1] - ants_u[:, a2]
|
|
698
|
+
|
|
699
|
+
fringe_move = jnp.einsum("bi,ti->tb", bl_ecef, s_hat_dot) / lam
|
|
700
|
+
fringe_stat = -bl_u * Omega_e * jnp.cos(jnp.deg2rad(dec)) / lam
|
|
701
|
+
fringe_freq = fringe_move - fringe_stat
|
|
702
|
+
|
|
703
|
+
return fringe_freq
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
@jit_with_doc
|
|
707
|
+
def calculate_sat_corr_time(
|
|
708
|
+
sat_xyz: Array,
|
|
709
|
+
ants_xyz: Array,
|
|
710
|
+
orbit_el: float,
|
|
711
|
+
lat: float,
|
|
712
|
+
dish_d: float,
|
|
713
|
+
freqs: Array,
|
|
714
|
+
) -> float:
|
|
715
|
+
"""Calculate the expected correlation time for a Gaussian process model of the RFI signal due to a satellite moving through the sidelobes of the primary beam.
|
|
716
|
+
|
|
717
|
+
Parameters
|
|
718
|
+
----------
|
|
719
|
+
rfi_xyz : Array (n_time, 3)
|
|
720
|
+
Position of the RFI source over time in metres
|
|
721
|
+
ants_xyz : Array (n_time, n_ant, 3)
|
|
722
|
+
Positions of the antennas over time in metres.
|
|
723
|
+
orbit_el : float
|
|
724
|
+
Orbit elevation in metres.
|
|
725
|
+
lat : float
|
|
726
|
+
Latitude of the antennas in degrees.
|
|
727
|
+
dish_d : float
|
|
728
|
+
Dish diameter in metres. The primary beam is modelled by an Airy disk based on this.
|
|
729
|
+
freqs : Array (n_freq,)
|
|
730
|
+
Observational frequencies in Hz.
|
|
731
|
+
|
|
732
|
+
Returns
|
|
733
|
+
-------
|
|
734
|
+
float
|
|
735
|
+
Expected minimum correlation time in seconds.
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
r = sat_xyz - ants_xyz.mean(axis=1)
|
|
739
|
+
R = jnp.linalg.norm(r, axis=-1).min()
|
|
740
|
+
|
|
741
|
+
R_sat = earth_radius(lat) + orbit_el
|
|
742
|
+
v_sat = jnp.sqrt(G * M_e / R_sat)
|
|
743
|
+
|
|
744
|
+
l = jnp.min(C / freqs) * R / (dish_d * v_sat) / 4
|
|
745
|
+
|
|
746
|
+
return l
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
@jit_with_doc
|
|
750
|
+
def angular_separation(rfi_xyz: Array, ants_xyz: Array, ra: float, dec: float) -> Array:
|
|
751
|
+
"""
|
|
752
|
+
Calculate the angular separation between the pointing direction of each
|
|
753
|
+
antenna and the satellite source.
|
|
754
|
+
|
|
755
|
+
Parameters
|
|
756
|
+
----------
|
|
757
|
+
rfi_xyz: ndarray (n_src, n_time, 3)
|
|
758
|
+
Position of the RFI sources in ECEF reference frame over time. This
|
|
759
|
+
is the same frame as `ants_xyz`.
|
|
760
|
+
ants_xyz: ndarray (n_time, n_ant, 3)
|
|
761
|
+
Position of the antennas in ECEF reference frame over time.
|
|
762
|
+
ra: float
|
|
763
|
+
Right ascension of the pointing in decimal degrees.
|
|
764
|
+
dec: float
|
|
765
|
+
Declination of the pointing in decimal degrees.
|
|
766
|
+
|
|
767
|
+
Returns
|
|
768
|
+
-------
|
|
769
|
+
angles: ndarray (n_src, n_time, n_ant)
|
|
770
|
+
Angular separation (in degrees) between the pointing direction and RFI source for each antenna.
|
|
771
|
+
"""
|
|
772
|
+
rfi_xyz = jnp.asarray(rfi_xyz)
|
|
773
|
+
ants_xyz = jnp.asarray(ants_xyz)
|
|
774
|
+
ra = jnp.asarray(ra).flatten()[0]
|
|
775
|
+
dec = jnp.asarray(dec).flatten()[0]
|
|
776
|
+
src_xyz = radec_to_XYZ(ra, dec)
|
|
777
|
+
ant_to_sat_xyz = rfi_xyz[:, :, None, :] - ants_xyz[None, :, :, :]
|
|
778
|
+
costheta = jnp.einsum("i,ljki->ljk", src_xyz, ant_to_sat_xyz) / jnp.linalg.norm(
|
|
779
|
+
ant_to_sat_xyz, axis=-1
|
|
780
|
+
)
|
|
781
|
+
angles = jnp.rad2deg(jnp.arccos(costheta))
|
|
782
|
+
return angles
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
@jit_with_doc
|
|
786
|
+
def Rotx(theta: float) -> Array:
|
|
787
|
+
"""
|
|
788
|
+
Define a rotation matrix about the 'x-axis' by an angle theta, in degrees.
|
|
789
|
+
|
|
790
|
+
Parameters
|
|
791
|
+
----------
|
|
792
|
+
theta: float
|
|
793
|
+
Rotation angle in degrees.
|
|
794
|
+
|
|
795
|
+
Returns
|
|
796
|
+
-------
|
|
797
|
+
R: ndarray (3, 3)
|
|
798
|
+
Rotation matrix.
|
|
799
|
+
"""
|
|
800
|
+
theta = jnp.asarray(theta).flatten()[0]
|
|
801
|
+
c = jnp.cos(jnp.deg2rad(theta))
|
|
802
|
+
s = jnp.sin(jnp.deg2rad(theta))
|
|
803
|
+
Rx = jnp.array([[1, 0, 0], [0, c, -s], [0, s, c]])
|
|
804
|
+
|
|
805
|
+
return Rx
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
@jit_with_doc
|
|
809
|
+
def Rotz(theta: float) -> Array:
|
|
810
|
+
"""
|
|
811
|
+
Define a rotation matrix about the 'z-axis' by an angle theta, in degrees.
|
|
812
|
+
|
|
813
|
+
Parameters
|
|
814
|
+
----------
|
|
815
|
+
theta: float
|
|
816
|
+
Rotation angle in degrees.
|
|
817
|
+
|
|
818
|
+
Returns
|
|
819
|
+
-------
|
|
820
|
+
R: ndarray (3, 3)
|
|
821
|
+
Rotation matrix.
|
|
822
|
+
"""
|
|
823
|
+
theta = jnp.asarray(theta).flatten()[0]
|
|
824
|
+
c = jnp.cos(jnp.deg2rad(theta))
|
|
825
|
+
s = jnp.sin(jnp.deg2rad(theta))
|
|
826
|
+
Rz = jnp.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
|
|
827
|
+
|
|
828
|
+
return Rz
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
@jit_with_doc
|
|
832
|
+
def orbit(
|
|
833
|
+
times: Array,
|
|
834
|
+
elevation: float,
|
|
835
|
+
inclination: float,
|
|
836
|
+
lon_asc_node: float,
|
|
837
|
+
periapsis: float,
|
|
838
|
+
) -> Array:
|
|
839
|
+
"""
|
|
840
|
+
Calculate orbital path of a satellite in perfect circular orbit.
|
|
841
|
+
|
|
842
|
+
Parameters
|
|
843
|
+
----------
|
|
844
|
+
times: ndarray (n_time,)
|
|
845
|
+
Times at which to evaluate the positions.
|
|
846
|
+
elevation: float
|
|
847
|
+
Elevation/Altitude of the orbit in metres.
|
|
848
|
+
inclination: float
|
|
849
|
+
Inclination angle of the orbit relative to the equatorial plane.
|
|
850
|
+
lon_asc_node: float
|
|
851
|
+
Longitude of the ascending node of the orbit. This is the longitude of
|
|
852
|
+
when the orbit crosses the equator from the south to the north.
|
|
853
|
+
periapsis: float
|
|
854
|
+
Perisapsis of the orbit. This is the angular starting point of the
|
|
855
|
+
orbit at t = 0.
|
|
856
|
+
|
|
857
|
+
Returns
|
|
858
|
+
-------
|
|
859
|
+
positions: ndarray (n_time, 3)
|
|
860
|
+
The position vector of the orbiting object at each specified time.
|
|
861
|
+
"""
|
|
862
|
+
times = jnp.asarray(times)
|
|
863
|
+
elevation = jnp.asarray(elevation).flatten()[0]
|
|
864
|
+
inclination = jnp.asarray(inclination).flatten()[0]
|
|
865
|
+
lon_asc_node = jnp.asarray(lon_asc_node).flatten()[0]
|
|
866
|
+
periapsis = jnp.asarray(periapsis).flatten()[0]
|
|
867
|
+
R = R_e + elevation
|
|
868
|
+
omega = jnp.sqrt(G * M_e / R**3)
|
|
869
|
+
r = R * jnp.array(
|
|
870
|
+
[jnp.cos(omega * (times)), jnp.sin(omega * (times)), jnp.zeros(len(times))]
|
|
871
|
+
)
|
|
872
|
+
R1 = Rotz(periapsis)
|
|
873
|
+
R2 = Rotx(inclination)
|
|
874
|
+
R3 = Rotz(lon_asc_node)
|
|
875
|
+
rt = (R3 @ R2 @ R1 @ r).T
|
|
876
|
+
|
|
877
|
+
return rt
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def orbit_vmap(
|
|
881
|
+
times: Array,
|
|
882
|
+
elevation: Array,
|
|
883
|
+
inclination: Array,
|
|
884
|
+
lon_asc_node: Array,
|
|
885
|
+
periapsis: Array,
|
|
886
|
+
) -> Array:
|
|
887
|
+
"""Calculate orbital path of a satellite in perfect circular orbit.
|
|
888
|
+
|
|
889
|
+
Parameters
|
|
890
|
+
----------
|
|
891
|
+
times: ndarray (n_time,)
|
|
892
|
+
Times at which to evaluate the positions.
|
|
893
|
+
elevation: nd_array (n_orbits,)
|
|
894
|
+
Elevation/Altitude of the orbit in metres.
|
|
895
|
+
inclination: nd_array (n_orbits,)
|
|
896
|
+
Inclination angle of the orbit relative to the equatorial plane.
|
|
897
|
+
lon_asc_node: nd_array (n_orbits,)
|
|
898
|
+
Longitude of the ascending node of the orbit. This is the longitude of
|
|
899
|
+
when the orbit crosses the equator from the south to the north.
|
|
900
|
+
periapsis: nd_array (n_orbits,)
|
|
901
|
+
Perisapsis of the orbit. This is the angular starting point of the
|
|
902
|
+
orbit at t = 0.
|
|
903
|
+
|
|
904
|
+
Returns
|
|
905
|
+
-------
|
|
906
|
+
positions: ndarray (n_orbits, n_time, 3)
|
|
907
|
+
The position vectors of the orbiting objects at each specified time.
|
|
908
|
+
"""
|
|
909
|
+
return vmap(orbit, in_axes=(None, 0, 0, 0, 0))(
|
|
910
|
+
times, elevation, inclination, lon_asc_node, periapsis
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
@jit_with_doc
|
|
915
|
+
def orbit_velocity(
|
|
916
|
+
times: Array,
|
|
917
|
+
elevation: float,
|
|
918
|
+
inclination: float,
|
|
919
|
+
lon_asc_node: float,
|
|
920
|
+
periapsis: float,
|
|
921
|
+
) -> Array:
|
|
922
|
+
"""
|
|
923
|
+
Calculate the velocity of a circular orbit at specific times.
|
|
924
|
+
|
|
925
|
+
Parameters
|
|
926
|
+
----------
|
|
927
|
+
times: ndarray (n_time,)
|
|
928
|
+
Times at which to evaluate the rotation matrices.
|
|
929
|
+
elevation: float
|
|
930
|
+
Elevation/Altitude of the orbit in metres.
|
|
931
|
+
inclination: float
|
|
932
|
+
Inclination angle of the orbit relative to the equatorial plane.
|
|
933
|
+
lon_asc_node: float
|
|
934
|
+
Longitude of the ascending node of the orbit. This is the longitude of
|
|
935
|
+
when the orbit crosses the equator from the south to the north.
|
|
936
|
+
periapsis: float
|
|
937
|
+
Perisapsis of the orbit. This is the angular starting point of the orbit
|
|
938
|
+
at t = 0.
|
|
939
|
+
|
|
940
|
+
Returns
|
|
941
|
+
-------
|
|
942
|
+
velocity: ndarray (n_time, 3)
|
|
943
|
+
The velocity vector at the specified times.
|
|
944
|
+
"""
|
|
945
|
+
times = jnp.asarray(times)
|
|
946
|
+
elevation = jnp.asarray(elevation)
|
|
947
|
+
inclination = jnp.asarray(inclination)
|
|
948
|
+
lon_asc_node = jnp.asarray(lon_asc_node)
|
|
949
|
+
periapsis = jnp.asarray(periapsis)
|
|
950
|
+
vel_vmap = vmap(jacrev(orbit, argnums=(0)), in_axes=(0, None, None, None, None))
|
|
951
|
+
velocity = vel_vmap(
|
|
952
|
+
times[:, None], elevation, inclination, lon_asc_node, periapsis
|
|
953
|
+
).reshape(len(times), 3)
|
|
954
|
+
|
|
955
|
+
return velocity
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
@jit_with_doc
|
|
959
|
+
def R_uvw(
|
|
960
|
+
times: Array,
|
|
961
|
+
elevation: float,
|
|
962
|
+
inclination: float,
|
|
963
|
+
lon_asc_node: float,
|
|
964
|
+
periapsis: float,
|
|
965
|
+
) -> Array:
|
|
966
|
+
"""
|
|
967
|
+
Calculate the rotation matrices at each time step to transform from an Earth
|
|
968
|
+
centric reference frame (ECI) to a satellite centric frame given the
|
|
969
|
+
parameters of its (circular) orbit.
|
|
970
|
+
|
|
971
|
+
Parameters
|
|
972
|
+
----------
|
|
973
|
+
times: ndarray (n_time,)
|
|
974
|
+
Times at which to evaluate the rotation matrices.
|
|
975
|
+
elevation: float
|
|
976
|
+
Elevation/Altitude of the orbit in metres.
|
|
977
|
+
inclination: float
|
|
978
|
+
Inclination angle of the orbit relative to the equatorial plane.
|
|
979
|
+
lon_asc_node: float
|
|
980
|
+
Longitude of the ascending node of the orbit. This is the longitude of
|
|
981
|
+
when the orbit crosses the equator from the south to the north.
|
|
982
|
+
periapsis: float
|
|
983
|
+
Perisapsis of the orbit. This is the angular starting point of the orbit
|
|
984
|
+
at t = 0.
|
|
985
|
+
|
|
986
|
+
Returns
|
|
987
|
+
-------
|
|
988
|
+
R: ndarray (3, 3)
|
|
989
|
+
The rotation matrix to orient to a satellite centric (RIC) frame defined
|
|
990
|
+
by the Radial, In-track, Cross-track components.
|
|
991
|
+
"""
|
|
992
|
+
times = jnp.asarray(times)
|
|
993
|
+
elevation = jnp.asarray(elevation)
|
|
994
|
+
inclination = jnp.asarray(inclination)
|
|
995
|
+
lon_asc_node = jnp.asarray(lon_asc_node)
|
|
996
|
+
periapsis = jnp.asarray(periapsis)
|
|
997
|
+
position = orbit(times, elevation, inclination, lon_asc_node, periapsis)
|
|
998
|
+
velocity = orbit_velocity(times, elevation, inclination, lon_asc_node, periapsis)
|
|
999
|
+
vel_hat = velocity / jnp.linalg.norm(velocity, axis=-1, keepdims=True)
|
|
1000
|
+
u_hat = position / jnp.linalg.norm(position, axis=-1, keepdims=True)
|
|
1001
|
+
w_hat = jnp.cross(u_hat, vel_hat)
|
|
1002
|
+
v_hat = jnp.cross(w_hat, u_hat)
|
|
1003
|
+
R = jnp.stack([u_hat.T, v_hat.T, w_hat.T])
|
|
1004
|
+
|
|
1005
|
+
return R
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
@jit_with_doc
|
|
1009
|
+
def RIC_dev(
|
|
1010
|
+
times: Array,
|
|
1011
|
+
true_orbit_params: Array,
|
|
1012
|
+
estimated_orbit_params: Array,
|
|
1013
|
+
) -> Array:
|
|
1014
|
+
"""
|
|
1015
|
+
Calculate the Radial (R), In-track (I) and Cross-track (C) deviations
|
|
1016
|
+
between two circular orbits given their orbit parameters at many time steps.
|
|
1017
|
+
|
|
1018
|
+
Parameters
|
|
1019
|
+
----------
|
|
1020
|
+
times: array (n_time,)
|
|
1021
|
+
Times at which to evaluate the RIC deviations.
|
|
1022
|
+
true_orb_params: ndarray (4,)
|
|
1023
|
+
Orbit parameters (elevation, inclination, lon_asc_node, periapsis) of
|
|
1024
|
+
the object of most interest. The more accurate measurement when
|
|
1025
|
+
comparing the same object.
|
|
1026
|
+
est_orb_params: array (elevation, inclination, lon_asc_node, periapsis)
|
|
1027
|
+
Estimated orbit parameters.
|
|
1028
|
+
|
|
1029
|
+
Returns
|
|
1030
|
+
-------
|
|
1031
|
+
RIC: ndarray (n_time, 3)
|
|
1032
|
+
Radial, In-track and Cross-track coordinates, in metres, of the orbiting
|
|
1033
|
+
body relative to the orbit defined by 'true_orbit_params'.
|
|
1034
|
+
"""
|
|
1035
|
+
times = jnp.asarray(times)
|
|
1036
|
+
true_orbit_params = jnp.asarray(true_orbit_params)
|
|
1037
|
+
estimated_orbit_params = jnp.asarray(estimated_orbit_params)
|
|
1038
|
+
true_xyz = orbit(times, *true_orbit_params)
|
|
1039
|
+
est_xyz = orbit(times, *estimated_orbit_params)
|
|
1040
|
+
R = R_uvw(times, *true_orbit_params)
|
|
1041
|
+
true_uvw = vmap(lambda x, y: x @ y, in_axes=(-1, 0))(R, true_xyz)
|
|
1042
|
+
est_uvw = vmap(lambda x, y: x @ y, in_axes=(-1, 0))(R, est_xyz)
|
|
1043
|
+
|
|
1044
|
+
return est_uvw - true_uvw
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
@jit_with_doc
|
|
1048
|
+
def orbit_fisher(times: Array, orbit_params: Array, RIC_std: Array) -> Array:
|
|
1049
|
+
"""
|
|
1050
|
+
Calculate the inverse covariance (Fisher) matrix in orbital elements
|
|
1051
|
+
induced by errors in the RIC frame of an orbiting object. This is
|
|
1052
|
+
essentially linear uncertainty propagation assuming Gaussian errors.
|
|
1053
|
+
|
|
1054
|
+
Parameters
|
|
1055
|
+
----------
|
|
1056
|
+
times: ndarray (n_time,)
|
|
1057
|
+
Times at which the RIC covariance/standard deviations is defined.
|
|
1058
|
+
orbit_params: ndarray (4,)
|
|
1059
|
+
Orbit parameters (elevation, inclination, lon_asc_node, periapsis) for
|
|
1060
|
+
the orbit defining the origin of the RIC frame.
|
|
1061
|
+
RIC_std: ndarray (3,)
|
|
1062
|
+
The standard deviation in the Radial, In-track and Cross-track
|
|
1063
|
+
directions.
|
|
1064
|
+
|
|
1065
|
+
Returns
|
|
1066
|
+
-------
|
|
1067
|
+
fisher: ndarray (4, 4)
|
|
1068
|
+
The inverse covariance (Fisher) matrix for the orbit parameters induced
|
|
1069
|
+
by the orbit uncertainties in the RIC frame.
|
|
1070
|
+
"""
|
|
1071
|
+
times = jnp.asarray(times)
|
|
1072
|
+
orbit_params = jnp.asarray(orbit_params)
|
|
1073
|
+
RIC_std = jnp.asarray(RIC_std)
|
|
1074
|
+
J = jacrev(RIC_dev, argnums=(2,))(times, orbit_params, orbit_params)[0]
|
|
1075
|
+
F = jnp.diag(1.0 / RIC_std**2)
|
|
1076
|
+
|
|
1077
|
+
def propagate(J, F):
|
|
1078
|
+
return J.T @ F @ J
|
|
1079
|
+
|
|
1080
|
+
fisher = vmap(propagate, in_axes=(0, None))(J, F).sum(axis=0)
|
|
1081
|
+
|
|
1082
|
+
return fisher
|