xradio 0.0.55__py3-none-any.whl → 0.0.58__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 (64) hide show
  1. xradio/__init__.py +2 -2
  2. xradio/_utils/_casacore/casacore_from_casatools.py +1001 -0
  3. xradio/_utils/_casacore/tables.py +6 -1
  4. xradio/_utils/coord_math.py +22 -23
  5. xradio/_utils/dict_helpers.py +76 -11
  6. xradio/_utils/schema.py +5 -2
  7. xradio/_utils/zarr/common.py +1 -73
  8. xradio/image/_util/_casacore/common.py +11 -3
  9. xradio/image/_util/_casacore/xds_from_casacore.py +59 -35
  10. xradio/image/_util/_casacore/xds_to_casacore.py +47 -16
  11. xradio/image/_util/_fits/xds_from_fits.py +172 -77
  12. xradio/image/_util/casacore.py +9 -4
  13. xradio/image/_util/common.py +4 -4
  14. xradio/image/_util/image_factory.py +8 -8
  15. xradio/image/image.py +45 -5
  16. xradio/measurement_set/__init__.py +19 -9
  17. xradio/measurement_set/_utils/__init__.py +1 -3
  18. xradio/measurement_set/_utils/_msv2/__init__.py +0 -0
  19. xradio/measurement_set/_utils/_msv2/_tables/read.py +35 -90
  20. xradio/measurement_set/_utils/_msv2/_tables/read_main_table.py +6 -686
  21. xradio/measurement_set/_utils/_msv2/_tables/table_query.py +13 -3
  22. xradio/measurement_set/_utils/_msv2/conversion.py +129 -145
  23. xradio/measurement_set/_utils/_msv2/create_antenna_xds.py +9 -16
  24. xradio/measurement_set/_utils/_msv2/create_field_and_source_xds.py +125 -221
  25. xradio/measurement_set/_utils/_msv2/msv2_to_msv4_meta.py +1 -2
  26. xradio/measurement_set/_utils/_msv2/msv4_info_dicts.py +13 -8
  27. xradio/measurement_set/_utils/_msv2/msv4_sub_xdss.py +27 -72
  28. xradio/measurement_set/_utils/_msv2/partition_queries.py +5 -262
  29. xradio/measurement_set/_utils/_msv2/subtables.py +0 -107
  30. xradio/measurement_set/_utils/_utils/interpolate.py +60 -0
  31. xradio/measurement_set/_utils/_zarr/encoding.py +2 -7
  32. xradio/measurement_set/convert_msv2_to_processing_set.py +0 -2
  33. xradio/measurement_set/load_processing_set.py +2 -2
  34. xradio/measurement_set/measurement_set_xdt.py +14 -14
  35. xradio/measurement_set/open_processing_set.py +1 -3
  36. xradio/measurement_set/processing_set_xdt.py +41 -835
  37. xradio/measurement_set/schema.py +96 -123
  38. xradio/schema/check.py +91 -97
  39. xradio/schema/dataclass.py +159 -22
  40. xradio/schema/export.py +99 -0
  41. xradio/schema/metamodel.py +51 -16
  42. xradio/schema/typing.py +5 -5
  43. {xradio-0.0.55.dist-info → xradio-0.0.58.dist-info}/METADATA +43 -11
  44. xradio-0.0.58.dist-info/RECORD +65 -0
  45. {xradio-0.0.55.dist-info → xradio-0.0.58.dist-info}/WHEEL +1 -1
  46. xradio/image/_util/fits.py +0 -13
  47. xradio/measurement_set/_utils/_msv2/_tables/load.py +0 -63
  48. xradio/measurement_set/_utils/_msv2/_tables/load_main_table.py +0 -487
  49. xradio/measurement_set/_utils/_msv2/_tables/read_subtables.py +0 -395
  50. xradio/measurement_set/_utils/_msv2/_tables/write.py +0 -320
  51. xradio/measurement_set/_utils/_msv2/_tables/write_exp_api.py +0 -385
  52. xradio/measurement_set/_utils/_msv2/chunks.py +0 -115
  53. xradio/measurement_set/_utils/_msv2/descr.py +0 -165
  54. xradio/measurement_set/_utils/_msv2/msv2_msv3.py +0 -7
  55. xradio/measurement_set/_utils/_msv2/partitions.py +0 -392
  56. xradio/measurement_set/_utils/_utils/cds.py +0 -40
  57. xradio/measurement_set/_utils/_utils/xds_helper.py +0 -404
  58. xradio/measurement_set/_utils/_zarr/read.py +0 -263
  59. xradio/measurement_set/_utils/_zarr/write.py +0 -329
  60. xradio/measurement_set/_utils/msv2.py +0 -106
  61. xradio/measurement_set/_utils/zarr.py +0 -133
  62. xradio-0.0.55.dist-info/RECORD +0 -77
  63. {xradio-0.0.55.dist-info → xradio-0.0.58.dist-info}/licenses/LICENSE.txt +0 -0
  64. {xradio-0.0.55.dist-info → xradio-0.0.58.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1001 @@
1
+ """This module serves as an API bridge from `casatools` to `python-casacore`.
2
+
3
+ Features:
4
+ - Returns C-order numpy arrays.
5
+ - Workaround fpr the tablerow/tablecolumn-related API differences essential for the `xradio` use case.
6
+
7
+ Note: not fully implemented; not intended to be a full API adapter layer.
8
+ """
9
+
10
+ import ast
11
+ import inspect
12
+ import logging
13
+ import os
14
+ import shutil
15
+ from functools import wraps
16
+ from typing import Any, Dict, List, Sequence, Union
17
+
18
+ # Configure casaconfig settings prior to casatools import
19
+ # this ensures optimal initialization and resource allocation for casatools
20
+ # Also see : https://casadocs.readthedocs.io/en/stable/api/casaconfig.html
21
+ try:
22
+ import casaconfig.config
23
+ except ModuleNotFoundError as exc:
24
+ raise ModuleNotFoundError(
25
+ f"casaconfig.config cannot be found, probably because the package "
26
+ "casatools is not available. If you are here that is probably because "
27
+ "python-casacore is not available either. MSv2 related functionality "
28
+ "requires either python-casacore or casatools. Import failure details: "
29
+ f"{exc}"
30
+ )
31
+
32
+ import numpy as np
33
+ import toolviper.utils.logger as logger
34
+
35
+ casaconfig.config.data_auto_update = False
36
+ casaconfig.config.measures_auto_update = False
37
+ casaconfig.config.nologger = False
38
+ casaconfig.config.nogui = False
39
+ casaconfig.config.agg = True
40
+
41
+
42
+ def get_logger_config():
43
+ """Retrieve logger configuration details.
44
+
45
+ This function checks if the logger has a `FileHandler` attached and retrieves
46
+ the log file name if available. It also checks if a `StreamHandler` is attached
47
+ to the logger.
48
+
49
+ Returns:
50
+ tuple: A tuple containing:
51
+ - logfile (str or None): The log file name if a `FileHandler` is found, otherwise `None`.
52
+ - has_stream_handler (bool): `True` if a `StreamHandler` is attached, otherwise `False`.
53
+ """
54
+ logfile = None
55
+ logger_instance = logging.getLogger()
56
+
57
+ # Check for FileHandler and extract log filename
58
+ for handler in logger_instance.handlers:
59
+ if isinstance(handler, logging.FileHandler):
60
+ logfile = handler.baseFilename
61
+ break
62
+
63
+ # Check if a StreamHandler is attached
64
+ has_stream_handler = any(
65
+ isinstance(handler, logging.StreamHandler)
66
+ for handler in logger_instance.handlers
67
+ )
68
+
69
+ return logfile, has_stream_handler
70
+
71
+
72
+ # Poropagate existing logger configuration to casatools
73
+ # this ensures consistent logging behavior across both application and casatools components
74
+
75
+ logfile, log2term = get_logger_config()
76
+ casaconfig.config.log2term = log2term
77
+ if logfile:
78
+ casaconfig.config.logfile = logfile
79
+ else:
80
+ casaconfig.config.logfile = "/dev/null"
81
+ casaconfig.config.nologfile = True
82
+
83
+ import casatools # noqa: E402 (because of previous config initialization)
84
+
85
+ casatools.logger.setglobal(True)
86
+ casatools.logger.ompSetNumThreads(1)
87
+
88
+
89
+ def _wrap_table(swig_object: Any) -> "table":
90
+ """Wraps a SWIG table object.
91
+
92
+ Parameters
93
+ ----------
94
+ swig_object : Any
95
+ The SWIG object to wrap.
96
+
97
+ Returns
98
+ -------
99
+ table
100
+ The wrapped table object.
101
+ """
102
+ return table(swig_object=swig_object)
103
+
104
+
105
+ def method_wrapper(method: Any) -> Any:
106
+ """Wraps a method to recursively transpose NumPy array results.
107
+
108
+ Parameters
109
+ ----------
110
+ method : callable
111
+ The method to wrap.
112
+
113
+ Returns
114
+ -------
115
+ callable
116
+ The wrapped method.
117
+ """
118
+
119
+ @wraps(method)
120
+ def wrapped(*args, **kwargs):
121
+ ret = method(*args, **kwargs)
122
+ return recursive_transpose(ret)
123
+
124
+ return wrapped
125
+
126
+
127
+ def recursive_transpose(val: Any) -> Any:
128
+ """Recursively transposes all NumPy arrays within the given object.
129
+
130
+ Parameters
131
+ ----------
132
+ val : Any
133
+ The object to process. It can be a dictionary, list, NumPy array, or other object.
134
+
135
+ Returns
136
+ -------
137
+ Any
138
+ The modified object with all NumPy arrays transposed.
139
+ """
140
+ if isinstance(val, np.ndarray) and val.flags.f_contiguous:
141
+ return val.T
142
+ elif isinstance(val, list):
143
+ return [recursive_transpose(item) for item in val]
144
+ elif isinstance(val, dict):
145
+ return {key: recursive_transpose(value) for key, value in val.items()}
146
+ else:
147
+ return val
148
+
149
+
150
+ def wrap_class_methods(cls: type) -> type:
151
+ """Class decorator to wrap all methods of a class, including inherited ones.
152
+
153
+ Parameters
154
+ ----------
155
+ cls : type
156
+ The class to wrap.
157
+
158
+ Returns
159
+ -------
160
+ type
161
+ The class with its methods wrapped.
162
+ """
163
+ for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
164
+ if callable(method):
165
+ setattr(cls, name, method_wrapper(method))
166
+ return cls
167
+
168
+
169
+ @wrap_class_methods
170
+ class table(casatools.table):
171
+ """A wrapper for the casatools table object.
172
+
173
+ Parameters
174
+ ----------
175
+ tablename : str, optional
176
+ The name of the table.
177
+ tabledesc : bool, optional
178
+ Table description.
179
+ nrow : int, optional
180
+ Number of rows.
181
+ readonly : bool, optional
182
+ Whether the table is read-only.
183
+ lockoptions : dict, optional
184
+ Locking options.
185
+ ack : bool, optional
186
+ Acknowledgment flag.
187
+ dminfo : dict, optional
188
+ Data manager information.
189
+ endian : str, optional
190
+ Endian type.
191
+ memorytable : bool, optional
192
+ Whether the table is in memory.
193
+ concatsubtables : list, optional
194
+ Concatenated subtables.
195
+ """
196
+
197
+ def __init__(
198
+ self,
199
+ tablename: str = "",
200
+ tabledesc: bool = False,
201
+ nrow: int = 0,
202
+ readonly: bool = True,
203
+ lockoptions: Dict = {},
204
+ ack: bool = True,
205
+ dminfo: Dict = {},
206
+ endian: str = "aipsrc",
207
+ memorytable: bool = False,
208
+ concatsubtables: List = [],
209
+ **kwargs,
210
+ ):
211
+ _tablename = tablename.replace("::", "/")
212
+ super().__init__(
213
+ tablename=_tablename, lockoptions=lockoptions, nomodify=readonly, **kwargs
214
+ )
215
+
216
+ def __enter__(self):
217
+ """Function to enter a with block."""
218
+ return self
219
+
220
+ def __exit__(self, type, value, traceback):
221
+ """Function to exit a with block which closes the table object."""
222
+ self.close()
223
+
224
+ def row(self, columnnames: List[str] = [], exclude: bool = False) -> "tablerow":
225
+ """Access rows in the table.
226
+
227
+ Parameters
228
+ ----------
229
+ columnnames : list of str, optional
230
+ Column names to include or exclude.
231
+ exclude : bool, optional
232
+ Whether to exclude the specified columns.
233
+
234
+ Returns
235
+ -------
236
+ tablerow
237
+ A tablerow object for accessing rows.
238
+ """
239
+ return tablerow(self, columnnames=columnnames, exclude=exclude)
240
+
241
+ def col(self, columnname: str) -> "tablecolumn":
242
+ """Access a specific column in the table.
243
+
244
+ Parameters
245
+ ----------
246
+ columnname : str
247
+ The name of the column to access.
248
+
249
+ Returns
250
+ -------
251
+ tablecolumn
252
+ A tablecolumn object for accessing column data.
253
+ """
254
+ return tablecolumn(self, columnname)
255
+
256
+ def taql(self, taqlcommand="TaQL expression"):
257
+ """Expose TaQL (Table Query Language) to the user.
258
+
259
+ This method allows the execution of a TaQL expression on the table.
260
+ It substitutes `$mtable` and `$gtable` in the provided `taqlcommand`
261
+ with the current table name. A temporary copy of the table is created
262
+ if it is not currently opened.
263
+
264
+ Parameters
265
+ ----------
266
+ taqlcommand : str, optional
267
+ The TaQL expression to execute. Default is `'TaQL expression'`.
268
+
269
+ Returns
270
+ -------
271
+ tb_query_to : object
272
+ The result of the TaQL query as a wrapped table object.
273
+
274
+ Notes
275
+ -----
276
+ For more details on TaQL, refer to:
277
+ https://casacore.github.io/casacore-notes/199.html
278
+
279
+ Examples
280
+ --------
281
+ >>> result_table = obj.taql('SELECT * FROM $mtable WHERE col1 > 5')
282
+ >>> print(result_table.name())
283
+ """
284
+ is_open = self.isopened(self.name())
285
+ if not is_open:
286
+ tablename = self.name() + "_copy"
287
+ tb_query_from = self.copy(tablename, deep=False, valuecopy=False)
288
+ else:
289
+ tablename = self.name()
290
+ tb_query_from = self
291
+ tb_query = taqlcommand.replace("$mtable", tablename).replace(
292
+ "$gtable", tablename
293
+ )
294
+ logger.debug(f"tb_query_from: {tb_query_from.name()}")
295
+ logger.debug(f"tb_query_cmd: {tb_query}")
296
+ tb_query_to = _wrap_table(swig_object=tb_query_from._swigobj.taql(tb_query))
297
+ if not is_open:
298
+ tb_query_from.close()
299
+ shutil.rmtree(tablename)
300
+ logger.debug(f"tb_query_to: {tb_query_to.name()}")
301
+ return tb_query_to
302
+
303
+ def getcolshapestring(self, *args, **kwargs):
304
+ """Get the shape of table columns as string representations.
305
+
306
+ This method retrieves the shapes of table columns and formats them as
307
+ reversed string representations of the shapes. It is useful for viewing
308
+ column dimensions in a human-readable format.
309
+
310
+ Parameters
311
+ ----------
312
+ *args : tuple
313
+ Positional arguments to pass to the superclass method.
314
+ **kwargs : dict
315
+ Keyword arguments to pass to the superclass method.
316
+
317
+ Returns
318
+ -------
319
+ list of str
320
+ A list of reversed shapes as strings.
321
+
322
+ Examples
323
+ --------
324
+ >>> shapes = obj.getcolshapestring()
325
+ >>> print(shapes)
326
+ ['[10, 5]', '[20, 15]']
327
+ """
328
+ ret = super().getcolshapestring(*args, **kwargs)
329
+ return [str(list(reversed(ast.literal_eval(shape)))) for shape in ret]
330
+
331
+ def getcellslice(self, columnname, rownr, blc, trc, incr=1):
332
+ """Retrieve a sliced portion of a cell from a specified column.
333
+
334
+ This method extracts a subarray from a cell within a table column,
335
+ given the bottom-left corner (BLC) and top-right corner (TRC) indices.
336
+ It also supports an optional increment (`incr`) to control step size.
337
+
338
+ Parameters
339
+ ----------
340
+ columnname : str
341
+ The name of the column from which to extract data.
342
+ rownr : int
343
+ The row number(s) from which to extract data. If a sequence is provided,
344
+ it is reversed before processing.
345
+ blc : Sequence[int]
346
+ The bottom-left corner indices of the slice.
347
+ trc : Sequence[int]
348
+ The top-right corner indices of the slice.
349
+ incr : int or Sequence[int], optional
350
+ Step size for slicing. If a sequence is provided, it is reversed.
351
+ If a single integer is given, it is expanded to match `blc` dimensions.
352
+ Defaults to 1.
353
+
354
+ Returns
355
+ -------
356
+ Any
357
+ The extracted slice from the specified column and row(s).
358
+
359
+ Notes
360
+ -----
361
+ - If `rownr` is a sequence, it is reversed before processing.
362
+ - The `blc`, `trc`, and `incr` parameters are converted to lists of integers.
363
+ - Calls the superclass method `getcellslice` for actual data retrieval.
364
+ """
365
+ if isinstance(blc, Sequence):
366
+ blc = list(map(int, blc[::-1]))
367
+ if isinstance(trc, Sequence):
368
+ trc = list(map(int, trc[::-1]))
369
+ if isinstance(incr, Sequence):
370
+ incr = incr[::-1]
371
+ else:
372
+ incr = [incr] * len(blc)
373
+ datatype = self.coldatatype(columnname)
374
+
375
+ ret = super().getcellslice(
376
+ columnname=columnname, rownr=rownr, blc=blc, trc=trc, incr=incr
377
+ )
378
+
379
+ if datatype == "float":
380
+ return ret.astype(np.float32)
381
+ else:
382
+ return ret
383
+
384
+ def putcellslice(self, columnname, rownr, value, blc, trc, incr=1):
385
+ """Retrieve a sliced portion of a cell from a specified column.
386
+
387
+ This method extracts a subarray from a cell within a table column,
388
+ given the bottom-left corner (BLC) and top-right corner (TRC) indices.
389
+ It also supports an optional increment (`incr`) to control step size.
390
+
391
+ Parameters
392
+ ----------
393
+ columnname : str
394
+ The name of the column from which to extract data.
395
+ rownr : int or Sequence[int]
396
+ The row number(s) from which to extract data. If a sequence is provided,
397
+ it is reversed before processing.
398
+ blc : Sequence[int]
399
+ The bottom-left corner indices of the slice.
400
+ trc : Sequence[int]
401
+ The top-right corner indices of the slice.
402
+ incr : int or Sequence[int], optional
403
+ Step size for slicing. If a sequence is provided, it is reversed.
404
+ If a single integer is given, it is expanded to match `blc` dimensions.
405
+ Defaults to 1.
406
+
407
+ Returns
408
+ -------
409
+ Any
410
+ The extracted slice from the specified column and row(s).
411
+
412
+ Notes
413
+ -----
414
+ - If `rownr` is a sequence, it is reversed before processing.
415
+ - The `blc`, `trc`, and `incr` parameters are converted to lists of integers.
416
+ - Calls the superclass method `getcellslice` for actual data retrieval.
417
+ """
418
+ if isinstance(rownr, Sequence):
419
+ rownr = rownr[::-1]
420
+ else:
421
+ rownr = [rownr] * len(blc)
422
+ rownr = 0
423
+ if isinstance(blc, Sequence):
424
+ blc = list(map(int, blc[::-1]))
425
+ if isinstance(trc, Sequence):
426
+ trc = list(map(int, trc[::-1]))
427
+ if isinstance(incr, Sequence):
428
+ incr = incr[::-1]
429
+ else:
430
+ incr = [incr] * len(blc)
431
+
432
+ super().putcellslice(
433
+ columnname=columnname,
434
+ rownr=rownr,
435
+ value=value.T,
436
+ blc=blc,
437
+ trc=trc,
438
+ incr=incr,
439
+ )
440
+ return
441
+
442
+ def putkeyword(
443
+ self, keyword: str, value: str | int | float | bool, makesubrecord: bool = False
444
+ ) -> None:
445
+ """Insert a keyword and its associated value into the record.
446
+
447
+ This method wraps the `casatools.tables.table`'s `putkeyword` method and handles
448
+ the insertion of a keyword and its corresponding value into the record, with a
449
+ specific conversion for NumPy scalar types.
450
+
451
+ NumPy scalar types in `value` are automatically converted to native Python
452
+ types before writing. This conversion is necessary because `casatools`
453
+ appears to exclude NumPy scalars in the keyword value (e.g., within
454
+ a nested directory) during serialization.
455
+
456
+
457
+ Parameters
458
+ ----------
459
+ keyword : str
460
+ The name of the keyword to insert.
461
+ value
462
+ The value associated with the keyword. NumPy scalars are automatically converted to native types.
463
+ makesubrecord : bool, optional
464
+ If True, creates a new subrecord for the keyword (default is False).
465
+
466
+ Returns
467
+ -------
468
+ None
469
+ """
470
+ super().putkeyword(
471
+ keyword,
472
+ _convert_numpy_scalars_to_native(value),
473
+ makesubrecord=makesubrecord,
474
+ )
475
+
476
+
477
+ def _convert_numpy_scalars_to_native(value: Any) -> Any:
478
+ """Recursively convert NumPy scalar types to their equivalent Python native types.
479
+
480
+ This function traverses nested data structures (e.g., dictionaries, lists, tuples) and replaces any NumPy scalar
481
+ types (e.g., `np.float64`, `np.int32`) with their native Python equivalents (e.g., `float`, `int`). This is
482
+ particularly useful before serializing data structures to formats like JSON, which do not natively support NumPy
483
+ scalar types.
484
+
485
+ Parameters
486
+ ----------
487
+ value
488
+ A scalar or nested structure (dictionary, list, or tuple) potentially containing NumPy scalar types.
489
+
490
+ Returns
491
+ -------
492
+ A new structure with NumPy scalars converted to native types. Original container types are preserved.
493
+ """
494
+ if isinstance(value, dict):
495
+ return {k: _convert_numpy_scalars_to_native(v) for k, v in value.items()}
496
+
497
+ elif isinstance(value, (list, tuple)):
498
+ # Preserve list or tuple type
499
+ return type(value)(_convert_numpy_scalars_to_native(item) for item in value)
500
+
501
+ elif isinstance(value, np.generic):
502
+ # Convert NumPy scalar to native Python type
503
+ return value.item()
504
+
505
+ return value
506
+
507
+
508
+ @wrap_class_methods
509
+ class image(casatools.image):
510
+ """A Wrapper class around `casatools.image` that provides python-casacore-like methods."""
511
+
512
+ def __init__(
513
+ self,
514
+ imagename,
515
+ axis=0,
516
+ maskname="mask0",
517
+ images=(),
518
+ values=None,
519
+ coordsys=None,
520
+ overwrite=True,
521
+ ashdf5=False,
522
+ mask=(),
523
+ shape=None,
524
+ tileshape=(),
525
+ ):
526
+ super().__init__()
527
+ self._imagename = imagename
528
+ self._maskname = maskname
529
+ if shape is None:
530
+ # self.open(*arg, **kwargs)
531
+ # Add a temporary filter to the CASA instance global logger log sink filter
532
+ # to suppress 'SEVERE' messages when probing images/
533
+ casatools.logger.filterMsg("Exception Reported: Exception")
534
+ self.open(imagename)
535
+ casatools.logger.clearFilterMsgList()
536
+ else:
537
+ if values is None:
538
+ self.fromshape(imagename, shape=list(shape[::-1]), overwrite=overwrite)
539
+ else:
540
+ self.fromarray(
541
+ imagename, pixels=np.full(shape, values).T, overwrite=overwrite
542
+ )
543
+ if maskname:
544
+ self.calcmask("T", name=maskname)
545
+ self.maskhandler("set", maskname)
546
+
547
+ def toworld(self, pixel):
548
+ world = super().toworld(pixel[::-1])
549
+ return world["numeric"][::-1]
550
+
551
+ def tofits(
552
+ self,
553
+ filename,
554
+ overwrite=True,
555
+ velocity=True,
556
+ optical=True,
557
+ bitpix=-32,
558
+ minpix=1,
559
+ maxpix=-1,
560
+ ):
561
+ super().tofits(
562
+ filename,
563
+ overwrite=overwrite,
564
+ velocity=velocity,
565
+ optical=optical,
566
+ bitpix=bitpix,
567
+ minpix=minpix,
568
+ maxpix=maxpix,
569
+ )
570
+
571
+ def getdata(self, blc=None, trc=None, inc=None):
572
+ """Retrieve image data as a chunk.
573
+
574
+ Parameters
575
+ ----------
576
+ blc : list of int, optional
577
+ Bottom-left corner of the region to extract. Defaults to `[-1]` (entire image).
578
+ trc : list of int, optional
579
+ Top-right corner of the region to extract. Defaults to `[-1]` (entire image).
580
+ inc : list of int, optional
581
+ Step size for slicing. Defaults to `[1]`.
582
+
583
+ Returns
584
+ -------
585
+ numpy.ndarray
586
+ The extracted data chunk.
587
+ """
588
+ if blc is None:
589
+ blc = [-1]
590
+ if trc is None:
591
+ trc = [-1]
592
+ if inc is None:
593
+ inc = [1]
594
+
595
+ if self.datatype() == "float":
596
+ return super().getchunk(blc, trc, inc).astype(np.float32)
597
+ else:
598
+ return super().getchunk(blc, trc, inc)
599
+
600
+ def getmask(self, blc=None, trc=None, inc=None):
601
+ """Retrieve image data as a chunk.
602
+
603
+ Parameters
604
+ ----------
605
+ blc : list of int, optional
606
+ Bottom-left corner of the region to extract. Defaults to `[-1]` (entire image).
607
+ trc : list of int, optional
608
+ Top-right corner of the region to extract. Defaults to `[-1]` (entire image).
609
+ inc : list of int, optional
610
+ Step size for slicing. Defaults to `[1]`.
611
+
612
+ Returns
613
+ -------
614
+ numpy.ndarray
615
+ The extracted data chunk.
616
+ """
617
+ if blc is None:
618
+ blc = [-1]
619
+ if trc is None:
620
+ trc = [-1]
621
+ if inc is None:
622
+ inc = [1]
623
+ # note the fliped sign:
624
+ # https://casacore.github.io/python-casacore/casacore_images.html#casacore.images.image.getmask
625
+ return ~super().getchunk(blc, trc, inc, getmask=True)
626
+
627
+ def put(self, masked_array):
628
+ """Put in data/mask into iatools.
629
+
630
+ Note: for casa mask table, the mask value defination is flipped:
631
+ True (not masked) or False (masked) values
632
+ """
633
+ self.putregion(masked_array.data.T, ~masked_array.mask.T)
634
+
635
+ def __del__(self):
636
+ """Ensure proper resource cleanup.
637
+
638
+ This method is automatically called when the object is deleted.
639
+ It ensures that any open resources are properly closed by calling
640
+ `unlock()` and `close()`.
641
+ """
642
+
643
+ # flushes any outstabding I/O to disk and close the tool instance.
644
+ # the explicut unlock() call is important for multiple-process parallel read downstream
645
+ # as the dask worker process might sometimes consider a freshly written from a different process
646
+ # not valid disk images (even the image dir has been formed).
647
+
648
+ # super().unlock() # taken care from xradio.image._util._casacore.common::_create_new_image
649
+ super().close()
650
+
651
+ def shape(self):
652
+ """Get the shape of the image.
653
+
654
+ Returns
655
+ -------
656
+ list of int
657
+ The shape of the image, with axes reversed for consistency.
658
+ """
659
+ return list(map(int, super().shape()[::-1]))
660
+
661
+ def coordinates(self):
662
+ """Get the coordinate system of the image.
663
+
664
+ Returns
665
+ -------
666
+ casatools.coordinatesystem
667
+ The coordinate system associated with the image.
668
+ """
669
+ return coordinatesystem(self)
670
+
671
+ def unit(self):
672
+ """Get the brightness unit of the image.
673
+
674
+ Returns
675
+ -------
676
+ str
677
+ The brightness unit of the image.
678
+ """
679
+ return self.brightnessunit()
680
+
681
+ def info(self):
682
+ """Retrieve image metadata including coordinates, misc info, and beam information.
683
+
684
+ Returns
685
+ -------
686
+ dict
687
+ Dictionary containing:
688
+ - 'imageinfo': Flattened image summary.
689
+ - 'coordinates': Coordinate system as a dictionary.
690
+ - 'miscinfo': Miscellaneous metadata.
691
+ """
692
+ # imageinfo = self.summary(list=False)
693
+ # imageinfo = self._flatten_multibeam(imageinfo)
694
+
695
+ return {
696
+ "imageinfo": self.imageinfo(),
697
+ "coordinates": self.coordsys().torecord(),
698
+ "miscinfo": self.miscinfo(),
699
+ "unit": self.brightnessunit(),
700
+ }
701
+
702
+ def imageinfo(self) -> dict:
703
+ """Retrieve metadata from the image table.
704
+
705
+ This method accesses the image table associated with the image name
706
+ and attempts to retrieve information stored under the 'imageinfo'
707
+ keyword. If the 'imageinfo' keyword is not found in the table,
708
+ a default dictionary containing basic information is returned.
709
+
710
+ Returns
711
+ -------
712
+ dict
713
+ A dictionary containing image metadata. This is either the
714
+ value associated with the 'imageinfo' keyword in the table,
715
+ or a default dictionary {'imagetype': 'Intensity',
716
+ 'objectname': ''} if the keyword is absent.
717
+ """
718
+ with table(self._imagename) as tb:
719
+ if "imageinfo" in tb.keywordnames():
720
+ image_metadata = tb.getkeyword("imageinfo")
721
+ else:
722
+ image_metadata = {"imagetype": "Intensity", "objectname": ""}
723
+
724
+ return image_metadata
725
+
726
+ def datatype(self):
727
+ return self.pixeltype()
728
+
729
+ def _flatten_multibeam(self, imageinfo):
730
+ """Flatten the per-plane beam information in the image metadata.
731
+
732
+ This method restructures the `perplanebeams` field in `imageinfo`
733
+ to make it more accessible by flattening the nested structure.
734
+
735
+ Parameters
736
+ ----------
737
+ imageinfo : dict
738
+ The image metadata containing per-plane beam information.
739
+
740
+ Returns
741
+ -------
742
+ dict
743
+ Updated `imageinfo` dictionary with flattened per-plane beam data.
744
+ """
745
+ if "perplanebeams" in imageinfo:
746
+ perplanebeams = imageinfo["perplanebeams"]["beams"]
747
+ perplanebeams_flat = {}
748
+ nchan = imageinfo["perplanebeams"]["nChannels"]
749
+ npol = imageinfo["perplanebeams"]["nStokes"]
750
+
751
+ for c in range(nchan):
752
+ for p in range(npol):
753
+ k = nchan * p + c
754
+ perplanebeams_flat["*" + str(k)] = perplanebeams["*" + str(c)][
755
+ "*" + str(p)
756
+ ]
757
+ imageinfo["perplanebeams"].pop("beams", None)
758
+ imageinfo["perplanebeams"].update(perplanebeams_flat)
759
+
760
+ return imageinfo
761
+
762
+
763
+ class coordinatesystem(casatools.coordsys):
764
+ """A wrapper around `casatools.coordsys` that provides python-casacore like methods"""
765
+
766
+ def __init__(self, image=None):
767
+ self._image = image
768
+ if image is None:
769
+ self._cs = casatools.coordsys()
770
+ else:
771
+ self._cs = image.coordsys()
772
+
773
+ def get_axes(self):
774
+ """Retrieve the names of the coordinate axes.
775
+
776
+ Returns
777
+ -------
778
+ list of str or list of lists
779
+ A list containing the names of each axis, grouped by coordinate type.
780
+ Spectral axes are returned as a single string instead of a list.
781
+ """
782
+ axes = []
783
+ axis_names = self._cs.names()
784
+ for axis_type in self.get_names():
785
+ axis_inds = self._cs.findcoordinate(axis_type).get("pixel")
786
+ axes_list = [axis_names[idx] for idx in axis_inds[::-1]]
787
+ if axis_type == "spectral":
788
+ axes_list = axes_list[0]
789
+ axes.append(axes_list)
790
+ return axes
791
+
792
+ def get_referencepixel(self):
793
+ """Get the reference pixel coordinates.
794
+
795
+ Returns
796
+ -------
797
+ list of float
798
+ The numeric reference pixel values, with axes reversed.
799
+ """
800
+ return self._cs.referencepixel()["numeric"][::-1]
801
+
802
+ def get_referencevalue(self):
803
+ """Get the reference value at the reference pixel.
804
+
805
+ Returns
806
+ -------
807
+ list of float
808
+ The numeric reference values, with axes reversed.
809
+ """
810
+ return self._cs.referencevalue()["numeric"][::-1]
811
+
812
+ def get_increment(self):
813
+ """Get the coordinate increments per pixel.
814
+
815
+ Returns
816
+ -------
817
+ list of float
818
+ The coordinate increment values, with axes reversed.
819
+ """
820
+ return self._cs.increment()["numeric"][::-1]
821
+
822
+ def get_unit(self):
823
+ """Get the units of the coordinate axes.
824
+
825
+ Returns
826
+ -------
827
+ list of str
828
+ The units of each axis, with axes reversed.
829
+ """
830
+ return self._cs.units()[::-1]
831
+
832
+ def get_names(self):
833
+ """Get the coordinate type names in lowercase.
834
+
835
+ Returns
836
+ -------
837
+ list of str
838
+ The coordinate type names, with axes reversed.
839
+ """
840
+ return list(map(str.lower, self._cs.coordinatetype()[::-1]))
841
+
842
+ def dict(self):
843
+ """Convert the coordinate system to a dictionary representation.
844
+
845
+ Returns
846
+ -------
847
+ dict
848
+ The coordinate system in CASA's dictionary format.
849
+ """
850
+ return self._cs.torecord()
851
+
852
+
853
+ class directioncoordinate(coordinatesystem):
854
+ def __init__(self, rec):
855
+ super().__init__()
856
+ self._rec = rec
857
+
858
+ def get_projection(self):
859
+ return self._rec["projection"]
860
+
861
+
862
+ class coordinates:
863
+ def __init__(self):
864
+ pass
865
+
866
+ class spectralcoordinate(coordinatesystem):
867
+ def __init__(self, rec):
868
+ super().__init__()
869
+ self._rec = rec
870
+
871
+ def get_restfrequency(self):
872
+ return self._rec["restfreq"]
873
+
874
+
875
+ @wrap_class_methods
876
+ class tablerow(casatools.tablerow):
877
+ """A wrapper for the casatools tablerow object.
878
+
879
+ Parameters
880
+ ----------
881
+ table : table
882
+ The table object to wrap.
883
+ columnnames : list of str, optional
884
+ Column names to include or exclude.
885
+ exclude : bool, optional
886
+ Whether to exclude the specified columns.
887
+ """
888
+
889
+ def __init__(
890
+ self, table: table, columnnames: List[str] = [], exclude: bool = False
891
+ ):
892
+ super().__init__(table, columnnames=columnnames, exclude=exclude)
893
+
894
+ @method_wrapper
895
+ def get(self, rownr: int) -> Dict[str, Any]:
896
+ """Retrieve data for a specific row.
897
+
898
+ Parameters
899
+ ----------
900
+ rownr : int
901
+ The row number to retrieve.
902
+
903
+ Returns
904
+ -------
905
+ dict
906
+ A dictionary containing row data.
907
+ """
908
+ return super().get(rownr)
909
+
910
+ def __getitem__(
911
+ self, key: Union[int, slice]
912
+ ) -> Union[Dict[str, Any], List[Dict[str, Any]]]:
913
+ """Retrieve rows using indexing or slicing.
914
+
915
+ Parameters
916
+ ----------
917
+ key : int or slice
918
+ The row index or slice to retrieve.
919
+
920
+ Returns
921
+ -------
922
+ dict or list of dict
923
+ The row(s) corresponding to the key.
924
+ """
925
+ if isinstance(key, slice):
926
+ return [self.get(irow) for irow in range(*key.indices(len(self)))]
927
+ elif isinstance(key, int):
928
+ return self.get(key)
929
+
930
+
931
+ @wrap_class_methods
932
+ class tablecolumn:
933
+ """A class representing a single column in a table.
934
+
935
+ Provides methods to access values in the column with indexing and slicing.
936
+
937
+ Parameters
938
+ ----------
939
+ table : Any
940
+ The table object containing the column.
941
+ columnname : str
942
+ The name of the column in the table.
943
+
944
+ Attributes
945
+ ----------
946
+ _table : Any
947
+ The table object containing the column.
948
+ _columnname : str
949
+ The name of the column in the table.
950
+ """
951
+
952
+ def __init__(self, table: Any, columnname: str):
953
+ self._table = table
954
+ self._columnname = columnname
955
+
956
+ @method_wrapper
957
+ def get(self, irow: int) -> Any:
958
+ """Get the value at a specific row in the column.
959
+
960
+ Parameters
961
+ ----------
962
+ irow : int
963
+ The index of the row to retrieve.
964
+
965
+ Returns
966
+ -------
967
+ Any
968
+ The value in the specified row of the column.
969
+ """
970
+ return self._table.getcell(self._columnname, irow)
971
+
972
+ def __getitem__(self, key: Union[int, slice]) -> Union[Any, List[Any]]:
973
+ """Get a value or a list of values from the column using indexing or slicing.
974
+
975
+ Parameters
976
+ ----------
977
+ key : int or slice
978
+ The index or slice to retrieve values from the column.
979
+
980
+ Returns
981
+ -------
982
+ Any or list of Any
983
+ The value(s) retrieved from the column.
984
+
985
+ Examples
986
+ --------
987
+ >>> table = MockTable()
988
+ >>> column = TableColumn(table, 'col1')
989
+ >>> column[0] # Get the first row's value
990
+ 42
991
+ >>> column[1:3] # Get values from rows 1 to 2
992
+ [43, 44]
993
+ """
994
+ if isinstance(key, slice):
995
+ return [self.get(irow) for irow in range(*key.indices(self._table.nrows()))]
996
+ elif isinstance(key, int):
997
+ return self.get(key)
998
+
999
+
1000
+ def tableexists(path):
1001
+ return os.path.isdir(path)