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.
- cubevis/LICENSE.rst +500 -0
- cubevis/__icons__/20px/fast-backward.svg +13 -0
- cubevis/__icons__/20px/fast-forward.svg +13 -0
- cubevis/__icons__/20px/step-backward.svg +12 -0
- cubevis/__icons__/20px/step-forward.svg +12 -0
- cubevis/__icons__/add-chan.png +0 -0
- cubevis/__icons__/add-chan.svg +84 -0
- cubevis/__icons__/add-cube.png +0 -0
- cubevis/__icons__/add-cube.svg +186 -0
- cubevis/__icons__/drag.png +0 -0
- cubevis/__icons__/drag.svg +109 -0
- cubevis/__icons__/mask-selected.png +0 -0
- cubevis/__icons__/mask.png +0 -0
- cubevis/__icons__/mask.svg +1 -0
- cubevis/__icons__/new-layer-sm-selected.png +0 -0
- cubevis/__icons__/new-layer-sm-selected.svg +88 -0
- cubevis/__icons__/new-layer-sm.png +0 -0
- cubevis/__icons__/new-layer-sm.svg +15 -0
- cubevis/__icons__/reset.png +0 -0
- cubevis/__icons__/reset.svg +11 -0
- cubevis/__icons__/sub-chan.png +0 -0
- cubevis/__icons__/sub-chan.svg +71 -0
- cubevis/__icons__/sub-cube.png +0 -0
- cubevis/__icons__/sub-cube.svg +95 -0
- cubevis/__icons__/zoom-to-fit.png +0 -0
- cubevis/__icons__/zoom-to-fit.svg +21 -0
- cubevis/__init__.py +58 -0
- cubevis/__js__/bokeh-3.6.1.min.js +728 -0
- cubevis/__js__/bokeh-tables-3.6.1.min.js +119 -0
- cubevis/__js__/bokeh-widgets-3.6.1.min.js +141 -0
- cubevis/__js__/casalib.min.js +1 -0
- cubevis/__js__/cubevisjs.min.js +62 -0
- cubevis/__version__.py +1 -0
- cubevis/apps/__init__.py +44 -0
- cubevis/apps/_createmask.py +461 -0
- cubevis/apps/_createregion.py +513 -0
- cubevis/apps/_interactiveclean.py +3260 -0
- cubevis/apps/_interactiveclean_wrappers.py +130 -0
- cubevis/apps/_ms_raster.py +815 -0
- cubevis/apps/_plotants.py +286 -0
- cubevis/apps/_plotbandpass.py +7 -0
- cubevis/bokeh/__init__.py +29 -0
- cubevis/bokeh/annotations/__init__.py +1 -0
- cubevis/bokeh/annotations/_ev_poly_annotation.py +6 -0
- cubevis/bokeh/components/__init__.py +28 -0
- cubevis/bokeh/format/__init__.py +31 -0
- cubevis/bokeh/format/_time_ticks.py +44 -0
- cubevis/bokeh/format/_wcs_ticks.py +45 -0
- cubevis/bokeh/models/__init__.py +4 -0
- cubevis/bokeh/models/_edit_span.py +7 -0
- cubevis/bokeh/models/_ev_text_input.py +6 -0
- cubevis/bokeh/models/_tip.py +37 -0
- cubevis/bokeh/models/_tip_button.py +50 -0
- cubevis/bokeh/sources/__init__.py +35 -0
- cubevis/bokeh/sources/_data_pipe.py +258 -0
- cubevis/bokeh/sources/_image_data_source.py +83 -0
- cubevis/bokeh/sources/_image_pipe.py +581 -0
- cubevis/bokeh/sources/_spectra_data_source.py +55 -0
- cubevis/bokeh/sources/_updatable_data_source.py +189 -0
- cubevis/bokeh/state/__init__.py +34 -0
- cubevis/bokeh/state/_initialize.py +164 -0
- cubevis/bokeh/state/_javascript.py +53 -0
- cubevis/bokeh/state/_palette.py +58 -0
- cubevis/bokeh/state/_session.py +44 -0
- cubevis/bokeh/state/js/bokeh-2.4.1.min.js +596 -0
- cubevis/bokeh/state/js/bokeh-gl-2.4.1.min.js +74 -0
- cubevis/bokeh/state/js/bokeh-tables-2.4.1.min.js +132 -0
- cubevis/bokeh/state/js/bokeh-widgets-2.4.1.min.js +118 -0
- cubevis/bokeh/state/js/casaguijs-v0.0.4.0-b2.4.min.js +49 -0
- cubevis/bokeh/state/js/casaguijs-v0.0.5.0-b2.4.min.js +49 -0
- cubevis/bokeh/state/js/casaguijs-v0.0.6.0-b2.4.min.js +49 -0
- cubevis/bokeh/state/js/casalib-v0.0.1.min.js +1 -0
- cubevis/bokeh/tools/__init__.py +31 -0
- cubevis/bokeh/tools/_cbreset_tool.py +52 -0
- cubevis/bokeh/tools/_drag_tool.py +61 -0
- cubevis/bokeh/utils/__init__.py +35 -0
- cubevis/bokeh/utils/_axes_labels.py +94 -0
- cubevis/bokeh/utils/_svg_icon.py +136 -0
- cubevis/data/__init__.py +1 -0
- cubevis/data/casaimage/__init__.py +114 -0
- cubevis/data/measurement_set/__init__.py +7 -0
- cubevis/data/measurement_set/_ms_data.py +178 -0
- cubevis/data/measurement_set/processing_set/__init__.py +30 -0
- cubevis/data/measurement_set/processing_set/_ps_concat.py +98 -0
- cubevis/data/measurement_set/processing_set/_ps_coords.py +78 -0
- cubevis/data/measurement_set/processing_set/_ps_data.py +213 -0
- cubevis/data/measurement_set/processing_set/_ps_io.py +55 -0
- cubevis/data/measurement_set/processing_set/_ps_raster_data.py +154 -0
- cubevis/data/measurement_set/processing_set/_ps_select.py +91 -0
- cubevis/data/measurement_set/processing_set/_ps_stats.py +218 -0
- cubevis/data/measurement_set/processing_set/_xds_data.py +149 -0
- cubevis/plot/__init__.py +1 -0
- cubevis/plot/ms_plot/__init__.py +29 -0
- cubevis/plot/ms_plot/_ms_plot.py +242 -0
- cubevis/plot/ms_plot/_ms_plot_constants.py +22 -0
- cubevis/plot/ms_plot/_ms_plot_selectors.py +348 -0
- cubevis/plot/ms_plot/_raster_plot.py +292 -0
- cubevis/plot/ms_plot/_raster_plot_inputs.py +116 -0
- cubevis/plot/ms_plot/_xds_plot_axes.py +110 -0
- cubevis/private/__java__/xml-casa-assembly-1.86.jar +0 -0
- cubevis/private/_gclean.py +798 -0
- cubevis/private/casashell/createmask.py +332 -0
- cubevis/private/casashell/iclean.py +4432 -0
- cubevis/private/casatasks/__init__.py +140 -0
- cubevis/private/casatasks/createmask.py +86 -0
- cubevis/private/casatasks/createregion.py +83 -0
- cubevis/private/casatasks/iclean.py +1831 -0
- cubevis/readme.rst +16 -0
- cubevis/remote/__init__.py +10 -0
- cubevis/remote/_gclean.py +61 -0
- cubevis/remote/_local.py +287 -0
- cubevis/remote/_remote_kernel.py +80 -0
- cubevis/toolbox/__init__.py +32 -0
- cubevis/toolbox/_app_context.py +74 -0
- cubevis/toolbox/_cube.py +3457 -0
- cubevis/toolbox/_region_list.py +197 -0
- cubevis/utils/_ResourceManager.py +86 -0
- cubevis/utils/__init__.py +620 -0
- cubevis/utils/_contextmgrchain.py +84 -0
- cubevis/utils/_conversion.py +93 -0
- cubevis/utils/_copydoc.py +55 -0
- cubevis/utils/_docenum.py +25 -0
- cubevis/utils/_import_protected_module.py +35 -0
- cubevis/utils/_logging.py +85 -0
- cubevis/utils/_pkgs.py +77 -0
- cubevis/utils/_regions.py +40 -0
- cubevis/utils/_static.py +66 -0
- cubevis/utils/_tiles.py +167 -0
- cubevis-0.5.2.dist-info/METADATA +151 -0
- cubevis-0.5.2.dist-info/RECORD +132 -0
- cubevis-0.5.2.dist-info/WHEEL +4 -0
- 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)
|