xradio 0.0.29__py3-none-any.whl → 0.0.31__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.
@@ -17,33 +17,35 @@ from xradio.vis._vis_utils._ms._tables.read import (
17
17
  )
18
18
  from xradio.vis._vis_utils._ms._tables.table_query import open_table_ro
19
19
  import graphviper.utils.logger as logger
20
-
21
-
22
- def cast_to_str(x):
23
- if isinstance(x, list):
24
- return x[0]
25
- else:
26
- return x
20
+ from xradio._utils.list_and_array import (
21
+ check_if_consistent,
22
+ unique_1d,
23
+ to_list,
24
+ to_np_array,
25
+ )
26
+ from xradio._utils.common import cast_to_str, convert_to_si_units, add_position_offsets
27
27
 
28
28
 
29
29
  def create_field_and_source_xds(
30
- in_file,
31
- field_id,
32
- spectral_window_id,
33
- field_times,
34
- is_single_dish,
30
+ in_file: str,
31
+ field_id: list,
32
+ spectral_window_id: int,
33
+ field_times: list,
34
+ is_single_dish: bool,
35
35
  time_min_max: Tuple[np.float64, np.float64],
36
36
  ephemeris_interp_time: Union[xr.DataArray, None] = None,
37
37
  ):
38
38
  """
39
39
  Create a field and source xarray dataset (xds) from the given input file, field ID, and spectral window ID.
40
+ Data is extracted from the FIELD and SOURCE tables and if there is ephemeris data, it is also extracted.
41
+ field_id will only be guaranteed to be a list of length 1 when partition_scheme does not include "FIELD_ID".
40
42
 
41
43
  Parameters:
42
44
  ----------
43
45
  in_file : str
44
46
  The path to the input file.
45
- field_id : int
46
- The ID of the field.
47
+ field_id : list of int
48
+ The field ids to select.
47
49
  spectral_window_id : int
48
50
  The ID of the spectral window.
49
51
  time_min_max : Tuple[np.float64, np.float46]
@@ -61,14 +63,13 @@ def create_field_and_source_xds(
61
63
 
62
64
  field_and_source_xds = xr.Dataset()
63
65
 
64
- field_and_source_xds, ephemeris_path, ephemeris_table_name = (
66
+ field_and_source_xds, ephemeris_path, ephemeris_table_name, source_id = (
65
67
  create_field_info_and_check_ephemeris(
66
68
  field_and_source_xds, in_file, field_id, field_times, is_single_dish
67
69
  )
68
70
  )
69
- source_id = field_and_source_xds.attrs["source_id"]
70
71
 
71
- if ephemeris_path is not None:
72
+ if field_and_source_xds.attrs["is_ephemeris"]:
72
73
  field_and_source_xds = extract_ephemeris_info(
73
74
  field_and_source_xds,
74
75
  ephemeris_path,
@@ -77,26 +78,16 @@ def create_field_and_source_xds(
77
78
  time_min_max,
78
79
  ephemeris_interp_time,
79
80
  )
80
- field_and_source_xds.attrs["is_ephemeris"] = True
81
- field_and_source_xds = extract_source_info(
82
- field_and_source_xds,
83
- in_file,
84
- True,
85
- source_id,
86
- spectral_window_id,
87
- )
88
81
 
89
- else:
90
- field_and_source_xds = extract_source_info(
91
- field_and_source_xds, in_file, False, source_id, spectral_window_id
92
- )
93
- field_and_source_xds.attrs["is_ephemeris"] = False
82
+ field_and_source_xds = extract_source_info(
83
+ field_and_source_xds, in_file, source_id, spectral_window_id
84
+ )
94
85
 
95
86
  logger.debug(
96
87
  f"create_field_and_source_xds() execution time {time.time() - start_time:0.2f} s"
97
88
  )
98
89
 
99
- return field_and_source_xds
90
+ return field_and_source_xds, source_id
100
91
 
101
92
 
102
93
  def extract_ephemeris_info(
@@ -128,10 +119,11 @@ def extract_ephemeris_info(
128
119
  xds : xr.Dataset
129
120
  The xarray dataset with the added ephemeris information.
130
121
  """
131
- # The JPL-Horizons ephemris table implmenation in CASA does not follow the standard way of defining measures.
122
+ # The JPL-Horizons ephemeris table implementation in CASA does not follow the standard way of defining measures.
132
123
  # Consequently a lot of hardcoding is needed to extract the information.
133
124
  # https://casadocs.readthedocs.io/en/latest/notebooks/external-data.html
134
125
 
