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,798 @@
1
+ ########################################################################3
2
+ # _gclean.py
3
+ #
4
+ # Copyright (C) 2021,2022,2023
5
+ # Associated Universities, Inc. Washington DC, USA.
6
+ #
7
+ # This script is free software; you can redistribute it and/or modify it
8
+ # under the terms of the GNU Library General Public License as published by
9
+ # the Free Software Foundation; either version 2 of the License, or (at your
10
+ # option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful, but WITHOUT
13
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
15
+ # License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Library General Public License
18
+ # along with this library; if not, write to the Free Software Foundation,
19
+ # Inc., 675 Massachusetts Ave, Cambridge, MA 02139, USA.
20
+ #
21
+ # Correspondence concerning AIPS++ should be adressed as follows:
22
+ # Internet email: casa-feedback@nrao.edu.
23
+ # Postal address: AIPS++ Project Office
24
+ # National Radio Astronomy Observatory
25
+ # 520 Edgemont Road
26
+ # Charlottesville, VA 22903-2475 USA
27
+ #
28
+ import os
29
+ import asyncio
30
+ from functools import reduce
31
+ import copy
32
+ import numpy as np
33
+ import shutil
34
+ import time
35
+ import subprocess
36
+
37
+ from casatasks.private.imagerhelpers.imager_return_dict import ImagingDict
38
+ from casatasks import deconvolve, tclean, imstat
39
+ from casatasks import casalog
40
+
41
+ ###
42
+ ### import check versions
43
+ ###
44
+ _GCV001 = True
45
+ _GCV002 = True
46
+ _GCV003 = True
47
+ _GCV004 = True
48
+
49
+
50
+ # from casatasks.private.imagerhelpers._gclean import gclean
51
+ class gclean:
52
+ '''gclean(...) creates a stream of convergence records which indicate
53
+ the convergence quaility of the tclean process. The initial record
54
+ describes the initial dirty image.
55
+ It is designed for use with the interactive clean GUI, but it could
56
+ be used independently. It can be used as a regular generator:
57
+ for rec in gclean( vis='refim_point_withline.ms', imagename='test', imsize=512, cell='12.0arcsec',
58
+ specmode='cube', interpolation='nearest', nchan=5, start='1.0GHz', width='0.2GHz',
59
+ pblimit=-1e-05, deconvolver='hogbom', niter=500, cyclefactor=3, scales=[0, 3, 10] ):
60
+ # use rec to decide when to stop, for example to check stopcode or peak residual:
61
+ # if (rec[0] > 1) or (min(rec[1][0][0]['peakRes']) < 0.001):
62
+ # break
63
+ print(rec)
64
+ or as an async generator:
65
+ async for rec in gclean( vis='refim_point_withline.ms', imagename='test', imsize=512, cell='12.0arcsec',
66
+ specmode='cube', interpolation='nearest', nchan=5, start='1.0GHz', width='0.2GHz',
67
+ pblimit=-1e-05, deconvolver='hogbom', niter=500, cyclefactor=3, scales=[0, 3, 10] ):
68
+ # use rec to decide when to stop
69
+ print(rec)
70
+
71
+
72
+ See also: __next__(...) for a description of the returned rec
73
+
74
+ TODO: do we need to preserve any hidden state between tclean calls for the iterbotsink and/or synthesisimager tools?
75
+ '''
76
+
77
+ def _tclean( self, *args, **kwargs ):
78
+ """ Calls tclean records the arguments in the local history of tclean calls.
79
+
80
+ The full tclean history for this instance can be retrieved via the cmds() method."""
81
+ arg_s = ', '.join( map( lambda a: self._history_filter(len(self._exe_cmds), None, repr(a)), args ) )
82
+ kw_s = ', '.join( map( lambda kv: self._history_filter(len(self._exe_cmds), kv[0], "%s=%s" % (kv[0],repr(kv[1]))), kwargs.items()) )
83
+ if len(arg_s) > 0 and len(kw_s) > 0:
84
+ parameters = arg_s + ", " + kw_s
85
+ else:
86
+ parameters = arg_s + kw_s
87
+ self._exe_cmds.append( "tclean( %s )" % parameters )
88
+ self._exe_cmds_per_iter[-1] += 1
89
+ return tclean( *args, **kwargs )
90
+
91
+ def _deconvolve( self, *args, **kwargs ):
92
+ """ Calls deconvolve records the arguments in the local history of deconvolve calls.
93
+
94
+ The full deconvolve history for this instance can be retrieved via the cmds() method."""
95
+ arg_s = ', '.join( map( lambda a: self._history_filter(len(self._exe_cmds), None, repr(a)), args ) )
96
+ kw_s = ', '.join( map( lambda kv: self._history_filter(len(self._exe_cmds), kv[0], "%s=%s" % (kv[0],repr(kv[1]))), kwargs.items()) )
97
+ if len(arg_s) > 0 and len(kw_s) > 0:
98
+ parameters = arg_s + ", " + kw_s
99
+ else:
100
+ parameters = arg_s + kw_s
101
+ self._exe_cmds.append( "deconvolve( %s )" % parameters )
102
+ self._exe_cmds_per_iter[-1] += 1
103
+ return deconvolve( *args, **kwargs )
104
+
105
+ def _remove_tree( self, directory ):
106
+ if os.path.isdir(directory):
107
+ shutil.rmtree(directory)
108
+ self._exe_cmds.append( f'''shutil.rmtree( {repr(directory)} )''' )
109
+ self._exe_cmds_per_iter[-1] += 1
110
+
111
+ def cmds( self, history=False ):
112
+ """ Returns the history of all tclean calls for this instance. If ``history``
113
+ is set to True then the full history will be returned, otherwise the commands
114
+ executed for generating the latest result are returned.
115
+ """
116
+
117
+ if history:
118
+ return self._exe_cmds
119
+ else:
120
+ if self._exe_cmds_per_iter[-1] > 0:
121
+ # Return the last N commands
122
+ return self._exe_cmds[-self._exe_cmds_per_iter[-1]:]
123
+ else:
124
+ # If convergence is hit, no commands were run so return nothing
125
+ return []
126
+
127
+
128
+ def update( self, msg ):
129
+ """ Interactive clean parameters update.
130
+
131
+ Args:
132
+ msg: dict with possible keys 'niter', 'cycleniter', 'nmajor', 'threshold', 'cyclefactor'
133
+
134
+ Returns:
135
+ stopcode : Stop code in case of error (-1 on error, 0 if no error), int
136
+ stopdesc : Exception error message, str
137
+ """
138
+ if 'niter' in msg:
139
+ try:
140
+ self._niter = int(msg['niter'])
141
+ if self._niter < -1:
142
+ return -1, f"niter must be >= -1"
143
+ except ValueError as err:
144
+ return -1, "niter must be an integer"
145
+
146
+ if 'cycleniter' in msg:
147
+ try:
148
+ self._cycleniter = int(msg['cycleniter'])
149
+ if self._cycleniter < -1:
150
+ return -1, f"cycleniter must be >= -1"
151
+ except ValueError:
152
+ return -1, "cycleniter must be an integer"
153
+
154
+ if 'nmajor' in msg:
155
+ try:
156
+ self._nmajor = int(msg['nmajor'])
157
+ if self._nmajor < -1:
158
+ return -1, f"nmajor must be >= -1"
159
+ except ValueError as e:
160
+ return -1, "nmajor must be an integer"
161
+
162
+ if 'threshold' in msg:
163
+ try:
164
+ self._threshold = float(msg['threshold'])
165
+ if self._threshold < 0:
166
+ return -1, f"threshold must be >= 0"
167
+ except ValueError:
168
+ if isinstance(msg['threshold'], str) and "jy" in msg['threshold'].lower():
169
+ self._threshold_to_float(msg['threshold']) # Convert str to float
170
+ else:
171
+ return -1, f"threshold must be a number, or a number with units (Jy/mJy/uJy)"
172
+
173
+
174
+ if 'nsigma' in msg:
175
+ try:
176
+ self._nsigma = float(msg['nsigma'])
177
+ if self._nsigma < 0:
178
+ return -1, f"nsigma must be >= 0"
179
+ except ValueError:
180
+ return -1, "nsigma must be a number"
181
+
182
+
183
+ if 'gain' in msg:
184
+ try:
185
+ self._gain = float(msg['gain'])
186
+ if self._gain <= 0:
187
+ return -1, f"gain must be > 0"
188
+ except ValueError:
189
+ return -1, "gain must be a number"
190
+
191
+
192
+ if 'cyclefactor' in msg:
193
+ try:
194
+ self._cyclefactor = float(msg['cyclefactor'])
195
+ if self._cyclefactor <= 0:
196
+ return -1, f"cyclefactor must be > 0"
197
+ except ValueError:
198
+ return -1, "cyclefactor must be a number"
199
+
200
+ ### Automasking parameters
201
+
202
+ if 'dogrowprune' in msg:
203
+ try:
204
+ self._dogrowprune = bool(msg['dogrowprune'])
205
+ except ValueError:
206
+ return -1, "dogrowprune must be a boolean"
207
+
208
+ if 'noisethreshold' in msg:
209
+ try:
210
+ self._noisethreshold = float(msg['noisethreshold'])
211
+ if self._noisethreshold < 0:
212
+ return -1, f"noisethreshold must be >= 0"
213
+ except ValueError:
214
+ return -1, "noisethreshold must be a number"
215
+
216
+ if 'sidelobethreshold' in msg:
217
+ try:
218
+ self._sidelobethreshold = float(msg['sidelobethreshold'])
219
+ if self._sidelobethreshold < 0:
220
+ return -1, f"sidelobethreshold must be >= 0"
221
+ except ValueError:
222
+ return -1, "sidelobethreshold must be a number"
223
+
224
+ if 'lownoisethreshold' in msg:
225
+ try:
226
+ self._lownoisethreshold = float(msg['lownoisethreshold'])
227
+ if self._lownoisethreshold < 0:
228
+ return -1, f"lownoisethreshold must be >= 0"
229
+ except ValueError:
230
+ return -1, "lownoisethreshold must be a number"
231
+
232
+ if 'minbeamfrac' in msg:
233
+ try:
234
+ self._minbeamfrac = float(msg['minbeamfrac'])
235
+ if self._minbeamfrac < 0:
236
+ return -1, f"minbeamfrac must be >= 0"
237
+ except ValueError:
238
+ return -1, "minbeamfrac must be a number"
239
+
240
+ if 'negativethreshold' in msg:
241
+ try:
242
+ self._negativethreshold = float(msg['negativethreshold'])
243
+ if self._negativethreshold < 0:
244
+ return -1, f"negativethreshold must be >= 0"
245
+ except ValueError:
246
+ return -1, "negativethreshold must be a number"
247
+
248
+
249
+
250
+ return 0, ""
251
+
252
+
253
+
254
+ def _threshold_to_float(self, msg=None):
255
+ # Convert threshold from string to float if necessary
256
+ if msg is not None:
257
+ if isinstance(msg, str):
258
+ if "mJy" in msg:
259
+ self._threshold = float(msg.replace("mJy", "")) / 1e3
260
+ elif "uJy" in msg:
261
+ self._threshold = float(msg.replace("uJy", "")) / 1e6
262
+ elif "Jy" in msg:
263
+ self._threshold = float(msg.replace("Jy", ""))
264
+ else:
265
+ if isinstance(self._threshold, str):
266
+ if "mJy" in self._threshold:
267
+ self._threshold = float(self._threshold.replace("mJy", "")) / 1e3
268
+ elif "uJy" in self._threshold:
269
+ self._threshold = float(self._threshold.replace("uJy", "")) / 1e6
270
+ elif "Jy" in self._threshold:
271
+ self._threshold = float(self._threshold.replace("Jy", ""))
272
+
273
+
274
+ def __init__(self, vis, imagename, selectdata=True, field='', spw='', timerange='', uvrange='', antenna='', scan='', observation='', intent='', datacolumn='corrected', imsize=[100], cell=[ ],
275
+ phasecenter='', projection='SIN', stokes='I', startmodel='', specmode='cube', reffreq='', nchan=-1, start='', width='', outframe='LSRK', veltype='radio', restfreq='',
276
+ interpolation='linear', perchanweightdensity=True, gridder='standard', facets=int(1), psfphasecenter='', wprojplanes=int(1), mosweight=True, aterm=True, psterm=False, wbawp=True,
277
+ conjbeams=False, usepointing=False, cfcache = '', pointingoffsetsigdev=[ ], vptable='', computepastep=float(360.0), rotatepastep=float(360.0), pblimit=0.2, normtype='flatnoise',
278
+ deconvolver='hogbom', smallscalebias=0.0, fusedthreshold=0, largestscale=-1, niter=0, threshold='0.1Jy', nsigma=0.0, cycleniter=-1, nmajor=1, cyclefactor=1.0, minpsffraction=0.05,
279
+ maxpsffraction=0.8, scales=[], restoringbeam='', pbcor=False, outlierfile='', nterms=int(2), weighting='natural', robust=float(0.5), noise='0.0Jy', uvtaper=[], npixels=0,
280
+ gain=float(0.1), pbmask=0.2, sidelobethreshold=3.0, noisethreshold=5.0, lownoisethreshold=1.5, negativethreshold=0.0, smoothfactor=float(1.0), minbeamfrac=0.3, cutthreshold=0.01,
281
+ growiterations=75, dogrowprune=True, minpercentchange=-1.0, verbose=False, fastnoise=True, savemodel='none', usemask='user', mask='', restoration=True, restart=True, calcres=True,
282
+ calcpsf=True, psfcutoff=float(0.35), parallel=False, history_filter=lambda index, arg, history_value: history_value ):
283
+
284
+ self._vis = vis
285
+ self._imagename = imagename
286
+ self._selectdata = selectdata
287
+ self._imsize = imsize
288
+ self._cell = cell
289
+ self._phasecenter = phasecenter
290
+ self._projection = projection
291
+ self._stokes = stokes
292
+ self._startmodel = startmodel
293
+ self._specmode = specmode
294
+ self._reffreq = reffreq
295
+ self._nchan = nchan
296
+ self._start = start
297
+ self._width = width
298
+ self._outframe = outframe
299
+ self._veltype = veltype
300
+ self._restfreq = restfreq
301
+ self._interpolation = interpolation
302
+ self._perchanweightdensity = perchanweightdensity
303
+ self._gridder = gridder
304
+ self._facets = facets
305
+ self._psfphasecenter = psfphasecenter
306
+ self._wprojplanes = wprojplanes
307
+ self._mosweight = mosweight
308
+ self._aterm = aterm
309
+ self._psterm = psterm
310
+ self._wbawp = wbawp
311
+ self._conjbeams = conjbeams
312
+ self._usepointing = usepointing
313
+ self._cfcache = cfcache
314
+ self._pointingoffsetsigdev = pointingoffsetsigdev
315
+ self._vpable = vptable
316
+ self._computepastep = computepastep
317
+ self._rotatepastep = rotatepastep
318
+ self._pblimit = pblimit
319
+ self._normtype = normtype
320
+ self._deconvolver = deconvolver
321
+ self._smallscalebias = smallscalebias
322
+ self._fusedthreshold = fusedthreshold
323
+ self._largestscale = largestscale
324
+ self._niter = niter
325
+ self._threshold = threshold
326
+ self._cycleniter = cycleniter
327
+ self._minpsffraction = minpsffraction
328
+ self._maxpsffraction = maxpsffraction
329
+ self._nsigma = nsigma
330
+ self._nmajor = nmajor
331
+ self._cyclefactor = cyclefactor
332
+ self._scales = scales
333
+ self._restoringbeam = restoringbeam
334
+ self._pbcor = pbcor
335
+ #self._outlierfile = outlierfile
336
+ self._nterms = nterms
337
+ self._exe_cmds = [ ]
338
+ self._exe_cmds_per_iter = [ ]
339
+ self._history_filter = history_filter
340
+ self._finalized = False
341
+ self._field = field
342
+ self._spw = spw
343
+ self._timerange = timerange
344
+ self._uvrange = uvrange
345
+ self._antenna = antenna
346
+ self._scan = scan
347
+ self._observation = observation
348
+ self._intent = intent
349
+ self._datacolumn = datacolumn
350
+ self._weighting = weighting
351
+ self._robust = robust
352
+ self._noise = noise
353
+ self._uvtaper = uvtaper
354
+ self._npixels = npixels
355
+ self._gain = gain
356
+ self._pbmask = pbmask
357
+ self._sidelobethreshold = sidelobethreshold
358
+ self._noisethreshold = noisethreshold
359
+ self._lownoisethreshold = lownoisethreshold
360
+ self._negativethreshold = negativethreshold
361
+ self._smoothfactor = smoothfactor
362
+ self._minbeamfrac = minbeamfrac
363
+ self._cutthreshold = cutthreshold
364
+ self._growiterations = growiterations
365
+ self._dogrowprune = dogrowprune
366
+ self._minpercentchange = minpercentchange
367
+ self._verbose = verbose
368
+ self._fastnoise = fastnoise
369
+ self._savemodel = savemodel
370
+ self._parallel = parallel
371
+ self._usemask = usemask
372
+ self._mask = mask
373
+ self._restoration = restoration
374
+ self._restart = restart
375
+ self._calcres = calcres
376
+ self._calcpsf = calcpsf
377
+ self._psfcutoff = psfcutoff
378
+ self.global_imdict = ImagingDict()
379
+ self.current_imdict = ImagingDict()
380
+ self._major_done = 0
381
+ self.hasit = False # Convergence flag
382
+ self._has_restored = False
383
+ self.stopdescription = '' # Convergence flag
384
+ self._initial_mask_exists = False
385
+ self._convergence_result = (None,None,None,None,None,{ 'chan': None, 'major': None })
386
+ # ^^^^ ^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^----->>> convergence info
387
+ # | | | | +----->>> Number of global iterations remaining for current run (niterleft)
388
+ # | | | +---------->>> Number of major cycles remaining for current run (nmajorleft)
389
+ # | | +---------------->>> major cycles done for current run (nmajordone)
390
+ # | +------------------>>> tclean stopcode
391
+ # +----------------------->>> tclean stopdescription
392
+
393
+ # Convert threshold from string to float, interpreting units.
394
+ # XXX : We should ideally use quantities, but we are trying to
395
+ # stick to "public API" funtions inside _gclean
396
+ self._threshold_to_float()
397
+
398
+
399
+ def __add_per_major_items( self, tclean_ret, major_ret, chan_ret ):
400
+ '''Add meta-data about the whole major cycle, including 'cyclethreshold'
401
+ '''
402
+
403
+ if 'cyclethreshold' in tclean_ret:
404
+
405
+ rdict = dict( major=dict( cyclethreshold=[tclean_ret['cyclethreshold']] if major_ret is None else (major_ret['cyclethreshold'] + [tclean_ret['cyclethreshold']]) ),
406
+ chan=chan_ret )
407
+ else:
408
+ rdict = dict( major=dict( cyclethreshold=major_ret['cyclethreshold'].append(tclean_ret['cyclethreshold']) ),
409
+ chan=chan_ret )
410
+
411
+ return rdict
412
+
413
+
414
+ def _calc_deconv_controls(self, imdict, niterleft=0, threshold=0, cycleniter=-1):
415
+ """
416
+ Calculate cycleniter and cyclethreshold for deconvolution.
417
+ """
418
+
419
+ use_cycleniter = niterleft #niter - imdict.returndict['iterdone']
420
+
421
+ if cycleniter > -1 : # User is forcing this number
422
+ use_cycleniter = min(cycleniter, use_cycleniter)
423
+
424
+ psffrac = imdict.returndict['maxpsfsidelobe'] * self._cyclefactor
425
+ psffrac = max(psffrac, self._minpsffraction)
426
+ psffrac = min(psffrac, self._maxpsffraction)
427
+
428
+ # TODO : This assumes the default field (i.e., field=0);
429
+ # This won't work for multiple fields.
430
+ cyclethreshold = psffrac * imdict.get_peakres()
431
+ cyclethreshold = max(cyclethreshold, threshold)
432
+
433
+ return int(use_cycleniter), cyclethreshold
434
+
435
+
436
+ def __update_convergence(self):
437
+ """
438
+ Accumulates the per-channel/stokes summaryminor keys across all major cycle calls so far.
439
+
440
+ The "iterDone" key will be replaced with "iterations", and for the "iterations" key,
441
+ the value in the returned cummulative record will be a rolling sum of iterations done
442
+ for tclean calls so far, one value per minor cycle.
443
+ For example, if there have been two clean calls, and in the first call channel 0 had
444
+ [1] iteration in 1 minor cycle, and for the second call channel 0 had [6, 10, 9, 1]
445
+ iterations in 4 minor cycles), then the resultant "iterations" key for channel 0 would be:
446
+ [1, 7, 17, 26, 27]
447
+ """
448
+
449
+ keys = ['modelFlux', 'iterDone', 'peakRes', 'stopCode', 'cycleThresh']
450
+
451
+ # Grab tuples of keys of interest
452
+ outrec = {}
453
+ for nn in range(self.global_imdict.nchan):
454
+ outrec[nn] = {}
455
+ for ss in range(self.global_imdict.nstokes):
456
+ outrec[nn][ss] = {}
457
+ for key in keys:
458
+ # Replace iterDone with iterations
459
+ if key == 'iterDone':
460
+ # Maintain cumulative sum of iterations per entry
461
+ outrec[nn][ss]['iterations'] = np.cumsum(self.global_imdict.get_key(key, stokes=ss, chan=nn))
462
+ # Replace iterDone with iterations
463
+ #outrec[nn][ss]['iterations'] = self.global_imdict.get_key(key, stokes=ss, chan=nn)
464
+ else:
465
+ outrec[nn][ss][key] = self.global_imdict.get_key(key, stokes=ss, chan=nn)
466
+
467
+ return outrec
468
+
469
+
470
+ def _check_initial_mask(self):
471
+ """
472
+ Check if a mask from a previous run exists on disk or not.
473
+ """
474
+
475
+ if self._usemask == 'user' and self._mask == '':
476
+ maskname = self._imagename + '.mask'
477
+
478
+ if os.path.exists(maskname):
479
+ self._initial_mask_exists = True
480
+ else:
481
+ self._initial_mask_exists = False
482
+
483
+ def _fix_initial_mask(self):
484
+ """
485
+ If on start up, no user mask is provided, then flip the initial mask to
486
+ be all zeros for interactive use.
487
+ """
488
+
489
+ from casatools import image
490
+ ia = image()
491
+
492
+ if self._usemask == 'user' and self._mask == '':
493
+ maskname = self._imagename + '.mask'
494
+
495
+ # This means the mask was newly created by deconvolve, so flip it
496
+ if os.path.exists(maskname) and self._initial_mask_exists is False:
497
+ ia.open(maskname)
498
+ ia.set(0.0)
499
+ ia.close()
500
+
501
+ def _update_peakres(self):
502
+ if self._deconvolver == 'mtmfs':
503
+ residname = self._imagename + '.residual.tt0'
504
+ else:
505
+ residname = self._imagename + '.residual'
506
+
507
+ maskname = self._imagename + '.mask'
508
+ if not os.path.exists(maskname):
509
+ maskname = ''
510
+
511
+ peakres = imstat(imagename=residname, mask=f'''"{maskname}"''')['max']
512
+ if len(maskname) > 0:
513
+ masksum = imstat(imagename=maskname)['sum']
514
+ else:
515
+ masksum = []
516
+
517
+ if len(peakres) > 0:
518
+ peakres = peakres[0]
519
+ else:
520
+ peakres = None
521
+
522
+ if len(masksum) > 0:
523
+ masksum = masksum[0]
524
+ else:
525
+ masksum = None
526
+
527
+ return peakres, masksum
528
+
529
+ def __next__( self ):
530
+ """ Runs tclean and returns the (stopcode, convergence result) when executed with the python builtin next() function.
531
+
532
+ The returned convergence result is a nested dictionary:
533
+ {
534
+ channel id: {
535
+ stokes id: {
536
+ summary key: [values, one per minor cycle]
537
+ },
538
+ },
539
+ }
540
+
541
+ See also: gclean.__update_convergence(...)
542
+ """
543
+
544
+ tclean_ret = {}
545
+ deconv_ret = {}
546
+
547
+ # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv------------>>> is done to produce an initial dirty image
548
+ if self._niter < 1 and self._convergence_result[2] is not None:
549
+ self._convergence_result = ( f'nothing to run, niter == {self._niter}',
550
+ self._convergence_result[1],
551
+ self._major_done,
552
+ self._nmajor,
553
+ self._niter,
554
+ self._convergence_result[5] )
555
+ return self._convergence_result
556
+ else:
557
+ ### CALL SEQUENCE:
558
+ ### tclean(niter=0),deconvolve(niter=0),tclean(niter=100),deconvolve(niter=0),tclean(niter=100),tclean(niter=0,restoration=True)
559
+ self._exe_cmds_per_iter.append(0)
560
+ try:
561
+ if self._convergence_result[1] is None:
562
+ # initial call to tclean(...) creates the initial dirty image with niter=0
563
+
564
+ # If calcres and calcpsf are False, no need to run initial tclean - assume image products already on disk.
565
+ if not (self._calcres == False and self._calcpsf == False):
566
+ casalog.post('Running initial major cycle to create first residual image.', 'INFO')
567
+ print('Running initial major cycle to create first residual image.')
568
+ tclean_ret = self._tclean( vis=self._vis, mask=self._mask, imagename=self._imagename, imsize=self._imsize, cell=self._cell, selectdata=self._selectdata, phasecenter=self._phasecenter, stokes=self._stokes,
569
+ startmodel=self._startmodel, specmode=self._specmode, projection=self._projection, reffreq=self._reffreq, gridder=self._gridder, wprojplanes=self._wprojplanes, facets=self._facets,
570
+ mosweight=self._mosweight, psfphasecenter = self._psfphasecenter, aterm=self._aterm, psterm=self._psterm, wbawp=self._wbawp, conjbeams=self._conjbeams, cfcache=self._cfcache,
571
+ usepointing=self._usepointing, computepastep=self._computepastep, rotatepastep=self._rotatepastep, normtype=self._normtype, fusedthreshold=self._fusedthreshold,
572
+ largestscale=self._largestscale, noise = self._noise, uvtaper=self._uvtaper, psfcutoff=self._psfcutoff, interpolation=self._interpolation,
573
+ perchanweightdensity=self._perchanweightdensity, nchan=self._nchan, start=self._start, width=self._width, veltype=self._veltype, restfreq=self._restfreq, outframe=self._outframe,
574
+ pointingoffsetsigdev=self._pointingoffsetsigdev, pblimit=self._pblimit, deconvolver=self._deconvolver, smallscalebias=self._smallscalebias, cyclefactor=self._cyclefactor,
575
+ scales=self._scales, restoringbeam=self._restoringbeam, pbcor=self._pbcor, nterms=self._nterms, field=self._field, spw=self._spw, timerange=self._timerange, uvrange=self._uvrange,
576
+ antenna=self._antenna, scan=self._scan, observation=self._observation, intent=self._intent, datacolumn=self._datacolumn, weighting=self._weighting, robust=self._robust,
577
+ npixels=self._npixels, interactive=False, niter=0, gain=self._gain, calcres=self._calcres, calcpsf=self._calcpsf, restoration=False, parallel=self._parallel, fullsummary=True)
578
+ # outlierfile = self._outlierfile,
579
+
580
+ # Check if a mask from a previous run exists on disk
581
+ self._check_initial_mask()
582
+
583
+ deconv_ret = self._deconvolve(imagename=self._imagename, startmodel=self._startmodel,
584
+ deconvolver=self._deconvolver, scales=self._scales, nterms=self._nterms,
585
+ smallscalebias=self._smallscalebias, restoration=False, restoringbeam=self._restoringbeam,
586
+ niter = 0, gain=self._gain, threshold=self._threshold, nsigma=self._nsigma,
587
+ interactive = False, fullsummary=True, fastnoise=self._fastnoise, usemask=self._usemask,
588
+ mask = self._mask, pbmask=self._pbmask, sidelobethreshold=self._sidelobethreshold,
589
+ noisethreshold=self._noisethreshold, lownoisethreshold=self._lownoisethreshold,
590
+ negativethreshold=self._negativethreshold, smoothfactor=self._smoothfactor,
591
+ minbeamfrac=self._minbeamfrac, cutthreshold=self._cutthreshold,
592
+ growiterations=self._growiterations, dogrowprune=self._dogrowprune,
593
+ verbose=self._verbose)
594
+
595
+ # If no mask from a previous run exists, over-write the ones with zeros for the default mask
596
+ self._fix_initial_mask()
597
+
598
+
599
+ if len(tclean_ret) > 0 and len(deconv_ret) > 0:
600
+ self.current_imdict.returndict = self.current_imdict.merge(tclean_ret, deconv_ret)
601
+ elif len(tclean_ret) > 0 and len(deconv_ret) == 0:
602
+ self.current_imdict.returndict = copy.deepcopy(tclean_ret)
603
+ elif len(tclean_ret) == 0 and len(deconv_ret) > 0:
604
+ self.current_imdict.returndict = copy.deepcopy(deconv_ret)
605
+ else: # Both dicts are empty, this should never happen
606
+ raise ValueError("Both tclean and deconvolve return dicts are empty. This should never happen.")
607
+
608
+ self.global_imdict.returndict = self.current_imdict.returndict
609
+
610
+ ## Initial call where niterleft and nmajorleft are same as original input values.
611
+ self.hasit, self.stopdescription = self.global_imdict.has_converged(self._niter, self._threshold, self._nmajor)
612
+
613
+ self.current_imdict.returndict['stopcode'] = self.hasit
614
+ self.current_imdict.returndict['stopDescription'] = self.stopdescription
615
+ self._major_done = 0
616
+ else:
617
+ # Reset convergence every time, since we return control to the GUI after a single major cycle
618
+ self.current_imdict.returndict['iterdone'] = 0.
619
+
620
+ # Mask can be updated here...
621
+ # Check for mask update - peakres + masksum
622
+ _peakres, _masksum = self._update_peakres()
623
+
624
+ self.hasit, self.stopdescription = self.global_imdict.has_converged(self._niter, self._threshold, self._nmajor, masksum=_masksum, peakres=_peakres)
625
+
626
+ #self.global_imdict.returndict['stopcode'] = self.hasit
627
+ #self.global_imdict.returndict['stopDescription'] = self.stopdescription
628
+
629
+ #self.current_imdict.returndict['stopcode'] = self.hasit
630
+ #self.current_imdict.returndict['stopDescription'] = self.stopdescription
631
+
632
+ # Has not, i.e., not converged
633
+ if self.hasit ==0 :
634
+ use_cycleniter, cyclethreshold = self._calc_deconv_controls(self.current_imdict, self._niter, self._threshold, self._cycleniter)
635
+
636
+ # Run the minor cycle
637
+ deconv_ret = self._deconvolve(imagename=self._imagename, startmodel=self._startmodel,
638
+ deconvolver=self._deconvolver, restoration=False,
639
+ threshold=cyclethreshold, niter=use_cycleniter, gain=self._gain, fullsummary=True)
640
+
641
+ # Run the major cycle
642
+ tclean_ret = self._tclean( vis=self._vis, imagename=self._imagename, imsize=self._imsize, cell=self._cell,
643
+ phasecenter=self._phasecenter, stokes=self._stokes, specmode=self._specmode, reffreq=self._reffreq,
644
+ gridder=self._gridder, wprojplanes=self._wprojplanes, mosweight=self._mosweight, psterm=self._psterm,
645
+ wbawp=self._wbawp, conjbeams=self._conjbeams, usepointing=self._usepointing, interpolation=self._interpolation,
646
+ perchanweightdensity=self._perchanweightdensity, nchan=self._nchan, start=self._start,
647
+ width=self._width, veltype=self._veltype, restfreq=self._restfreq, outframe=self._outframe,
648
+ pointingoffsetsigdev=self._pointingoffsetsigdev, pblimit=self._pblimit, deconvolver=self._deconvolver,
649
+ smallscalebias=self._smallscalebias, cyclefactor=self._cyclefactor, scales=self._scales,
650
+ restoringbeam=self._restoringbeam, pbcor=self._pbcor, nterms=self._nterms, field=self._field,
651
+ spw=self._spw, timerange=self._timerange, uvrange=self._uvrange, antenna=self._antenna,
652
+ scan=self._scan, observation=self._observation, intent=self._intent, datacolumn=self._datacolumn,
653
+ weighting=self._weighting, robust=self._robust, npixels=self._npixels, interactive=False,
654
+ niter=0, restart=True, calcpsf=False, calcres=True, restoration=False, threshold=self._threshold,
655
+ nsigma=self._nsigma, cycleniter=self._cycleniter, nmajor=1, gain=self._gain,
656
+ sidelobethreshold=self._sidelobethreshold, noisethreshold=self._noisethreshold,
657
+ lownoisethreshold=self._lownoisethreshold, negativethreshold=self._negativethreshold,
658
+ minbeamfrac=self._minbeamfrac, growiterations=self._growiterations, dogrowprune=self._dogrowprune,
659
+ minpercentchange=self._minpercentchange, fastnoise=self._fastnoise, savemodel=self._savemodel,
660
+ maxpsffraction=self._maxpsffraction,
661
+ minpsffraction=self._minpsffraction, parallel=self._parallel, fullsummary=True )
662
+
663
+ # Replace return dict with new return dict
664
+ # The order of the dicts into merge is important.
665
+ self.current_imdict.returndict = self.current_imdict.merge(tclean_ret, deconv_ret)
666
+
667
+ # Append new return dict to global return dict
668
+ self.global_imdict.returndict = self.global_imdict.concat(self.global_imdict.returndict, self.current_imdict.returndict)
669
+ self._major_done = self.current_imdict.returndict['nmajordone']
670
+
671
+ ## Decrement count for the major cycle just done...
672
+ self.__decrement_counts()
673
+
674
+ cycleniterleft = self._cycleniter - self.current_imdict.returndict['iterdone']
675
+
676
+ # Use global imdict for convergence check
677
+ if deconv_ret['stopcode'] == 7: ## Tell the convergence checker that the mask is zero and iterations were skipped
678
+ self.hasit, self.stopdescription = self.global_imdict.has_converged(self._niter, self._threshold, self._nmajor, masksum=0)
679
+ else:
680
+ self.hasit, self.stopdescription = self.global_imdict.has_converged(self._niter, self._threshold, self._nmajor, cycleniter=cycleniterleft)
681
+
682
+
683
+ self.global_imdict.returndict['stopcode'] = self.hasit
684
+ self.global_imdict.returndict['stopDescription'] = self.stopdescription
685
+
686
+ if not self.hasit and self._usemask == 'auto-multithresh':
687
+ # If we haven't converged, run deconvolve to update the mask
688
+ # Note : This is only necessary if using auto-multithresh. If using the interactive viewer to draw, the mask gets updated as the regions
689
+ # are added or removed in the interactive viewer.
690
+ self._deconvolve(imagename=self._imagename, startmodel=self._startmodel, deconvolver=self._deconvolver, restoration=False, threshold=self._threshold, niter=0,
691
+ nsigma=self._nsigma, fullsummary=True, fastnoise=self._fastnoise, usemask=self._usemask, mask='', pbmask=self._pbmask,
692
+ sidelobethreshold=self._sidelobethreshold, noisethreshold=self._noisethreshold, lownoisethreshold=self._lownoisethreshold,
693
+ negativethreshold=self._negativethreshold, smoothfactor=self._smoothfactor, minbeamfrac=self._minbeamfrac, cutthreshold=self._cutthreshold,
694
+ growiterations=self._growiterations, dogrowprune=self._dogrowprune, verbose=self._verbose)
695
+
696
+
697
+ if len(self.global_imdict.returndict) > 0 and 'summaryminor' in self.global_imdict.returndict and sum(map(len,self.global_imdict.returndict['summaryminor'].values())) > 0:
698
+ self._convergence_result = ( self.global_imdict.returndict['stopDescription'] if 'stopDescription' in self.global_imdict.returndict else '',
699
+ self.global_imdict.returndict['stopcode'] if 'stopcode' in self.global_imdict.returndict else 0,
700
+ self._major_done,
701
+ self._nmajor,
702
+ self._niter,
703
+ self.__add_per_major_items( self.global_imdict.returndict,
704
+ self._convergence_result[5]['major'],
705
+ self.__update_convergence()))
706
+ else:
707
+ self._convergence_result = ( f'tclean returned an empty result',
708
+ self._convergence_result[1],
709
+ self._major_done,
710
+ self._nmajor,
711
+ self._niter,
712
+ self._convergence_result[5] )
713
+ except Exception as e:
714
+ self._convergence_result = ( str(e),
715
+ -1,
716
+ self._major_done,
717
+ self._nmajor,
718
+ self._niter,
719
+ self._convergence_result[5] )
720
+ return self._convergence_result
721
+
722
+ return self._convergence_result
723
+
724
+ def __decrement_counts( self ):
725
+ ## Update niterleft and nmajorleft now.
726
+ if self.hasit == 0: ##If not yet converged.
727
+ if self._nmajor != -1: ## If -1, don't touch it.
728
+ self._nmajor = self._nmajor - 1
729
+ if self._nmajor<0: ## Force a floor
730
+ self._nmajor=0
731
+ self._niter = self._niter - self.current_imdict.get_key('iterdone')
732
+ if self._niter<0: ## This can happen when we're counting niter across channels in a single minor cycle set, and it crosses the total.
733
+ self._niter=0 ## Force a floor
734
+ else:
735
+ return ##If convergence has been reached, don't try to decrement further.
736
+
737
+
738
+ def __reflect_stop( self ):
739
+ ## if python wasn't hacky, you would be able to try/except/raise in lambda
740
+ time.sleep(1)
741
+ try:
742
+ return self.__next__( )
743
+ except StopIteration:
744
+ raise StopAsyncIteration
745
+
746
+ async def __anext__( self ):
747
+ ### asyncio.run cannot be used here because this is called
748
+ ### within an asyncio loop...
749
+ loop = asyncio.get_event_loop( )
750
+ result = await loop.run_in_executor( None, self.__reflect_stop )
751
+ return result
752
+
753
+ def __iter__( self ):
754
+ return self
755
+
756
+ def __aiter__( self ):
757
+ return self
758
+
759
+ def __split_filename( self, path ):
760
+ return os.path.splitext(os.path.basename(path))
761
+
762
+ def __default_mask_name( self ):
763
+ imgparts = self.__split_filename( self._imagename )
764
+ return f'{imgparts[0]}.mask'
765
+
766
+ def __del__(self):
767
+ if not self._has_restored:
768
+ self.restore()
769
+
770
+ def mask(self):
771
+ #return self.__default_mask_name() if self._mask == '' else self._mask
772
+ return f'{self._imagename}.mask' if self._mask == '' else self._mask
773
+
774
+ def reset(self):
775
+ #if not self._finalized:
776
+ # raise RuntimeError('attempt to reset a gclean run that has not been finalized')
777
+ self._finalized = False
778
+ self._convergence_result = ( None,
779
+ self._convergence_result[1],
780
+ self._major_done,
781
+ self._nmajor,
782
+ self._niter,
783
+ self._convergence_result[5] )
784
+
785
+
786
+ def restore(self):
787
+ """ Restores the final image, and returns a path to the restored image. """
788
+ deconv_ret = self._deconvolve(imagename=self._imagename,
789
+ deconvolver=self._deconvolver,
790
+ restoration=True, niter=0,
791
+ fullsummary=True)
792
+
793
+ self._has_restored = True
794
+
795
+ return { "image": f"{self._imagename}.image" }
796
+
797
+ def has_next(self):
798
+ return not self._finalized