xradio 0.0.33__py3-none-any.whl → 0.0.36__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.
@@ -0,0 +1,479 @@
1
+ import graphviper.utils.logger as logger
2
+ import time
3
+ from typing import Tuple, Union
4
+
5
+ import numpy as np
6
+ import xarray as xr
7
+ import os
8
+
9
+ from xradio.vis._vis_utils._ms.subtables import subt_rename_ids
10
+ from xradio.vis._vis_utils._ms._tables.read import (
11
+ load_generic_table,
12
+ convert_casacore_time_to_mjd,
13
+ make_taql_where_between_min_max,
14
+ )
15
+ from xradio._utils.schema import convert_generic_xds_to_xradio_schema
16
+ from xradio.vis._vis_utils._ms.msv4_sub_xdss import interpolate_to_time
17
+
18
+ from xradio._utils.list_and_array import (
19
+ check_if_consistent,
20
+ unique_1d,
21
+ to_list,
22
+ to_np_array,
23
+ )
24
+
25
+
26
+ def create_antenna_xds(
27
+ in_file: str,
28
+ spectral_window_id: int,
29
+ antenna_id: list,
30
+ feed_id: list,
31
+ telescope_name: str,
32
+ time_min_max: Tuple[np.float64, np.float64],
33
+ phase_cal_interp_time: Union[xr.DataArray, None] = None,
34
+ ) -> xr.Dataset:
35
+ """
36
+ Create an Xarray Dataset containing antenna information.
37
+
38
+ Parameters
39
+ ----------
40
+ in_file : str
41
+ Path to the input MSv2.
42
+ spectral_window_id : int
43
+ Spectral window ID.
44
+ antenna_id : list
45
+ List of antenna IDs.
46
+ feed_id : list
47
+ List of feed IDs.
48
+ telescope_name : str
49
+ Name of the telescope.
50
+ time_min_max : Tuple[np.float46, np.float64]
51
+ Min / max times to constrain loading (usually to the time range relevant to an MSv4)
52
+ phase_cal_interp_time : Union[xr.DataArray, None]
53
+ Time axis to interpolate the data vars to (usually main MSv4 time)
54
+
55
+ Returns
56
+ ----------
57
+ xr.Dataset: Xarray Dataset containing the antenna information.
58
+ """
59
+ ant_xds = xr.Dataset(attrs={"type": "antenna"})
60
+
61
+ ant_xds = extract_antenna_info(ant_xds, in_file, antenna_id, telescope_name)
62
+
63
+ ant_xds = extract_feed_info(
64
+ ant_xds, in_file, antenna_id, feed_id, spectral_window_id
65
+ )
66
+
67
+ ant_xds = extract_phase_cal_info(
68
+ ant_xds, in_file, spectral_window_id, time_min_max, phase_cal_interp_time
69
+ ) # Only used in VLBI.
70
+ ant_xds = extract_gain_curve_info(
71
+ ant_xds, in_file, spectral_window_id
72
+ ) # Only used in VLBI.
73
+
74
+ ant_xds.attrs["overall_telescope_name"] = telescope_name
75
+ return ant_xds
76
+
77
+
78
+ def extract_antenna_info(
79
+ ant_xds: xr.Dataset, in_file: str, antenna_id: list, telescope_name: str
80
+ ) -> xr.Dataset:
81
+ """Reformats MSv2 Antenna table content to MSv4 schema.
82
+
83
+ Parameters
84
+ ----------
85
+ ant_xds : xr.Dataset
86
+ The dataset that will be updated with antenna information.
87
+ in_file : str
88
+ Path to the input MSv2.
89
+ antenna_id : list
90
+ A list of antenna IDs to extract information for.
91
+ telescope_name : str
92
+ The name of the telescope.
93
+
94
+ Returns
95
+ -------
96
+ xr.Dataset
97
+ Dataset updated to contain the antenna information.
98
+ """
99
+ to_new_data_variables = {
100
+ "POSITION": ["ANTENNA_POSITION", ["antenna_name", "cartesian_pos_label"]],
101
+ "OFFSET": ["ANTENNA_FEED_OFFSET", ["antenna_name", "cartesian_pos_label"]],
102
+ "DISH_DIAMETER": ["ANTENNA_DISH_DIAMETER", ["antenna_name"]],
103
+ }
104
+
105
+ to_new_coords = {
106
+ "NAME": ["antenna_name", ["antenna_name"]],
107
+ "STATION": ["station", ["antenna_name"]],
108
+ "MOUNT": ["mount", ["antenna_name"]],
109
+ "PHASED_ARRAY_ID": ["phased_array_id", ["antenna_name"]],
110
+ "antenna_id": ["antenna_id", ["antenna_name"]],
111
+ }
112
+
113
+ # Read ANTENNA table into a Xarray Dataset.
114
+ unique_antenna_id = unique_1d(
115
+ antenna_id
116
+ ) # Also ensures that it is sorted otherwise TaQL will give wrong results.
117
+
118
+ generic_ant_xds = load_generic_table(
119
+ in_file,
120
+ "ANTENNA",
121
+ rename_ids=subt_rename_ids["ANTENNA"],
122
+ taql_where=f" where (ROWID() IN [{','.join(map(str,unique_antenna_id))}])", # order is not guaranteed
123
+ )
124
+ generic_ant_xds = generic_ant_xds.assign_coords({"antenna_id": unique_antenna_id})
125
+ generic_ant_xds = generic_ant_xds.sel(
126
+ antenna_id=antenna_id, drop=False
127
+ ) # Make sure the antenna_id order is correct.
128
+
129
+ # ['OFFSET', 'POSITION', 'DISH_DIAMETER', 'FLAG_ROW', 'MOUNT', 'NAME', 'STATION']
130
+ ant_xds = xr.Dataset()
131
+ ant_xds = ant_xds.assign_coords({"cartesian_pos_label": ["x", "y", "z"]})
132
+
133
+ ant_xds = convert_generic_xds_to_xradio_schema(
134
+ generic_ant_xds, ant_xds, to_new_data_variables, to_new_coords
135
+ )
136
+
137
+ ant_xds["ANTENNA_DISH_DIAMETER"].attrs.update({"units": ["m"], "type": "quantity"})
138
+
139
+ ant_xds["ANTENNA_FEED_OFFSET"].attrs["type"] = "earth_location_offset"
140
+ ant_xds["ANTENNA_FEED_OFFSET"].attrs["coordinate_system"] = "geocentric"
141
+ ant_xds["ANTENNA_POSITION"].attrs["coordinate_system"] = "geocentric"
142
+
143
+ if telescope_name in ["ALMA", "VLA", "NOEMA", "EVLA"]:
144
+ # antenna_name = ant_xds["antenna_name"].values + "_" + ant_xds["station"].values
145
+ # works on laptop but fails in github test runner with error:
146
+ # numpy.core._exceptions._UFuncNoLoopError: ufunc 'add' did not contain a loop with signature matching types (dtype('<U4'), dtype('<U4')) -> None
147
+
148
+ # Also doesn't work on github test runner:
149
+ # antenna_name = ant_xds["antenna_name"].values
150
+ # antenna_name = np._core.defchararray.add(antenna_name, "_")
151
+ # antenna_name = np._core.defchararray.add(
152
+ # antenna_name,
153
+ # ant_xds["station"].values,
154
+ # )
155
+
156
+ # None of the native numpy functions work on the github test runner.
157
+ antenna_name = ant_xds["antenna_name"].values
158
+ station = ant_xds["station"].values
159
+ antenna_name = np.array(
160
+ list(map(lambda x, y: x + "_" + y, antenna_name, station))
161
+ )
162
+
163
+ ant_xds["antenna_name"] = xr.DataArray(antenna_name, dims=["antenna_name"])
164
+ ant_xds.attrs["relocatable_antennas"] = True
165
+ else:
166
+ ant_xds.attrs["relocatable_antennas"] = False
167
+
168
+ return ant_xds
169
+
170
+
171
+ def extract_feed_info(
172
+ ant_xds: xr.Dataset,
173
+ in_file: str,
174
+ antenna_id: list,
175
+ feed_id: int,
176
+ spectral_window_id: int,
177
+ ) -> xr.Dataset:
178
+ """
179
+ Reformats MSv2 Feed table content to MSv4 schema.
180
+
181
+ Parameters
182
+ ----------
183
+ ant_xds : xr.Dataset
184
+ Xarray Dataset containing antenna information.
185
+ in_file : str
186
+ Path to the input MSv2.
187
+ antenna_id : list
188
+ List of antenna IDs.
189
+ feed_id : int
190
+ Feed ID.
191
+ spectral_window_id : int
192
+ Spectral window ID.
193
+
194
+ Returns
195
+ -------
196
+ xr.Dataset
197
+ Dataset updated to contain the feed information.
198
+ """
199
+
200
+ # Extract feed information
201
+ generic_feed_xds = load_generic_table(
202
+ in_file,
203
+ "FEED",
204
+ rename_ids=subt_rename_ids["FEED"],
205
+ taql_where=f" where (ANTENNA_ID IN [{','.join(map(str, ant_xds.antenna_id.values))}]) AND (FEED_ID IN [{','.join(map(str, feed_id))}])",
206
+ ) # Some Lofar and MeerKAT data have the spw column set to -1 so we can't use '(SPECTRAL_WINDOW_ID = {spectral_window_id})'
207
+
208
+ feed_spw = np.unique(generic_feed_xds.SPECTRAL_WINDOW_ID)
209
+ if len(feed_spw) == 1 and feed_spw[0] == -1:
210
+ generic_feed_xds = generic_feed_xds.isel(SPECTRAL_WINDOW_ID=0, drop=True)
211
+ else:
212
+ if spectral_window_id not in feed_spw:
213
+ return ant_xds # For some spw the feed table is empty (this is the case with ALMA spw WVR#NOMINAL).
214
+ else:
215
+ generic_feed_xds = generic_feed_xds.sel(
216
+ SPECTRAL_WINDOW_ID=spectral_window_id, drop=True
217
+ )
218
+
219
+ assert len(generic_feed_xds.TIME) == len(
220
+ antenna_id
221
+ ), "Can only process feed table with a single time entry for an feed, antenna and spectral_window_id."
222
+ generic_feed_xds = generic_feed_xds.sel(
223
+ ANTENNA_ID=antenna_id, drop=False
224
+ ) # Make sure the antenna_id is in the same order as the xds.
225
+
226
+ num_receptors = np.ravel(generic_feed_xds.NUM_RECEPTORS)
227
+ num_receptors = unique_1d(num_receptors[~np.isnan(num_receptors)])
228
+
229
+ assert (
230
+ len(num_receptors) == 1
231
+ ), "The number of receptors must be constant in feed table."
232
+
233
+ to_new_data_variables = {
234
+ "BEAM_OFFSET": [
235
+ "BEAM_OFFSET",
236
+ ["antenna_name", "receptor_label", "sky_dir_label"],
237
+ ],
238
+ "RECEPTOR_ANGLE": ["RECEPTOR_ANGLE", ["antenna_name", "receptor_label"]],
239
+ # "pol_response": ["POLARIZATION_RESPONSE", ["antenna_name", "receptor_label", "receptor_name_"]] #repeated dim creates problems.
240
+ "FOCUS_LENGTH": ["FOCUS_LENGTH", ["antenna_name"]], # optional
241
+ # "position": ["ANTENNA_FEED_OFFSET",["antenna_name", "cartesian_pos_label"]] #Will be added to the existing position in ant_xds
242
+ }
243
+
244
+ to_new_coords = {
245
+ "POLARIZATION_TYPE": ["polarization_type", ["antenna_name", "receptor_label"]]
246
+ }
247
+
248
+ ant_xds = convert_generic_xds_to_xradio_schema(
249
+ generic_feed_xds,
250
+ ant_xds,
251
+ to_new_data_variables,
252
+ to_new_coords=to_new_coords,
253
+ )
254
+
255
+ # print('ant_xds["ANTENNA_FEED_OFFSET"]',ant_xds["ANTENNA_FEED_OFFSET"].data)
256
+ # print('generic_feed_xds["POSITION"].data',generic_feed_xds["POSITION"].data)
257
+ ant_xds["ANTENNA_FEED_OFFSET"] = (
258
+ ant_xds["ANTENNA_FEED_OFFSET"] + generic_feed_xds["POSITION"].data
259
+ )
260
+ coords = {}
261
+ # coords["receptor_label"] = "pol_" + np.arange(ant_xds.sizes["receptor_label"]).astype(str) #Works on laptop but fails in github test runner.
262
+ coords["receptor_label"] = np.array(
263
+ list(
264
+ map(
265
+ lambda x, y: x + "_" + y,
266
+ ["pol"] * ant_xds.sizes["receptor_label"],
267
+ np.arange(ant_xds.sizes["receptor_label"]).astype(str),
268
+ )
269
+ )
270
+ )
271
+
272
+ coords["sky_dir_label"] = ["ra", "dec"]
273
+ ant_xds = ant_xds.assign_coords(coords)
274
+ return ant_xds
275
+
276
+
277
+ def extract_gain_curve_info(
278
+ ant_xds: xr.Dataset, in_file: str, spectral_window_id: int
279
+ ) -> xr.Dataset:
280
+ """
281
+ Reformats MSv2 GAIN CURVE table content to MSv4 schema.
282
+
283
+ Parameters
284
+ ----------
285
+ ant_xds : xr.Dataset
286
+ The dataset that will be updated with gain curve information.
287
+ in_file : str
288
+ Path to the input MSv2.
289
+ spectral_window_id : int
290
+ The ID of the spectral window.
291
+
292
+ Returns
293
+ -------
294
+ xr.Dataset
295
+ The updated antenna dataset with gain curve information.
296
+ """
297
+ if os.path.exists(
298
+ os.path.join(in_file, "GAIN_CURVE")
299
+ ): # Check if the table exists.
300
+ generic_gain_curve_xds = load_generic_table(
301
+ in_file,
302
+ "GAIN_CURVE",
303
+ taql_where=f" where (ANTENNA_ID IN [{','.join(map(str,ant_xds.antenna_id.values))}]) AND (SPECTRAL_WINDOW_ID = {spectral_window_id})",
304
+ )
305
+
306
+ if (
307
+ generic_gain_curve_xds.data_vars
308
+ ): # Some times the gain_curve table is empty (this is the case with ngEHT simulation data we have).
309
+
310
+ assert (
311
+ len(generic_gain_curve_xds.SPECTRAL_WINDOW_ID) == 1
312
+ ), "Only one spectral window is supported."
313
+ generic_gain_curve_xds = generic_gain_curve_xds.isel(
314
+ SPECTRAL_WINDOW_ID=0, drop=True
315
+ ) # Drop the spectral window dimension as it is singleton.
316
+
317
+ assert (
318
+ len(generic_gain_curve_xds.TIME) == 1
319
+ ), "Only one gain curve measurement per antenna is supported."
320
+ generic_gain_curve_xds = generic_gain_curve_xds.isel(TIME=0, drop=True)
321
+
322
+ generic_gain_curve_xds = generic_gain_curve_xds.sel(
323
+ ANTENNA_ID=ant_xds.antenna_id, drop=False
324
+ ) # Make sure the antenna_id is in the same order as the xds .
325
+
326
+ to_new_data_variables = {
327
+ "INTERVAL": ["GAIN_CURVE_INTERVAL", ["antenna_name"]],
328
+ "GAIN": [
329
+ "GAIN_CURVE",
330
+ ["antenna_name", "poly_term", "receptor_label"],
331
+ ],
332
+ "SENSITIVITY": [
333
+ "GAIN_CURVE_SENSITIVITY",
334
+ ["antenna_name", "receptor_label"],
335
+ ],
336
+ }
337
+
338
+ to_new_coords = {
339
+ "TYPE": ["gain_curve_type", ["antenna_name"]],
340
+ }
341
+
342
+ # print(generic_gain_curve_xds)
343
+
344
+ ant_xds = convert_generic_xds_to_xradio_schema(
345
+ generic_gain_curve_xds,
346
+ ant_xds,
347
+ to_new_data_variables,
348
+ to_new_coords,
349
+ )
350
+ ant_xds["GAIN_CURVE"] = ant_xds["GAIN_CURVE"].transpose(
351
+ "antenna_name", "receptor_label", "poly_term"
352
+ )
353
+
354
+ return ant_xds
355
+
356
+ else:
357
+ return ant_xds
358
+
359
+
360
+ def extract_phase_cal_info(
361
+ ant_xds, path, spectral_window_id, time_min_max, phase_cal_interp_time
362
+ ):
363
+ """
364
+ Reformats MSv2 Phase Cal table content to MSv4 schema.
365
+
366
+ Parameters
367
+ ----------
368
+ ant_xds : xr.Dataset
369
+ The dataset that will be updated with phase cal information.
370
+ in_file : str
371
+ Path to the input MSv2.
372
+ spectral_window_id : int
373
+ The ID of the spectral window.
374
+ time_min_max : Tuple[np.float46, np.float64]
375
+ Min / max times to constrain loading (usually to the time range relevant to an MSv4)
376
+ interp_time : Union[xr.DataArray, None]
377
+ Time axis to interpolate the data vars to (usually main MSv4 time)
378
+
379
+ Returns
380
+ -------
381
+ xr.Dataset
382
+ The updated antenna dataset with phase cal information.
383
+ """
384
+
385
+ if os.path.exists(os.path.join(path, "PHASE_CAL")):
386
+
387
+ # Only read data between the min and max times of the visibility data in the MSv4.
388
+ taql_time_range = make_taql_where_between_min_max(
389
+ time_min_max, path, "PHASE_CAL", "TIME"
390
+ )
391
+ generic_phase_cal_xds = load_generic_table(
392
+ path,
393
+ "PHASE_CAL",
394
+ timecols=["TIME"],
395
+ taql_where=f" {taql_time_range} AND (ANTENNA_ID IN [{','.join(map(str,ant_xds.antenna_id.values))}]) AND (SPECTRAL_WINDOW_ID = {spectral_window_id})",
396
+ )
397
+
398
+ assert (
399
+ len(generic_phase_cal_xds.SPECTRAL_WINDOW_ID) == 1
400
+ ), "Only one spectral window is supported."
401
+ generic_phase_cal_xds = generic_phase_cal_xds.isel(
402
+ SPECTRAL_WINDOW_ID=0, drop=True
403
+ ) # Drop the spectral window dimension as it is singleton.
404
+
405
+ generic_phase_cal_xds = generic_phase_cal_xds.sel(
406
+ ANTENNA_ID=ant_xds.antenna_id, drop=False
407
+ ) # Make sure the antenna_id is in the same order as the xds.
408
+
409
+ to_new_data_variables = {
410
+ "INTERVAL": ["PHASE_CAL_INTERVAL", ["antenna_name", "time_phase_cal"]],
411
+ "TONE_FREQUENCY": [
412
+ "PHASE_CAL_TONE_FREQUENCY",
413
+ ["antenna_name", "time_phase_cal", "tone_label", "receptor_label"],
414
+ ],
415
+ "PHASE_CAL": [
416
+ "PHASE_CAL",
417
+ ["antenna_name", "time_phase_cal", "tone_label", "receptor_label"],
418
+ ],
419
+ "CABLE_CAL": ["PHASE_CAL_CABLE_CAL", ["antenna_name", "time_phase_cal"]],
420
+ }
421
+
422
+ to_new_coords = {
423
+ "TIME": ["time_phase_cal", ["time_phase_cal"]],
424
+ }
425
+
426
+ ant_xds = convert_generic_xds_to_xradio_schema(
427
+ generic_phase_cal_xds, ant_xds, to_new_data_variables, to_new_coords
428
+ )
429
+ ant_xds["PHASE_CAL"] = ant_xds["PHASE_CAL"].transpose(
430
+ "antenna_name", "time_phase_cal", "receptor_label", "tone_label"
431
+ )
432
+
433
+ ant_xds["PHASE_CAL_TONE_FREQUENCY"] = ant_xds[
434
+ "PHASE_CAL_TONE_FREQUENCY"
435
+ ].transpose("antenna_name", "time_phase_cal", "receptor_label", "tone_label")
436
+
437
+ # ant_xds = ant_xds.assign_coords({"tone_label" : "freq_" + np.arange(ant_xds.sizes["tone_label"]).astype(str)}) #Works on laptop but fails in github test runner.
438
+ ant_xds = ant_xds.assign_coords(
439
+ {
440
+ "tone_label": np.array(
441
+ list(
442
+ map(
443
+ lambda x, y: x + "_" + y,
444
+ ["freq"] * ant_xds.sizes["tone_label"],
445
+ np.arange(ant_xds.sizes["tone_label"]).astype(str),
446
+ )
447
+ )
448
+ )
449
+ }
450
+ )
451
+
452
+ ant_xds["time_phase_cal"] = (
453
+ ant_xds.time_phase_cal.astype("float64").astype("float64") / 10**9
454
+ )
455
+
456
+ ant_xds = interpolate_to_time(
457
+ ant_xds, phase_cal_interp_time, "antenna_xds", time_name="time_phase_cal"
458
+ )
459
+
460
+ time_coord_attrs = {
461
+ "type": "time",
462
+ "units": ["s"],
463
+ "scale": "UTC",
464
+ "format": "UNIX",
465
+ }
466
+
467
+ # If we interpolate rename the time_ephemeris_axis axis to time.
468
+ if phase_cal_interp_time is not None:
469
+ time_coord = {"time": ("time_phase_cal", phase_cal_interp_time.data)}
470
+ ant_xds = ant_xds.assign_coords(time_coord)
471
+ ant_xds.coords["time"].attrs.update(time_coord_attrs)
472
+ ant_xds = ant_xds.swap_dims({"time_phase_cal": "time"}).drop_vars(
473
+ "time_phase_cal"
474
+ )
475
+
476
+ return ant_xds
477
+
478
+ else:
479
+ return ant_xds