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,969 @@
|
|
|
1
|
+
import jax.numpy as jnp
|
|
2
|
+
from jax import config, Array
|
|
3
|
+
|
|
4
|
+
import dask.array as da
|
|
5
|
+
import numpy as np
|
|
6
|
+
import xarray as xr
|
|
7
|
+
|
|
8
|
+
from tabsim.dask.coordinates import (
|
|
9
|
+
ENU_to_ITRF,
|
|
10
|
+
ITRF_to_UVW,
|
|
11
|
+
ENU_to_GEO,
|
|
12
|
+
GEO_to_XYZ_vmap0,
|
|
13
|
+
ITRF_to_XYZ,
|
|
14
|
+
orbit_vmap,
|
|
15
|
+
radec_to_lmn,
|
|
16
|
+
angular_separation,
|
|
17
|
+
)
|
|
18
|
+
from tabsim.dask.interferometry import (
|
|
19
|
+
astro_vis,
|
|
20
|
+
astro_vis_gauss,
|
|
21
|
+
astro_vis_exp,
|
|
22
|
+
rfi_vis,
|
|
23
|
+
add_noise,
|
|
24
|
+
airy_beam,
|
|
25
|
+
Pv_to_Sv,
|
|
26
|
+
SEFD_to_noise_std,
|
|
27
|
+
int_sample_times,
|
|
28
|
+
generate_gains,
|
|
29
|
+
generate_fourier_gains,
|
|
30
|
+
apply_gains,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from tabsim.jax.interferometry import int_sample_times
|
|
34
|
+
|
|
35
|
+
from tabsim.jax.coordinates import (
|
|
36
|
+
itrf_to_geo,
|
|
37
|
+
alt_az_of_source,
|
|
38
|
+
gmsa_from_jd,
|
|
39
|
+
mjd_to_jd,
|
|
40
|
+
secs_to_days,
|
|
41
|
+
)
|
|
42
|
+
from tabsim.tools import beam_size
|
|
43
|
+
from tabsim.write import construct_observation_ds, write_ms
|
|
44
|
+
from tabsim.dask.extras import get_chunksizes
|
|
45
|
+
from tabsim.tle import get_satellite_positions, ants_pos, sat_distance
|
|
46
|
+
|
|
47
|
+
config.update("jax_enable_x64", True)
|
|
48
|
+
|
|
49
|
+
from astropy.time import Time
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Telescope(object):
|
|
53
|
+
"""
|
|
54
|
+
Construct an Observation object defining a radio interferometry
|
|
55
|
+
observation.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
latitude: float
|
|
60
|
+
Latitude of the telescope.
|
|
61
|
+
longitude: float
|
|
62
|
+
Longitude of the telescope.
|
|
63
|
+
elevation: float
|
|
64
|
+
Elevation of the telescope.
|
|
65
|
+
ENU_path: str
|
|
66
|
+
Path to a txt file containing the ENU coordinates of each antenna.
|
|
67
|
+
ENU_array: ndarray (n_ant, 3)
|
|
68
|
+
ENU coordinates of each antenna.
|
|
69
|
+
name: str
|
|
70
|
+
Name of the telescope.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
latitude: float,
|
|
76
|
+
longitude: float,
|
|
77
|
+
elevation: float = 0.0,
|
|
78
|
+
ENU_array: Array = None,
|
|
79
|
+
ENU_path: str = None,
|
|
80
|
+
ITRF_array: Array = None,
|
|
81
|
+
ITRF_path: str = None,
|
|
82
|
+
tel_name: str = None,
|
|
83
|
+
n_ant: int = None,
|
|
84
|
+
):
|
|
85
|
+
self.tel_name = tel_name
|
|
86
|
+
self.latitude = da.asarray(latitude)
|
|
87
|
+
self.longitude = da.asarray(longitude)
|
|
88
|
+
self.elevation = da.asarray(elevation)
|
|
89
|
+
self.GEO = da.asarray([latitude, longitude, elevation])
|
|
90
|
+
self.ITRF = None
|
|
91
|
+
self.ENU = None
|
|
92
|
+
self.n_ant = n_ant
|
|
93
|
+
|
|
94
|
+
if ENU_array is not None or ENU_path is not None:
|
|
95
|
+
self.createArrayENU(ENU_array, ENU_path)
|
|
96
|
+
if ITRF_array is not None or ITRF_path is not None:
|
|
97
|
+
self.createArrayITRF(ITRF_array, ITRF_path)
|
|
98
|
+
if self.ITRF is None and self.ENU is None:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"One of ('ENU_array', 'ENU_path', 'ITRF_array', 'ITRF_path') must be provided to create a Telescope object."
|
|
101
|
+
)
|
|
102
|
+
self.n_ant = len(self.ITRF)
|
|
103
|
+
|
|
104
|
+
def __str__(self):
|
|
105
|
+
msg = """\nTelescope Location
|
|
106
|
+
------------------
|
|
107
|
+
Latitude : {latitude}
|
|
108
|
+
Longitude : {longitude}
|
|
109
|
+
Elevation : {elevation}\n"""
|
|
110
|
+
params = {
|
|
111
|
+
"latitude": self.latitude,
|
|
112
|
+
"longitude": self.longitude,
|
|
113
|
+
"elevation": self.elevation,
|
|
114
|
+
}
|
|
115
|
+
params = {k: v.compute() for k, v in params.items()}
|
|
116
|
+
return msg.format(**params)
|
|
117
|
+
|
|
118
|
+
def createArrayENU(self, ENU_array=None, ENU_path=None):
|
|
119
|
+
if ENU_array is not None:
|
|
120
|
+
self.ENU = ENU_array
|
|
121
|
+
elif ENU_path is not None:
|
|
122
|
+
self.ENU = np.loadtxt(ENU_path, usecols=(0, 1, 2), max_rows=self.n_ant)
|
|
123
|
+
else:
|
|
124
|
+
self.ENU = None
|
|
125
|
+
msg = """Error : East-North-Up coordinates are needed either in an
|
|
126
|
+
array or as a csv like file."""
|
|
127
|
+
print(msg)
|
|
128
|
+
return
|
|
129
|
+
self.ENU = da.asarray(self.ENU)
|
|
130
|
+
self.ENU_path = ENU_path
|
|
131
|
+
self.GEO_ants = ENU_to_GEO(self.GEO, self.ENU)
|
|
132
|
+
self.ITRF = ENU_to_ITRF(self.ENU, self.latitude, self.longitude, self.elevation)
|
|
133
|
+
|
|
134
|
+
def createArrayITRF(self, ITRF_array, ITRF_path):
|
|
135
|
+
if ITRF_array is not None:
|
|
136
|
+
self.ITRF = ITRF_array
|
|
137
|
+
elif ITRF_path is not None:
|
|
138
|
+
self.ITRF = np.loadtxt(ITRF_path, usecols=(0, 1, 2), max_rows=self.n_ant)
|
|
139
|
+
else:
|
|
140
|
+
self.ITRF = None
|
|
141
|
+
msg = """Error : ITRF antenna coordinates are needed either in an
|
|
142
|
+
array or as a csv like file."""
|
|
143
|
+
print(msg)
|
|
144
|
+
return
|
|
145
|
+
self.ITRF = da.asarray(self.ITRF)
|
|
146
|
+
self.GEO_ants = da.asarray(itrf_to_geo(self.ITRF.compute()))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Observation(Telescope):
|
|
150
|
+
"""
|
|
151
|
+
Construct an Observation object defining a radio interferometry
|
|
152
|
+
observation.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
latitude: float
|
|
157
|
+
Latitude of the telescope.
|
|
158
|
+
longitude: float
|
|
159
|
+
Longitude of the telescope.
|
|
160
|
+
elevation: float
|
|
161
|
+
Elevation of the telescope.
|
|
162
|
+
ra: float
|
|
163
|
+
Right Ascension of the phase centre.
|
|
164
|
+
dec: float
|
|
165
|
+
Declination of the phase centre.
|
|
166
|
+
times: ndarray (n_time,)
|
|
167
|
+
Time centroids of each data point in seconds as a Greenwich Mean Sidereal Time (GMST).
|
|
168
|
+
freqs: ndarray (n_freq,)
|
|
169
|
+
Frequency centroids for each observation channel in Hz.
|
|
170
|
+
SEFD: ndarray (n_freq,)
|
|
171
|
+
System Equivalent Flux Density of the telescope over frequency.
|
|
172
|
+
chan_width: float
|
|
173
|
+
Frequency channel width in Hz. Only used if `n_freq=1`, else calculated from `freqs`.
|
|
174
|
+
ENU_path: str
|
|
175
|
+
Path to a txt file containing the ENU coordinates of each antenna.
|
|
176
|
+
ENU_array: ndarray (n_ant, 3)
|
|
177
|
+
ENU coordinates of each antenna.
|
|
178
|
+
dish_d: float
|
|
179
|
+
Diameter of each antenna dish.
|
|
180
|
+
random_seed: int
|
|
181
|
+
Random seed to use for random number generator.
|
|
182
|
+
auto_corrs: bool
|
|
183
|
+
Flag to include autocorrelations in simulation.
|
|
184
|
+
no_w: bool
|
|
185
|
+
Whether to zero out the w-component of the baselines.
|
|
186
|
+
n_int_samples: int
|
|
187
|
+
Number of samples per time step which are then averaged. Must be
|
|
188
|
+
large enough to capture time-smearing of RFI sources on longest
|
|
189
|
+
baseline.
|
|
190
|
+
tel_name: str
|
|
191
|
+
Name of the telescope.
|
|
192
|
+
target_name: str
|
|
193
|
+
Name fo the target field.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
latitude: float,
|
|
199
|
+
longitude: float,
|
|
200
|
+
elevation: float,
|
|
201
|
+
ra: float,
|
|
202
|
+
dec: float,
|
|
203
|
+
freqs: Array,
|
|
204
|
+
SEFD: Array,
|
|
205
|
+
times_mjd: Array,
|
|
206
|
+
int_time: float = 2.0,
|
|
207
|
+
chan_width: float = 209e3,
|
|
208
|
+
ENU_array: Array = None,
|
|
209
|
+
ENU_path: str = None,
|
|
210
|
+
ITRF_array: Array = None,
|
|
211
|
+
ITRF_path: str = None,
|
|
212
|
+
n_ant: int = None,
|
|
213
|
+
dish_d: float = 13.5,
|
|
214
|
+
random_seed: int = 0,
|
|
215
|
+
auto_corrs: bool = False,
|
|
216
|
+
no_w: bool = False,
|
|
217
|
+
n_int_samples: int = 4,
|
|
218
|
+
tel_name: str = "MeerKAT",
|
|
219
|
+
target_name: str = "unknown",
|
|
220
|
+
max_chunk_MB: float = 100.0,
|
|
221
|
+
):
|
|
222
|
+
super().__init__(
|
|
223
|
+
latitude,
|
|
224
|
+
longitude,
|
|
225
|
+
elevation,
|
|
226
|
+
ENU_array,
|
|
227
|
+
ENU_path,
|
|
228
|
+
ITRF_array,
|
|
229
|
+
ITRF_path,
|
|
230
|
+
tel_name,
|
|
231
|
+
n_ant,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
n_time = len(times_mjd)
|
|
235
|
+
start_mjd = times_mjd[0]
|
|
236
|
+
# times = (times_mjd - times_mjd[0]) * 24 * 3600
|
|
237
|
+
times = da.linspace(0, int_time * n_time, n_time, endpoint=False)
|
|
238
|
+
times_mjd = start_mjd + secs_to_days(times)
|
|
239
|
+
|
|
240
|
+
n_int_samples = n_int_samples + 1 if n_int_samples % 2 == 0 else n_int_samples
|
|
241
|
+
|
|
242
|
+
self.target_name = target_name
|
|
243
|
+
self.auto_corrs = auto_corrs
|
|
244
|
+
|
|
245
|
+
a1, a2 = jnp.triu_indices(self.n_ant, 0 if auto_corrs else 1)
|
|
246
|
+
self.n_bl = len(a1)
|
|
247
|
+
|
|
248
|
+
self.ant_chunk = self.n_ant
|
|
249
|
+
self.bl_chunk = self.n_bl
|
|
250
|
+
|
|
251
|
+
self.a1 = da.asarray(a1, chunks=(self.bl_chunk,))
|
|
252
|
+
self.a2 = da.asarray(a2, chunks=(self.bl_chunk,))
|
|
253
|
+
|
|
254
|
+
self.ra = da.asarray(ra)
|
|
255
|
+
self.dec = da.asarray(dec)
|
|
256
|
+
|
|
257
|
+
chunksize = get_chunksizes(
|
|
258
|
+
len(times), len(freqs), n_int_samples, self.n_bl, max_chunk_MB
|
|
259
|
+
)
|
|
260
|
+
self.time_chunk = chunksize["time"]
|
|
261
|
+
self.time_fine_chunk = self.time_chunk * n_int_samples
|
|
262
|
+
self.freq_chunk = chunksize["freq"]
|
|
263
|
+
|
|
264
|
+
self.times = da.asarray(times).rechunk(self.time_chunk)
|
|
265
|
+
self.times_mjd = da.asarray(times_mjd).rechunk(self.time_chunk)
|
|
266
|
+
self.int_time = int_time
|
|
267
|
+
self.n_int_samples = n_int_samples
|
|
268
|
+
|
|
269
|
+
self.times_fine = da.asarray(
|
|
270
|
+
int_sample_times(self.times.compute(), n_int_samples)
|
|
271
|
+
).rechunk(self.time_fine_chunk)
|
|
272
|
+
|
|
273
|
+
self.times_mjd_fine = start_mjd + secs_to_days(self.times_fine)
|
|
274
|
+
|
|
275
|
+
#######################################
|
|
276
|
+
# dt = da.diff(self.times_fine)[:1]
|
|
277
|
+
# dt_jd = da.diff(self.times_mjd_fine)[:1]
|
|
278
|
+
|
|
279
|
+
# self.times_fine = da.concatenate([self.times_fine, dt]).rechunk(
|
|
280
|
+
# self.time_fine_chunk + 1
|
|
281
|
+
# )
|
|
282
|
+
# self.times_mjd_fine = da.concatenate([self.times_mjd_fine, dt_jd]).rechunk(
|
|
283
|
+
# self.time_fine_chunk + 1
|
|
284
|
+
# )
|
|
285
|
+
#######################################
|
|
286
|
+
|
|
287
|
+
self.n_time = len(times)
|
|
288
|
+
self.n_time_fine = len(self.times_fine)
|
|
289
|
+
self.t_idx = da.arange(
|
|
290
|
+
self.n_int_samples // 2, self.n_time_fine, self.n_int_samples
|
|
291
|
+
).rechunk(self.time_chunk)
|
|
292
|
+
|
|
293
|
+
self.gsa = da.asarray(
|
|
294
|
+
Time(self.times_mjd_fine.compute(), format="mjd")
|
|
295
|
+
.sidereal_time("mean", "greenwich")
|
|
296
|
+
.hour
|
|
297
|
+
* 15,
|
|
298
|
+
chunks=(self.time_fine_chunk,),
|
|
299
|
+
)
|
|
300
|
+
self.gha = (self.gsa - self.ra) % 360
|
|
301
|
+
self.lsa = (self.gsa + longitude) % 360
|
|
302
|
+
self.lha = (((self.gha + longitude) % 360 - 180) % 360) - 180
|
|
303
|
+
self.altaz = da.asarray(alt_az_of_source(self.lsa.compute(), latitude, ra, dec))
|
|
304
|
+
|
|
305
|
+
self.freqs = da.asarray(freqs).rechunk((self.freq_chunk,))
|
|
306
|
+
self.chan_width = da.diff(freqs)[0] if len(freqs) > 1 else chan_width
|
|
307
|
+
self.n_freq = len(freqs)
|
|
308
|
+
self.lamda = 299792458.0 / self.freqs
|
|
309
|
+
|
|
310
|
+
self.SEFD = da.asarray(SEFD) * da.ones(self.n_freq, chunks=(self.freq_chunk,))
|
|
311
|
+
self.noise_std = SEFD_to_noise_std(self.SEFD, self.chan_width, self.int_time)
|
|
312
|
+
|
|
313
|
+
self.dish_d = da.asarray(dish_d)
|
|
314
|
+
self.fov = beam_size(dish_d, freqs.max(), fwhp=False)
|
|
315
|
+
|
|
316
|
+
self.ants_uvw = ITRF_to_UVW(self.ITRF, self.gha, self.dec)
|
|
317
|
+
|
|
318
|
+
if no_w:
|
|
319
|
+
self.ants_uvw[:, :, -1] = 0.0
|
|
320
|
+
|
|
321
|
+
self.bl_uvw = self.ants_uvw[:, self.a1, :] - self.ants_uvw[:, self.a2, :]
|
|
322
|
+
self.mag_uvw = da.linalg.norm(self.bl_uvw[0], axis=-1)
|
|
323
|
+
self.syn_bw = beam_size(self.mag_uvw.max().compute(), freqs.max())
|
|
324
|
+
|
|
325
|
+
self.ants_xyz = ITRF_to_XYZ(self.ITRF, self.gsa)
|
|
326
|
+
self.vis_rfi = da.zeros(
|
|
327
|
+
shape=(self.n_time, self.n_bl, self.n_freq),
|
|
328
|
+
chunks=(self.time_chunk, self.bl_chunk, self.freq_chunk),
|
|
329
|
+
dtype=jnp.complex128,
|
|
330
|
+
)
|
|
331
|
+
self.vis_ast = da.zeros(
|
|
332
|
+
shape=(self.n_time, self.n_bl, self.n_freq),
|
|
333
|
+
chunks=(self.time_chunk, self.bl_chunk, self.freq_chunk),
|
|
334
|
+
dtype=jnp.complex128,
|
|
335
|
+
)
|
|
336
|
+
self.gains_ants = da.ones(
|
|
337
|
+
shape=(self.n_time, self.n_ant, self.n_freq),
|
|
338
|
+
chunks=(self.time_chunk, self.ant_chunk, self.freq_chunk),
|
|
339
|
+
dtype=jnp.complex128,
|
|
340
|
+
)
|
|
341
|
+
self.random_seed = random_seed
|
|
342
|
+
|
|
343
|
+
self.n_ast = 0
|
|
344
|
+
self.n_p_ast = 0
|
|
345
|
+
self.n_g_ast = 0
|
|
346
|
+
self.n_e_ast = 0
|
|
347
|
+
self.n_rfi = 0
|
|
348
|
+
self.n_rfi_satellite = 0
|
|
349
|
+
self.n_rfi_stationary = 0
|
|
350
|
+
self.n_rfi_tle_satellite = 0
|
|
351
|
+
|
|
352
|
+
self.create_source_dicts()
|
|
353
|
+
|
|
354
|
+
def __str__(self):
|
|
355
|
+
msg = """
|
|
356
|
+
Observation Details
|
|
357
|
+
-------------------
|
|
358
|
+
Phase Centre (ra, dec) : ({ra:.1f}, {dec:.1f}) deg.
|
|
359
|
+
Local Hour Angle range : ({lha_min:.1f}, {lha_max:.1f}) deg.
|
|
360
|
+
Source Altitude range : ({alt_min:.1f}, {alt_max:.1f}) deg.
|
|
361
|
+
Number of antennas : {n_ant}
|
|
362
|
+
Number of baselines : {n_bl}
|
|
363
|
+
Autocorrelations : {auto_corrs}
|
|
364
|
+
|
|
365
|
+
Frequency range : ({freq_min:.0f} - {freq_max:.0f}) MHz
|
|
366
|
+
Channel width : {chan_width:.0f} kHz
|
|
367
|
+
Number of channels : {n_freq}
|
|
368
|
+
|
|
369
|
+
Observation time : ({time_min} - {time_max})
|
|
370
|
+
Integration time : {int_time:.0f} s
|
|
371
|
+
Sampling rate : {sampling_rate:.1f} Hz
|
|
372
|
+
Number of time steps : {n_time}
|
|
373
|
+
|
|
374
|
+
Source Details
|
|
375
|
+
--------------
|
|
376
|
+
Number of ast. sources : {n_ast}
|
|
377
|
+
Number of RFI sources : {n_rfi}
|
|
378
|
+
Number of satellite RFI : {n_sat}
|
|
379
|
+
Number of stationary RFI : {n_stat}"""
|
|
380
|
+
|
|
381
|
+
params = {
|
|
382
|
+
"ra": self.ra,
|
|
383
|
+
"dec": self.dec,
|
|
384
|
+
"n_ant": self.n_ant,
|
|
385
|
+
"n_bl": self.n_bl,
|
|
386
|
+
"auto_corrs": self.auto_corrs,
|
|
387
|
+
"freq_min": self.freqs.min() / 1e6,
|
|
388
|
+
"freq_max": self.freqs.max() / 1e6,
|
|
389
|
+
"time_min": Time(self.times_mjd.min(), format="mjd").strftime(
|
|
390
|
+
"%Y-%m-%d %H:%M:%S"
|
|
391
|
+
),
|
|
392
|
+
"time_max": Time(self.times_mjd.max(), format="mjd").strftime(
|
|
393
|
+
"%Y-%m-%d %H:%M:%S"
|
|
394
|
+
),
|
|
395
|
+
"lha_min": self.lha.min(),
|
|
396
|
+
"lha_max": self.lha.max(),
|
|
397
|
+
"alt_min": self.altaz[:, 0].min(),
|
|
398
|
+
"alt_max": self.altaz[:, 0].max(),
|
|
399
|
+
"chan_width": self.chan_width / 1e3,
|
|
400
|
+
"n_freq": self.n_freq,
|
|
401
|
+
"int_time": self.int_time,
|
|
402
|
+
"n_time": self.n_time,
|
|
403
|
+
"sampling_rate": self.n_int_samples / self.int_time,
|
|
404
|
+
}
|
|
405
|
+
params = {
|
|
406
|
+
k: v.compute() if isinstance(v, da.Array) else v for k, v in params.items()
|
|
407
|
+
}
|
|
408
|
+
params.update(
|
|
409
|
+
{
|
|
410
|
+
"n_ast": self.n_ast,
|
|
411
|
+
"n_rfi": self.n_rfi,
|
|
412
|
+
"n_sat": self.n_rfi_satellite,
|
|
413
|
+
"n_tle_sat": self.n_rfi_tle_satellite,
|
|
414
|
+
"n_stat": self.n_rfi_stationary,
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
return super().__str__() + msg.format(**params)
|
|
419
|
+
|
|
420
|
+
def create_source_dicts(self):
|
|
421
|
+
self.ast_p_I = []
|
|
422
|
+
self.ast_p_lmn = []
|
|
423
|
+
self.ast_p_radec = []
|
|
424
|
+
|
|
425
|
+
self.ast_g_I = []
|
|
426
|
+
self.ast_g_lmn = []
|
|
427
|
+
self.ast_g_radec = []
|
|
428
|
+
self.ast_g_major = []
|
|
429
|
+
self.ast_g_minor = []
|
|
430
|
+
self.ast_g_pos_angle = []
|
|
431
|
+
|
|
432
|
+
self.ast_e_I = []
|
|
433
|
+
self.ast_e_lmn = []
|
|
434
|
+
self.ast_e_radec = []
|
|
435
|
+
self.ast_e_major = []
|
|
436
|
+
|
|
437
|
+
self.rfi_satellite_xyz = []
|
|
438
|
+
self.rfi_satellite_orbit = []
|
|
439
|
+
self.rfi_satellite_ang_sep = []
|
|
440
|
+
self.rfi_satellite_A_app = []
|
|
441
|
+
|
|
442
|
+
self.rfi_tle_satellite_xyz = []
|
|
443
|
+
self.rfi_tle_satellite_orbit = []
|
|
444
|
+
self.rfi_tle_satellite_ang_sep = []
|
|
445
|
+
self.rfi_tle_satellite_A_app = []
|
|
446
|
+
self.norad_ids = []
|
|
447
|
+
|
|
448
|
+
self.rfi_stationary_xyz = []
|
|
449
|
+
self.rfi_stationary_geo = []
|
|
450
|
+
self.rfi_stationary_ang_sep = []
|
|
451
|
+
self.rfi_stationary_A_app = []
|
|
452
|
+
|
|
453
|
+
def addAstro(self, I: Array, ra: Array, dec: Array):
|
|
454
|
+
"""
|
|
455
|
+
Add a set of astronomical sources to the observation.
|
|
456
|
+
|
|
457
|
+
Parameters
|
|
458
|
+
----------
|
|
459
|
+
I: ndarray (n_src, n_time, n_freq) or
|
|
460
|
+
Intensity of the sources in Jy. If I.ndim==2, then this is assumed
|
|
461
|
+
to the spectrogram (n_time, n_freq) of a single source. If
|
|
462
|
+
I.ndim==1, then this is assumed to be the spectral profile of a
|
|
463
|
+
single source.
|
|
464
|
+
ra: array (n_src,)
|
|
465
|
+
Right ascension of the sources in degrees.
|
|
466
|
+
dec: array (n_src,)
|
|
467
|
+
Declination of the sources in degrees.
|
|
468
|
+
"""
|
|
469
|
+
I = da.atleast_2d(I)
|
|
470
|
+
if I.ndim == 2:
|
|
471
|
+
I = da.expand_dims(I, axis=0)
|
|
472
|
+
I = I * da.ones(
|
|
473
|
+
shape=(I.shape[0], self.n_time, self.n_freq),
|
|
474
|
+
chunks=(I.shape[0], self.time_chunk, self.freq_chunk),
|
|
475
|
+
)
|
|
476
|
+
ra = da.atleast_1d(ra)
|
|
477
|
+
dec = da.atleast_1d(dec)
|
|
478
|
+
lmn = radec_to_lmn(ra, dec, [self.ra, self.dec])
|
|
479
|
+
theta = da.rad2deg(da.arcsin(da.linalg.norm(lmn[:, :-1], axis=-1)))
|
|
480
|
+
I_app = (
|
|
481
|
+
I
|
|
482
|
+
* (airy_beam(theta[:, None, None], self.freqs, self.dish_d)[:, :, 0, :])
|
|
483
|
+
** 2
|
|
484
|
+
)
|
|
485
|
+
vis_ast = astro_vis(I_app, self.bl_uvw[self.t_idx], lmn, self.freqs)
|
|
486
|
+
|
|
487
|
+
self.ast_p_I.append(I)
|
|
488
|
+
self.ast_p_lmn.append(lmn)
|
|
489
|
+
self.ast_p_radec.append(jnp.array([ra, dec]))
|
|
490
|
+
self.n_p_ast += len(I)
|
|
491
|
+
self.n_ast += len(I)
|
|
492
|
+
|
|
493
|
+
self.vis_ast += vis_ast
|
|
494
|
+
|
|
495
|
+
def addAstroGauss(
|
|
496
|
+
self,
|
|
497
|
+
I: Array,
|
|
498
|
+
major: Array,
|
|
499
|
+
minor: Array,
|
|
500
|
+
pos_angle: Array,
|
|
501
|
+
ra: Array,
|
|
502
|
+
dec: Array,
|
|
503
|
+
):
|
|
504
|
+
"""
|
|
505
|
+
Add a set of astronomical sources to the observation.
|
|
506
|
+
|
|
507
|
+
Parameters
|
|
508
|
+
----------
|
|
509
|
+
I: Array (n_src, n_time, n_freq) or
|
|
510
|
+
Intensity of the sources in Jy. If I.ndim==2, then this is assumed
|
|
511
|
+
to the spectrogram (n_time, n_freq) of a single source. If
|
|
512
|
+
I.ndim==1, then this is assumed to be the spectral profile of a
|
|
513
|
+
single source.
|
|
514
|
+
major: Array (n_src,)
|
|
515
|
+
FWHM of major axis of sources in arcseconds.
|
|
516
|
+
major: Array (n_src,)
|
|
517
|
+
FWHM of minor axis of sources in arcseconds.
|
|
518
|
+
pos_angle: Array (n_src,)
|
|
519
|
+
Position angle of sources in degrees west of north for the major axis.
|
|
520
|
+
ra: Array (n_src,)
|
|
521
|
+
Right ascension of the sources in degrees.
|
|
522
|
+
dec: Array (n_src,)
|
|
523
|
+
Declination of the sources in degrees.
|
|
524
|
+
"""
|
|
525
|
+
I = da.atleast_2d(I)
|
|
526
|
+
if I.ndim == 2:
|
|
527
|
+
I = da.expand_dims(I, axis=0)
|
|
528
|
+
I = I * da.ones(
|
|
529
|
+
shape=(I.shape[0], self.n_time, self.n_freq),
|
|
530
|
+
chunks=(I.shape[0], self.time_chunk, self.freq_chunk),
|
|
531
|
+
)
|
|
532
|
+
major = da.atleast_1d(major)
|
|
533
|
+
minor = da.atleast_1d(minor)
|
|
534
|
+
pos_angle = da.atleast_1d(pos_angle)
|
|
535
|
+
ra = da.atleast_1d(ra)
|
|
536
|
+
dec = da.atleast_1d(dec)
|
|
537
|
+
lmn = radec_to_lmn(ra, dec, [self.ra, self.dec])
|
|
538
|
+
theta = da.rad2deg(da.arcsin(da.linalg.norm(lmn[:, :-1], axis=-1)))
|
|
539
|
+
I_app = (
|
|
540
|
+
I
|
|
541
|
+
* (airy_beam(theta[:, None, None], self.freqs, self.dish_d)[:, :, 0, :])
|
|
542
|
+
** 2
|
|
543
|
+
)
|
|
544
|
+
vis_ast = astro_vis_gauss(
|
|
545
|
+
I_app, major, minor, pos_angle, self.bl_uvw[self.t_idx], lmn, self.freqs
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
self.ast_g_major.append(major)
|
|
549
|
+
self.ast_g_minor.append(minor)
|
|
550
|
+
self.ast_g_pos_angle.append(pos_angle)
|
|
551
|
+
self.ast_g_I.append(I)
|
|
552
|
+
self.ast_g_lmn.append(lmn)
|
|
553
|
+
self.ast_g_radec.append(jnp.array([ra, dec]))
|
|
554
|
+
self.n_g_ast += len(I)
|
|
555
|
+
self.n_ast += len(I)
|
|
556
|
+
|
|
557
|
+
self.vis_ast += vis_ast
|
|
558
|
+
|
|
559
|
+
def addAstroExp(self, I: Array, shape: Array, ra: Array, dec: Array):
|
|
560
|
+
"""
|
|
561
|
+
Add a set of astronomical sources to the observation.
|
|
562
|
+
|
|
563
|
+
Parameters
|
|
564
|
+
----------
|
|
565
|
+
I: Array (n_src, n_time, n_freq) or
|
|
566
|
+
Intensity of the sources in Jy. If I.ndim==2, then this is assumed
|
|
567
|
+
to the spectrogram (n_time, n_freq) of a single source. If
|
|
568
|
+
I.ndim==1, then this is assumed to be the spectral profile of a
|
|
569
|
+
single source.
|
|
570
|
+
shape: array (n_src,)
|
|
571
|
+
Shape of gaussian sources. Only circular gaussians accepted for now.
|
|
572
|
+
ra: array (n_src,)
|
|
573
|
+
Right ascension of the sources in degrees.
|
|
574
|
+
dec: array (n_src,)
|
|
575
|
+
Declination of the sources in degrees.
|
|
576
|
+
"""
|
|
577
|
+
I = da.atleast_2d(I)
|
|
578
|
+
if I.ndim == 2:
|
|
579
|
+
I = da.expand_dims(I, axis=0)
|
|
580
|
+
I = I * da.ones(
|
|
581
|
+
shape=(I.shape[0], self.n_time, self.n_freq),
|
|
582
|
+
chunks=(I.shape[0], self.time_chunk, self.freq_chunk),
|
|
583
|
+
)
|
|
584
|
+
shape = da.atleast_1d(shape)
|
|
585
|
+
ra = da.atleast_1d(ra)
|
|
586
|
+
dec = da.atleast_1d(dec)
|
|
587
|
+
lmn = radec_to_lmn(ra, dec, [self.ra, self.dec])
|
|
588
|
+
theta = da.rad2deg(da.arcsin(da.linalg.norm(lmn[:, :-1], axis=-1)))
|
|
589
|
+
I_app = (
|
|
590
|
+
I
|
|
591
|
+
* (airy_beam(theta[:, None, None], self.freqs, self.dish_d)[:, :, 0, :])
|
|
592
|
+
** 2
|
|
593
|
+
)
|
|
594
|
+
vis_ast = astro_vis_exp(I_app, shape, self.bl_uvw[self.t_idx], lmn, self.freqs)
|
|
595
|
+
|
|
596
|
+
self.ast_e_major.append(shape)
|
|
597
|
+
self.ast_e_I.append(I)
|
|
598
|
+
self.ast_e_lmn.append(lmn)
|
|
599
|
+
self.ast_e_radec.append(jnp.array([ra, dec]))
|
|
600
|
+
self.n_e_ast += len(I)
|
|
601
|
+
self.n_ast += len(I)
|
|
602
|
+
|
|
603
|
+
self.vis_ast += vis_ast
|
|
604
|
+
|
|
605
|
+
def addSatelliteRFI(
|
|
606
|
+
self,
|
|
607
|
+
Pv: Array,
|
|
608
|
+
elevation: Array,
|
|
609
|
+
inclination: Array,
|
|
610
|
+
lon_asc_node: Array,
|
|
611
|
+
periapsis: Array,
|
|
612
|
+
):
|
|
613
|
+
"""
|
|
614
|
+
Add a satellite-based source of RFI to the observation.
|
|
615
|
+
|
|
616
|
+
Parameters
|
|
617
|
+
----------
|
|
618
|
+
Pv: ndarray (n_src, n_time_fine, n_freq)
|
|
619
|
+
Specific Emission Power in W/Hz. If Pv.ndim==1, it is assumed to be
|
|
620
|
+
of shape (n_freq,) and is the spectrum of a single RFI source. If
|
|
621
|
+
Pv.ndim==2, it is assumed to be of shape (n_time_fine, n_freq) and
|
|
622
|
+
is the spectrogram of a single RFI source.
|
|
623
|
+
elevation: ndarray (n_src,)
|
|
624
|
+
Elevation/Altitude of the orbit in metres.
|
|
625
|
+
inclination: ndarray (n_src,)
|
|
626
|
+
Inclination angle of the orbit relative to the equatorial plane.
|
|
627
|
+
lon_asc_node: ndarray (n_src,)
|
|
628
|
+
Longitude of the ascending node of the orbit. This is the longitude of
|
|
629
|
+
when the orbit crosses the equator from the south to the north.
|
|
630
|
+
periapsis: ndarray (n_src,)
|
|
631
|
+
Perisapsis of the orbit. This is the angular starting point of the orbit
|
|
632
|
+
at t = 0.
|
|
633
|
+
"""
|
|
634
|
+
Pv = da.atleast_2d(Pv)
|
|
635
|
+
if Pv.ndim == 2:
|
|
636
|
+
Pv = da.expand_dims(Pv, axis=0)
|
|
637
|
+
Pv = (
|
|
638
|
+
Pv
|
|
639
|
+
* da.ones(
|
|
640
|
+
shape=(1, self.n_time_fine, self.n_freq),
|
|
641
|
+
)
|
|
642
|
+
).rechunk((-1, self.time_fine_chunk, self.freq_chunk))
|
|
643
|
+
elevation = da.asarray(da.atleast_1d(elevation), chunks=(-1,))
|
|
644
|
+
inclination = da.asarray(da.atleast_1d(inclination), chunks=(-1,))
|
|
645
|
+
lon_asc_node = da.asarray(da.atleast_1d(lon_asc_node), chunks=(-1,))
|
|
646
|
+
periapsis = da.asarray(da.atleast_1d(periapsis), chunks=(-1,))
|
|
647
|
+
|
|
648
|
+
rfi_xyz = orbit_vmap(
|
|
649
|
+
self.times_fine, elevation, inclination, lon_asc_node, periapsis
|
|
650
|
+
)
|
|
651
|
+
# rfi_xyz is shape (n_src,n_time_fine,3)
|
|
652
|
+
# self.ants_xyz is shape (n_time_fine,n_ant,3)
|
|
653
|
+
distances = da.linalg.norm(
|
|
654
|
+
self.ants_xyz[None, :, :, :] - rfi_xyz[:, :, None, :], axis=-1
|
|
655
|
+
)
|
|
656
|
+
# distances is shape (n_src,n_time_fine,n_ant)
|
|
657
|
+
I = Pv_to_Sv(Pv, distances)
|
|
658
|
+
# I is shape (n_src,n_time_fine,n_ant,n_freq)
|
|
659
|
+
|
|
660
|
+
angular_seps = angular_separation(rfi_xyz, self.ants_xyz, self.ra, self.dec)
|
|
661
|
+
|
|
662
|
+
# angular_seps is shape (n_src,n_time_fine,n_ant)
|
|
663
|
+
rfi_A_app = da.sqrt(da.abs(I)) * airy_beam(
|
|
664
|
+
angular_seps, self.freqs, self.dish_d
|
|
665
|
+
)
|
|
666
|
+
# self.rfi_A_app is shape (n_src,n_time_fine,n_ant,n_freqs)
|
|
667
|
+
# distances is shape (n_src,n_time_fine,n_ant)
|
|
668
|
+
# self.ants_uvw is shape (n_time_fine,n_ant,3)
|
|
669
|
+
|
|
670
|
+
vis_rfi = rfi_vis(
|
|
671
|
+
rfi_A_app.reshape(
|
|
672
|
+
(-1, self.n_time, self.n_int_samples, self.n_ant, self.n_freq)
|
|
673
|
+
),
|
|
674
|
+
(distances + self.ants_uvw[None, :, :, -1]).reshape(
|
|
675
|
+
(-1, self.n_time, self.n_int_samples, self.n_ant)
|
|
676
|
+
),
|
|
677
|
+
self.freqs,
|
|
678
|
+
self.a1,
|
|
679
|
+
self.a2,
|
|
680
|
+
)
|
|
681
|
+
self.vis_rfi += vis_rfi
|
|
682
|
+
|
|
683
|
+
orbits = da.stack([elevation, inclination, lon_asc_node, periapsis], axis=1)
|
|
684
|
+
|
|
685
|
+
self.rfi_satellite_xyz.append(rfi_xyz)
|
|
686
|
+
self.rfi_satellite_orbit.append(orbits)
|
|
687
|
+
self.rfi_satellite_ang_sep.append(angular_seps)
|
|
688
|
+
self.rfi_satellite_A_app.append(rfi_A_app)
|
|
689
|
+
self.n_rfi_satellite += len(I)
|
|
690
|
+
self.n_rfi += len(I)
|
|
691
|
+
|
|
692
|
+
def addTLESatelliteRFI(
|
|
693
|
+
self,
|
|
694
|
+
Pv: Array,
|
|
695
|
+
norad_ids: list[int],
|
|
696
|
+
tles: Array,
|
|
697
|
+
):
|
|
698
|
+
"""
|
|
699
|
+
Add a satellite-based source of RFI to the observation.
|
|
700
|
+
|
|
701
|
+
Parameters
|
|
702
|
+
----------
|
|
703
|
+
Pv: ndarray (n_src, n_time_fine, n_freq)
|
|
704
|
+
Specific Emission Power in W/Hz. If Pv.ndim==1, it is assumed to be
|
|
705
|
+
of shape (n_freq,) and is the spectrum of a single RFI source. If
|
|
706
|
+
Pv.ndim==2, it is assumed to be of shape (n_time_fine, n_freq) and
|
|
707
|
+
is the spectrogram of a single RFI source.
|
|
708
|
+
norad_ids: list[int] (n_src,)
|
|
709
|
+
NORAD IDs for the satellites to include.
|
|
710
|
+
tles: Array (n_src, 2)
|
|
711
|
+
TLEs of the satellites corresponding to the NORAD IDs.
|
|
712
|
+
"""
|
|
713
|
+
Pv = da.atleast_2d(Pv)
|
|
714
|
+
if Pv.ndim == 2:
|
|
715
|
+
Pv = da.expand_dims(Pv, axis=0)
|
|
716
|
+
Pv = (
|
|
717
|
+
Pv
|
|
718
|
+
* da.ones(
|
|
719
|
+
shape=(1, self.n_time_fine, self.n_freq),
|
|
720
|
+
)
|
|
721
|
+
).rechunk((-1, self.time_fine_chunk, self.freq_chunk))
|
|
722
|
+
norad_ids = da.asarray(da.atleast_1d(norad_ids), chunks=(-1,))
|
|
723
|
+
|
|
724
|
+
rfi_xyz = da.asarray(
|
|
725
|
+
get_satellite_positions(tles, mjd_to_jd(self.times_mjd_fine.compute())),
|
|
726
|
+
chunks=(-1, self.time_fine_chunk, 3),
|
|
727
|
+
)
|
|
728
|
+
tles = da.asarray(da.atleast_2d(tles), chunks=(-1,))
|
|
729
|
+
# rfi_xyz is shape (n_src,n_time_fine,3)
|
|
730
|
+
# self.ants_xyz is shape (n_time_fine,n_ant,3)
|
|
731
|
+
distances = da.linalg.norm(
|
|
732
|
+
self.ants_xyz[None, :, :, :] - rfi_xyz[:, :, None, :], axis=-1
|
|
733
|
+
)
|
|
734
|
+
# distances = da.asarray(da.linalg.norm(
|
|
735
|
+
# ants_pos(self.ITRF.compute(), mjd_to_jd(self.times_mjd_fine.compute()))[None, :, :, :] - rfi_xyz[:, :, None, :], axis=-1
|
|
736
|
+
# ), chunks=(-1, self.time_fine_chunk, self.n_ant))
|
|
737
|
+
# distances is shape (n_src,n_time_fine,n_ant)
|
|
738
|
+
I = Pv_to_Sv(Pv, distances)
|
|
739
|
+
# I is shape (n_src,n_time_fine,n_ant,n_freq)
|
|
740
|
+
|
|
741
|
+
angular_seps = angular_separation(rfi_xyz, self.ants_xyz, self.ra, self.dec)
|
|
742
|
+
|
|
743
|
+
# angular_seps is shape (n_src,n_time_fine,n_ant)
|
|
744
|
+
rfi_A_app = da.sqrt(da.abs(I)) * airy_beam(
|
|
745
|
+
angular_seps, self.freqs, self.dish_d
|
|
746
|
+
)
|
|
747
|
+
# rfi_A_app = da.ones((Pv.shape[0], self.n_time_fine, self.n_ant, self.n_freq), chunks=(-1, self.time_fine_chunk, self.n_ant, self.freq_chunk))
|
|
748
|
+
# self.rfi_A_app is shape (n_src,n_time_fine,n_ant,n_freqs)
|
|
749
|
+
# distances is shape (n_src,n_time_fine,n_ant)
|
|
750
|
+
# self.ants_uvw is shape (n_time_fine,n_ant,3)
|
|
751
|
+
|
|
752
|
+
vis_rfi = rfi_vis(
|
|
753
|
+
rfi_A_app.reshape(
|
|
754
|
+
(-1, self.n_time, self.n_int_samples, self.n_ant, self.n_freq)
|
|
755
|
+
),
|
|
756
|
+
(distances + self.ants_uvw[None, :, :, -1]).reshape(
|
|
757
|
+
(-1, self.n_time, self.n_int_samples, self.n_ant)
|
|
758
|
+
),
|
|
759
|
+
self.freqs,
|
|
760
|
+
self.a1,
|
|
761
|
+
self.a2,
|
|
762
|
+
)
|
|
763
|
+
self.vis_rfi += vis_rfi
|
|
764
|
+
|
|
765
|
+
self.rfi_tle_satellite_xyz.append(rfi_xyz)
|
|
766
|
+
self.rfi_tle_satellite_orbit.append(tles)
|
|
767
|
+
self.rfi_tle_satellite_ang_sep.append(angular_seps)
|
|
768
|
+
self.rfi_tle_satellite_A_app.append(rfi_A_app)
|
|
769
|
+
self.norad_ids.append(norad_ids)
|
|
770
|
+
self.n_rfi_tle_satellite += len(I)
|
|
771
|
+
self.n_rfi += len(I)
|
|
772
|
+
|
|
773
|
+
def addStationaryRFI(
|
|
774
|
+
self,
|
|
775
|
+
Pv: Array,
|
|
776
|
+
latitude: Array,
|
|
777
|
+
longitude: Array,
|
|
778
|
+
elevation: Array,
|
|
779
|
+
):
|
|
780
|
+
"""
|
|
781
|
+
Add a stationary source of RFI to the observation.
|
|
782
|
+
|
|
783
|
+
Parameters
|
|
784
|
+
----------
|
|
785
|
+
Pv: ndarray (n_src, n_time_fine, n_freq)
|
|
786
|
+
Specific Emission Power in W/Hz. If Pv.ndim==1, it is assumed to be
|
|
787
|
+
of shape (n_freq,) and is the spectrum of a single RFI source. If
|
|
788
|
+
Pv.ndim==2, it is assumed to be of shape (n_time_fine, n_freq) and
|
|
789
|
+
is the spectrogram of a single RFI source.
|
|
790
|
+
latitude: ndarray (n_src,)
|
|
791
|
+
Geopgraphic latitude of the source in degrees.
|
|
792
|
+
longitude: ndarray (n_src,)
|
|
793
|
+
Geographic longitude of the source in degrees.
|
|
794
|
+
elevation: ndarray (n_src,)
|
|
795
|
+
Elevation/Altitude of the source above sea level in metres.
|
|
796
|
+
"""
|
|
797
|
+
Pv = da.atleast_2d(Pv)
|
|
798
|
+
if Pv.ndim == 2:
|
|
799
|
+
Pv = da.expand_dims(Pv, axis=0)
|
|
800
|
+
Pv = (
|
|
801
|
+
Pv
|
|
802
|
+
* da.ones(
|
|
803
|
+
shape=(1, self.n_time_fine, self.n_freq),
|
|
804
|
+
)
|
|
805
|
+
).rechunk((-1, self.time_fine_chunk, self.freq_chunk))
|
|
806
|
+
latitude = da.asarray(da.atleast_1d(latitude), chunks=(-1,))
|
|
807
|
+
longitude = da.asarray(da.atleast_1d(longitude), chunks=(-1,))
|
|
808
|
+
elevation = da.asarray(da.atleast_1d(elevation), chunks=(-1,))
|
|
809
|
+
|
|
810
|
+
rfi_geo = (
|
|
811
|
+
da.stack([latitude, longitude, elevation], axis=1)[:, None, :]
|
|
812
|
+
* da.ones(shape=(1, self.n_time_fine, 3))
|
|
813
|
+
).rechunk(
|
|
814
|
+
(-1, self.time_fine_chunk, 3),
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
# rfi_geo is shape (n_src,n_time,3)
|
|
818
|
+
rfi_xyz = GEO_to_XYZ_vmap0(rfi_geo, self.times_fine)
|
|
819
|
+
# rfi_xyz is shape (n_src,n_time_fine,3)
|
|
820
|
+
# self.ants_xyz is shape (n_time_fine,n_ant,3)
|
|
821
|
+
distances = da.linalg.norm(
|
|
822
|
+
self.ants_xyz[None, :, :, :] - rfi_xyz[:, :, None, :], axis=-1
|
|
823
|
+
)
|
|
824
|
+
# distances is shape (n_src,n_time_fine,n_ant)
|
|
825
|
+
I = Pv_to_Sv(Pv, distances)
|
|
826
|
+
# I is shape (n_src,n_time,n_ant,n_freq)
|
|
827
|
+
|
|
828
|
+
angular_seps = angular_separation(rfi_xyz, self.ants_xyz, self.ra, self.dec)
|
|
829
|
+
rfi_A_app = da.sqrt(da.abs(I)) * airy_beam(
|
|
830
|
+
angular_seps, self.freqs, self.dish_d
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
# self.rfi_A_app is shape (n_src,n_time_fine,n_ant,n_freqs)
|
|
834
|
+
# self.ants_uvw is shape (n_time_fine,n_ant,3)
|
|
835
|
+
vis_rfi = rfi_vis(
|
|
836
|
+
rfi_A_app.reshape(
|
|
837
|
+
(-1, self.n_time, self.n_int_samples, self.n_ant, self.n_freq)
|
|
838
|
+
),
|
|
839
|
+
(distances + self.ants_uvw[None, :, :, -1]).reshape(
|
|
840
|
+
(-1, self.n_time, self.n_int_samples, self.n_ant)
|
|
841
|
+
),
|
|
842
|
+
self.freqs,
|
|
843
|
+
self.a1,
|
|
844
|
+
self.a2,
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
self.vis_rfi += vis_rfi
|
|
848
|
+
|
|
849
|
+
positions = da.stack([latitude, longitude, elevation], axis=1)
|
|
850
|
+
|
|
851
|
+
self.rfi_stationary_xyz.append(rfi_xyz)
|
|
852
|
+
self.rfi_stationary_geo.append(positions)
|
|
853
|
+
self.rfi_stationary_ang_sep.append(angular_seps)
|
|
854
|
+
self.rfi_stationary_A_app.append(rfi_A_app)
|
|
855
|
+
self.n_rfi_stationary += len(I)
|
|
856
|
+
self.n_rfi += len(I)
|
|
857
|
+
|
|
858
|
+
def addGains(
|
|
859
|
+
self,
|
|
860
|
+
G0_mean: float,
|
|
861
|
+
G0_std: float,
|
|
862
|
+
Gt_std_amp: float,
|
|
863
|
+
Gt_std_phase: float,
|
|
864
|
+
Gt_corr_amp: float,
|
|
865
|
+
Gt_corr_phase: float,
|
|
866
|
+
random_seed=None,
|
|
867
|
+
):
|
|
868
|
+
"""Add complex antenna gains to the simulation. Gain amplitudes and phases
|
|
869
|
+
are modelled as linear time-variates. Gains for all antennas at t = 0
|
|
870
|
+
are randomly sampled from a Gaussian described by the G0 parameters.
|
|
871
|
+
The rate of change of both ampltudes and phases are sampled from a zero
|
|
872
|
+
mean Gaussian with standard deviation as provided.
|
|
873
|
+
|
|
874
|
+
Parameters
|
|
875
|
+
----------
|
|
876
|
+
G0_mean: float
|
|
877
|
+
Mean of Gaussian at t = 0.
|
|
878
|
+
G0_std: float
|
|
879
|
+
Standard deviation of Gaussian at t = 0.
|
|
880
|
+
Gt_std_amp: float
|
|
881
|
+
Standard deviation of Gaussian describing the rate of change in the
|
|
882
|
+
gain amplitudes in 1/seconds.
|
|
883
|
+
Gt_std_phase: float
|
|
884
|
+
Standard deviation of Gaussian describing the rate of change in the
|
|
885
|
+
gain phases in rad/seconds.
|
|
886
|
+
random_seed: int, optional
|
|
887
|
+
Random number generator key.
|
|
888
|
+
"""
|
|
889
|
+
# self.gains_ants = generate_gains(
|
|
890
|
+
# G0_mean,
|
|
891
|
+
# G0_std,
|
|
892
|
+
# Gt_std_amp,
|
|
893
|
+
# Gt_std_phase,
|
|
894
|
+
# self.times,
|
|
895
|
+
# self.n_ant,
|
|
896
|
+
# self.n_freq,
|
|
897
|
+
# random_seed if random_seed else self.random_seed,
|
|
898
|
+
# ).rechunk((self.time_chunk, self.ant_chunk, self.freq_chunk))
|
|
899
|
+
self.gains_ants = generate_fourier_gains(
|
|
900
|
+
G0_mean,
|
|
901
|
+
G0_std,
|
|
902
|
+
Gt_std_amp,
|
|
903
|
+
Gt_std_phase,
|
|
904
|
+
Gt_corr_amp,
|
|
905
|
+
Gt_corr_phase,
|
|
906
|
+
self.times_mjd,
|
|
907
|
+
self.n_ant,
|
|
908
|
+
self.n_freq,
|
|
909
|
+
random_seed if random_seed else self.random_seed,
|
|
910
|
+
).rechunk((self.time_chunk, self.ant_chunk, self.freq_chunk))
|
|
911
|
+
|
|
912
|
+
def calculate_vis(self, flags: bool = True, random_seed=None):
|
|
913
|
+
"""
|
|
914
|
+
Calculate the total gain amplified visibilities, average down to the
|
|
915
|
+
originally defined sampling rate and add noise.
|
|
916
|
+
"""
|
|
917
|
+
self.vis_uncal = apply_gains(
|
|
918
|
+
self.vis_ast, self.vis_rfi, self.gains_ants, self.a1, self.a2
|
|
919
|
+
).rechunk((self.time_chunk, self.bl_chunk, self.freq_chunk))
|
|
920
|
+
self.vis_obs, self.noise_data = add_noise(
|
|
921
|
+
self.vis_uncal,
|
|
922
|
+
self.noise_std,
|
|
923
|
+
random_seed if random_seed else self.random_seed,
|
|
924
|
+
)
|
|
925
|
+
self.vis_cal = apply_gains(
|
|
926
|
+
self.vis_obs,
|
|
927
|
+
da.zeros_like(self.vis_obs),
|
|
928
|
+
1.0 / self.gains_ants,
|
|
929
|
+
self.a1,
|
|
930
|
+
self.a2,
|
|
931
|
+
)
|
|
932
|
+
if flags:
|
|
933
|
+
if self.noise_std.mean() > 0:
|
|
934
|
+
self.flags = (
|
|
935
|
+
da.abs(self.vis_cal - self.vis_ast)
|
|
936
|
+
> 3.0 * self.noise_std[None, None, :]
|
|
937
|
+
) # * da.sqrt(2)
|
|
938
|
+
else:
|
|
939
|
+
self.flags = (
|
|
940
|
+
da.abs(self.vis_cal - self.vis_ast)
|
|
941
|
+
> 3.0 * da.std(self.vis_model, axis=0)[None, ...]
|
|
942
|
+
)
|
|
943
|
+
else:
|
|
944
|
+
self.flags = da.zeros(shape=self.vis_cal.shape, dtype=bool)
|
|
945
|
+
|
|
946
|
+
self.dataset = construct_observation_ds(self)
|
|
947
|
+
return self.dataset
|
|
948
|
+
|
|
949
|
+
def write_to_zarr(self, path: str = "Observation", overwrite: bool = False):
|
|
950
|
+
"""
|
|
951
|
+
Write the visibilities to disk using zarr format.
|
|
952
|
+
"""
|
|
953
|
+
mode = "w" if overwrite else "w-"
|
|
954
|
+
self.dataset.to_zarr(path, mode=mode)
|
|
955
|
+
self.dataset = xr.open_zarr(path)
|
|
956
|
+
return self.dataset
|
|
957
|
+
|
|
958
|
+
def write_to_ms(
|
|
959
|
+
self,
|
|
960
|
+
path: str = "Observation.ms",
|
|
961
|
+
overwrite: bool = False,
|
|
962
|
+
ds: xr.Dataset = None,
|
|
963
|
+
):
|
|
964
|
+
"""
|
|
965
|
+
Write the visibilities to disk using Measurement Set format.
|
|
966
|
+
"""
|
|
967
|
+
if ds is None:
|
|
968
|
+
ds = self.dataset
|
|
969
|
+
write_ms(ds, path, overwrite=overwrite)
|