cubevis 0.5.2__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 (132) hide show
  1. cubevis/LICENSE.rst +500 -0
  2. cubevis/__icons__/20px/fast-backward.svg +13 -0
  3. cubevis/__icons__/20px/fast-forward.svg +13 -0
  4. cubevis/__icons__/20px/step-backward.svg +12 -0
  5. cubevis/__icons__/20px/step-forward.svg +12 -0
  6. cubevis/__icons__/add-chan.png +0 -0
  7. cubevis/__icons__/add-chan.svg +84 -0
  8. cubevis/__icons__/add-cube.png +0 -0
  9. cubevis/__icons__/add-cube.svg +186 -0
  10. cubevis/__icons__/drag.png +0 -0
  11. cubevis/__icons__/drag.svg +109 -0
  12. cubevis/__icons__/mask-selected.png +0 -0
  13. cubevis/__icons__/mask.png +0 -0
  14. cubevis/__icons__/mask.svg +1 -0
  15. cubevis/__icons__/new-layer-sm-selected.png +0 -0
  16. cubevis/__icons__/new-layer-sm-selected.svg +88 -0
  17. cubevis/__icons__/new-layer-sm.png +0 -0
  18. cubevis/__icons__/new-layer-sm.svg +15 -0
  19. cubevis/__icons__/reset.png +0 -0
  20. cubevis/__icons__/reset.svg +11 -0
  21. cubevis/__icons__/sub-chan.png +0 -0
  22. cubevis/__icons__/sub-chan.svg +71 -0
  23. cubevis/__icons__/sub-cube.png +0 -0
  24. cubevis/__icons__/sub-cube.svg +95 -0
  25. cubevis/__icons__/zoom-to-fit.png +0 -0
  26. cubevis/__icons__/zoom-to-fit.svg +21 -0
  27. cubevis/__init__.py +58 -0
  28. cubevis/__js__/bokeh-3.6.1.min.js +728 -0
  29. cubevis/__js__/bokeh-tables-3.6.1.min.js +119 -0
  30. cubevis/__js__/bokeh-widgets-3.6.1.min.js +141 -0
  31. cubevis/__js__/casalib.min.js +1 -0
  32. cubevis/__js__/cubevisjs.min.js +62 -0
  33. cubevis/__version__.py +1 -0
  34. cubevis/apps/__init__.py +44 -0
  35. cubevis/apps/_createmask.py +461 -0
  36. cubevis/apps/_createregion.py +513 -0
  37. cubevis/apps/_interactiveclean.py +3260 -0
  38. cubevis/apps/_interactiveclean_wrappers.py +130 -0
  39. cubevis/apps/_ms_raster.py +815 -0
  40. cubevis/apps/_plotants.py +286 -0
  41. cubevis/apps/_plotbandpass.py +7 -0
  42. cubevis/bokeh/__init__.py +29 -0
  43. cubevis/bokeh/annotations/__init__.py +1 -0
  44. cubevis/bokeh/annotations/_ev_poly_annotation.py +6 -0
  45. cubevis/bokeh/components/__init__.py +28 -0
  46. cubevis/bokeh/format/__init__.py +31 -0
  47. cubevis/bokeh/format/_time_ticks.py +44 -0
  48. cubevis/bokeh/format/_wcs_ticks.py +45 -0
  49. cubevis/bokeh/models/__init__.py +4 -0
  50. cubevis/bokeh/models/_edit_span.py +7 -0
  51. cubevis/bokeh/models/_ev_text_input.py +6 -0
  52. cubevis/bokeh/models/_tip.py +37 -0
  53. cubevis/bokeh/models/_tip_button.py +50 -0
  54. cubevis/bokeh/sources/__init__.py +35 -0
  55. cubevis/bokeh/sources/_data_pipe.py +258 -0
  56. cubevis/bokeh/sources/_image_data_source.py +83 -0
  57. cubevis/bokeh/sources/_image_pipe.py +581 -0
  58. cubevis/bokeh/sources/_spectra_data_source.py +55 -0
  59. cubevis/bokeh/sources/_updatable_data_source.py +189 -0
  60. cubevis/bokeh/state/__init__.py +34 -0
  61. cubevis/bokeh/state/_initialize.py +164 -0
  62. cubevis/bokeh/state/_javascript.py +53 -0
  63. cubevis/bokeh/state/_palette.py +58 -0
  64. cubevis/bokeh/state/_session.py +44 -0
  65. cubevis/bokeh/state/js/bokeh-2.4.1.min.js +596 -0
  66. cubevis/bokeh/state/js/bokeh-gl-2.4.1.min.js +74 -0
  67. cubevis/bokeh/state/js/bokeh-tables-2.4.1.min.js +132 -0
  68. cubevis/bokeh/state/js/bokeh-widgets-2.4.1.min.js +118 -0
  69. cubevis/bokeh/state/js/casaguijs-v0.0.4.0-b2.4.min.js +49 -0
  70. cubevis/bokeh/state/js/casaguijs-v0.0.5.0-b2.4.min.js +49 -0
  71. cubevis/bokeh/state/js/casaguijs-v0.0.6.0-b2.4.min.js +49 -0
  72. cubevis/bokeh/state/js/casalib-v0.0.1.min.js +1 -0
  73. cubevis/bokeh/tools/__init__.py +31 -0
  74. cubevis/bokeh/tools/_cbreset_tool.py +52 -0
  75. cubevis/bokeh/tools/_drag_tool.py +61 -0
  76. cubevis/bokeh/utils/__init__.py +35 -0
  77. cubevis/bokeh/utils/_axes_labels.py +94 -0
  78. cubevis/bokeh/utils/_svg_icon.py +136 -0
  79. cubevis/data/__init__.py +1 -0
  80. cubevis/data/casaimage/__init__.py +114 -0
  81. cubevis/data/measurement_set/__init__.py +7 -0
  82. cubevis/data/measurement_set/_ms_data.py +178 -0
  83. cubevis/data/measurement_set/processing_set/__init__.py +30 -0
  84. cubevis/data/measurement_set/processing_set/_ps_concat.py +98 -0
  85. cubevis/data/measurement_set/processing_set/_ps_coords.py +78 -0
  86. cubevis/data/measurement_set/processing_set/_ps_data.py +213 -0
  87. cubevis/data/measurement_set/processing_set/_ps_io.py +55 -0
  88. cubevis/data/measurement_set/processing_set/_ps_raster_data.py +154 -0
  89. cubevis/data/measurement_set/processing_set/_ps_select.py +91 -0
  90. cubevis/data/measurement_set/processing_set/_ps_stats.py +218 -0
  91. cubevis/data/measurement_set/processing_set/_xds_data.py +149 -0
  92. cubevis/plot/__init__.py +1 -0
  93. cubevis/plot/ms_plot/__init__.py +29 -0
  94. cubevis/plot/ms_plot/_ms_plot.py +242 -0
  95. cubevis/plot/ms_plot/_ms_plot_constants.py +22 -0
  96. cubevis/plot/ms_plot/_ms_plot_selectors.py +348 -0
  97. cubevis/plot/ms_plot/_raster_plot.py +292 -0
  98. cubevis/plot/ms_plot/_raster_plot_inputs.py +116 -0
  99. cubevis/plot/ms_plot/_xds_plot_axes.py +110 -0
  100. cubevis/private/__java__/xml-casa-assembly-1.86.jar +0 -0
  101. cubevis/private/_gclean.py +798 -0
  102. cubevis/private/casashell/createmask.py +332 -0
  103. cubevis/private/casashell/iclean.py +4432 -0
  104. cubevis/private/casatasks/__init__.py +140 -0
  105. cubevis/private/casatasks/createmask.py +86 -0
  106. cubevis/private/casatasks/createregion.py +83 -0
  107. cubevis/private/casatasks/iclean.py +1831 -0
  108. cubevis/readme.rst +16 -0
  109. cubevis/remote/__init__.py +10 -0
  110. cubevis/remote/_gclean.py +61 -0
  111. cubevis/remote/_local.py +287 -0
  112. cubevis/remote/_remote_kernel.py +80 -0
  113. cubevis/toolbox/__init__.py +32 -0
  114. cubevis/toolbox/_app_context.py +74 -0
  115. cubevis/toolbox/_cube.py +3457 -0
  116. cubevis/toolbox/_region_list.py +197 -0
  117. cubevis/utils/_ResourceManager.py +86 -0
  118. cubevis/utils/__init__.py +620 -0
  119. cubevis/utils/_contextmgrchain.py +84 -0
  120. cubevis/utils/_conversion.py +93 -0
  121. cubevis/utils/_copydoc.py +55 -0
  122. cubevis/utils/_docenum.py +25 -0
  123. cubevis/utils/_import_protected_module.py +35 -0
  124. cubevis/utils/_logging.py +85 -0
  125. cubevis/utils/_pkgs.py +77 -0
  126. cubevis/utils/_regions.py +40 -0
  127. cubevis/utils/_static.py +66 -0
  128. cubevis/utils/_tiles.py +167 -0
  129. cubevis-0.5.2.dist-info/METADATA +151 -0
  130. cubevis-0.5.2.dist-info/RECORD +132 -0
  131. cubevis-0.5.2.dist-info/WHEEL +4 -0
  132. cubevis-0.5.2.dist-info/licenses/LICENSE +504 -0