126
+ # Only read data between the min and max times of the visibility data in the MSv4.
135
127
  min_max_mjd = (
136
128
  convert_casacore_time_to_mjd(time_min_max[0]),
137
129
  convert_casacore_time_to_mjd(time_min_max[1]),
@@ -143,11 +135,14 @@ def extract_ephemeris_info(
143
135
  path, table_name, timecols=["MJD"], taql_where=taql_time_range
144
136
  )
145
137
 
146
- # print(ephemeris_xds)
147
-
148
- assert len(ephemeris_xds.ephemeris_id) == 1, "Non standard ephemeris table."
149
- ephemeris_xds = ephemeris_xds.isel(ephemeris_id=0)
138
+ assert (
139
+ len(ephemeris_xds.ephemeris_id) == 1
140
+ ), "Non standard ephemeris table. Only a single ephemeris is allowed per MSv4."
141
+ ephemeris_xds = ephemeris_xds.isel(
142
+ ephemeris_id=0
143
+ ) # Collapse the ephemeris_id dimension.
150
144
 
145
+ # Get meta data.
151
146
  ephemeris_meta = ephemeris_xds.attrs["other"]["msv2"]["ctds_attrs"]
152
147
  ephemris_column_description = ephemeris_xds.attrs["other"]["msv2"]["ctds_attrs"][
153
148
  "column_descriptions"
@@ -170,11 +165,16 @@ def extract_ephemeris_info(
170
165
 
171
166
  coords = {
172
167
  "ellipsoid_pos_label": ["lon", "lat", "dist"],
173
- "time": ephemeris_xds["time"].data,
168
+ "ephem_time": ephemeris_xds[
169
+ "time"
170
+ ].data, # We are using the "ephem_time" label because it might not match the optional time axis of the source and field info. If ephemeris_interpolate=True then rename it to time.
174
171
  "sky_pos_label": ["ra", "dec", "dist"],
175
172
  }
176
173
 
177
- xds["SOURCE_POSITION"] = xr.DataArray(
174
+ temp_xds = xr.Dataset()
175
+
176
+ # Add mandatory data: SOURCE_POSITION
177
+ temp_xds["SOURCE_POSITION"] = xr.DataArray(
178
178
  np.column_stack(
179
179
  (
180
180
  ephemeris_xds["ra"].data,
@@ -182,22 +182,23 @@ def extract_ephemeris_info(
182
182
  ephemeris_xds["rho"].data,
183
183
  )
184
184
  ),
185
- dims=["time", "sky_pos_label"],
185
+ dims=["ephem_time", "sky_pos_label"],
186
186
  )
187
- # Have to use cast_to_str because the ephemris table units are not consistently in a list or a string.
187
+ # Have to use cast_to_str because the ephemeris table units are not consistently in a list or a string.
188
188
  sky_coord_units = [
189
189
  cast_to_str(ephemris_column_description["RA"]["keywords"][unit_keyword]),
190
190
  cast_to_str(ephemris_column_description["DEC"]["keywords"][unit_keyword]),
191
191
  cast_to_str(ephemris_column_description["Rho"]["keywords"][unit_keyword]),
192
192
  ]
193
- xds["SOURCE_POSITION"].attrs.update(
193
+ temp_xds["SOURCE_POSITION"].attrs.update(
194
194
  {"type": "sky_coord", "frame": sky_coord_frame, "units": sky_coord_units}
195
195
  )
196
196
 
197
- xds["SOURCE_RADIAL_VELOCITY"] = xr.DataArray(
198
- ephemeris_xds["radvel"].data, dims=["time"]
197
+ # Add mandatory data: SOURCE_RADIAL_VELOCITY
198
+ temp_xds["SOURCE_RADIAL_VELOCITY"] = xr.DataArray(
199
+ ephemeris_xds["radvel"].data, dims=["ephem_time"]
199
200
  )
200
- xds["SOURCE_RADIAL_VELOCITY"].attrs.update(
201
+ temp_xds["SOURCE_RADIAL_VELOCITY"].attrs.update(
201
202
  {
202
203
  "type": "quantity",
203
204
  "units": [
@@ -208,15 +209,16 @@ def extract_ephemeris_info(
208
209
  }
209
210
  )
210
211
 
212
+ # Add mandatory data: OBSERVATION_POSITION
211
213
  observation_position = [
212
214
  ephemeris_meta["GeoLong"],
213
215
  ephemeris_meta["GeoLat"],
214
216
  ephemeris_meta["GeoDist"],
215
217
  ]
216
- xds["OBSERVATION_POSITION"] = xr.DataArray(
218
+ temp_xds["OBSERVATION_POSITION"] = xr.DataArray(
217
219
  observation_position, dims=["ellipsoid_pos_label"]
218
220
  )
219
- xds["OBSERVATION_POSITION"].attrs.update(
221
+ temp_xds["OBSERVATION_POSITION"].attrs.update(
220
222
  {
221
223
  "type": "location",
222
224
  "units": ["deg", "deg", "m"],
@@ -227,14 +229,12 @@ def extract_ephemeris_info(
227
229
  }
228
230
  ) # I think the units are ['deg','deg','m'] and 'WGS84'.
229
231
 
230
- # Add optional data
231
- # NORTH_POLE_POSITION_ANGLE
232
-
232
+ # Add optional data NORTH_POLE_POSITION_ANGLE and NORTH_POLE_ANGULAR_DISTANCE
233
233
  if "np_ang" in ephemeris_xds.data_vars:
234
- xds["NORTH_POLE_POSITION_ANGLE"] = xr.DataArray(
235
- ephemeris_xds["np_ang"].data, dims=["time"]
234
+ temp_xds["NORTH_POLE_POSITION_ANGLE"] = xr.DataArray(
235
+ ephemeris_xds["np_ang"].data, dims=["ephem_time"]
236
236
  )
237
- xds["NORTH_POLE_POSITION_ANGLE"].attrs.update(
237
+ temp_xds["NORTH_POLE_POSITION_ANGLE"].attrs.update(
238
238
  {
239
239
  "type": "quantity",
240
240
  "units": [
@@ -246,10 +246,10 @@ def extract_ephemeris_info(
246
246
  )
247
247
 
248
248
  if "np_dist" in ephemeris_xds.data_vars:
249
- xds["NORTH_POLE_ANGULAR_DISTANCE"] = xr.DataArray(
250
- ephemeris_xds["np_dist"].data, dims=["time"]
249
+ temp_xds["NORTH_POLE_ANGULAR_DISTANCE"] = xr.DataArray(
250
+ ephemeris_xds["np_dist"].data, dims=["ephem_time"]
251
251
  )
252
- xds["NORTH_POLE_ANGULAR_DISTANCE"].attrs.update(
252
+ temp_xds["NORTH_POLE_ANGULAR_DISTANCE"].attrs.update(
253
253
  {
254
254
  "type": "quantity",
255
255
  "units": [
@@ -260,8 +260,9 @@ def extract_ephemeris_info(
260
260
  }
261
261
  )
262
262
 
263
+ # Add optional data: SUB_OBSERVER_POSITION and SUB_SOLAR_POSITION
263
264
  if "disklong" in ephemeris_xds.data_vars:
264
- xds["SUB_OBSERVER_POSITION"] = xr.DataArray(
265
+ temp_xds["SUB_OBSERVER_POSITION"] = xr.DataArray(
265
266
  np.column_stack(
266
267
  (
267
268
  ephemeris_xds["disklong"].data,
@@ -269,7 +270,7 @@ def extract_ephemeris_info(
269
270
  np.zeros(ephemeris_xds["disklong"].shape),
270
271
  )
271
272
  ),
272
- dims=["time", "ellipsoid_pos_label"],
273
+ dims=["ephem_time", "ellipsoid_pos_label"],
273
274
  )
274
275
 
275
276
  if "DiskLong" in ephemris_column_description:
@@ -279,7 +280,7 @@ def extract_ephemeris_info(
279
280
  units_key_lon = "diskLong"
280
281
  units_key_lat = "diskLat"
281
282
 
282
- xds["SUB_OBSERVER_POSITION"].attrs.update(
283
+ temp_xds["SUB_OBSERVER_POSITION"].attrs.update(
283
284
  {
284
285
  "type": "location",
285
286
  "ellipsoid": "NA",
@@ -302,7 +303,7 @@ def extract_ephemeris_info(
302
303
  )
303
304
 
304
305
  if "si_lon" in ephemeris_xds.data_vars:
305
- xds["SUB_SOLAR_POSITION"] = xr.DataArray(
306
+ temp_xds["SUB_SOLAR_POSITION"] = xr.DataArray(
306
307
  np.column_stack(
307
308
  (
308
309
  ephemeris_xds["si_lon"].data,
@@ -310,9 +311,9 @@ def extract_ephemeris_info(
310
311
  ephemeris_xds["r"].data,
311
312
  )
312
313
  ),
313
- dims=["time", "ellipsoid_pos_label"],
314
+ dims=["ephem_time", "ellipsoid_pos_label"],
314
315
  )
315
- xds["SUB_SOLAR_POSITION"].attrs.update(
316
+ temp_xds["SUB_SOLAR_POSITION"].attrs.update(
316
317
  {
317
318
  "type": "location",
318
319
  "ellipsoid": "NA",
@@ -332,11 +333,12 @@ def extract_ephemeris_info(
332
333
  }
333
334
  )
334
335
 
336
+ # Add optional data: HELIOCENTRIC_RADIAL_VELOCITY
335
337
  if "rdot" in ephemeris_xds.data_vars:
336
- xds["HELIOCENTRIC_RADIAL_VELOCITY"] = xr.DataArray(
337
- ephemeris_xds["rdot"].data, dims=["time"]
338
+ temp_xds["HELIOCENTRIC_RADIAL_VELOCITY"] = xr.DataArray(
339
+ ephemeris_xds["rdot"].data, dims=["ephem_time"]
338
340
  )
339
- xds["HELIOCENTRIC_RADIAL_VELOCITY"].attrs.update(
341
+ temp_xds["HELIOCENTRIC_RADIAL_VELOCITY"].attrs.update(
340
342
  {
341
343
  "type": "quantity",
342
344
  "units": [
@@ -347,11 +349,12 @@ def extract_ephemeris_info(
347
349
  }
348
350
  )
349
351
 
352
+ # Add optional data: OBSERVER_PHASE_ANGLE
350
353
  if "phang" in ephemeris_xds.data_vars:
351
- xds["OBSERVER_PHASE_ANGLE"] = xr.DataArray(
352
- ephemeris_xds["phang"].data, dims=["time"]
354
+ temp_xds["OBSERVER_PHASE_ANGLE"] = xr.DataArray(
355
+ ephemeris_xds["phang"].data, dims=["ephem_time"]
353
356
  )
354
- xds["OBSERVER_PHASE_ANGLE"].attrs.update(
357
+ temp_xds["OBSERVER_PHASE_ANGLE"].attrs.update(
355
358
  {
356
359
  "type": "quantity",
357
360
  "units": [
@@ -362,29 +365,52 @@ def extract_ephemeris_info(
362
365
  }
363
366
  )
364
367
 
365
- xds = xds.assign_coords(coords)
366
- xds["time"].attrs.update(
368
+ temp_xds = temp_xds.assign_coords(coords)
369
+ temp_xds["ephem_time"].attrs.update(
367
370
  {"type": "time", "units": ["s"], "scale": "UTC", "format": "UNIX"}
368
371
  )
369
372
 
370
- xds = convert_to_si_units(xds)
371
- xds = interpolate_to_time(xds, interp_time, "field_and_source_xds")
373
+ # Convert to si units and interpolate if ephemeris_interpolate=True:
374
+ temp_xds = convert_to_si_units(temp_xds)
375
+ temp_xds = interpolate_to_time(
376
+ temp_xds, interp_time, "field_and_source_xds", time_name="ephem_time"
377
+ )
372
378
 
379
+ # If we interpolate rename the ephem_time axis to time.
380
+ if interp_time is not None:
381
+ temp_xds = temp_xds.swap_dims({"ephem_time": "time"}).drop_vars("ephem_time")
382
+
383
+ xds = xr.merge([xds, temp_xds])
384
+
385
+ # Add the SOURCE_POSITION to the FIELD_PHASE_CENTER or FIELD_REFERENCE_CENTER. Ephemeris obs: When loaded from the MSv2 field table the FIELD_REFERENCE_CENTER or FIELD_PHASE_CENTER only contain an offset from the SOURCE_POSITION.
386
+ # We also need to add a distance dimension to the FIELD_PHASE_CENTER or FIELD_REFERENCE_CENTER to match the SOURCE_POSITION.
387
+ # FIELD_PHASE_CENTER is used for interferometer data and FIELD_REFERENCE_CENTER is used for single dish data.
373
388
  if is_single_dish:
374
- xds["FIELD_REFERENCE_CENTER"] = xr.DataArray(
389
+ center_dv = "FIELD_REFERENCE_CENTER"
390
+ else:
391
+ center_dv = "FIELD_PHASE_CENTER"
392
+
393
+ if "time" in xds[center_dv].coords:
394
+ assert (
395
+ interp_time is not None
396
+ ), 'ephemeris_interpolate must be True if there is ephemeris data and multiple fields (this will occur if "FIELD_ID" is not in partition_scheme).'
397
+
398
+ xds[center_dv] = xr.DataArray(
375
399
  add_position_offsets(
376
- np.append(xds["FIELD_REFERENCE_CENTER"].data, 0),
377
- xds["SOURCE_POSITION"].data,
400
+ np.column_stack(
401
+ (xds[center_dv].values, np.zeros(xds[center_dv].values.shape[0]))
402
+ ),
403
+ xds["SOURCE_POSITION"].values,
378
404
  ),
379
- dims=["time", "sky_pos_label"],
405
+ dims=[xds["SOURCE_POSITION"].dims[0], "sky_pos_label"],
380
406
  )
381
407
  else:
382
- xds["FIELD_PHASE_CENTER"] = xr.DataArray(
408
+ xds[center_dv] = xr.DataArray(
383
409
  add_position_offsets(
384
- np.append(xds["FIELD_PHASE_CENTER"].data, 0),
385
- xds["SOURCE_POSITION"].data,
410
+ np.append(xds[center_dv].values, 0),
411
+ xds["SOURCE_POSITION"].values,
386
412
  ),
387
- dims=["time", "sky_pos_label"],
413
+ dims=[xds["SOURCE_POSITION"].dims[0], "sky_pos_label"],
388
414
  )
389
415
 
390
416
  xds["FIELD_PHASE_CENTER"].attrs.update(xds["SOURCE_POSITION"].attrs)
@@ -392,56 +418,7 @@ def extract_ephemeris_info(
392
418
  return xds
393
419
 
394
420
 
395
- def add_position_offsets(dv_1, dv_2):
396
- new_pos = dv_1 + dv_2
397
-
398
- while np.any(new_pos[:, 0] > np.pi) or np.any(new_pos[:, 0] < -np.pi):
399
- new_pos[:, 0] = np.where(
400
- new_pos[:, 0] > np.pi, new_pos[:, 0] - 2 * np.pi, new_pos[:, 0]
401
- )
402
- new_pos[:, 0] = np.where(
403
- new_pos[:, 0] < -np.pi, new_pos[:, 0] + 2 * np.pi, new_pos[:, 0]
404
- )
405
-
406
- while np.any(new_pos[:, 1] > np.pi / 2) or np.any(new_pos[:, 1] < -np.pi / 2):
407
- new_pos[:, 1] = np.where(
408
- new_pos[:, 1] > np.pi / 2, new_pos[:, 1] - np.pi, new_pos[:, 1]
409
- )
410
- new_pos[:, 1] = np.where(
411
- new_pos[:, 1] < -np.pi / 2, new_pos[:, 1] + np.pi, new_pos[:, 1]
412
- )
413
-
414
- return new_pos
415
-
416
-
417
- def convert_to_si_units(xds):
418
- for data_var in xds.data_vars:
419
- if "units" in xds[data_var].attrs:
420
- for u_i, u in enumerate(xds[data_var].attrs["units"]):
421
- if u == "km":
422
- xds[data_var][..., u_i] = xds[data_var][..., u_i] * 1e3
423
- xds[data_var].attrs["units"][u_i] = "m"
424
- if u == "km/s":
425
- xds[data_var][..., u_i] = xds[data_var][..., u_i] * 1e3
426
- xds[data_var].attrs["units"][u_i] = "m/s"
427
- if u == "deg":
428
- xds[data_var][..., u_i] = xds[data_var][..., u_i] * np.pi / 180
429
- xds[data_var].attrs["units"][u_i] = "rad"
430
- if u == "Au" or u == "AU":
431
- xds[data_var][..., u_i] = xds[data_var][..., u_i] * 149597870700
432
- xds[data_var].attrs["units"][u_i] = "m"
433
- if u == "Au/d" or u == "AU/d":
434
- xds[data_var][..., u_i] = (
435
- xds[data_var][..., u_i] * 149597870700 / 86400
436
- )
437
- xds[data_var].attrs["units"][u_i] = "m/s"
438
- if u == "arcsec":
439
- xds[data_var][..., u_i] = xds[data_var][..., u_i] * np.pi / 648000
440
- xds[data_var].attrs["units"][u_i] = "rad"
441
- return xds
442
-
443
-
444
- def extract_source_info(xds, path, is_ephemeris, source_id, spectral_window_id):
421
+ def extract_source_info(xds, path, source_id, spectral_window_id):
445
422
  """
446
423
  Extracts source information from the given path and adds it to the xarray dataset.
447
424
 
@@ -451,8 +428,6 @@ def extract_source_info(xds, path, is_ephemeris, source_id, spectral_window_id):
451
428
  The xarray dataset to which the source information will be added.
452
429
  path : str
453
430
  The path to the input file.
454
- is_ephemeris : bool
455
- Flag indicating if the source is an ephemeris.
456
431
  source_id : int
457
432
  The ID of the source.
458
433
  spectral_window_id : int
@@ -464,65 +439,104 @@ def extract_source_info(xds, path, is_ephemeris, source_id, spectral_window_id):
464
439
  The xarray dataset with the added source information.
465
440
  """
466
441
 
467
- if source_id == -1:
442
+ is_ephemeris = xds.attrs[
443
+ "is_ephemeris"
444
+ ] # If ephemeris data is present we ignore the SOURCE_DIRECTION in the source table.
445
+
446
+ if all(source_id == -1):
468
447
  logger.warning(
469
448
  f"Source_id is -1. No source information will be included in the field_and_source_xds."
470
449
  )
471
- xds.attrs["source_name"] = "None"
450
+ xds = xds.assign_coords(
451
+ {"source_name": "Unknown"}
452
+ ) # Need to add this for ps.summary() to work.
472
453
  return xds
473
454
 
455
+ from xradio._utils.list_and_array import check_if_consistent, unique_1d
456
+
457
+ unique_source_id = unique_1d(source_id)
458
+ taql_where = f"where (SOURCE_ID IN [{','.join(map(str, unique_source_id))}]) AND (SPECTRAL_WINDOW_ID = {spectral_window_id})"
459
+
474
460
  source_xds = read_generic_table(
475
461
  path,
476
462
  "SOURCE",
477
463
  ignore=["SOURCE_MODEL"], # Trying to read SOURCE_MODEL causes an error.
478
- taql_where=f"where SOURCE_ID = {source_id} AND SPECTRAL_WINDOW_ID = {spectral_window_id}",
464
+ taql_where=taql_where,
479
465
  )
480
466
 
481
467
  if len(source_xds.data_vars) == 0: # The source xds is empty.
482
468
  logger.warning(
483
469
  f"SOURCE table empty for source_id {source_id} and spectral_window_id {spectral_window_id}."
484
470
  )
485
- xds.attrs["source_name"] = "None"
471
+ xds = xds.assign_coords(
472
+ {"source_name": "Unknown"}
473
+ ) # Need to add this for ps.summary() to work.
486
474
  return xds
487
475
 
488
- assert (
489
- len(source_xds.source_id) == 1
490
- ), "Can only process source table with a single source_id and spectral_window_id for a given MSv4 partition."
491
476
  assert (
492
477
  len(source_xds.spectral_window_id) == 1
493
- ), "Can only process source table with a single source_id and spectral_window_id for a given MSv4 partition."
478
+ ), "Can only process source table with a single spectral_window_id for a given MSv4 partition."
479
+
480
+ # This source table time is not the same as the time in the field_and_source_xds that is derived from the main MSv4 time axis.
481
+ # The source_id maps to the time axis in the field_and_source_xds. That is why "if len(source_id) == 1" is used to check if there should be a time axis.
494
482
  assert (
495
483
  len(source_xds.time) == 1
496
484
  ), "Can only process source table with a single time entry for a source_id and spectral_window_id."
497
- source_xds = source_xds.isel(time=0, source_id=0, spectral_window_id=0)
498
485
 
499
- xds.attrs["source_name"] = str(source_xds["name"].data)
500
- xds.attrs["code"] = str(source_xds["code"].data)
486
+ source_xds = source_xds.isel(time=0, spectral_window_id=0, drop=True)
501
487
  source_column_description = source_xds.attrs["other"]["msv2"]["ctds_attrs"][
502
488
  "column_descriptions"
503
489
  ]
504
490
 
491
+ # Get source name (the time axis is optional and will probably be required if the partition scheme does not include 'FIELD_ID' or 'SOURCE_ID'.).
492
+ # Note again that this optional time axis has nothing to do with the original time axis in the source table that we drop.
493
+ coords = {}
494
+ if len(source_id) == 1:
495
+ source_xds = source_xds.sel(source_id=source_id[0])
496
+ coords["source_name"] = (
497
+ source_xds["name"].values.item() + "_" + str(source_id[0])
498
+ )
499
+ direction_dims = ["sky_dir_label"]
500
+ # coords["source_id"] = source_id[0]
501
+ else:
502
+ source_xds = source_xds.sel(source_id=source_id)
503
+ coords["source_name"] = (
504
+ "time",
505
+ np.char.add(
506
+ source_xds["name"].data, np.char.add("_", source_id.astype(str))
507
+ ),
508
+ )
509
+ direction_dims = ["time", "sky_dir_label"]
510
+ # coords["source_id"] = ("time", source_id)
511
+
512
+ # If ephemeris data is present we ignore the SOURCE_DIRECTION.
505
513
  if not is_ephemeris:
506
514
  msv4_measure = column_description_casacore_to_msv4_measure(
507
515
  source_column_description["DIRECTION"]
508
516
  )
509
517
  xds["SOURCE_DIRECTION"] = xr.DataArray(
510
- source_xds["direction"].data, dims=["sky_dir_label"]
518
+ source_xds["direction"].data, dims=direction_dims
511
519
  )
512
520
  xds["SOURCE_DIRECTION"].attrs.update(msv4_measure)
513
521
 
514
- # msv4_measure = column_description_casacore_to_msv4_measure(
515
- # source_column_description["PROPER_MOTION"]
516
- # )
517
- # xds["SOURCE_PROPER_MOTION"] = xr.DataArray(
518
- # source_xds["proper_motion"].data, dims=["sky_dir_label"]
519
- # )
520
- # xds["SOURCE_PROPER_MOTION"].attrs.update(msv4_measure)
522
+ # Do we have line data:
523
+ if source_xds["num_lines"].data.ndim == 0:
524
+ num_lines = np.array([source_xds["num_lines"].data.item()])
525
+ else:
526
+ num_lines = source_xds["num_lines"].data
527
+
528
+ if any(num_lines > 0):
521
529
 
522
- # ['DIRECTION', 'PROPER_MOTION', 'CALIBRATION_GROUP', 'CODE', 'INTERVAL', 'NAME', 'NUM_LINES', 'SOURCE_ID', 'SPECTRAL_WINDOW_ID', 'TIME', 'POSITION', 'TRANSITION', 'REST_FREQUENCY', 'SYSVEL']
523
- if source_xds["num_lines"] > 0:
524
- coords = {"line_name": source_xds["transition"].data}
525
- xds = xds.assign_coords(coords)
530
+ if len(source_id) == 1:
531
+ coords_lines = {"line_name": source_xds["transition"].data}
532
+ xds = xds.assign_coords(coords_lines)
533
+ line_dims = ["line_label"]
534
+ else:
535
+ coords_lines = {
536
+ "line_name": (("time", "line_label"), source_xds["transition"].data)
537
+ }
538
+ xds = xds.assign_coords(coords_lines)
539
+ line_dims = ["time", "line_label"]
526
540
 
527
541
  optional_data_variables = {
528
542
  "rest_frequency": "LINE_REST_FREQUENCY",
@@ -533,8 +547,9 @@ def extract_source_info(xds, path, is_ephemeris, source_id, spectral_window_id):
533
547
  msv4_measure = column_description_casacore_to_msv4_measure(
534
548
  source_column_description[generic_name.upper()]
535
549
  )
550
+
536
551
  xds[msv4_name] = xr.DataArray(
537
- source_xds[generic_name].data, dims=["line_name"]
552
+ source_xds[generic_name].data, dims=line_dims
538
553
  )
539
554
  xds[msv4_name].attrs.update(msv4_measure)
540
555
 
@@ -546,10 +561,11 @@ def extract_source_info(xds, path, is_ephemeris, source_id, spectral_window_id):
546
561
  )
547
562
  assert (
548
563
  False
549
- ), "Doppler table present. Please open an issue on https://github.com/casangi/xradio/issues so that we can addd support for this."
564
+ ), "Doppler table present. Please open an issue on https://github.com/casangi/xradio/issues so that we can add support for this."
550
565
  except:
551
566
  pass
552
567
 
568
+ xds = xds.assign_coords(coords)
553
569
  return xds
554
570
 
555
571
 
@@ -577,47 +593,41 @@ def create_field_info_and_check_ephemeris(
577
593
  ephemeris_table_name : str
578
594
  The name of the ephemeris table.
579
595
  """
596
+
597
+ # Federico do know how to do this taql query?
598
+ unqiue_field_id = unique_1d(
599
+ field_id
600
+ ) # field_ids can be repeated so that the time mapping is correct if there are multiple fields. The read_generic_table required unique field_ids.
601
+ taql_where = f"where (ROWID() IN [{','.join(map(str, unqiue_field_id))}])"
580
602
  field_xds = read_generic_table(
581
603
  in_file,
582
604
  "FIELD",
583
605
  rename_ids=subt_rename_ids["FIELD"],
584
- ) # .sel(field_id=field_id)
585
- # print('1****',field_xds)
586
- # field_xds['field_id'] = np.arange(len(field_xds.field_id))
587
-
588
- assert len(field_xds.poly_id) == 1, "Polynomial field positions not supported."
589
- field_xds = field_xds.isel(poly_id=0)
590
- field_xds = field_xds.sel(field_id=field_id)
591
-
592
- from xradio._utils.array import check_if_consistent
593
-
594
- source_id = check_if_consistent(field_xds.source_id, "source_id")
595
-
596
- # print('source_id', source_id)
597
- # print(field_xds)
598
- # print('***')
599
- # print(field_xds.field_id)
600
- # print('***')
601
- # print(field_id)
602
-
603
- field_and_source_xds.attrs.update(
604
- {
605
- "field_name": str(field_xds["name"].data),
606
- "field_code": str(field_xds["code"].data),
607
- # "field_id": field_id,
608
- "source_id": source_id,
609
- }
606
+ taql_where=taql_where,
610
607
  )
611
608
 
609
+ assert (
610
+ len(field_xds.poly_id) == 1
611
+ ), "Polynomial field positions not supported. Please open an issue on https://github.com/casangi/xradio/issues so that we can add support for this."
612
+ field_xds = field_xds.isel(poly_id=0, drop=True)
613
+ # field_xds = field_xds.assign_coords({'field_id':field_xds['field_id'].data})
614
+ field_xds = field_xds.assign_coords({"field_id": unqiue_field_id})
615
+ field_xds = field_xds.sel(field_id=field_id, drop=False)
616
+ source_id = to_np_array(field_xds.source_id.values)
617
+
612
618
  ephemeris_table_name = None
613
619
  ephemeris_path = None
614
620
  is_ephemeris = False
621
+ field_and_source_xds.attrs["is_ephemeris"] = (
622
+ False # If we find a path to the ephemeris table we will set this to True.
623
+ )
615
624
 
616
- # Need to check if ephemeris_id is present and if epehemeris table is present.
625
+ # Need to check if ephemeris_id is present and if ephemeris table is present.
617
626
  if "ephemeris_id" in field_xds:
618
627
  ephemeris_id = check_if_consistent(
619
628
  field_xds.ephemeris_id, "ephemeris_id"
620
629
  ) # int(field_xds["ephemeris_id"].data)
630
+
621
631
  if ephemeris_id > -1:
622
632
  files = os.listdir(os.path.join(in_file, "FIELD"))
623
633
  ephemeris_table_name_start = "EPHEM" + str(ephemeris_id)
@@ -625,6 +635,7 @@ def create_field_info_and_check_ephemeris(
625
635
  ephemeris_name_table_index = [
626
636
  i for i in range(len(files)) if ephemeris_table_name_start in files[i]
627
637
  ]
638
+
628
639
  assert len(ephemeris_name_table_index) == 1, (
629
640
  "More than one ephemeris table which starts with "
630
641
  + ephemeris_table_name_start
@@ -635,28 +646,12 @@ def create_field_info_and_check_ephemeris(
635
646
  e_index = ephemeris_name_table_index[0]
636
647
  ephemeris_path = os.path.join(in_file, "FIELD")
637
648
  ephemeris_table_name = files[e_index]
649
+ field_and_source_xds.attrs["is_ephemeris"] = True
638
650
  else:
639
651
  logger.warning(
640
652
  f"Could not find ephemeris table for field_id {field_id}. Ephemeris information will not be included in the field_and_source_xds."
641
653
  )
642
654
 
643
- # if is_ephemeris:
644
- # field_data_variables = {
645
- # "delay_dir": "FIELD_DELAY_CENTER_OFFSET",
646
- # "phase_dir": "FIELD_PHASE_CENTER_OFFSET",
647
- # "reference_dir": "FIELD_REFERENCE_CENTER_OFFSET",
648
- # }
649
- # field_measures_type = "sky_coord_offset"
650
- # field_and_source_xds.attrs["field_and_source_xds_type"] = "ephemeris"
651
- # else:
652
- # field_data_variables = {
653
- # "delay_dir": "FIELD_DELAY_CENTER",
654
- # "phase_dir": "FIELD_PHASE_CENTER",
655
- # "reference_dir": "FIELD_REFERENCE_CENTER",
656
- # }
657
- # field_measures_type = "sky_coord"
658
- # field_and_source_xds.attrs["field_and_source_xds_type"] = "standard"
659
-
660
655
  if is_single_dish:
661
656
  field_data_variables = {
662
657
  "reference_dir": "FIELD_REFERENCE_CENTER",
@@ -674,14 +669,21 @@ def create_field_info_and_check_ephemeris(
674
669
  coords["sky_dir_label"] = ["ra", "dec"]
675
670
  field_column_description = field_xds.attrs["other"]["msv2"]["ctds_attrs"][
676
671
  "column_descriptions"
677
- ] # Keys are ['DELAY_DIR', 'PHASE_DIR', 'REFERENCE_DIR', 'CODE', 'FLAG_ROW', 'NAME', 'NUM_POLY', 'SOURCE_ID', 'TIME']
672
+ ]
678
673
 
679
674
  coords = {}
680
- coords["sky_dir_label"] = ["ra", "dec"]
675
+ # field_times is the same as the time axis in the main MSv4 dataset and is used if more than one field is present.
681
676
  if field_times is not None:
682
677
  coords["time"] = field_times
683
678
  dims = ["time", "sky_dir_label"]
679
+ coords["field_name"] = (
680
+ "time",
681
+ np.char.add(field_xds["name"].data, np.char.add("_", field_id.astype(str))),
682
+ )
683
+ # coords["field_id"] = ("time", field_id)
684
684
  else:
685
+ coords["field_name"] = field_xds["name"].values.item() + "_" + str(field_id)
686
+ # coords["field_id"] = field_id
685
687
  dims = ["sky_dir_label"]
686
688
 
687
689
  for generic_name, msv4_name in field_data_variables.items():
@@ -707,4 +709,4 @@ def create_field_info_and_check_ephemeris(
707
709
  field_and_source_xds[msv4_name].attrs["type"] = field_measures_type
708
710
 
709
711
  field_and_source_xds = field_and_source_xds.assign_coords(coords)
710
- return field_and_source_xds, ephemeris_path, ephemeris_table_name
712
+ return field_and_source_xds, ephemeris_path, ephemeris_table_name, source_id