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.
@@ -5,9 +5,7 @@ from typing import Tuple, Union
5
5
  import numpy as np
6
6
  import xarray as xr
7
7
 
8
- from xradio.vis._vis_utils._ms.msv2_to_msv4_meta import (
9
- column_description_casacore_to_msv4_measure,
10
- )
8
+ from xradio._utils.schema import column_description_casacore_to_msv4_measure
11
9
  from xradio.vis._vis_utils._ms.msv4_sub_xdss import interpolate_to_time
12
10
  from xradio.vis._vis_utils._ms.subtables import subt_rename_ids
13
11
  from xradio.vis._vis_utils._ms._tables.read import (
@@ -32,7 +30,7 @@ def create_field_and_source_xds(
32
30
  is_single_dish: bool,
33
31
  time_min_max: Tuple[np.float64, np.float64],
34
32
  ephemeris_interp_time: Union[xr.DataArray, None] = None,
35
- ):
33
+ ) -> tuple[xr.Dataset, int]:
36
34
  """
37
35
  Create a field and source xarray dataset (xds) from the given input file, field ID, and spectral window ID.
38
36
  Data is extracted from the FIELD and SOURCE tables and if there is ephemeris data, it is also extracted.
@@ -46,6 +44,10 @@ def create_field_and_source_xds(
46
44
  The field ids to select.
47
45
  spectral_window_id : int
48
46
  The ID of the spectral window.
47
+ field_times: list
48
+ Time data for field. It is the same as the time axis in the main MSv4 dataset and is used if more than one field is present.
49
+ is_single_dish: bool
50
+ whether the main xds has single-dish (SPECTRUM) data
49
51
  time_min_max : Tuple[np.float64, np.float46]
50
52
  Min / max times to constrain loading (usually to the time range relevant to an MSv4)
51
53
  ephemeris_interp_time : Union[xr.DataArray, None]
@@ -55,14 +57,16 @@ def create_field_and_source_xds(
55
57
  -------
56
58
  field_and_source_xds : xr.Dataset
57
59
  The xarray dataset containing the field and source information.
60
+ num_lines : int
61
+ Sum of num_lines for all unique sources.
58
62
  """
59
63
 
60
64
  start_time = time.time()
61
65
 
62
- field_and_source_xds = xr.Dataset()
66
+ field_and_source_xds = xr.Dataset(attrs={"type": "field_and_source"})
63
67
 