@@ -0,0 +1,620 @@
1
+ ########################################################################
2
+ #
3
+ # Copyright (C) 2021,2022,2023
4
+ # Associated Universities, Inc. Washington DC, USA.
5
+ #
6
+ # This script is free software; you can redistribute it and/or modify it
7
+ # under the terms of the GNU Library General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or (at your
9
+ # option) any later version.
10
+ #
11
+ # This library is distributed in the hope that it will be useful, but WITHOUT
12
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
14
+ # License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Library General Public License
17
+ # along with this library; if not, write to the Free Software Foundation,
18
+ # Inc., 675 Massachusetts Ave, Cambridge, MA 02139, USA.
19
+ #
20
+ # Correspondence concerning AIPS++ should be adressed as follows:
21
+ # Internet email: casa-feedback@nrao.edu.
22
+ # Postal address: AIPS++ Project Office
23
+ # National Radio Astronomy Observatory
24
+ # 520 Edgemont Road
25
+ # Charlottesville, VA 22903-2475 USA
26
+ #
27
+ ########################################################################
28
+ '''General utility functions used by the ``cubevis`` tools and applications.'''
29
+
30
+ import inspect
31
+ import itertools
32
+ import urllib.request
33
+ import urllib.error
34
+ from os.path import dirname, join
35
+ import sys
36
+
37
+ from itertools import groupby, chain
38
+ from socket import socket
39
+ from os import path as __path
40
+ from ._ResourceManager import _ResourceManager
41
+ from ._logging import get_logger
42
+ from ._regions import polygon_indexes
43
+ from ._docenum import DocEnum
44
+ from ._copydoc import copydoc
45
+ from ._pkgs import find_pkg, load_pkg
46
+
47
+ from astropy import units
48
+ from regions import PixCoord
49
+ from regions import RectanglePixelRegion, PolygonPixelRegion
50
+
51
+ try:
52
+ from casatools import regionmanager
53
+
54
+ __have_casatools = True
55
+ except ImportError:
56
+ __have_casatools = False
57
+
58
+ logger = get_logger()
59
+
60
+ from ._conversion import pack_arrays
61
+ from ._conversion import strip_arrays
62
+ from ._conversion import serialize, deserialize
63
+ from ._static import static_vars, static_dir
64
+ from ._tiles import TMSTiles
65
+ from ._contextmgrchain import ContextMgrChain
66
+ from ._import_protected_module import ImportProtectedModule
67
+
68
+ @static_vars(mgr=None)
69
+ def resource_manager( ):
70
+ if resource_manager.mgr is None:
71
+ resource_manager.mgr = _ResourceManager( )
72
+ return resource_manager.mgr
73
+
74
+ def reset_resource_manager( ):
75
+ if resource_manager.mgr is not None:
76
+ resource_manager.mgr = _ResourceManager( )
77
+
78
+ def path_to_url(path):
79
+ '''Convert a single filesystem path to a URL.
80
+
81
+ If the string specified in the ``path`` parameter exists. It is turned into a
82
+ fully qualified path and converted to a URL and returned. If ``path`` does not
83
+ exist, ``path`` is returned unchanged.
84
+
85
+ Parameters
86
+ ----------
87
+ path: str
88
+ path to be checked and expanded
89
+
90
+ Returns
91
+ -------
92
+ str
93
+ ``path`` converted to a URL if ``path`` exists, otherwise ``path`` unchanged
94
+ '''
95
+ if __path.exists(path):
96
+ return "file://" + __path.abspath(path)
97
+ else:
98
+ jslib_path = join(dirname(dirname(__file__)),'__js__',path)
99
+ if __path.exists(jslib_path):
100
+ return "file://" + __path.abspath(jslib_path)
101
+ else:
102
+ return path
103
+
104
+
105
+ def find_ws_address(address='127.0.0.1'):
106
+ '''Find free port on ``address`` network and return a tuple with ``address`` and port number
107
+
108
+ This function uses the low level socket function to find a free port and return
109
+ a tuple representing the address plus port number.
110
+
111
+ Parameters
112
+ ----------
113
+ address: str
114
+ network to be probed for an available port
115
+
116
+ Returns
117
+ -------
118
+ tuple of str and int
119
+ network address (`str`) and port number (`int`)
120
+ '''
121
+ sock = socket()
122
+ sock.bind((address, 0))
123
+ result = sock.getsockname()
124
+ sock.close()
125
+ return result
126
+
127
+
128
+ def partition(pred, iterable):
129
+ '''Split ``iterable`` into two lists based on ``pred`` predicate.
130
+ '''
131
+ trues = []
132
+ falses = []
133
+ for item in iterable:
134
+ if pred(item):
135
+ trues.append(item)
136
+ else:
137
+ falses.append(item)
138
+ return trues, falses
139
+
140
+
141
+ def error_msg(*args, **kwargs):
142
+ '''standard method for reporting errors which do not result in aborting out of python
143
+
144
+ This function takes the standard set of arguments that the python ``print`` function takes.
145
+ The primary difference is that the output will go to ``stderr`` and perhaps other error
146
+ logs.
147
+ '''
148
+ print(*args, file=sys.stderr, **kwargs)
149
+
150
+
151
+ @static_vars(msgs=dict(
152
+ casatools='{package} is not available so interactive clean and plotants will not work',
153
+ casatasks='{package} is not available so interactive clean will not work'
154
+ ), reported={})
155
+ def warn_import(package):
156
+ '''standard method for reporting (optional) package import failure
157
+
158
+ Parameters
159
+ ----------
160
+ package: str
161
+ name of a package whose attempted import failed
162
+ '''
163
+ if package not in warn_import.reported:
164
+ warn_import.reported[package] = True
165
+ error_msg("warning, %s" % warn_import.msgs[package].format(package=package))
166
+
167
+
168
+ @static_vars(url='http://clients3.google.com/generate_204')
169
+ def have_network():
170
+ '''check to see if an active network with general internet connectivity
171
+ is available. returns ``True`` if we have internet connectivity and
172
+ ``False`` if we do not.
173
+ '''
174
+ ###
175
+ ### see: https://stackoverflow.com/questions/50558000/test-internet-connection-for-python3
176
+ ###
177
+ try:
178
+ with urllib.request.urlopen(have_network.url) as response:
179
+ return response.status == 204
180
+ except urllib.error.HTTPError:
181
+ ### http error
182
+ return False
183
+ except urllib.error.ContentTooShortError:
184
+ return False
185
+ except urllib.error.URLError:
186
+ return False
187
+ except Exception:
188
+ return False
189
+
190
+
191
+ def ranges(iterable, order=sorted, key=lambda x: x):
192
+ '''collect elements of ``iterable`` into tuple ranges where each tuple represents
193
+ a concesecutive range within the iterable. ``key`` can be used to provide ranges
194
+ for other objects where ``key(element)`` returns the key to be used for sorting
195
+ into ranges'''
196
+ for a, b in groupby(enumerate(
197
+ order(iterable, key=key) if 'key' in inspect.getfullargspec(order).kwonlyargs else order(iterable)),
198
+ lambda pair: key(pair[1]) - pair[0]):
199
+ b = list(b)
200
+ yield b[0][1], b[-1][1]
201
+
202
+
203
+ def contiguous_ranges(iterable, order=sorted, key=lambda x: x):
204
+ '''split iterable into contiguous index sequence, i.e. reduce consecutive index runs
205
+ to a tuple containing the first and last. ``iterable`` can be a sequence of values that
206
+ can be subtracted or a list of more complex values where the function ``key`` returns
207
+ the key to be used for ordering. ``iterable`` is ordered by ``order`` which by default
208
+ sorts ``iterable``.'''
209
+ unique = list(set(iterable))
210
+ for a, b in groupby(
211
+ enumerate(order(unique, key=key) if 'key' in inspect.getfullargspec(order).kwonlyargs else order(unique)),
212
+ lambda pair: key(pair[1]) - pair[0]):
213
+ b = list(b)
214
+ yield b[0][1], b[-1][1]
215
+
216
+
217
+ def expand_range_incl(r):
218
+ '''expand the tuple supplied as ``r`` into a range which *includes* the first
219
+ and last element ``r``'''
220
+ if r[0] == r[1]:
221
+ return [0]
222
+ else:
223
+ return range(r[0], r[1] + 1)
224
+
225
+
226
+ def __get_wcs_object(csys: 'image.coordsys', naxis=2) -> 'image.coordsys()':
227
+ """Generate world coordinate object from coordinate system.
228
+
229
+ Returns:
230
+ w: world coordinate object
231
+ """
232
+ try:
233
+ import numpy as np
234
+ from astropy.wcs import WCS
235
+
236
+ w = WCS(naxis=naxis)
237
+
238
+ rad_to_deg = 180/np.pi
239
+ w.wcs.crpix = csys.referencepixel()['numeric'][0:2]
240
+ w.wcs.cdelt = csys.increment()['numeric'][0:2]*rad_to_deg
241
+ w.wcs.crval = csys.referencevalue()['numeric'][0:2] * rad_to_deg
242
+ w.wcs.ctype = ['RA---SIN', 'DEC--SIN']
243
+
244
+ return w
245
+
246
+ except ImportError as error:
247
+ print('Error importing module:: ' + str(error))
248
+
249
+ except Exception as error:
250
+ print(error)
251
+
252
+ def __index_to_stokes(index: int):
253
+ """Convert stokes axis index to alphabetic value.
254
+
255
+ Args:
256
+ index (int): enumerated index defining stokes value.
257
+
258
+ Returns:
259
+ str: String indicating stokes value.
260
+ """
261
+ STOKES_MAP = ['I', 'Q', 'U', 'V']
262
+
263
+ try:
264
+ return [STOKES_MAP[i] for i in index]
265
+ except TypeError:
266
+ return STOKES_MAP[index]
267
+
268
+ def index_to_stokes(index: int):
269
+ """Convert stokes axis index to alphabetic value.
270
+
271
+ Args:
272
+ index (int): enumerated index defining stokes value.
273
+
274
+ Returns:
275
+ str: String indicating stokes value.
276
+ """
277
+ STOKES_MAP = ['I', 'Q', 'U', 'V']
278
+
279
+ try:
280
+ return [STOKES_MAP[i] for i in index]
281
+ except TypeError:
282
+ return STOKES_MAP[index]
283
+
284
+ def __get_center_pixels(params: dict):
285
+ """Get center pixel offset value from dictionary.
286
+
287
+ Args:
288
+ params (dict): Submask from mask grouping used by itertools
289
+
290
+ Returns:
291
+ list: Pixel offset defined in submask.
292
+ """
293
+ return params['d']
294
+
295
+ def __write_casa_region(region_object: 'astropy.region', coord:str, polygon_shape: str)->list:
296
+ ''' Convert astropy region to casa region string.
297
+
298
+ Parameters
299
+ ----------
300
+ region_objects: astropy region
301
+ Region definition in pixel or world coordinates
302
+ coord: str
303
+ Coordinate system that should be used in the returned masks. Allowed values are 'pixel', 'world'.
304
+ polygon_shape: str
305
+ Region shape definition: rect: centerbox/rotbox, poly:poly
306
+
307
+ '''
308
+
309
+ meta_value = []
310
+ for key, value in region_object.meta.items():
311
+ if key == 'corr':
312
+ meta_value.append('{key}=[{pol}]'.format(key=key, pol=value))
313
+ elif key == 'range':
314
+ meta_value.append('{key}=[{lower}chan, {upper}chan]'.format(key=key, lower=str(value[0]), upper=str(value[1])))
315
+ else:
316
+ print('Unknown key: {} skipping'.format(key))
317
+
318
+ meta = ', '.join(meta_value)
319
+
320
+ if polygon_shape=='rect':
321
+ if coord=='pixel':
322
+ return 'centerbox[[{x} pix, {y} pix], [{width} pix, {height} pix]], {meta}'.format(
323
+ x=region_object.center.x,
324
+ y=region_object.center.y,
325
+ width=region_object.width,
326
+ height=region_object.height,
327
+ meta=meta
328
+ )
329
+
330
+ if coord=='world':
331
+ return 'rotbox[[{ra}, {dec}], [{width}, {height}], {angle}], {meta}'.format(
332
+ ra=region_object.center.ra,
333
+ dec=region_object.center.dec,
334
+ width=region_object.width,
335
+ height=region_object.height,
336
+ angle=region_object.angle,
337
+ meta=meta
338
+ )
339
+
340
+ else:
341
+ raise RuntimeError('Unknown coordinate value: {}'.format(coord))
342
+
343
+ if polygon_shape=='poly':
344
+ if coord=='pixel':
345
+ coords_array = []
346
+ for value in region_object.vertices:
347
+ coords_array.append('[{x} pix, {y} pix]'.format(x=value.x, y=value.y))
348
+
349
+ coords = ', '.join(coords_array)
350
+
351
+ return 'poly[{coords}], {meta}'.format(coords=coords, meta=meta)
352
+
353
+ if coord=='world':
354
+ coords_array = []
355
+ for value in region_object.vertices:
356
+ coords_array.append('[{ra}, {dec}]'.format(ra=value.ra, dec=value.dec))
357
+
358
+ coords = ', '.join(coords_array)
359
+
360
+ return 'poly[{coords}], {meta}'.format(coords=coords, meta=meta)
361
+ else:
362
+ raise RuntimeError('Unknown coordinate value: {}'.format(coord))
363
+ else:
364
+ raise RuntimeError('Unknown polygon shape: {}'.format(polygon_shape))
365
+
366
+ def convert_masks(masks: dict, coord='pixel', cdesc=None)->list:
367
+ '''Convert masks in standard format (as defined by ``CubeMask.jsmask_to_raw``) into
368
+ other formats like list of CRTF, single region, etc.
369
+
370
+ Parameters
371
+ ----------
372
+ masks: dict
373
+ Dictionary containing ``masks`` and ``polys`` keys. The values for ``masks`` are the polygon
374
+ references for each channel and the values for ``polys`` contain the points making up each
375
+ polygon.
376
+ coord: str
377
+ Coordinate system that should be used in the returned masks. Allowed values are 'pixel'.
378
+ cdesc: dict
379
+ Dictionary containing ``csys`` and ``shape`` which describes the coordinate system to be
380
+ used for creating world coordinate coordinates. The ``shape`` is required along with the
381
+ coordinate system for coordinate conversion.
382
+ '''
383
+
384
+ if cdesc is None or 'csys' not in cdesc or 'shape' not in cdesc:
385
+ raise RuntimeError('region operations requires a coordinate description (cdesc parameter)')
386
+ else:
387
+ if not isinstance( cdesc['shape'], tuple ):
388
+ raise RuntimeError("coordinate description must contain a 'shape' element of type 'tuple'")
389
+
390
+ if isinstance( cdesc['csys'], dict ):
391
+ csys = cdesc['csys']
392
+
393
+
394
+ full_mask_params = []
395
+
396
+ # Get the wcs object of the request format is world
397
+ if coord == 'world':
398
+ wcs = __get_wcs_object(cdesc['csys'])
399
+
400
+ # Build list containing masks with channel and polaization information
401
+ # in a more accesible way for the grouping funciton
402
+ for index, mask in masks['masks'].items():
403
+ for sub_mask in mask:
404
+ sub_mask['s'], sub_mask['c'] = index
405
+ full_mask_params.append(sub_mask)
406
+
407
+ region_list = []
408
+
409
+ # Group masks by unique center pixel and determine channel range. Extract
410
+ # mask properties and build astropy region for each center pixel and geometry
411
+ # including region meta data.
412
+ for key, props in itertools.groupby(full_mask_params, __get_center_pixels):
413
+ channel_range = []
414
+ for prop in props:
415
+ channel_range.append(prop['c'])
416
+ poly = prop['p']
417
+ center_pixels = prop['d']
418
+
419
+ mask_shape = masks['polys'][poly]['type']
420
+
421
+ xs = masks['polys'][poly]['geometry']['xs']
422
+ ys = masks['polys'][poly]['geometry']['ys']
423
+
424
+ width = max(xs) - min(xs)
425
+ height = max(ys) - min(ys)
426
+
427
+ stokes_index = prop['s']
428
+
429
+ if mask_shape=='rect':
430
+ region = RectanglePixelRegion(
431
+ PixCoord(x=center_pixels[0], y=center_pixels[1]),
432
+ width=width, height=height, angle=0*units.deg
433
+ )
434
+ elif mask_shape=='poly':
435
+ region = PolygonPixelRegion(
436
+ vertices=PixCoord(x=xs, y=ys)
437
+ )
438
+
439
+ else:
440
+ raise RuntimeError('Invalid mask shape: {func}-->mask={mask}'.format(func=__convert_masks.__qualname__, mask=mask_shape))
441
+
442
+ if coord=='world':
443
+ region = region.to_sky(wcs)
444
+ region.meta = {
445
+ 'corr': __index_to_stokes(stokes_index),
446
+ 'range': [min(channel_range), max(channel_range)]
447
+ }
448
+
449
+ region_list.append(__write_casa_region(region, coord=coord, polygon_shape=mask_shape))
450
+ if coord=='pixel':
451
+
452
+ region.meta = {
453
+ 'corr': __index_to_stokes(stokes_index),
454
+ 'range': [min(channel_range), max(channel_range)]
455
+ }
456
+
457
+ region_list.append(__write_casa_region(region, coord=coord, polygon_shape=mask_shape))
458
+
459
+ return region_list
460
+
461
+ def __convert_masks(masks, format='crtf', coord='pixel', ret_type='str', cdesc=None):
462
+ '''Convert masks in standard format (as defined by ``CubeMask.jsmask_to_raw``) into
463
+ other formats like list of CRTF, single region, etc.
464
+
465
+ Parameters
466
+ ----------
467
+ masks: dict
468
+ Dictionary containing ``masks`` and ``polys`` keys. The values for ``masks`` are the polygon
469
+ references for each channel and the values for ``polys`` contain the points making up each
470
+ polygon.
471
+ format: str
472
+ Format string that indicates the format that should be returned. Allowed values are
473
+ 'region' or 'crtf'.
474
+ coord: str
475
+ Coordinate system that should be used in the returned masks. Allowed values are 'pixel'.
476
+ ret_type: str
477
+ Data structure that should be returned the allowed values are 'str', 'list', 'singleton'
478
+ cdesc: dict
479
+ Dictionary containing ``csys`` and ``shape`` which describes the coordinate system to be
480
+ used for creating world coordinate coordinates. The ``shape`` is required along with the
481
+ coordinate system for coordinate conversion.
482
+ '''
483
+ if format == 'region':
484
+ if __have_casatools == False:
485
+ raise RuntimeError('casatools is not available, cannot create CASA regions')
486
+ if cdesc is None or 'csys' not in cdesc or 'shape' not in cdesc:
487
+ raise RuntimeError('region operations requires a coordinate description (cdesc parameter)')
488
+ else:
489
+ if not isinstance(cdesc['shape'], tuple):
490
+ raise RuntimeError("coordinate description must contain a 'shape' element of type 'tuple'")
491
+ if isinstance(cdesc['csys'], dict):
492
+ csys_dict = cdesc['csys']
493
+ else:
494
+ csys_dict = cdesc['csys'].torecord()
495
+
496
+ unique_polygons = {}
497
+ ###
498
+ ### collect masks ordered by (poly_index,dx,dy)
499
+ ###
500
+ for index, mask in masks['masks'].items():
501
+ for poly in mask:
502
+ unique_poly = tuple([poly['p']] + poly['d'])
503
+ if unique_poly in unique_polygons:
504
+ unique_polygons[unique_poly] += [index]
505
+ else:
506
+ unique_polygons[unique_poly] = [index]
507
+
508
+ polygon_definitions = {index: poly for index, poly in masks['polys'].items()}
509
+
510
+ if format == 'region' and coord == 'pixel':
511
+ rg = regionmanager()
512
+ rg.setcoordinates(csys_dict)
513
+
514
+ ###
515
+ ### create a region list given the index of the polygon used, the x/y translation, and the
516
+ ### channels the region should be found on
517
+ ###
518
+ def create_regions_rgn(poly_index, xlate, channels):
519
+ def create_result(shape, points):
520
+ return [rg.fromtext(
521
+ f"{shape}[{points}],range=[{chan_range[0]}chan,{chan_range[1]}chan],corr=[{','.join(index_to_stokes(expand_range_incl(stokes_range)))}]" ,
522
+ shape=list(cdesc['shape']))
523
+ for chan_range in contiguous_ranges([c[1] for c in channels]) for stokes_range in
524
+ contiguous_ranges([c[0] for c in channels])]
525
+
526
+ if polygon_definitions[poly_index]['type'] == 'poly':
527
+ poly_pts = ','.join([f"[{x + xlate[0]}pix,{y + xlate[1]}pix]"
528
+ for x in polygon_definitions[poly_index]['geometry']['xs']
529
+ for y in polygon_definitions[poly_index]['geometry']['ys']])
530
+ return create_result('poly', poly_pts)
531
+ if polygon_definitions[poly_index]['type'] == 'rect':
532
+ xs = polygon_definitions[poly_index]['geometry']['xs']
533
+ ys = polygon_definitions[poly_index]['geometry']['ys']
534
+ box_pts = f"[{min(xs) + xlate[0]}pix,{min(ys) + xlate[1]}pix],[{max(xs) + xlate[0]}pix,{max(ys) + xlate[1]}pix]"
535
+ return create_result('box', box_pts)
536
+
537
+ ### flatten list of unique polygon regions
538
+ result = list(chain.from_iterable(
539
+ [create_regions_rgn(index[0], index[1:], channels) for index, channels in unique_polygons.items()]))
540
+ if ret_type == 'singleton':
541
+ if len(result) == 0:
542
+ raise RuntimeError("no regions created")
543
+ if len(result) == 1:
544
+ return result[0]
545
+ ###return dict(enumerate(result))) ## see: CAS-13764
546
+ return rg.makeunion(dict(map(lambda t: (str(t[0]), t[1]), list(enumerate(result)))))
547
+ if ret_type == 'list':
548
+ return result
549
+ raise RuntimeError(f"unknown ret_type for region format ({ret_type})")
550
+
551
+ if format == 'crtf' and coord == 'pixel':
552
+ ###
553
+ ### create a region list given the index of the polygon used, the x/y translation, and the
554
+ ### channels the region should be found on
555
+ ###
556
+ def create_regions_crt(poly_index, xlate, channels):
557
+ def create_result(shape, points):
558
+ return [
559
+ f"{shape}[{points}],range=[{chan_range[0]}chan,{chan_range[1]}chan],corr=[{','.join(index_to_stokes(expand_range_incl(stokes_range)))}]"
560
+ for chan_range in contiguous_ranges([c[1] for c in channels]) for stokes_range in
561
+ contiguous_ranges([c[0] for c in channels])]
562
+
563
+ if polygon_definitions[poly_index]['type'] == 'poly':
564
+ poly_pts = ','.join([f"[{x + xlate[0]}pix,{y + xlate[1]}pix]"
565
+ for x in polygon_definitions[poly_index]['geometry']['xs']
566
+ for y in polygon_definitions[poly_index]['geometry']['ys']])
567
+ return create_result('poly', poly_pts)
568
+ if polygon_definitions[poly_index]['type'] == 'rect':
569
+ xs = polygon_definitions[poly_index]['geometry']['xs']
570
+ ys = polygon_definitions[poly_index]['geometry']['ys']
571
+ box_pts = f"[{min(xs) + xlate[0]}pix,{min(ys) + xlate[1]}pix],[{max(xs) + xlate[0]}pix,{max(ys) + xlate[1]}pix]"
572
+ return create_result('box', box_pts)
573
+
574
+ ###
575
+ ### generate CFTF
576
+ ###
577
+ result = [create_regions_crt(index[0], index[1:], channels) for index, channels in unique_polygons.items()]
578
+ if ret_type == 'str':
579
+ return '\n'.join(chain.from_iterable(result))
580
+ if ret_type == 'list':
581
+ return list(chain.from_iterable(result))
582
+ raise RuntimeError(f"unknown ret_type for crtf format ({ret_type})")
583
+ raise RuntimeError(f"invalid format ({format}), or coord ({coord})")
584
+
585
+
586
+ def set_attributes(obj, **kw):
587
+ '''Given an object and a set of keyword arguments, set the attributes
588
+ in the object that correspond to the keywords to the specified values.
589
+
590
+ Parameters
591
+ ----------
592
+ obj: object
593
+ Object whose attributes should be set
594
+ kw: keyword and object
595
+ Attributes to be set
596
+
597
+ Returns
598
+ -------
599
+ object
600
+ ``obj`` parameter
601
+ '''
602
+ for k, v in kw.items():
603
+ if hasattr(obj,k): setattr(obj, k, v)
604
+ return obj
605
+
606
+
607
+ def is_notebook() -> bool:
608
+ try:
609
+ shell = get_ipython().__class__.__name__
610
+ if shell == 'ZMQInteractiveShell':
611
+ return True # Jupyter notebook or qtconsole
612
+ elif shell == 'TerminalInteractiveShell':
613
+ return False # Terminal running IPython
614
+ else:
615
+ if get_ipython().__class__.__module__ == 'google.colab._shell':
616
+ return True # Google Colab
617
+ else:
618
+ return False # Other type (?)
619
+ except NameError:
620
+ return False
@@ -0,0 +1,84 @@
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+
5
+ from functools import wraps
6
+
7
+ ###
8
+ ### from: https://stackoverflow.com/a/38730848
9
+ ###
10
+ class ContextMgrChain(object):
11
+
12
+ def __init__(self, *managers):
13
+ self.managers = managers
14
+ self.stack = []
15
+ self.values = []
16
+
17
+ async def push(self, manager):
18
+ try:
19
+ if hasattr(manager, '__aenter__'):
20
+ value = await manager.__aenter__()
21
+ else:
22
+ value = manager.__enter__()
23
+
24
+ self.stack.append(manager)
25
+ self.values.append(value)
26
+ return value
27
+ except:
28
+ # if we encounter an exception somewhere along our enters,
29
+ # we'll stop adding to the stack, and pop everything we've
30
+ # added so far, to simulate what would happen when an inner
31
+ # block raised an exception.
32
+ swallow = await self.__aexit__(*sys.exc_info())
33
+ if not swallow:
34
+ raise
35
+
36
+ async def __aenter__(self):
37
+ value = None
38
+
39
+ for manager in self.managers:
40
+ value = await self.push(manager)
41
+
42
+ return value
43
+
44
+ async def __aexit__(self, exc_type, exc, tb):
45
+ excChanged = False
46
+ swallow = False # default value
47
+ while self.stack:
48
+ # no matter what the outcome, we want to attempt to call __aexit__ on
49
+ # all context managers
50
+ try:
51
+ swallow = await self._pop(exc_type, exc, tb)
52
+ if swallow:
53
+ # if we swallow an exception on an inner cm, outer cms would
54
+ # not receive it at all...
55
+ exc_type = None
56
+ exc = None
57
+ tb = None
58
+ except:
59
+ # if we encounter an exception while exiting, that is the
60
+ # new execption we send upward
61
+ excChanged = True
62
+ (exc_type, exc, tb) = sys.exc_info()
63
+ swallow = False
64
+
65
+ if exc is None:
66
+ # when we make it to the end, if exc is None, it was swallowed
67
+ # somewhere along the line, and we've exited everything successfully,
68
+ # so tell python to swallow the exception for real
69
+ return True
70
+ elif excChanged:
71
+ # if the exception has been changed, we need to raise it here
72
+ # because otherwise python will just raise the original exception
73
+ if not swallow:
74
+ raise exc
75
+ else:
76
+ # we have the original exception still, we just let python handle it...
77
+ return swallow
78
+
79
+ async def _pop(self, exc_type, exc, tb):
80
+ manager = self.stack.pop()
81
+ if hasattr(manager, '__aexit__'):
82
+ return await manager.__aexit__(exc_type, exc, tb)
83
+ else:
84
+ return manager.__exit__(exc_type, exc, tb)