xradio 1.0.2__py3-none-any.whl → 1.1.0__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.
Files changed (44) hide show
  1. xradio/_utils/_casacore/casacore_from_casatools.py +1 -1
  2. xradio/_utils/dict_helpers.py +38 -7
  3. xradio/_utils/list_and_array.py +26 -3
  4. xradio/_utils/schema.py +44 -0
  5. xradio/_utils/xarray_helpers.py +63 -0
  6. xradio/_utils/zarr/common.py +4 -2
  7. xradio/image/__init__.py +4 -2
  8. xradio/image/_util/_casacore/common.py +2 -1
  9. xradio/image/_util/_casacore/xds_from_casacore.py +105 -51
  10. xradio/image/_util/_casacore/xds_to_casacore.py +117 -52
  11. xradio/image/_util/_fits/xds_from_fits.py +124 -36
  12. xradio/image/_util/_zarr/common.py +0 -1
  13. xradio/image/_util/casacore.py +133 -16
  14. xradio/image/_util/common.py +6 -5
  15. xradio/image/_util/image_factory.py +466 -27
  16. xradio/image/image.py +72 -100
  17. xradio/image/image_xds.py +262 -0
  18. xradio/image/schema.py +85 -0
  19. xradio/measurement_set/__init__.py +5 -4
  20. xradio/measurement_set/_utils/_msv2/_tables/read.py +7 -3
  21. xradio/measurement_set/_utils/_msv2/conversion.py +6 -9
  22. xradio/measurement_set/_utils/_msv2/create_field_and_source_xds.py +1 -0
  23. xradio/measurement_set/_utils/_msv2/msv4_sub_xdss.py +1 -1
  24. xradio/measurement_set/_utils/_utils/interpolate.py +5 -0
  25. xradio/measurement_set/_utils/_utils/partition_attrs.py +0 -1
  26. xradio/measurement_set/convert_msv2_to_processing_set.py +9 -9
  27. xradio/measurement_set/load_processing_set.py +2 -2
  28. xradio/measurement_set/measurement_set_xdt.py +83 -93
  29. xradio/measurement_set/open_processing_set.py +1 -1
  30. xradio/measurement_set/processing_set_xdt.py +33 -26
  31. xradio/schema/check.py +70 -19
  32. xradio/schema/common.py +0 -1
  33. xradio/testing/__init__.py +0 -0
  34. xradio/testing/_utils/__template__.py +58 -0
  35. xradio/testing/measurement_set/__init__.py +58 -0
  36. xradio/testing/measurement_set/checker.py +131 -0
  37. xradio/testing/measurement_set/io.py +22 -0
  38. xradio/testing/measurement_set/msv2_io.py +1854 -0
  39. {xradio-1.0.2.dist-info → xradio-1.1.0.dist-info}/METADATA +64 -23
  40. xradio-1.1.0.dist-info/RECORD +75 -0
  41. {xradio-1.0.2.dist-info → xradio-1.1.0.dist-info}/WHEEL +1 -1
  42. xradio-1.0.2.dist-info/RECORD +0 -66
  43. {xradio-1.0.2.dist-info → xradio-1.1.0.dist-info}/licenses/LICENSE.txt +0 -0
  44. {xradio-1.0.2.dist-info → xradio-1.1.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  import copy
2
2
  import os
3
+ import time
3
4
 
4
5
  import dask.array as da
5
6
  import numpy as np
@@ -12,9 +13,19 @@ try:
12
13
  except ImportError:
13
14
  import xradio._utils._casacore.casacore_from_casatools as tables
14
15
 
15
- from .common import _active_mask, _create_new_image, _object_name, _pointing_center
16
- from ..common import _aperture_or_sky, _compute_sky_reference_pixel, _doppler_types
17
- from ...._utils._casacore.tables import open_table_rw
16
+ from xradio.image._util._casacore.common import (
17
+ _image_flag,
18
+ _beam_fit_params,
19
+ _create_new_image,
20
+ _object_name,
21
+ _pointing_center,
22
+ )
23
+ from xradio.image._util.common import (
24
+ _aperture_or_sky,
25
+ _compute_sky_reference_pixel,
26
+ _doppler_types,
27
+ )
28
+ from xradio._utils._casacore.tables import open_table_rw
18
29
 
19
30
 
20
31
  def _compute_direction_dict(xds: xr.Dataset) -> dict:
@@ -23,31 +34,35 @@ def _compute_direction_dict(xds: xr.Dataset) -> dict:
23
34
  for a CASA image coordinate system
24
35
  """
25
36
  direction = {}
26
- xds_dir = xds.attrs["direction"]
37
+
38
+ xds_dir = xds.attrs["coordinate_system_info"]
27
39
  direction["_axes_sizes"] = np.array(
28
40
  [xds.sizes[dim] for dim in ("l", "m")], dtype=np.int32
29
41
  )
30
42
  direction["_image_axes"] = np.array([2, 3], dtype=np.int32)
31
- direction["system"] = xds_dir["reference"]["attrs"]["equinox"].upper()
43
+ direction["system"] = xds_dir["reference_direction"]["attrs"]["equinox"].upper()
32
44
  if direction["system"] == "J2000.0":
33
45
  direction["system"] = "J2000"
34
46
  direction["projection"] = xds_dir["projection"]
35
47
  direction["projection_parameters"] = xds_dir["projection_parameters"]
36
48
  direction["units"] = [
37
- xds_dir["reference"]["attrs"]["units"],
38
- xds_dir["reference"]["attrs"]["units"],
49
+ xds_dir["reference_direction"]["attrs"]["units"],
50
+ xds_dir["reference_direction"]["attrs"]["units"],
39
51
  ]
40
- direction["crval"] = np.array(xds_dir["reference"]["data"])
52
+ direction["crval"] = np.array(xds_dir["reference_direction"]["data"])
41
53
  direction["cdelt"] = np.array((xds.l[1] - xds.l[0], xds.m[1] - xds.m[0]))
42
54
  direction["crpix"] = _compute_sky_reference_pixel(xds)
43
- direction["pc"] = np.array(xds_dir["pc"])
55
+ direction["pc"] = np.array(xds_dir["pixel_coordinate_transformation_matrix"])
44
56
  direction["axes"] = ["Right Ascension", "Declination"]
45
57
  direction["conversionSystem"] = direction["system"]
46
- for s in ["longpole", "latpole"]:
58
+ for i, s in enumerate(["longpole", "latpole"]):
47
59
  m = "lonpole" if s == "longpole" else s
48
60
  # lonpole, latpole are numerical values in degrees in casa images
49
61
  direction[s] = float(
50
- Angle(str(xds_dir[m]["data"]) + xds_dir[m]["attrs"]["units"]).deg
62
+ Angle(
63
+ str(xds_dir["native_pole_direction"]["data"][i])
64
+ + xds_dir["native_pole_direction"]["attrs"]["units"]
65
+ ).deg
51
66
  )
52
67
  return direction
53
68
 
@@ -205,50 +220,94 @@ def _coord_dict_from_xds(xds: xr.Dataset) -> dict:
205
220
 
206
221
 
207
222
  def _history_from_xds(xds: xr.Dataset, image: str) -> None:
208
- nrows = len(xds.history.row) if "row" in xds.data_vars else 0
223
+ """
224
+ Write history from xds attributes to CASA image logtable.
225
+
226
+ Parameters
227
+ ----------
228
+ xds : xr.Dataset
229
+ Dataset with potential history attribute (stored as dict).
230
+ image : str
231
+ Path to the CASA image.
232
+
233
+ Notes
234
+ -----
235
+ History is stored in data variable attributes (e.g., SKY.attrs['history']),
236
+ not in the main dataset attributes.
237
+ """
238
+ # Check if history exists in data variable attributes (SKY, APERTURE, etc.)
239
+ history_dict = None
240
+ for var_name in xds.data_vars:
241
+ if "history" in xds[var_name].attrs:
242
+ history_dict = xds[var_name].attrs["history"]
243
+ break
244
+
245
+ # Also check dataset-level attributes as a fallback
246
+ if history_dict is None and "history" in xds.attrs:
247
+ history_dict = xds.attrs["history"]
248
+
249
+ if history_dict is None or not isinstance(history_dict, dict):
250
+ return
251
+
252
+ # Convert dict back to xr.Dataset to access data
253
+ try:
254
+ history_xds = xr.Dataset.from_dict(history_dict)
255
+ except Exception:
256
+ # If conversion fails, history dict may be malformed, skip writing
257
+ return
258
+
259
+ # Check for row in both dims and coords (it could be either depending on the xarray version)
260
+ nrows = 0
261
+ if "row" in history_xds.dims:
262
+ nrows = history_xds.sizes["row"]
263
+ elif "row" in history_xds.coords:
264
+ nrows = len(history_xds.row)
265
+ elif "row" in history_xds.data_vars:
266
+ # row might also be a data variable
267
+ nrows = len(history_xds.row)
268
+
209
269
  if nrows > 0:
210
270
  # TODO need to implement nrows == 0 case
211
271
  with open_table_rw(os.sep.join([image, "logtable"])) as tb:
212
272
  tb.addrows(nrows + 1)
213
273
  for c in ["TIME", "PRIORITY", "MESSAGE", "LOCATION", "OBJECT_ID"]:
214
- vals = xds.history[c].values
215
- if c == "TIME":
216
- k = time.time() + 40587 * 86400
217
- elif c == "PRIORITY":
218
- k = "INFO"
219
- elif c == "MESSAGE":
220
- k = (
221
- "Wrote xds to "
222
- + os.path.basename(image)
223
- + " using cngi_io.xds_to_casa_image_2()"
224
- )
225
- elif c == "LOCATION":
226
- k = "cngi_io.xds_to_casa_image_2"
227
- elif c == "OBJECT_ID":
228
- k = ""
229
- vals = np.append(vals, k)
230
- tb.putcol(c, vals)
274
+ if c in history_xds.data_vars or c in history_xds.coords:
275
+ vals = history_xds[c].values
276
+ if c == "TIME":
277
+ k = time.time() + 40587 * 86400
278
+ elif c == "PRIORITY":
279
+ k = "INFO"
280
+ elif c == "MESSAGE":
281
+ k = (
282
+ "Wrote xds to "
283
+ + os.path.basename(image)
284
+ + " using cngi_io.xds_to_casa_image_2()"
285
+ )
286
+ elif c == "LOCATION":
287
+ k = "cngi_io.xds_to_casa_image_2"
288
+ elif c == "OBJECT_ID":
289
+ k = ""
290
+ vals = np.append(vals, k)
291
+ tb.putcol(c, vals)
231
292
 
232
293
 
233
294
  def _imageinfo_dict_from_xds(xds: xr.Dataset) -> dict:
234
295
  ii = {}
235
296
  ap_sky = _aperture_or_sky(xds)
236
- ii["imagetype"] = (
237
- xds[ap_sky].attrs["image_type"] if "image_type" in xds[ap_sky].attrs else ""
238
- )
297
+ ii["imagetype"] = xds[ap_sky].attrs["type"] if "type" in xds[ap_sky].attrs else ""
239
298
  ii["objectname"] = (
240
299
  xds[ap_sky].attrs[_object_name] if _object_name in xds[ap_sky].attrs else ""
241
300
  )
242
- if "BEAM" in xds.data_vars:
301
+ if "BEAM_FIT_PARAMS" in xds.data_vars:
243
302
  # multi beam
244
303
  pp = {}
245
304
  pp["nChannels"] = xds.sizes["frequency"]
246
305
  pp["nStokes"] = xds.sizes["polarization"]
247
306
 
248
- bu = xds.BEAM.attrs["units"]
307
+ bu = xds.BEAM_FIT_PARAMS.attrs["units"]
249
308
  chan = 0
250
309
  polarization = 0
251
- bv = xds.BEAM.values
310
+ bv = xds.BEAM_FIT_PARAMS.values
252
311
  for i in range(pp["nChannels"] * pp["nStokes"]):
253
312
  bp = bv[0][chan][polarization][:]
254
313
  b = {
@@ -281,7 +340,9 @@ def _imageinfo_dict_from_xds(xds: xr.Dataset) -> dict:
281
340
 
282
341
 
283
342
  def _write_casa_data(xds: xr.Dataset, image_full_path: str) -> None:
343
+
284
344
  sky_ap = _aperture_or_sky(xds)
345
+
285
346
  if xds[sky_ap].shape[0] != 1:
286
347
  raise RuntimeError("XDS can only be converted if it has exactly one time plane")
287
348
  trans_coords = (
@@ -290,9 +351,8 @@ def _write_casa_data(xds: xr.Dataset, image_full_path: str) -> None:
290
351
  else ("frequency", "polarization", "v", "u")
291
352
  )
292
353
  casa_image_shape = xds[sky_ap].isel(time=0).transpose(*trans_coords).shape[::-1]
293
- active_mask = (
294
- xds[sky_ap].attrs["active_mask"] if _active_mask in xds[sky_ap].attrs else ""
295
- )
354
+ flag = xds[sky_ap].attrs[_image_flag] if _image_flag in xds[sky_ap].attrs else ""
355
+ # TODO this all needs to be refactored to use flag rather than mask
296
356
  masks = []
297
357
  masks_rec = {}
298
358
  mask_rec = {
@@ -311,10 +371,14 @@ def _write_casa_data(xds: xr.Dataset, image_full_path: str) -> None:
311
371
  }
312
372
  for m in xds.data_vars:
313
373
  attrs = xds[m].attrs
314
- if "image_type" in attrs and attrs["image_type"] == "Mask":
374
+ if "type" in attrs and attrs["type"] in ("mask", "flag"):
315
375
  masks_rec[m] = mask_rec
316
376
  masks_rec[m]["mask"] = f"Table: {os.sep.join([image_full_path, m])}"
317
377
  masks.append(m)
378
+ if flag and flag in xds.data_vars and flag not in masks:
379
+ masks_rec[flag] = mask_rec
380
+ masks_rec[flag]["mask"] = f"Table: {os.sep.join([image_full_path, flag])}"
381
+ masks.append(flag)
318
382
  myvars = [sky_ap]
319
383
  myvars.extend(masks)
320
384
  # xr.apply_ufunc seems to like stripping attributes from xds and its coordinates,
@@ -327,15 +391,15 @@ def _write_casa_data(xds: xr.Dataset, image_full_path: str) -> None:
327
391
  do_mask_nans = False
328
392
  there_are_nans = nan_mask.any()
329
393
  if there_are_nans:
330
- has_active_mask = bool(active_mask)
331
- if not has_active_mask:
394
+ has_flag = bool(flag)
395
+ if not has_flag:
332
396
  do_mask_nans = True
333
397
  else:
334
- notted_active_mask = xr.apply_ufunc(
335
- da.logical_not, xds[active_mask].copy(deep=True), dask="allowed"
398
+ notted_flag = xr.apply_ufunc(
399
+ da.logical_not, xds[flag].copy(deep=True), dask="allowed"
336
400
  )
337
401
  some_nans_are_not_already_masked = xr.apply_ufunc(
338
- da.logical_and, nan_mask, notted_active_mask, dask="allowed"
402
+ da.logical_and, nan_mask, notted_flag, dask="allowed"
339
403
  )
340
404
  do_mask_nans = some_nans_are_not_already_masked.any()
341
405
  if do_mask_nans:
@@ -350,8 +414,8 @@ def _write_casa_data(xds: xr.Dataset, image_full_path: str) -> None:
350
414
  ] = f"Table: {os.sep.join([image_full_path, mask_name])}"
351
415
  masks.append(mask_name)
352
416
  arr_masks[mask_name] = nan_mask
353
- if active_mask:
354
- mask_name = f"mask_xds_nans_or_{active_mask}"
417
+ if flag:
418
+ mask_name = f"mask_xds_nans_or_{flag}"
355
419
  i = 0
356
420
  while mask_name in masks:
357
421
  mask_name = f"mask_xds_nans{i}"
@@ -365,21 +429,22 @@ def _write_casa_data(xds: xr.Dataset, image_full_path: str) -> None:
365
429
  # apparent reason, so make a copy of the xds to run it on
366
430
  arr_masks[mask_name] = xr.apply_ufunc(
367
431
  da.logical_or,
368
- xds[active_mask].copy(deep=True),
432
+ xds[flag].copy(deep=True),
369
433
  nan_mask,
370
434
  dask="allowed",
371
435
  )
372
- active_mask = mask_name
436
+ flag = mask_name
373
437
  data_type = "complex" if "u" in xds.coords else "float"
374
- _write_initial_image(xds, image_full_path, active_mask, casa_image_shape[::-1])
438
+
439
+ _write_initial_image(xds, image_full_path, flag, casa_image_shape[::-1])
375
440
  for v in myvars:
376
- _write_pixels(v, active_mask, image_full_path, xds)
441
+ _write_pixels(v, flag, image_full_path, xds)
377
442
  for name, v in arr_masks.items():
378
- _write_pixels(name, active_mask, image_full_path, xds, v)
443
+ _write_pixels(name, flag, image_full_path, xds, v)
379
444
  if masks:
380
445
  with open_table_rw(image_full_path) as tb:
381
446
  tb.putkeyword("masks", masks_rec)
382
- tb.putkeyword("Image_defaultmask", active_mask)
447
+ tb.putkeyword("Image_defaultmask", flag)
383
448
 
384
449
 
385
450
  def _write_initial_image(
@@ -13,12 +13,14 @@ from astropy.time import Time
13
13
 
14
14
  from xradio._utils.coord_math import _deg_to_rad
15
15
  from xradio._utils.dict_helpers import (
16
+ make_direction_location_dict,
16
17
  make_quantity,
17
18
  make_spectral_coord_reference_dict,
18
19
  make_skycoord_dict,
19
20
  make_time_measure_dict,
20
21
  )
21
22
 
23
+ from xradio._utils.list_and_array import to_python_type
22
24
  from xradio.measurement_set._utils._utils.stokes_types import stokes_types
23
25
  from xradio.image._util.common import (
24
26
  _compute_linear_world_values,
@@ -34,6 +36,11 @@ from xradio.image._util.common import (
34
36
  _l_m_attr_notes,
35
37
  )
36
38
 
39
+ from xradio.image._util._casacore.common import (
40
+ _image_flag,
41
+ _beam_fit_params,
42
+ )
43
+
37
44
 
38
45
  def _fits_image_to_xds(
39
46
  img_full_path: str,
@@ -41,6 +48,7 @@ def _fits_image_to_xds(
41
48
  verbose: bool,
42
49
  do_sky_coords: bool,
43
50
  compute_mask: bool,
51
+ image_type: str = "SKY",
44
52
  ) -> dict:
45
53
  """
46
54
  compute_mask : bool, optional
@@ -65,13 +73,27 @@ def _fits_image_to_xds(
65
73
  sphr_dims = helpers["sphr_dims"]
66
74
  ary = _read_image_array(img_full_path, chunks, helpers, verbose)
67
75
  dim_order = _get_xds_dim_order(sphr_dims)
68
- xds = _add_sky_or_aperture(xds, ary, dim_order, header, helpers, sphr_dims)
76
+ xds = _add_sky_or_aperture(
77
+ xds, ary, dim_order, header, helpers, sphr_dims, image_type
78
+ )
69
79
  xds.attrs = attrs
70
80
  xds = _add_coord_attrs(xds, helpers)
71
81
  if helpers["has_multibeam"]:
72
- xds = _do_multibeam(xds, img_full_path)
82
+ xds = _do_multibeam(xds, img_full_path, image_type=image_type)
83
+ xds[image_type.upper()].attrs[_beam_fit_params] = (
84
+ "BEAM_FIT_PARAMS_" + image_type.upper()
85
+ )
86
+ xds["BEAM_FIT_PARAMS_" + image_type.upper()].attrs["type"] = (
87
+ "beam_fit_params_" + image_type.lower()
88
+ )
73
89
  elif "beam" in helpers and helpers["beam"] is not None:
74
- xds = _add_beam(xds, helpers)
90
+ xds = _add_beam(xds, helpers, image_type)
91
+ xds[image_type.upper()].attrs[_beam_fit_params] = (
92
+ "BEAM_FIT_PARAMS_" + image_type.upper()
93
+ )
94
+ xds["BEAM_FIT_PARAMS_" + image_type.upper()].attrs["type"] = (
95
+ "beam_fit_params_" + image_type.lower()
96
+ )
75
97
  return xds
76
98
 
77
99
 
@@ -116,7 +138,7 @@ def _add_vel_attrs(xds: xr.Dataset, helpers: dict) -> xr.Dataset:
116
138
  vel_coord = xds.coords["velocity"]
117
139
  meta = {"units": "m/s"}
118
140
  if helpers["has_freq"]:
119
- meta["doppler_type"] = helpers.get("doppler", "RADIO")
141
+ meta["doppler_type"] = helpers.get("doppler", "radio")
120
142
  else:
121
143
  meta["doppler_type"] = _doppler_types[0]
122
144
  meta["type"] = "doppler"
@@ -151,7 +173,7 @@ def _is_freq_like(v: str) -> bool:
151
173
  return v.startswith("FREQ") or v == "VOPT" or v == "VRAD"
152
174
 
153
175
 
154
- def _xds_direction_attrs_from_header(helpers: dict, header) -> dict:
176
+ def _xds_coordinate_system_info_attrs_from_header(helpers: dict, header) -> dict:
155
177
  # helpers is modified in place, headers is not modified
156
178
  t_axes = helpers["t_axes"]
157
179
  p0 = header[f"CTYPE{t_axes[0]}"][-3:]
@@ -161,8 +183,8 @@ def _xds_direction_attrs_from_header(helpers: dict, header) -> dict:
161
183
  f"Projections for direction axes ({p0}, {p1}) differ, but they "
162
184
  "must be the same"
163
185
  )
164
- direction = {}
165
- direction["projection"] = p0
186
+ coordinate_system_info = {}
187
+ coordinate_system_info["projection"] = p0
166
188
  helpers["projection"] = p0
167
189
  ref_sys = header["RADESYS"]
168
190
  ref_eqx = None if ref_sys.upper() == "ICRS" else header["EQUINOX"]
@@ -171,7 +193,9 @@ def _xds_direction_attrs_from_header(helpers: dict, header) -> dict:
171
193
  helpers["ref_sys"] = ref_sys
172
194
  helpers["ref_eqx"] = ref_eqx
173
195
  # fits does not support conversion frames
174
- direction["reference"] = make_skycoord_dict([0.0, 0.0], units="rad", frame=ref_sys)
196
+ coordinate_system_info["reference_direction"] = make_skycoord_dict(
197
+ [0.0, 0.0], units="rad", frame=ref_sys
198
+ )
175
199
  dir_axes = helpers["dir_axes"]
176
200
  ddata = []
177
201
  dunits = []
@@ -182,15 +206,20 @@ def _xds_direction_attrs_from_header(helpers: dict, header) -> dict:
182
206
  # direction["reference"]["value"][i] = x.value
183
207
  x = helpers["cdelt"][i] * u.Unit(_get_unit(helpers["cunit"][i]))
184
208
  dunits.append("rad")
185
- direction["reference"] = make_skycoord_dict(ddata, units=dunits, frame=ref_sys)
186
- if ref_eqx is not None:
187
- direction["reference"]["attrs"]["equinox"] = ref_eqx.lower()
188
- direction["latpole"] = make_quantity(
189
- header["LATPOLE"] * _deg_to_rad, "rad", dims=["l", "m"]
209
+ coordinate_system_info["reference_direction"] = make_skycoord_dict(
210
+ ddata, units=dunits, frame=ref_sys
190
211
  )
191
- direction["lonpole"] = make_quantity(
192
- header["LONPOLE"] * _deg_to_rad, "rad", dims=["l", "m"]
212
+ if ref_eqx is not None:
213
+ coordinate_system_info["reference_direction"]["attrs"][
214
+ "equinox"
215
+ ] = ref_eqx.lower()
216
+
217
+ coordinate_system_info["native_pole_direction"] = make_direction_location_dict(
218
+ [header["LONPOLE"] * _deg_to_rad, header["LATPOLE"] * _deg_to_rad],
219
+ units="rad",
220
+ frame="native_projection",
193
221
  )
222
+
194
223
  pc = np.zeros([2, 2])
195
224
  for i in (0, 1):
196
225
  for j in (0, 1):
@@ -205,10 +234,61 @@ def _xds_direction_attrs_from_header(helpers: dict, header) -> dict:
205
234
  f"Could not find PC{dir_axes[i]+1}_{dir_axes[j]+1} or "
206
235
  f"PC0{dir_axes[i]+1}_0{dir_axes[j]+1} in FITS header"
207
236
  )
208
- direction["pc"] = pc
237
+ coordinate_system_info["pixel_coordinate_transformation_matrix"] = to_python_type(
238
+ pc
239
+ )
209
240
  # Is there really no fits header parameter for projection_parameters?
210
- direction["projection_parameters"] = np.array([0.0, 0.0])
211
- return direction
241
+ coordinate_system_info["projection_parameters"] = [0.0, 0.0]
242
+ return coordinate_system_info
243
+
244
+ # direction = {}
245
+ # direction["projection"] = p0
246
+ # helpers["projection"] = p0
247
+ # ref_sys = header["RADESYS"]
248
+ # ref_eqx = None if ref_sys.upper() == "ICRS" else header["EQUINOX"]
249
+ # if ref_sys == "FK5" and ref_eqx == 2000:
250
+ # ref_eqx = "J2000.0"
251
+ # helpers["ref_sys"] = ref_sys
252
+ # helpers["ref_eqx"] = ref_eqx
253
+ # # fits does not support conversion frames
254
+ # direction["reference"] = make_skycoord_dict([0.0, 0.0], units="rad", frame=ref_sys)
255
+ # dir_axes = helpers["dir_axes"]
256
+ # ddata = []
257
+ # dunits = []
258
+ # for i in dir_axes:
259
+ # x = helpers["crval"][i] * u.Unit(_get_unit(helpers["cunit"][i]))
260
+ # x = x.to("rad")
261
+ # ddata.append(x.value)
262
+ # # direction["reference"]["value"][i] = x.value
263
+ # x = helpers["cdelt"][i] * u.Unit(_get_unit(helpers["cunit"][i]))
264
+ # dunits.append("rad")
265
+ # direction["reference"] = make_skycoord_dict(ddata, units=dunits, frame=ref_sys)
266
+ # if ref_eqx is not None:
267
+ # direction["reference"]["attrs"]["equinox"] = ref_eqx.lower()
268
+ # direction["latpole"] = make_quantity(
269
+ # header["LATPOLE"] * _deg_to_rad, "rad", dims=["l", "m"]
270
+ # )
271
+ # direction["lonpole"] = make_quantity(
272
+ # header["LONPOLE"] * _deg_to_rad, "rad", dims=["l", "m"]
273
+ # )
274
+ # pc = np.zeros([2, 2])
275
+ # for i in (0, 1):
276
+ # for j in (0, 1):
277
+ # # dir_axes are now 0-based, but fits needs 1-based
278
+ # try:
279
+ # pc[i][j] = header[f"PC{dir_axes[i]+1}_{dir_axes[j]+1}"]
280
+ # except KeyError:
281
+ # try:
282
+ # pc[i][j] = header[f"PC0{dir_axes[i]+1}_0{dir_axes[j]+1}"]
283
+ # except KeyError:
284
+ # raise RuntimeError(
285
+ # f"Could not find PC{dir_axes[i]+1}_{dir_axes[j]+1} or "
286
+ # f"PC0{dir_axes[i]+1}_0{dir_axes[j]+1} in FITS header"
287
+ # )
288
+ # direction["pc"] = pc
289
+ # # Is there really no fits header parameter for projection_parameters?
290
+ # direction["projection_parameters"] = np.array([0.0, 0.0])
291
+ # return direction
212
292
 
213
293
 
214
294
  def _fits_header_c_values_to_metadata(helpers: dict, header) -> None:
@@ -258,7 +338,7 @@ def _get_telescope_metadata(helpers: dict, header) -> dict:
258
338
  "type": "location",
259
339
  "units": "rad",
260
340
  },
261
- "data": np.array([long, lat]),
341
+ "data": [long, lat],
262
342
  "dims": ["ellipsoid_dir_label"],
263
343
  "coords": {
264
344
  "ellipsoid_dir_label": {
@@ -276,7 +356,7 @@ def _get_telescope_metadata(helpers: dict, header) -> dict:
276
356
  "type": "location",
277
357
  "units": "m",
278
358
  },
279
- "data": np.array([r]),
359
+ "data": [r],
280
360
  "dims": ["ellipsoid_dis_label"],
281
361
  "coords": {
282
362
  "ellipsoid_dis_label": {
@@ -465,7 +545,9 @@ def _fits_header_to_xds_attrs(
465
545
  else:
466
546
  raise RuntimeError("Could not find both direction axes")
467
547
  if dir_axes is not None:
468
- attrs["direction"] = _xds_direction_attrs_from_header(helpers, header)
548
+ attrs["coordinate_system_info"] = _xds_coordinate_system_info_attrs_from_header(
549
+ helpers, header
550
+ )
469
551
  helpers["has_mask"] = False
470
552
  if compute_mask:
471
553
  # 🧠 Why the primary.data reference here is Safe (does not cause
@@ -527,7 +609,7 @@ def _fits_header_to_xds_attrs(
527
609
  data=Time(header["DATE-OBS"], format="isot").mjd,
528
610
  units=["d"],
529
611
  scale=header["TIMESYS"],
530
- time_format="MJD",
612
+ time_format="mjd",
531
613
  )
532
614
 
533
615
  # TODO complete _make_history_xds when spec has been finalized
@@ -608,7 +690,8 @@ def _create_coords(
608
690
  else:
609
691
  # Fourier image
610
692
  coords["u"], coords["v"] = _get_uv_values(helpers)
611
- coords["beam_param"] = ["major", "minor", "pa"]
693
+ coords["beam_params_label"] = ["major", "minor", "pa"]
694
+
612
695
  xds = xr.Dataset(coords=coords)
613
696
  return xds
614
697
 
@@ -698,7 +781,7 @@ def _get_velocity_values(helpers: dict) -> list:
698
781
  # FIXME change namee, even if there is only a single beam, we make a
699
782
  # multi beam array using it. If we have a beam, it will always be
700
783
  # "mutltibeam" is name is redundant and confusing
701
- def _do_multibeam(xds: xr.Dataset, imname: str) -> xr.Dataset:
784
+ def _do_multibeam(xds: xr.Dataset, imname: str, image_type: str = "SKY") -> xr.Dataset:
702
785
  """Only run if we are sure there are multiple beams"""
703
786
  hdulist = fits.open(imname)
704
787
  for hdu in hdulist:
@@ -720,31 +803,33 @@ def _do_multibeam(xds: xr.Dataset, imname: str) -> xr.Dataset:
720
803
  beam_array[:, :, :, i] = (
721
804
  (beam_array[:, :, :, i] * units[i]).to("rad").value
722
805
  )
723
- return _create_beam_data_var(xds, beam_array)
806
+ return _create_beam_data_var(xds, beam_array, image_type)
724
807
  raise RuntimeError(
725
808
  "It looks like there should be a BEAMS table but no "
726
809
  "such table found in FITS file"
727
810
  )
728
811
 
729
812
 
730
- def _add_beam(xds: xr.Dataset, helpers: dict) -> xr.Dataset:
813
+ def _add_beam(xds: xr.Dataset, helpers: dict, image_type: str = "SKY") -> xr.Dataset:
731
814
  nchan = xds.sizes["frequency"]
732
815
  npol = xds.sizes["polarization"]
733
816
  beam_array = np.zeros([1, nchan, npol, 3])
734
817
  beam_array[0, :, :, 0] = helpers["beam"]["bmaj"]["data"]
735
818
  beam_array[0, :, :, 1] = helpers["beam"]["bmin"]["data"]
736
819
  beam_array[0, :, :, 2] = helpers["beam"]["pa"]["data"]
737
- return _create_beam_data_var(xds, beam_array)
820
+ return _create_beam_data_var(xds, beam_array, image_type)
738
821
 
739
822
 
740
- def _create_beam_data_var(xds: xr.Dataset, beam_array: np.array) -> xr.Dataset:
823
+ def _create_beam_data_var(
824
+ xds: xr.Dataset, beam_array: np.array, image_type: str = "SKY"
825
+ ) -> xr.Dataset:
741
826
  xdb = xr.DataArray(
742
- beam_array, dims=["time", "frequency", "polarization", "beam_param"]
827
+ beam_array, dims=["time", "frequency", "polarization", "beam_params_label"]
743
828
  )
744
- xdb = xdb.rename("BEAM")
745
- xdb = xdb.assign_coords(beam_param=["major", "minor", "pa"])
829
+ xdb = xdb.rename("BEAM_FIT_PARAMS_" + image_type.upper())
830
+ xdb = xdb.assign_coords(beam_params_label=["major", "minor", "pa"])
746
831
  xdb.attrs["units"] = "rad"
747
- xds["BEAM"] = xdb
832
+ xds["BEAM_FIT_PARAMS_" + image_type.upper()] = xdb
748
833
  return xds
749
834
 
750
835
 
@@ -779,7 +864,9 @@ def _add_sky_or_aperture(
779
864
  header,
780
865
  helpers: dict,
781
866
  has_sph_dims: bool,
867
+ image_type: str = "SKY",
782
868
  ) -> xr.Dataset:
869
+ # TODO add code to recognize aperture images and set image_type accordingly
783
870
  xda = xr.DataArray(ary, dims=dim_order)
784
871
  for h, a in zip(
785
872
  ["BUNIT", "BTYPE", "OBJECT", "OBSERVER"],
@@ -792,16 +879,17 @@ def _add_sky_or_aperture(
792
879
  xda.attrs["telescope"] = _get_telescope_metadata(helpers, header)
793
880
  xda.attrs["description"] = None
794
881
  xda.attrs["user"] = _user_attrs_from_header(header)
795
- name = "SKY" if has_sph_dims else "APERTURE"
882
+ # name = "SKY" if has_sph_dims else "APERTURE"
883
+ name = image_type
796
884
  xda = xda.rename(name)
797
885
  xds[xda.name] = xda
798
886
  if helpers["has_mask"]:
799
887
  pp = da if type(xda[0].data) == dask.array.core.Array else np
800
888
  mask = pp.isnan(xda)
801
889
  mask.attrs = {}
802
- mask = mask.rename("MASK0")
803
- xds["MASK0"] = mask
804
- xda.attrs["active_mask"] = "MASK0"
890
+ mask = mask.rename("FLAG_" + name.upper())
891
+ xds["FLAG_" + name.upper()] = mask
892
+ xds[name.upper()].attrs[_image_flag] = "FLAG_" + name.upper()
805
893
  xda = xda.rename(name)
806
894
  xds[xda.name] = xda
807
895
  return xds
@@ -1,6 +1,5 @@
1
1
  import numpy as np
2
2
 
3
-
4
3
  _np_types = {
5
4
  "complex128": np.complex128,
6
5
  "complex64": np.complex64,