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,581 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2021,2022,2023,2024
|
|
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
|
+
'''Implementation of ``ImagePipe`` class which provides a ``websockets``
|
|
29
|
+
implementation for CASA images which allows for interacitve display
|
|
30
|
+
of image cube channels in response to user input.'''
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import sys
|
|
34
|
+
import json
|
|
35
|
+
import asyncio
|
|
36
|
+
from uuid import uuid4
|
|
37
|
+
|
|
38
|
+
from . import DataPipe
|
|
39
|
+
from bokeh.util.compiler import TypeScript
|
|
40
|
+
from bokeh.core.properties import Tuple, String, Int, Instance, Nullable
|
|
41
|
+
from bokeh.models.callbacks import Callback
|
|
42
|
+
from bokeh.plotting import ColumnDataSource
|
|
43
|
+
from ..state import casalib_url, cubevisjs_url
|
|
44
|
+
|
|
45
|
+
import numpy as np
|
|
46
|
+
try:
|
|
47
|
+
import casatools as ct
|
|
48
|
+
from casatools import regionmanager
|
|
49
|
+
from casatools import image as imagetool
|
|
50
|
+
except:
|
|
51
|
+
ct = None
|
|
52
|
+
from cubevis.utils import warn_import
|
|
53
|
+
warn_import('casatools')
|
|
54
|
+
|
|
55
|
+
from ...utils import pack_arrays, partition, resource_manager, strip_arrays
|
|
56
|
+
|
|
57
|
+
class ImagePipe(DataPipe):
|
|
58
|
+
"""The `ImagePipe` allows for updates to Bokeh plots from a CASA or CNGI
|
|
59
|
+
|
|
60
|
+
image. This is done using a `websocket`. A `ImagePipe` is created with
|
|
61
|
+
the path to the image, and then it is used as the input to an
|
|
62
|
+
`ImageDataSource` or a `SpectraDataSource`. This allows a single CASA
|
|
63
|
+
or CNGI imge to be opened once and shared among multiple Bokeh plots,
|
|
64
|
+
for example ploting an image channel and a plot of a spectrum from the
|
|
65
|
+
image cube.
|
|
66
|
+
|
|
67
|
+
Attributes
|
|
68
|
+
----------
|
|
69
|
+
address: tuple of string and int
|
|
70
|
+
the string is the IP address for the network that should be used and the
|
|
71
|
+
integer is the port number, see ``cubevis.utils.find_ws_address``
|
|
72
|
+
init_script: JavaScript
|
|
73
|
+
this javascript is run when this DataPipe object is initialized. init_script
|
|
74
|
+
is used to run caller JavaScript which needs to be run at initialization time.
|
|
75
|
+
This is optional and does not need to be set.
|
|
76
|
+
"""
|
|
77
|
+
__im_path = None
|
|
78
|
+
__im = None
|
|
79
|
+
__chan_shape = None
|
|
80
|
+
|
|
81
|
+
shape = Tuple( Int, Int, Int, Int, help="shape: [ RA, DEC, Stokes, Spectral ]" )
|
|
82
|
+
dataid = String( )
|
|
83
|
+
fits_header_json = Nullable( String, help="""JSON representation of image FITS header for world coordinate labeling""" )
|
|
84
|
+
_histogram_source = Nullable(Instance(ColumnDataSource), help='''
|
|
85
|
+
data source for (raw) image channel histogram of intensities used with a "figure.quad(...)"
|
|
86
|
+
''')
|
|
87
|
+
|
|
88
|
+
__javascript__ = [ casalib_url( ), cubevisjs_url( ) ]
|
|
89
|
+
|
|
90
|
+
def __open_image( self, image ):
|
|
91
|
+
if self.__img is not None:
|
|
92
|
+
self.__img.close( )
|
|
93
|
+
self.__stokes_labels = None
|
|
94
|
+
self.__img = imagetool( )
|
|
95
|
+
self.__rgn = regionmanager( )
|
|
96
|
+
try:
|
|
97
|
+
self.__img.open(image)
|
|
98
|
+
self.__image_path = image
|
|
99
|
+
except Exception as ex:
|
|
100
|
+
self.__img = None
|
|
101
|
+
self.__image_path = None
|
|
102
|
+
raise RuntimeError(f'could not open image: {image}') from ex
|
|
103
|
+
imshape = self.__img.shape( )
|
|
104
|
+
if self.__msk is not None and all(self.__msk.shape( ) != imshape):
|
|
105
|
+
raise RuntimeError(f'mismatch between image shape ({imshape}) and mask shape ({self.__msk.shape( )})')
|
|
106
|
+
if self.__chan_shape is None: self.__chan_shape = list(imshape[0:2])
|
|
107
|
+
|
|
108
|
+
def __open_mask( self, mask ):
|
|
109
|
+
if mask is None:
|
|
110
|
+
self.__mask_path = None
|
|
111
|
+
return
|
|
112
|
+
if self.__msk is not None:
|
|
113
|
+
self.__msk.close( )
|
|
114
|
+
self.__msk = imagetool( )
|
|
115
|
+
try:
|
|
116
|
+
self.__msk.open(mask)
|
|
117
|
+
self.__mask_path = mask
|
|
118
|
+
except Exception as ex:
|
|
119
|
+
self.__msk = None
|
|
120
|
+
self.__mask_path = None
|
|
121
|
+
raise RuntimeError(f'could not open mask: {mask}') from ex
|
|
122
|
+
mskshape = self.__msk.shape( )
|
|
123
|
+
if self.__img is not None and all(self.__img.shape( ) != mskshape):
|
|
124
|
+
raise RuntimeError(f'mismatch between image shape ({self.__img.shape( )}) and mask shape ({mskshape})')
|
|
125
|
+
if self.__chan_shape is None: self.__chan_shape = list(mskshape[0:2])
|
|
126
|
+
|
|
127
|
+
def __close_mask( self ):
|
|
128
|
+
if self.__msk is not None:
|
|
129
|
+
self.__msk.close( )
|
|
130
|
+
self.__msk = None
|
|
131
|
+
|
|
132
|
+
def pixel_value( self, chan, index ):
|
|
133
|
+
channel = self.__get_chan(chan)
|
|
134
|
+
index[0] = min( index[0], channel.shape[0] - 1 )
|
|
135
|
+
index[1] = min( index[1], channel.shape[1] - 1 )
|
|
136
|
+
index[0] = max( index[0], 0 )
|
|
137
|
+
index[1] = max( index[1], 0 )
|
|
138
|
+
return np.squeeze(channel[index[0],index[1]])
|
|
139
|
+
|
|
140
|
+
def stokes_labels( self ):
|
|
141
|
+
"""Returns stokes plane labels"""
|
|
142
|
+
if self.__stokes_labels is None:
|
|
143
|
+
self.__stokes_labels = self.__img.coordsys( ).stokes( )
|
|
144
|
+
return self.__stokes_labels
|
|
145
|
+
|
|
146
|
+
def __get_chan( self, index ):
|
|
147
|
+
def newest_ctime( path ):
|
|
148
|
+
files = os.listdir(path)
|
|
149
|
+
paths = [os.path.join(path, basename) for basename in files]
|
|
150
|
+
return max( map( os.path.getctime, paths ) )
|
|
151
|
+
|
|
152
|
+
image_ctime = newest_ctime( self.__image_path )
|
|
153
|
+
if image_ctime > self.__cached_chan_ctime or \
|
|
154
|
+
self.__cached_chan_index[0] != index[0] or \
|
|
155
|
+
self.__cached_chan_index[1] != index[1] or \
|
|
156
|
+
self.__cached_chan is None :
|
|
157
|
+
if self.__img is None:
|
|
158
|
+
raise RuntimeError('no image is available')
|
|
159
|
+
###
|
|
160
|
+
### ensure that the channel index is within cube shape
|
|
161
|
+
###
|
|
162
|
+
index = list(index) # index is potentially a python tuple
|
|
163
|
+
index[0] = min( index[0], self.__chan_shape[0] - 1 )
|
|
164
|
+
index[1] = min( index[1], self.__chan_shape[1] - 1 )
|
|
165
|
+
index[0] = max( index[0], 0 )
|
|
166
|
+
index[1] = max( index[1], 0 )
|
|
167
|
+
self.__cached_chan_index = index
|
|
168
|
+
self.__cached_chan_ctime = image_ctime
|
|
169
|
+
self.__cached_chan = self.__img.getchunk( blc=[0,0] + index,
|
|
170
|
+
trc=self.__chan_shape + index )
|
|
171
|
+
return self.__cached_chan
|
|
172
|
+
|
|
173
|
+
### ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
|
|
174
|
+
### the element type of image pixels retrieved from the CASA image are float64, but it
|
|
175
|
+
### seems like 256 is the greatest number of colors in the colormaps currrently used
|
|
176
|
+
### for pseudo color within interactive clean...
|
|
177
|
+
### ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
|
|
178
|
+
def channel( self, index, pixel_type ):
|
|
179
|
+
"""Retrieve one channel from the image cube. The `index` should be a
|
|
180
|
+
two element list of integers. The first integer is the ''stokes'' axis
|
|
181
|
+
in the image cube. The second integer is the ''channel'' axis in the
|
|
182
|
+
image cube.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
index: [ int, int ]
|
|
187
|
+
list containing first the ''stokes'' index and second the ''channel'' index
|
|
188
|
+
pixel_type: numpy type
|
|
189
|
+
the numpy type for the pixel elements of the returned channel
|
|
190
|
+
"""
|
|
191
|
+
def quantize( nptype, image_plane ):
|
|
192
|
+
### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
193
|
+
### Note:
|
|
194
|
+
### (1) the histogram sent to GUI is ALWAYS be histogram based on the raw image (THIS IS HANDLED ABOVE)
|
|
195
|
+
### (2) the scaled portion of the matrix should be the non-cropped portion
|
|
196
|
+
### (3) the lower cropped portion should be set to the min scaled value
|
|
197
|
+
### (4) the upper cropped portion should be set to the max scaled value
|
|
198
|
+
### (5) a histogram should be created with the resulting (completely filled) array
|
|
199
|
+
### (6) then this histogram should be used with the (completely filled) array with numpy.digitize( ) to create
|
|
200
|
+
### the uint8 array
|
|
201
|
+
### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
202
|
+
exclude_below = None
|
|
203
|
+
exclude_above = None
|
|
204
|
+
included = None
|
|
205
|
+
|
|
206
|
+
### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
207
|
+
### Sort out the relationship between channel min/max and user specified min/max
|
|
208
|
+
### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
209
|
+
amin = image_plane.min( ) ## array min
|
|
210
|
+
amax = image_plane.max( ) ## array max
|
|
211
|
+
rg = [ amin if len(self.__quant_adjustments['bounds'][0]) == 0 else self.__quant_adjustments['bounds'][0][0],
|
|
212
|
+
amax if len(self.__quant_adjustments['bounds'][1]) == 0 else self.__quant_adjustments['bounds'][1][0] ]
|
|
213
|
+
umin = min(rg) ## user specified min
|
|
214
|
+
umax = max(rg) ## user specified max
|
|
215
|
+
if umin > amin:
|
|
216
|
+
## elements that are masked to the minumum color for the image
|
|
217
|
+
exclude_below = image_plane < umin
|
|
218
|
+
if umax < amax:
|
|
219
|
+
## elements that are masked to the maximum color for the image
|
|
220
|
+
exclude_above = image_plane > umax
|
|
221
|
+
|
|
222
|
+
### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
223
|
+
### Set up access masks
|
|
224
|
+
### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
225
|
+
if exclude_below is not None and exclude_above is not None:
|
|
226
|
+
included = np.logical_not( np.logical_or( exclude_below, exclude_above ) )
|
|
227
|
+
elif exclude_below is not None:
|
|
228
|
+
included = np.logical_not( exclude_below )
|
|
229
|
+
elif exclude_above is not None:
|
|
230
|
+
included = np.logical_not( exclude_above )
|
|
231
|
+
|
|
232
|
+
### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
233
|
+
### Apply the scaling function to the included pixels
|
|
234
|
+
### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
235
|
+
selected_scaling = self.__quant_adjustments['transfer']['scaling']
|
|
236
|
+
if selected_scaling != 'linear':
|
|
237
|
+
if selected_scaling not in self.__quant_scaling:
|
|
238
|
+
print( f'''error: ${selected_scaling} is not a known scaling...''', file=sys.stderr )
|
|
239
|
+
result = image_plane
|
|
240
|
+
else:
|
|
241
|
+
normalize = 0 if umin > 0 else -umin
|
|
242
|
+
result = np.ma.zeros(image_plane.shape,image_plane.dtype)
|
|
243
|
+
result[included] = self.__quant_scaling[selected_scaling]( image_plane[included]+normalize if included is not None else image_plane+normalize,
|
|
244
|
+
**self.__quant_adjustments['transfer']['args'] )
|
|
245
|
+
if exclude_below is not None:
|
|
246
|
+
result[exclude_below] = result[included].min( )
|
|
247
|
+
if exclude_above is not None:
|
|
248
|
+
result[exclude_above] = result[included].max( )
|
|
249
|
+
else:
|
|
250
|
+
result = image_plane
|
|
251
|
+
|
|
252
|
+
### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
253
|
+
### Histogram of the scaled
|
|
254
|
+
### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
|
|
255
|
+
edges = np.histogram_bin_edges( result, bins=254, range=( umin, umax ) )
|
|
256
|
+
|
|
257
|
+
return np.digitize( result, edges, right=True ).astype(nptype)
|
|
258
|
+
|
|
259
|
+
if self.__img is None:
|
|
260
|
+
raise RuntimeError('no image is available')
|
|
261
|
+
if np.issubdtype( pixel_type, np.integer ):
|
|
262
|
+
return quantize( pixel_type,
|
|
263
|
+
np.squeeze( self.__get_chan(index) ) ).transpose( )
|
|
264
|
+
else:
|
|
265
|
+
return np.squeeze( self.__get_chan(index) ).astype(pixel_type).transpose( )
|
|
266
|
+
|
|
267
|
+
def have_mask0( self ):
|
|
268
|
+
"""Check to see if the synthesis imaging 'mask0' mask exists
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
bool:
|
|
273
|
+
''True'' if the cube contains an internal ''mask0'' mask otherwise ''False''
|
|
274
|
+
"""
|
|
275
|
+
if self.__img is None:
|
|
276
|
+
raise RuntimeError('no image is available')
|
|
277
|
+
return 'mask0' in self.__img.maskhandler('get')
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def mask0( self, index ):
|
|
281
|
+
"""Within the image, there can be an arbitrary number of INTERNAL masks. They can
|
|
282
|
+
have arbitrary names. The synthesis imaging module uses a mask named 'mask0'. This
|
|
283
|
+
mask is used in processing (it MAY represent the beam).
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
index: [ int, int ]
|
|
288
|
+
list containing first the ''stokes'' index and second the ''channel'' index
|
|
289
|
+
"""
|
|
290
|
+
### ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
|
|
291
|
+
### tclean does not maintain mask0. Instead, calls to tclean can result in the
|
|
292
|
+
### internal, mask0 being lost. Because of this, once a good copy of this internal
|
|
293
|
+
### mask is retrieved it is reused. Urvashi says that reusing one copy throughout
|
|
294
|
+
### should be fine (Fri Mar 31 13:54:17 EDT 2023)
|
|
295
|
+
### ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
|
|
296
|
+
if self.__img is None:
|
|
297
|
+
raise RuntimeError('no image is available')
|
|
298
|
+
if self.__mask0_cache is None and self.have_mask0( ):
|
|
299
|
+
self.__mask0_cache = self.__img.getregion(getmask=True)
|
|
300
|
+
if self.__mask0_cache is not None:
|
|
301
|
+
return self.__mask0_cache[:,:,index[0],index[1]]
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
def have_mask( self ):
|
|
305
|
+
"""Check to see if a mask exists.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
bool:
|
|
310
|
+
''True'' if a mask cube is available otherwise ''False''
|
|
311
|
+
"""
|
|
312
|
+
return self.__msk is not None
|
|
313
|
+
|
|
314
|
+
def mask( self, index, modify=False ):
|
|
315
|
+
"""Retrieve one channel mask from the mask cube. The `index` should be a
|
|
316
|
+
two element list of integers. The first integer is the ''stokes'' axis
|
|
317
|
+
in the image cube. The second integer is the ''channel'' axis in the
|
|
318
|
+
image cube.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
index: [ int, int ]
|
|
323
|
+
list containing first the ''stokes'' index and second the ''channel'' index
|
|
324
|
+
modify: boolean
|
|
325
|
+
If true, it implies that the channel mask is being retrieved for modification
|
|
326
|
+
and updating the channel on disk. If false, it implies that the channel mask
|
|
327
|
+
is being retrieved for display.
|
|
328
|
+
"""
|
|
329
|
+
if self.__msk is None:
|
|
330
|
+
raise RuntimeError(f'cannot retrieve mask at {repr(index)} because no mask cube exists')
|
|
331
|
+
return ( np.squeeze( self.__msk.getchunk( blc=[0,0] + index,
|
|
332
|
+
trc=self.__chan_shape + index) ).astype(np.bool_).transpose( )
|
|
333
|
+
if modify == False else
|
|
334
|
+
np.squeeze( self.__msk.getchunk( blc=[0,0] + index,
|
|
335
|
+
trc=self.__chan_shape + index) ) )
|
|
336
|
+
|
|
337
|
+
def mask_value( self, chan, index ):
|
|
338
|
+
if self.__msk is None:
|
|
339
|
+
raise RuntimeError(f'cannot retrieve mask at {repr(index)} because no mask cube exists')
|
|
340
|
+
try:
|
|
341
|
+
pv = self.__msk.pixelvalue( index + chan )
|
|
342
|
+
return int(pv['value']['value'])
|
|
343
|
+
except:
|
|
344
|
+
pass
|
|
345
|
+
return -1
|
|
346
|
+
|
|
347
|
+
def set_mask_name( self, new_mask_path ):
|
|
348
|
+
self.__close_mask( )
|
|
349
|
+
self.__open_mask( new_mask_path )
|
|
350
|
+
|
|
351
|
+
def put_mask( self, index, mask ):
|
|
352
|
+
"""Replace one channel mask with the mask specified as the second parameter.
|
|
353
|
+
The `index` should be a two element list of integers. The first integer is the
|
|
354
|
+
''stokes'' axis in the image cube. The second integer is the ''channel'' axis
|
|
355
|
+
in the image cube. The assumption is that the :code:`mask` parameter was retrieved
|
|
356
|
+
from the mask cube using the :code:`mask(...)` function with the :code:`modify`
|
|
357
|
+
parameter set to :code:`True`.
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
index: [ int, int ]
|
|
362
|
+
list containing first the ''stokes'' index and second the ''channel'' index
|
|
363
|
+
mask: numpy.ndarray
|
|
364
|
+
two dimensional array to replace the existing mask for the channel specified
|
|
365
|
+
by :code:`index`
|
|
366
|
+
"""
|
|
367
|
+
if self.__msk is None:
|
|
368
|
+
raise RuntimeError(f'cannot replace mask at {repr(index)} because no mask cube exists')
|
|
369
|
+
if mask.dtype == bool:
|
|
370
|
+
### cannot put bools with putchunk
|
|
371
|
+
self.__msk.putchunk( blc=[0,0] + index, pixels=mask.astype(np.uint8) )
|
|
372
|
+
else:
|
|
373
|
+
self.__msk.putchunk( blc=[0,0] + index, pixels=mask )
|
|
374
|
+
|
|
375
|
+
def spectrum( self, index, mask=False ):
|
|
376
|
+
"""Retrieve one spectrum from the image cube. The `index` should be a
|
|
377
|
+
three element list of integers. The first integer is the ''right
|
|
378
|
+
ascension'' axis, the second integer is the ''declination'' axis,
|
|
379
|
+
and the third integer is the ''stokes'' axis.
|
|
380
|
+
|
|
381
|
+
Parameters
|
|
382
|
+
----------
|
|
383
|
+
index: [ int, int, int ]
|
|
384
|
+
list containing first the ''right ascension'', the ''declination'' and
|
|
385
|
+
the ''stokes'' axis
|
|
386
|
+
"""
|
|
387
|
+
index = list(map( lambda i: 0 if i is None else i, index ))
|
|
388
|
+
if index[0] >= self.shape[0]:
|
|
389
|
+
index[0] = self.shape[0] - 1
|
|
390
|
+
if index[1] >= self.shape[1]:
|
|
391
|
+
index[1] = self.shape[1] - 1
|
|
392
|
+
if self.__img is None:
|
|
393
|
+
raise RuntimeError('no image is available')
|
|
394
|
+
result_mask = np.squeeze( self.__msk.getchunk( blc=index + [0],
|
|
395
|
+
trc=index + [self.shape[-1]] ) ) if self.__msk and mask else None
|
|
396
|
+
result = np.squeeze( self.__img.getchunk( blc=index + [0],
|
|
397
|
+
trc=index + [self.shape[-1]] ) )
|
|
398
|
+
### should return spectral freq etc.
|
|
399
|
+
### here for X rather than just the index
|
|
400
|
+
try:
|
|
401
|
+
if mask:
|
|
402
|
+
return { 'chan': list(range(len(result))), 'pixel': list(result) }, None if result_mask is None else list(result_mask.astype(bool))
|
|
403
|
+
else:
|
|
404
|
+
return { 'chan': list(range(len(result))), 'pixel': list(result) }
|
|
405
|
+
except Exception as e:
|
|
406
|
+
## In this case, result is not iterable (e.g.) only one channel in the cube.
|
|
407
|
+
## A zero length numpy ndarray has no shape and looks like a float but it is
|
|
408
|
+
## an ndarray.
|
|
409
|
+
if mask:
|
|
410
|
+
return { 'chan': [0], 'pixel': [float(result)] }, None if result_mask is None else [ bool(result_mask) ]
|
|
411
|
+
else:
|
|
412
|
+
return { 'chan': [0], 'pixel': [float(result)] }
|
|
413
|
+
|
|
414
|
+
def histogram_source( self, data ):
|
|
415
|
+
if not self._histogram_source:
|
|
416
|
+
self._histogram_source = ColumnDataSource( data=data )
|
|
417
|
+
return self._histogram_source
|
|
418
|
+
|
|
419
|
+
async def _image_message_handler( self, cmd ):
|
|
420
|
+
if cmd['action'] == 'channel':
|
|
421
|
+
chan = self.channel(cmd['index'],np.uint8)
|
|
422
|
+
mask = { } if self.__msk is None else { 'msk': [ pack_arrays( self.mask(cmd['index']) ) ] }
|
|
423
|
+
_mask0 = self.mask0(cmd['index'])
|
|
424
|
+
mask0 = { } if _mask0 is None else { 'msk0': [ pack_arrays(_mask0) ] }
|
|
425
|
+
histogram = self.histogram( cmd['index'] ) if self._histogram_source else { }
|
|
426
|
+
if self._stats:
|
|
427
|
+
#statistics for the displayed plane of the image cubea
|
|
428
|
+
statistics = self.statistics( cmd['index'] )
|
|
429
|
+
return { 'chan': { 'img': [ pack_arrays(chan) ],
|
|
430
|
+
**mask0,
|
|
431
|
+
**mask },
|
|
432
|
+
'stats': { 'labels': list(statistics.keys( )), 'values': pack_arrays(list(statistics.values( ))) },
|
|
433
|
+
'hist': histogram,
|
|
434
|
+
'id': cmd['id'] }
|
|
435
|
+
else:
|
|
436
|
+
return { 'chan': { 'img': [ pack_arrays(chan) ],
|
|
437
|
+
**mask0,
|
|
438
|
+
**mask },
|
|
439
|
+
'hist': histogram,
|
|
440
|
+
'id': cmd['id'] }
|
|
441
|
+
|
|
442
|
+
elif cmd['action'] == 'spectrum':
|
|
443
|
+
return { 'spectrum': pack_arrays( self.spectrum(cmd['index']) ), 'id': cmd['id'] }
|
|
444
|
+
elif cmd['action'] == 'adjust-colormap':
|
|
445
|
+
if cmd['bounds'] == "reset":
|
|
446
|
+
self.__quant_adjustments = { 'bounds': [ [ ], [ ] ],
|
|
447
|
+
'transfer': {'scaling': 'linear'} }
|
|
448
|
+
else:
|
|
449
|
+
### later a function should be provided for setting the quantization transfer function
|
|
450
|
+
self.__quant_adjustments = { 'bounds': cmd['bounds'], 'transfer': cmd['transfer'] }
|
|
451
|
+
### ensure that the cached channel is not used...
|
|
452
|
+
self.__cached_chan = None
|
|
453
|
+
return { 'result': 'OK', 'id': cmd['id'] }
|
|
454
|
+
|
|
455
|
+
def __init__( self, image, *args, mask=None, stats=False, **kwargs ):
|
|
456
|
+
super( ).__init__( *args, **kwargs, )
|
|
457
|
+
|
|
458
|
+
self.dataid = str(uuid4( ))
|
|
459
|
+
|
|
460
|
+
if ct is None:
|
|
461
|
+
raise RuntimeError('cannot open an image because casatools is not available')
|
|
462
|
+
|
|
463
|
+
self.__img = None
|
|
464
|
+
self.__msk = None
|
|
465
|
+
self.__fits_header = None
|
|
466
|
+
self.__fits_header_str = ''
|
|
467
|
+
resource_manager( ).reg_at_exit( self, '__del__' )
|
|
468
|
+
self._stats = stats
|
|
469
|
+
self.__open_image( image )
|
|
470
|
+
self.__open_mask( mask )
|
|
471
|
+
self.__mask0_cache = None
|
|
472
|
+
self.shape = list(self.__img.shape( ))
|
|
473
|
+
if not self.fits_header_json:
|
|
474
|
+
self.__fits_header = self.__img.fitsheader(exclude="HISTORY")
|
|
475
|
+
self.__fits_header_str = self.__img.fitsheader(retstr=True,exclude="HISTORY")
|
|
476
|
+
if self.__fits_header:
|
|
477
|
+
self.fits_header_json = json.dumps(strip_arrays(self.__fits_header))
|
|
478
|
+
self.__session = None
|
|
479
|
+
self.__stokes_labels = None
|
|
480
|
+
self.__mask_statistics = False
|
|
481
|
+
|
|
482
|
+
###
|
|
483
|
+
### the last channel retrieved is kept around for pixel retrieval
|
|
484
|
+
###
|
|
485
|
+
self.__cached_chan = None
|
|
486
|
+
self.__cached_chan_index = None
|
|
487
|
+
self.__cached_chan_ctime = 0
|
|
488
|
+
|
|
489
|
+
###
|
|
490
|
+
### quantization controls to affect how pseudo colors are displayed
|
|
491
|
+
###
|
|
492
|
+
self.__quant_adjustments = { 'bounds': [ [ ], [ ] ],
|
|
493
|
+
'transfer': {'scaling': 'linear'} }
|
|
494
|
+
self.__quant_scaling = { 'log': lambda chan,alpha: np.ma.log(alpha * chan + 1.0) / np.ma.log(alpha + 1.0),
|
|
495
|
+
'sqrt': lambda chan: np.ma.sqrt(chan),
|
|
496
|
+
'square': lambda chan: np.square(chan),
|
|
497
|
+
'gamma': lambda chan,gamma: np.ma.power(chan,gamma),
|
|
498
|
+
'power': lambda chan,alpha: (np.ma.power(alpha,chan) - 1.0) / alpha }
|
|
499
|
+
|
|
500
|
+
super( ).register( self.dataid, self._image_message_handler )
|
|
501
|
+
|
|
502
|
+
def __del__(self):
|
|
503
|
+
if self.__rgn:
|
|
504
|
+
self.__rgn.done( )
|
|
505
|
+
if self.__img != None:
|
|
506
|
+
self.__img.close()
|
|
507
|
+
self.__img.done()
|
|
508
|
+
self.__img = None
|
|
509
|
+
self.__stokes_labels = None
|
|
510
|
+
|
|
511
|
+
def fits_header( self ):
|
|
512
|
+
return ( self.__fits_header, self.__fits_header_str )
|
|
513
|
+
|
|
514
|
+
def coorddesc( self ):
|
|
515
|
+
ia = imagetool( )
|
|
516
|
+
ia.open(self.__image_path)
|
|
517
|
+
csys = ia.coordsys( )
|
|
518
|
+
ia.close( )
|
|
519
|
+
return { 'csys': csys, 'shape': tuple(self.shape) }
|
|
520
|
+
|
|
521
|
+
def statistics_config( self, use_mask=None ):
|
|
522
|
+
'''Configure the behavior of the statistics function.
|
|
523
|
+
use_mask indicates that if a mask is available, the statistics should
|
|
524
|
+
be based upon the portion of the image included in the mask instead of
|
|
525
|
+
the whole channel.'''
|
|
526
|
+
if self.__mask_path and use_mask is not None:
|
|
527
|
+
self.__mask_statistics = bool(use_mask)
|
|
528
|
+
|
|
529
|
+
def statistics( self, index ):
|
|
530
|
+
"""Retrieve statistics for one channel from the image cube. The `index`
|
|
531
|
+
should be a two element list of integers. The first integer is the
|
|
532
|
+
''stokes'' axis in the image cube. The second integer is the ''channel''
|
|
533
|
+
axis in the image cube.
|
|
534
|
+
|
|
535
|
+
Parameters
|
|
536
|
+
----------
|
|
537
|
+
index: [ int, int ]
|
|
538
|
+
list containing first the ''stokes'' index and second the ''channel'' index
|
|
539
|
+
"""
|
|
540
|
+
def singleton( potential_nonlist ):
|
|
541
|
+
# convert a list of a single element to the element
|
|
542
|
+
return potential_nonlist if len(potential_nonlist) != 1 else potential_nonlist[0]
|
|
543
|
+
def sort_result( unsorted_dictionary ):
|
|
544
|
+
part = partition( lambda s: (s.startswith('trc') or s.startswith('blc')), sorted(unsorted_dictionary.keys( )) )
|
|
545
|
+
return { k: unsorted_dictionary[k] for k in part[1] + part[0] }
|
|
546
|
+
|
|
547
|
+
reg = self.__rgn.box( [0,0] + index, self.__chan_shape + index )
|
|
548
|
+
###
|
|
549
|
+
### This seems like it should work:
|
|
550
|
+
###
|
|
551
|
+
# rawstats = self.__img.statistics( region=reg )
|
|
552
|
+
###
|
|
553
|
+
### but it does not so we have to create a one-use image tool (see CAS-13625)
|
|
554
|
+
###
|
|
555
|
+
ia = imagetool( )
|
|
556
|
+
ia.open(self.__image_path)
|
|
557
|
+
if self.__mask_statistics:
|
|
558
|
+
### mask is an LEL expression and quotes prevents a name containing
|
|
559
|
+
### numbers from being interpreted as an expression
|
|
560
|
+
rawstats = ia.statistics( region=reg, mask=f'''"{self.__mask_path}"''' )
|
|
561
|
+
else:
|
|
562
|
+
rawstats = ia.statistics( region=reg )
|
|
563
|
+
ia.close( )
|
|
564
|
+
return sort_result( { k: singleton([ x.item( ) for x in v ]) if isinstance(v,np.ndarray) else v for k,v in rawstats.items( ) } )
|
|
565
|
+
|
|
566
|
+
def histogram( self, index ):
|
|
567
|
+
"""Calculate histogram (Bokeh Quad) extents for update of colormap adjuster (or anything
|
|
568
|
+
else that wants a histogram of image intensities.
|
|
569
|
+
|
|
570
|
+
Parameters
|
|
571
|
+
----------
|
|
572
|
+
index: [ int, int ]
|
|
573
|
+
list containing first the ''stokes'' index and second the ''channel'' index
|
|
574
|
+
"""
|
|
575
|
+
if not self._histogram_source:
|
|
576
|
+
return { }
|
|
577
|
+
|
|
578
|
+
chan = self.__get_chan(index)
|
|
579
|
+
bins = np.linspace( chan.min( ), chan.max( ), len(self._histogram_source.data['top'])+1 )
|
|
580
|
+
hist, edges = np.histogram( chan, density=False, bins=bins )
|
|
581
|
+
return dict( left=list(edges[:-1]), right=list(edges[1:]), top=list(hist), bottom=[0]*len(hist) )
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
########################################################################
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2021,2022
|
|
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
|
+
'''Data source for plotting spectra from a CASA image.'''
|
|
29
|
+
|
|
30
|
+
from bokeh.plotting import ColumnDataSource
|
|
31
|
+
from bokeh.util.compiler import TypeScript
|
|
32
|
+
from bokeh.core.properties import Instance
|
|
33
|
+
from . import ImagePipe
|
|
34
|
+
from ..state import casalib_url, cubevisjs_url
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SpectraDataSource(ColumnDataSource):
|
|
38
|
+
"""Implementation of a ``ColumnDataSource`` customized for spectral lines
|
|
39
|
+
from `CASA`/`CNGI` image cubes. This is designed to use an `ImagePipe` to
|
|
40
|
+
update the spectral line plot displayed in a browser, app or notebook with
|
|
41
|
+
`bokeh`.
|
|
42
|
+
|
|
43
|
+
Attributes
|
|
44
|
+
----------
|
|
45
|
+
image_source: ImagePipe
|
|
46
|
+
the conduit for updating the spectral line data
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
image_source = Instance(ImagePipe)
|
|
50
|
+
|
|
51
|
+
__javascript__ = [ casalib_url( ), cubevisjs_url( ) ]
|
|
52
|
+
|
|
53
|
+
def __init__( self, *args, **kwargs ):
|
|
54
|
+
super( ).__init__( *args, **kwargs )
|
|
55
|
+
self.data = self.image_source.spectra( [ 0, 0, 0 ] )
|