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