xradio 0.0.34__py3-none-any.whl → 0.0.37__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,482 @@
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 = ant_xds.assign_coords({"cartesian_pos_label": ["x", "y", "z"]})
131
+
132
+ ant_xds = convert_generic_xds_to_xradio_schema(
133
+ generic_ant_xds, ant_xds, to_new_data_variables, to_new_coords
134
+ )
135
+
136
+ ant_xds["ANTENNA_DISH_DIAMETER"].attrs.update({"units": ["m"], "type": "quantity"})
137
+
138
+ ant_xds["ANTENNA_FEED_OFFSET"].attrs["type"] = "earth_location_offset"
139
+ ant_xds["ANTENNA_FEED_OFFSET"].attrs["coordinate_system"] = "geocentric"
140
+ ant_xds["ANTENNA_POSITION"].attrs["coordinate_system"] = "geocentric"
141
+
142
+ if telescope_name in ["ALMA", "VLA", "NOEMA", "EVLA"]:
143
+ # antenna_name = ant_xds["antenna_name"].values + "_" + ant_xds["station"].values
144
+ # works on laptop but fails in github test runner with error:
145
+ # numpy.core._exceptions._UFuncNoLoopError: ufunc 'add' did not contain a loop with signature matching types (dtype('<U4'), dtype('<U4')) -> None
146
+
147
+ # Also doesn't work on github test runner:
148
+ # antenna_name = ant_xds["antenna_name"].values
149
+ # antenna_name = np._core.defchararray.add(antenna_name, "_")
150
+ # antenna_name = np._core.defchararray.add(
151
+ # antenna_name,
152
+ # ant_xds["station"].values,
153
+ # )
154
+
155
+ # None of the native numpy functions work on the github test runner.
156
+ antenna_name = ant_xds["antenna_name"].values
157
+ station = ant_xds["station"].values
158
+ antenna_name = np.array(
159
+ list(map(lambda x, y: x + "_" + y, antenna_name, station))
160
+ )
161
+
162
+ ant_xds["antenna_name"] = xr.DataArray(antenna_name, dims=["antenna_name"])
163
+ ant_xds.attrs["relocatable_antennas"] = True
164
+ else:
165
+ ant_xds.attrs["relocatable_antennas"] = False
166
+
167
+ return ant_xds
168
+
169
+
170
+ def extract_feed_info(
171
+ ant_xds: xr.Dataset,
172
+ in_file: str,
173
+ antenna_id: list,
174
+ feed_id: int,
175
+ spectral_window_id: int,
176
+ ) -> xr.Dataset:
177
+ """
178
+ Reformats MSv2 Feed table content to MSv4 schema.
179
+
180
+ Parameters
181
+ ----------
182
+ ant_xds : xr.Dataset
183
+ Xarray Dataset containing antenna information.
184
+ in_file : str
185
+ Path to the input MSv2.
186
+ antenna_id : list
187
+ List of antenna IDs.
188
+ feed_id : int
189
+ Feed ID.
190
+ spectral_window_id : int
191
+ Spectral window ID.
192
+
193
+ Returns
194
+ -------
195
+ xr.Dataset
196
+ Dataset updated to contain the feed information.
197
+ """
198
+
199
+ # Extract feed information
200
+ generic_feed_xds = load_generic_table(
201
+ in_file,
202
+ "FEED",
203
+ rename_ids=subt_rename_ids["FEED"],
204
+ taql_where=f" where (ANTENNA_ID IN [{','.join(map(str, ant_xds.antenna_id.values))}]) AND (FEED_ID IN [{','.join(map(str, feed_id))}])",
205
+ ) # Some Lofar and MeerKAT data have the spw column set to -1 so we can't use '(SPECTRAL_WINDOW_ID = {spectral_window_id})'
206
+
207
+ feed_spw = np.unique(generic_feed_xds.SPECTRAL_WINDOW_ID)
208
+ if len(feed_spw) == 1 and feed_spw[0] == -1:
209
+ generic_feed_xds = generic_feed_xds.isel(SPECTRAL_WINDOW_ID=0, drop=True)
210
+ else:
211
+ if spectral_window_id not in feed_spw:
212
+ return ant_xds # For some spw the feed table is empty (this is the case with ALMA spw WVR#NOMINAL).
213
+ else:
214
+ generic_feed_xds = generic_feed_xds.sel(
215
+ SPECTRAL_WINDOW_ID=spectral_window_id, drop=True
216
+ )
217
+
218
+ assert len(generic_feed_xds.TIME) == len(
219
+ antenna_id
220
+ ), "Can only process feed table with a single time entry for an feed, antenna and spectral_window_id."
221
+ generic_feed_xds = generic_feed_xds.sel(
222
+ ANTENNA_ID=antenna_id, drop=False
223
+ ) # Make sure the antenna_id is in the same order as the xds.
224
+
225
+ num_receptors = np.ravel(generic_feed_xds.NUM_RECEPTORS)
226
+ num_receptors = unique_1d(num_receptors[~np.isnan(num_receptors)])
227
+
228
+ assert (
229
+ len(num_receptors) == 1
230
+ ), "The number of receptors must be constant in feed table."
231
+
232
+ to_new_data_variables = {
233
+ "BEAM_OFFSET": [
234
+ "BEAM_OFFSET",
235
+ ["antenna_name", "receptor_label", "sky_dir_label"],
236
+ ],
237
+ "RECEPTOR_ANGLE": ["RECEPTOR_ANGLE", ["antenna_name", "receptor_label"]],
238
+ # "pol_response": ["POLARIZATION_RESPONSE", ["antenna_name", "receptor_label", "receptor_name_"]] #repeated dim creates problems.
239
+ "FOCUS_LENGTH": ["FOCUS_LENGTH", ["antenna_name"]], # optional
240
+ # "position": ["ANTENNA_FEED_OFFSET",["antenna_name", "cartesian_pos_label"]] #Will be added to the existing position in ant_xds
241
+ }
242
+
243
+ to_new_coords = {
244
+ "POLARIZATION_TYPE": ["polarization_type", ["antenna_name", "receptor_label"]]
245
+ }
246
+
247
+ ant_xds = convert_generic_xds_to_xradio_schema(
248
+ generic_feed_xds,
249
+ ant_xds,
250
+ to_new_data_variables,
251
+ to_new_coords=to_new_coords,
252
+ )
253
+
254
+ # print('ant_xds["ANTENNA_FEED_OFFSET"]',ant_xds["ANTENNA_FEED_OFFSET"].data)
255
+ # print('generic_feed_xds["POSITION"].data',generic_feed_xds["POSITION"].data)
256
+ feed_offset_attrs = ant_xds["ANTENNA_FEED_OFFSET"].attrs
257
+ ant_xds["ANTENNA_FEED_OFFSET"] = (
258
+ ant_xds["ANTENNA_FEED_OFFSET"] + generic_feed_xds["POSITION"].data
259
+ )
260
+ # recover attrs after arithmetic operation
261
+ ant_xds["ANTENNA_FEED_OFFSET"].attrs.update(feed_offset_attrs)
262
+
263
+ coords = {}
264
+ # coords["receptor_label"] = "pol_" + np.arange(ant_xds.sizes["receptor_label"]).astype(str) #Works on laptop but fails in github test runner.
265
+ coords["receptor_label"] = np.array(
266
+ list(
267
+ map(
268
+ lambda x, y: x + "_" + y,
269
+ ["pol"] * ant_xds.sizes["receptor_label"],
270
+ np.arange(ant_xds.sizes["receptor_label"]).astype(str),
271
+ )
272
+ )
273
+ )
274
+
275
+ coords["sky_dir_label"] = ["ra", "dec"]
276
+ ant_xds = ant_xds.assign_coords(coords)
277
+ return ant_xds
278
+
279
+
280
+ def extract_gain_curve_info(
281
+ ant_xds: xr.Dataset, in_file: str, spectral_window_id: int
282
+ ) -> xr.Dataset:
283
+ """
284
+ Reformats MSv2 GAIN CURVE table content to MSv4 schema.
285
+
286
+ Parameters
287
+ ----------
288
+ ant_xds : xr.Dataset
289
+ The dataset that will be updated with gain curve information.
290
+ in_file : str
291
+ Path to the input MSv2.
292
+ spectral_window_id : int
293
+ The ID of the spectral window.
294
+
295
+ Returns
296
+ -------
297
+ xr.Dataset
298
+ The updated antenna dataset with gain curve information.
299
+ """
300
+ if os.path.exists(
301
+ os.path.join(in_file, "GAIN_CURVE")
302
+ ): # Check if the table exists.
303
+ generic_gain_curve_xds = load_generic_table(
304
+ in_file,
305
+ "GAIN_CURVE",
306
+ taql_where=f" where (ANTENNA_ID IN [{','.join(map(str,ant_xds.antenna_id.values))}]) AND (SPECTRAL_WINDOW_ID = {spectral_window_id})",
307
+ )
308
+
309
+ if (
310
+ generic_gain_curve_xds.data_vars
311
+ ): # Some times the gain_curve table is empty (this is the case with ngEHT simulation data we have).
312
+
313
+ assert (
314
+ len(generic_gain_curve_xds.SPECTRAL_WINDOW_ID) == 1
315
+ ), "Only one spectral window is supported."
316
+ generic_gain_curve_xds = generic_gain_curve_xds.isel(
317
+ SPECTRAL_WINDOW_ID=0, drop=True
318
+ ) # Drop the spectral window dimension as it is singleton.
319
+
320
+ assert (
321
+ len(generic_gain_curve_xds.TIME) == 1
322
+ ), "Only one gain curve measurement per antenna is supported."
323
+ generic_gain_curve_xds = generic_gain_curve_xds.isel(TIME=0, drop=True)
324
+
325
+ generic_gain_curve_xds = generic_gain_curve_xds.sel(
326
+ ANTENNA_ID=ant_xds.antenna_id, drop=False
327
+ ) # Make sure the antenna_id is in the same order as the xds .
328
+
329
+ to_new_data_variables = {
330
+ "INTERVAL": ["GAIN_CURVE_INTERVAL", ["antenna_name"]],
331
+ "GAIN": [
332
+ "GAIN_CURVE",
333
+ ["antenna_name", "poly_term", "receptor_label"],
334
+ ],
335
+ "SENSITIVITY": [
336
+ "GAIN_CURVE_SENSITIVITY",
337
+ ["antenna_name", "receptor_label"],
338
+ ],
339
+ }
340
+
341
+ to_new_coords = {
342
+ "TYPE": ["gain_curve_type", ["antenna_name"]],
343
+ }
344
+
345
+ # print(generic_gain_curve_xds)
346
+
347
+ ant_xds = convert_generic_xds_to_xradio_schema(
348
+ generic_gain_curve_xds,
349
+ ant_xds,
350
+ to_new_data_variables,
351
+ to_new_coords,
352
+ )
353
+ ant_xds["GAIN_CURVE"] = ant_xds["GAIN_CURVE"].transpose(
354
+ "antenna_name", "receptor_label", "poly_term"
355
+ )
356
+
357
+ return ant_xds
358
+
359
+ else:
360
+ return ant_xds
361
+
362
+
363
+ def extract_phase_cal_info(
364
+ ant_xds, path, spectral_window_id, time_min_max, phase_cal_interp_time
365
+ ):
366
+ """
367
+ Reformats MSv2 Phase Cal table content to MSv4 schema.
368
+
369
+ Parameters
370
+ ----------
371
+ ant_xds : xr.Dataset
372
+ The dataset that will be updated with phase cal information.
373
+ in_file : str
374
+ Path to the input MSv2.
375
+ spectral_window_id : int
376
+ The ID of the spectral window.
377
+ time_min_max : Tuple[np.float46, np.float64]
378
+ Min / max times to constrain loading (usually to the time range relevant to an MSv4)
379
+ interp_time : Union[xr.DataArray, None]
380
+ Time axis to interpolate the data vars to (usually main MSv4 time)
381
+
382
+ Returns
383
+ -------
384
+ xr.Dataset
385
+ The updated antenna dataset with phase cal information.
386
+ """
387
+
388
+ if os.path.exists(os.path.join(path, "PHASE_CAL")):
389
+
390
+ # Only read data between the min and max times of the visibility data in the MSv4.
391
+ taql_time_range = make_taql_where_between_min_max(
392
+ time_min_max, path, "PHASE_CAL", "TIME"
393
+ )
394
+ generic_phase_cal_xds = load_generic_table(
395
+ path,
396
+ "PHASE_CAL",
397
+ timecols=["TIME"],
398
+ 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})",
399
+ )
400
+
401
+ assert (
402
+ len(generic_phase_cal_xds.SPECTRAL_WINDOW_ID) == 1
403
+ ), "Only one spectral window is supported."
404
+ generic_phase_cal_xds = generic_phase_cal_xds.isel(
405
+ SPECTRAL_WINDOW_ID=0, drop=True
406
+ ) # Drop the spectral window dimension as it is singleton.
407
+
408
+ generic_phase_cal_xds = generic_phase_cal_xds.sel(
409
+ ANTENNA_ID=ant_xds.antenna_id, drop=False
410
+ ) # Make sure the antenna_id is in the same order as the xds.
411
+
412
+ to_new_data_variables = {
413
+ "INTERVAL": ["PHASE_CAL_INTERVAL", ["antenna_name", "time_phase_cal"]],
414
+ "TONE_FREQUENCY": [
415
+ "PHASE_CAL_TONE_FREQUENCY",
416
+ ["antenna_name", "time_phase_cal", "tone_label", "receptor_label"],
417
+ ],
418
+ "PHASE_CAL": [
419
+ "PHASE_CAL",
420
+ ["antenna_name", "time_phase_cal", "tone_label", "receptor_label"],
421
+ ],
422
+ "CABLE_CAL": ["PHASE_CAL_CABLE_CAL", ["antenna_name", "time_phase_cal"]],
423
+ }
424
+
425
+ to_new_coords = {
426
+ "TIME": ["time_phase_cal", ["time_phase_cal"]],
427
+ }
428
+
429
+ ant_xds = convert_generic_xds_to_xradio_schema(
430
+ generic_phase_cal_xds, ant_xds, to_new_data_variables, to_new_coords
431
+ )
432
+ ant_xds["PHASE_CAL"] = ant_xds["PHASE_CAL"].transpose(
433
+ "antenna_name", "time_phase_cal", "receptor_label", "tone_label"
434
+ )
435
+
436
+ ant_xds["PHASE_CAL_TONE_FREQUENCY"] = ant_xds[
437
+ "PHASE_CAL_TONE_FREQUENCY"
438
+ ].transpose("antenna_name", "time_phase_cal", "receptor_label", "tone_label")
439
+
440
+ # 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.
441
+ ant_xds = ant_xds.assign_coords(
442
+ {
443
+ "tone_label": np.array(
444
+ list(
445
+ map(
446
+ lambda x, y: x + "_" + y,
447
+ ["freq"] * ant_xds.sizes["tone_label"],
448
+ np.arange(ant_xds.sizes["tone_label"]).astype(str),
449
+ )
450
+ )
451
+ )
452
+ }
453
+ )
454
+
455
+ ant_xds["time_phase_cal"] = (
456
+ ant_xds.time_phase_cal.astype("float64").astype("float64") / 10**9
457
+ )
458
+
459
+ ant_xds = interpolate_to_time(
460
+ ant_xds, phase_cal_interp_time, "antenna_xds", time_name="time_phase_cal"
461
+ )
462
+
463
+ time_coord_attrs = {
464
+ "type": "time",
465
+ "units": ["s"],
466
+ "scale": "UTC",
467
+ "format": "UNIX",
468
+ }
469
+
470
+ # If we interpolate rename the time_ephemeris_axis axis to time.
471
+ if phase_cal_interp_time is not None:
472
+ time_coord = {"time": ("time_phase_cal", phase_cal_interp_time.data)}
473
+ ant_xds = ant_xds.assign_coords(time_coord)
474
+ ant_xds.coords["time"].attrs.update(time_coord_attrs)
475
+ ant_xds = ant_xds.swap_dims({"time_phase_cal": "time"}).drop_vars(
476
+ "time_phase_cal"
477
+ )
478
+
479
+ return ant_xds
480
+
481
+ else:
482
+ return ant_xds