64
68
  field_and_source_xds, ephemeris_path, ephemeris_table_name, source_id = (
65
- create_field_info_and_check_ephemeris(
69
+ extract_field_info_and_check_ephemeris(
66
70
  field_and_source_xds, in_file, field_id, field_times, is_single_dish
67
71
  )
68
72
  )
@@ -77,7 +81,7 @@ def create_field_and_source_xds(
77
81
  ephemeris_interp_time,
78
82
  )
79
83
 
80
- field_and_source_xds = extract_source_info(
84
+ field_and_source_xds, num_lines = extract_source_info(
81
85
  field_and_source_xds, in_file, source_id, spectral_window_id
82
86
  )
83
87
 
@@ -95,7 +99,7 @@ def create_field_and_source_xds(
95
99
  if np.unique(field_and_source_xds[center_dv], axis=0).shape[0] == 1:
96
100
  field_and_source_xds = field_and_source_xds.isel(time=0).drop_vars("time")
97
101
 
98
- return field_and_source_xds, source_id
102
+ return field_and_source_xds, source_id, num_lines
99
103
 
100
104
 
101
105
  def extract_ephemeris_info(
@@ -119,7 +123,7 @@ def extract_ephemeris_info(
119
123
  The name of the ephemeris table.
120
124
  time_min_max : Tuple[np.float46, np.float64]
121
125
  Min / max times to constrain loading (usually to the time range relevant to an MSv4)
122
- ephemeris_interp_time : Union[xr.DataArray, None]
126
+ interp_time : Union[xr.DataArray, None]
123
127
  Time axis to interpolate the data vars to (usually main MSv4 time)
124
128
 
125
129
  Returns:
@@ -172,18 +176,17 @@ def extract_ephemeris_info(
172
176
  else:
173
177
  unit_keyword = "QuantumUnits"
174
178
 
179
+ # We are using the "time_ephemeris_axis" 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.
175
180
  coords = {
176
181
  "ellipsoid_pos_label": ["lon", "lat", "dist"],
177
- "ephem_time": ephemeris_xds[
178
- "time"
179
- ].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.
182
+ "time_ephemeris_axis": ephemeris_xds["time"].data,
180
183
  "sky_pos_label": ["ra", "dec", "dist"],
181
184
  }
182
185
 
183
186
  temp_xds = xr.Dataset()
184
187
 
185
- # Add mandatory data: SOURCE_POSITION
186
- temp_xds["SOURCE_POSITION"] = xr.DataArray(
188
+ # Add mandatory data: SOURCE_LOCATION (POSITION / sky_pos_label)
189
+ temp_xds["SOURCE_LOCATION"] = xr.DataArray(
187
190
  np.column_stack(
188
191
  (
189
192
  ephemeris_xds["RA"].data,
@@ -191,7 +194,7 @@ def extract_ephemeris_info(
191
194
  ephemeris_xds["Rho"].data,
192
195
  )
193
196
  ),
194
- dims=["ephem_time", "sky_pos_label"],
197
+ dims=["time_ephemeris_axis", "sky_pos_label"],
195
198
  )
196
199
  # Have to use cast_to_str because the ephemeris table units are not consistently in a list or a string.
197
200
  sky_coord_units = [
@@ -199,13 +202,13 @@ def extract_ephemeris_info(
199
202
  cast_to_str(ephemris_column_description["DEC"]["keywords"][unit_keyword]),
200
203
  cast_to_str(ephemris_column_description["Rho"]["keywords"][unit_keyword]),
201
204
  ]
202
- temp_xds["SOURCE_POSITION"].attrs.update(
205
+ temp_xds["SOURCE_LOCATION"].attrs.update(
203
206
  {"type": "sky_coord", "frame": sky_coord_frame, "units": sky_coord_units}
204
207
  )
205
208
 
206
209
  # Add mandatory data: SOURCE_RADIAL_VELOCITY
207
210
  temp_xds["SOURCE_RADIAL_VELOCITY"] = xr.DataArray(
208
- ephemeris_xds["RadVel"].data, dims=["ephem_time"]
211
+ ephemeris_xds["RadVel"].data, dims=["time_ephemeris_axis"]
209
212
  )
210
213
  temp_xds["SOURCE_RADIAL_VELOCITY"].attrs.update(
211
214
  {
@@ -241,7 +244,7 @@ def extract_ephemeris_info(
241
244
  # Add optional data NORTH_POLE_POSITION_ANGLE and NORTH_POLE_ANGULAR_DISTANCE
242
245
  if "NP_ang" in ephemeris_xds.data_vars:
243
246
  temp_xds["NORTH_POLE_POSITION_ANGLE"] = xr.DataArray(
244
- ephemeris_xds["NP_ang"].data, dims=["ephem_time"]
247
+ ephemeris_xds["NP_ang"].data, dims=["time_ephemeris_axis"]
245
248
  )
246
249
  temp_xds["NORTH_POLE_POSITION_ANGLE"].attrs.update(
247
250
  {
@@ -256,7 +259,7 @@ def extract_ephemeris_info(
256
259
 
257
260
  if "NP_dist" in ephemeris_xds.data_vars:
258
261
  temp_xds["NORTH_POLE_ANGULAR_DISTANCE"] = xr.DataArray(
259
- ephemeris_xds["NP_dist"].data, dims=["ephem_time"]
262
+ ephemeris_xds["NP_dist"].data, dims=["time_ephemeris_axis"]
260
263
  )
261
264
  temp_xds["NORTH_POLE_ANGULAR_DISTANCE"].attrs.update(
262
265
  {
@@ -286,7 +289,7 @@ def extract_ephemeris_info(
286
289
  np.zeros(ephemeris_xds[key_lon].shape),
287
290
  )
288
291
  ),
289
- dims=["ephem_time", "ellipsoid_pos_label"],
292
+ dims=["time_ephemeris_axis", "ellipsoid_pos_label"],
290
293
  )
291
294
 
292
295
  temp_xds["SUB_OBSERVER_POSITION"].attrs.update(
@@ -316,7 +319,7 @@ def extract_ephemeris_info(
316
319
  ephemeris_xds["r"].data,
317
320
  )
318
321
  ),
319
- dims=["ephem_time", "ellipsoid_pos_label"],
322
+ dims=["time_ephemeris_axis", "ellipsoid_pos_label"],
320
323
  )
321
324
  temp_xds["SUB_SOLAR_POSITION"].attrs.update(
322
325
  {
@@ -341,7 +344,7 @@ def extract_ephemeris_info(
341
344
  # Add optional data: HELIOCENTRIC_RADIAL_VELOCITY
342
345
  if "rdot" in ephemeris_xds.data_vars:
343
346
  temp_xds["HELIOCENTRIC_RADIAL_VELOCITY"] = xr.DataArray(
344
- ephemeris_xds["rdot"].data, dims=["ephem_time"]
347
+ ephemeris_xds["rdot"].data, dims=["time_ephemeris_axis"]
345
348
  )
346
349
  temp_xds["HELIOCENTRIC_RADIAL_VELOCITY"].attrs.update(
347
350
  {
@@ -357,7 +360,7 @@ def extract_ephemeris_info(
357
360
  # Add optional data: OBSERVER_PHASE_ANGLE
358
361
  if "phang" in ephemeris_xds.data_vars:
359
362
  temp_xds["OBSERVER_PHASE_ANGLE"] = xr.DataArray(
360
- ephemeris_xds["phang"].data, dims=["ephem_time"]
363
+ ephemeris_xds["phang"].data, dims=["time_ephemeris_axis"]
361
364
  )
362
365
  temp_xds["OBSERVER_PHASE_ANGLE"].attrs.update(
363
366
  {
@@ -371,24 +374,33 @@ def extract_ephemeris_info(
371
374
  )
372
375
 
373
376
  temp_xds = temp_xds.assign_coords(coords)
374
- temp_xds["ephem_time"].attrs.update(
375
- {"type": "time", "units": ["s"], "scale": "UTC", "format": "UNIX"}
376
- )
377
+ time_coord_attrs = {
378
+ "type": "time",
379
+ "units": ["s"],
380
+ "scale": "UTC",
381
+ "format": "UNIX",
382
+ }
383
+ temp_xds["time_ephemeris_axis"].attrs.update(time_coord_attrs)
377
384
 
378
385
  # Convert to si units and interpolate if ephemeris_interpolate=True:
379
386
  temp_xds = convert_to_si_units(temp_xds)
380
387
  temp_xds = interpolate_to_time(
381
- temp_xds, interp_time, "field_and_source_xds", time_name="ephem_time"
388
+ temp_xds, interp_time, "field_and_source_xds", time_name="time_ephemeris_axis"
382
389
  )
383
390
 
384
- # If we interpolate rename the ephem_time axis to time.
391
+ # If we interpolate rename the time_ephemeris_axis axis to time.
385
392
  if interp_time is not None:
386
- temp_xds = temp_xds.swap_dims({"ephem_time": "time"}).drop_vars("ephem_time")
393
+ time_coord = {"time": ("time_ephemeris_axis", interp_time.data)}
394
+ temp_xds = temp_xds.assign_coords(time_coord)
395
+ temp_xds.coords["time"].attrs.update(time_coord_attrs)
396
+ temp_xds = temp_xds.swap_dims({"time_ephemeris_axis": "time"}).drop_vars(
397
+ "time_ephemeris_axis"
398
+ )
387
399
 
388
400
  xds = xr.merge([xds, temp_xds])
389
401
 
390
- # 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.
391
- # We also need to add a distance dimension to the FIELD_PHASE_CENTER or FIELD_REFERENCE_CENTER to match the SOURCE_POSITION.
402
+ # Add the SOURCE_LOCATION 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_LOCATION.
403
+ # We also need to add a distance dimension to the FIELD_PHASE_CENTER or FIELD_REFERENCE_CENTER to match the SOURCE_LOCATION.
392
404
  # FIELD_PHASE_CENTER is used for interferometer data and FIELD_REFERENCE_CENTER is used for single dish data.
393
405
  if is_single_dish:
394
406
  center_dv = "FIELD_REFERENCE_CENTER"
@@ -405,25 +417,27 @@ def extract_ephemeris_info(
405
417
  np.column_stack(
406
418
  (xds[center_dv].values, np.zeros(xds[center_dv].values.shape[0]))
407
419
  ),
408
- xds["SOURCE_POSITION"].values,
420
+ xds["SOURCE_LOCATION"].values,
409
421
  ),
410
- dims=[xds["SOURCE_POSITION"].dims[0], "sky_pos_label"],
422
+ dims=[xds["SOURCE_LOCATION"].dims[0], "sky_pos_label"],
411
423
  )
412
424
  else:
413
425
  xds[center_dv] = xr.DataArray(
414
426
  add_position_offsets(
415
427
  np.append(xds[center_dv].values, 0),
416
- xds["SOURCE_POSITION"].values,
428
+ xds["SOURCE_LOCATION"].values,
417
429
  ),
418
- dims=[xds["SOURCE_POSITION"].dims[0], "sky_pos_label"],
430
+ dims=[xds["SOURCE_LOCATION"].dims[0], "sky_pos_label"],
419
431
  )
420
432
 
421
- xds[center_dv].attrs.update(xds["SOURCE_POSITION"].attrs)
433
+ xds[center_dv].attrs.update(xds["SOURCE_LOCATION"].attrs)
422
434
 
423
435
  return xds
424
436
 
425
437
 
426
- def extract_source_info(xds, path, source_id, spectral_window_id):
438
+ def extract_source_info(
439
+ xds: xr.Dataset, path: str, source_id: int, spectral_window_id: int
440
+ ) -> tuple[xr.Dataset, int]:
427
441
  """
428
442
  Extracts source information from the given path and adds it to the xarray dataset.
429
443
 
@@ -442,6 +456,8 @@ def extract_source_info(xds, path, source_id, spectral_window_id):
442
456
  -------
443
457
  xds : xr.Dataset
444
458
  The xarray dataset with the added source information.
459
+ num_lines : int
460
+ Sum of num_lines for all unique sources extracted.
445
461
  """
446
462
  coords = {}
447
463
  is_ephemeris = xds.attrs[
@@ -455,7 +471,14 @@ def extract_source_info(xds, path, source_id, spectral_window_id):
455
471
  xds = xds.assign_coords(
456
472
  {"source_name": "Unknown"}
457
473
  ) # Need to add this for ps.summary() to work.
458
- return xds
474
+ return xds, 0
475
+
476
+ if not os.path.isdir(os.path.join(path, "SOURCE")):
477
+ logger.warning(
478
+ f"Could not find SOURCE table for source_id {source_id}. Source information will not be included in the field_and_source_xds."
479
+ )
480
+ xds = xds.assign_coords({"source_name": "Unknown"})
481
+ return xds, 0
459
482
 
460
483
  unique_source_id = unique_1d(source_id)
461
484
  taql_where = f"where (SOURCE_ID IN [{','.join(map(str, unique_source_id))}]) AND (SPECTRAL_WINDOW_ID = {spectral_window_id})"
@@ -474,7 +497,7 @@ def extract_source_info(xds, path, source_id, spectral_window_id):
474
497
  xds = xds.assign_coords(
475
498
  {"source_name": "Unknown"}
476
499
  ) # Need to add this for ps.summary() to work.
477
- return xds
500
+ return xds, 0
478
501
 
479
502
  assert (
480
503
  len(source_xds.SPECTRAL_WINDOW_ID) == 1
@@ -530,8 +553,12 @@ def extract_source_info(xds, path, source_id, spectral_window_id):
530
553
  else:
531
554
  direction_var = source_xds[direction_msv2_col]
532
555
 
533
- xds["SOURCE_DIRECTION"] = xr.DataArray(direction_var.data, dims=direction_dims)
534
- xds["SOURCE_DIRECTION"].attrs.update(msv4_measure)
556
+ # SOURCE_LOCATION (DIRECTION / sky_dir_label)
557
+ xds["SOURCE_LOCATION"] = xr.DataArray(direction_var.data, dims=direction_dims)
558
+ location_msv4_measure = column_description_casacore_to_msv4_measure(
559
+ source_column_description[direction_msv2_col]
560
+ )
561
+ xds["SOURCE_LOCATION"].attrs.update(location_msv4_measure)
535
562
 
536
563
  # Do we have line data:
537
564
  if source_xds["NUM_LINES"].data.ndim == 0:
@@ -557,12 +584,19 @@ def extract_source_info(xds, path, source_id, spectral_window_id):
557
584
  transition_var_data, max(transition_var_data.shape, vars_shape)
558
585
  )
559
586
 
587
+ line_label_data = np.arange(coords_lines_data.shape[-1]).astype(str)
560
588
  if len(source_id) == 1:
561
- coords_lines = {"line_name": coords_lines_data}
589
+ coords_lines = {
590
+ "line_name": ("line_label", coords_lines_data),
591
+ "line_label": line_label_data,
592
+ }
562
593
  xds = xds.assign_coords(coords_lines)
563
594
  line_dims = ["line_label"]
564
595
  else:
565
- coords_lines = {"line_name": (("time", "line_label"), coords_lines_data)}
596
+ coords_lines = {
597
+ "line_name": (("time", "line_label"), coords_lines_data),
598
+ "line_label": line_label_data,
599
+ }
566
600
  xds = xds.assign_coords(coords_lines)
567
601
  line_dims = ["time", "line_label"]
568
602
 
@@ -594,10 +628,13 @@ def extract_source_info(xds, path, source_id, spectral_window_id):
594
628
  pass
595
629
 
596
630
  xds = xds.assign_coords(coords)
597
- return xds
598
631
 
632
+ _, unique_source_ids_indices = np.unique(source_xds.SOURCE_ID, return_index=True)
599
633
 
600
- def create_field_info_and_check_ephemeris(
634
+ return xds, np.sum(num_lines[unique_source_ids_indices])
635
+
636
+
637
+ def extract_field_info_and_check_ephemeris(
601
638
  field_and_source_xds, in_file, field_id, field_times, is_single_dish
602
639
  ):
603
640
  """
@@ -738,4 +775,13 @@ def create_field_info_and_check_ephemeris(
738
775
  field_and_source_xds[msv4_name].attrs["type"] = field_measures_type
739
776
 
740
777
  field_and_source_xds = field_and_source_xds.assign_coords(coords)
778
+ if "time" in field_and_source_xds:
779
+ time_column_description = field_xds.attrs["other"]["msv2"]["ctds_attrs"][
780
+ "column_descriptions"
781
+ ]["TIME"]
782
+ time_msv4_measure = column_description_casacore_to_msv4_measure(
783
+ time_column_description
784
+ )
785
+ field_and_source_xds.coords["time"].attrs.update(time_msv4_measure)
786
+
741
787
  return field_and_source_xds, ephemeris_path, ephemeris_table_name, source_id
@@ -1,4 +1,5 @@
1
1
  import graphviper.utils.logger as logger
2
+ from xradio._utils.schema import column_description_casacore_to_msv4_measure
2
3
 
3
4
  col_to_data_variable_names = {
4
5
  "FLOAT_DATA": "SPECTRUM",
@@ -30,111 +31,6 @@ col_to_coord_names = {
30
31
  "ANTENNA2": "baseline_ant2_id",
31
32
  }
32
33
 
33
- # Map casacore measures to astropy
34
- casacore_to_msv4_measure_type = {
35
- "quanta": {
36
- "type": "quantity",
37
- },
38
- "direction": {"type": "sky_coord", "Ref": "frame", "Ref_map": {"J2000": "fk5"}},
39
- "epoch": {"type": "time", "Ref": "scale", "Ref_map": {"UTC": "utc"}},
40
- "frequency": {
41
- "type": "spectral_coord",
42
- "Ref": "frame",
43
- "Ref_map": {
44
- "REST": "REST",
45
- "LSRK": "LSRK",
46
- "LSRD": "LSRD",
47
- "BARY": "BARY",
48
- "GEO": "GEO",
49
- "TOPO": "TOPO",
50
- "GALACTO": "GALACTO",
51
- "LGROUP": "LGROUP",
52
- "CMB": "CMB",
53
- "Undefined": "Undefined",
54
- },
55
- },
56
- "position": {
57
- "type": "earth_location",
58
- "Ref": "ellipsoid",
59
- "Ref_map": {"ITRF": "GRS80"},
60
- },
61
- "uvw": {"type": "uvw", "Ref": "frame", "Ref_map": {"ITRF": "GRS80"}},
62
- "radialvelocity": {"type": "quantity"},
63
- }
64
-
65
- casa_frequency_frames = [
66
- "REST",
67
- "LSRK",
68
- "LSRD",
69
- "BARY",
70
- "GEO",
71
- "TOPO",
72
- "GALACTO",
73
- "LGROUP",
74
- "CMB",
75
- "Undefined",
76
- ]
77
-
78
- casa_frequency_frames_codes = [0, 1, 2, 3, 4, 5, 6, 7, 8, 64]
79
-
80
-
81
- def column_description_casacore_to_msv4_measure(
82
- casacore_column_description, ref_code=None, time_format="UNIX"
83
- ):
84
- import numpy as np
85
-
86
- msv4_measure = {}
87
- if "MEASINFO" in casacore_column_description["keywords"]:
88
- measinfo = casacore_column_description["keywords"]["MEASINFO"]
89
-
90
- # Get conversion information
91
- msv4_measure_conversion = casacore_to_msv4_measure_type[measinfo["type"]]
92
-
93
- # Convert type, copy unit
94
- msv4_measure["type"] = msv4_measure_conversion["type"]
95
- msv4_measure["units"] = list(
96
- casacore_column_description["keywords"]["QuantumUnits"]
97
- )
98
-
99
- # Reference frame to convert?
100
- if "Ref" in msv4_measure_conversion:
101
- # Find reference frame
102
- if "TabRefCodes" in measinfo:
103
- ref_index = np.where(measinfo["TabRefCodes"] == ref_code)[0][0]
104
- casa_ref = measinfo["TabRefTypes"][ref_index]
105
- elif "Ref" in measinfo:
106
- casa_ref = measinfo["Ref"]
107
- elif measinfo["type"] == "frequency":
108
- # Some MSv2 don't have the "TabRefCodes".
109
- ref_index = np.where(casa_frequency_frames_codes == ref_code)[0][0]
110
- casa_ref = casa_frequency_frames[ref_index]
111
- else:
112
- logger.debug(
113
- f"Could not determine {measinfo['type']} measure "
114
- "reference frame!"
115
- )
116
-
117
- # Convert into MSv4 representation of reference frame, warn if unknown
118
- if casa_ref in msv4_measure_conversion.get("Ref_map", {}):
119
- casa_ref = msv4_measure_conversion["Ref_map"][casa_ref]
120
- else:
121
- logger.debug(
122
- f"Unknown reference frame for {measinfo['type']} "
123
- f"measure, using verbatim: {casa_ref}"
124
- )
125
-
126
- msv4_measure[msv4_measure_conversion["Ref"]] = casa_ref
127
-
128
- if msv4_measure["type"] == "time":
129
- msv4_measure["format"] = time_format
130
- elif "QuantumUnits" in casacore_column_description["keywords"]:
131
- msv4_measure = {
132
- "type": "quantity",
133
- "units": list(casacore_column_description["keywords"]["QuantumUnits"]),
134
- }
135
-
136
- return msv4_measure
137
-
138
34
 
139
35
  def create_attribute_metadata(col, main_column_descriptions):
140
36
  attrs_metadata = column_description_casacore_to_msv4_measure(