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,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 ] )