cubevis 0.5.19__py3-none-any.whl → 0.5.21__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.

Potentially problematic release.


This version of cubevis might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  ########################################################################
2
2
  #
3
- # Copyright (C) 2022,2023,2024
3
+ # Copyright (C) 2022,2023,2024,2025
4
4
  # Associated Universities, Inc. Washington DC, USA.
5
5
  #
6
6
  # This script is free software; you can redistribute it and/or modify it
@@ -28,52 +28,18 @@
28
28
  '''implementation of the ``InteractiveClean`` application for interactive control
29
29
  of tclean'''
30
30
 
31
- ###
32
- ### Useful for debugging
33
- ###
34
- ###from cubevis.bokeh.state import initialize_bokeh
35
- ###initialize_bokeh( bokehjs_subst=".../bokeh-3.2.2.js" )
36
- ###
37
- import os
38
- import copy
39
- import asyncio
40
- import shutil
41
- import websockets
42
- from os.path import basename, abspath, exists
43
- from uuid import uuid4
44
- from html import escape as html_escape
45
- from contextlib import asynccontextmanager
46
- from bokeh.models import Button, TextInput, Checkbox, Div, LinearAxis, CustomJS, Spacer, Span, HoverTool, DataRange1d, Step, InlineStyleSheet
47
- from bokeh.events import ModelEvent, MouseEnter
48
- from bokeh.models import TabPanel, Tabs
49
- from bokeh.plotting import ColumnDataSource, figure, show
50
- from bokeh.layouts import column, row, layout
51
- from bokeh.io import reset_output as reset_bokeh_output, output_notebook
52
- from bokeh.models.dom import HTML
53
-
54
- from bokeh.models.ui.tooltips import Tooltip
55
- from cubevis.bokeh.models import TipButton, Tip, EvTextInput
56
- from cubevis.utils import resource_manager, reset_resource_manager, is_interactive_jupyter, find_pkg, load_pkg
57
- from cubevis.utils import ContextMgrChain as CMC
58
-
59
- # pylint: disable=no-name-in-module
60
- from casatasks.private.imagerhelpers.imager_return_dict import ImagingDict
31
+ from pprint import pprint
61
32
 
33
+ import sys
34
+ from os.path import exists
62
35
  from casatasks.private.imagerhelpers.input_parameters import ImagerParameters
63
- # pylint: enable=no-name-in-module
64
36
 
65
- from cubevis.utils import find_ws_address, convert_masks
66
- from cubevis.toolbox import CubeMask, AppContext
67
- from cubevis.bokeh.utils import svg_icon
68
- from cubevis.bokeh.sources import DataPipe
69
- from cubevis.utils import DocEnum
70
-
71
- from ._interactiveclean_wrappers import SharedWidgets
72
-
73
- USE_MULTIPLE_GCLEAN_HACK=False
37
+ from cubevis.utils import find_pkg, load_pkg
38
+ from cubevis.toolbox import InteractiveCleanUI
39
+ from cubevis import exe
74
40
 
75
41
  class InteractiveClean:
76
- '''InteractiveClean(...) implements interactive clean using Bokeh
42
+ r'''InteractiveClean(...) implements interactive clean using Bokeh
77
43
  tclean ---- Radio Interferometric Image Reconstruction
78
44
 
79
45
  Form images from visibilities and reconstruct a sky model.
@@ -1839,89 +1805,10 @@ class InteractiveClean:
1839
1805
 
1840
1806
 
1841
1807
  '''
1842
- def __stop( self, _=None ):
1843
- self.__result_future.set_result(self.__retrieve_result( ))
1844
-
1845
- def _abort_handler( self, err ):
1846
- self._error_result = err
1847
- self.__stop( )
1848
-
1849
- def __reset( self ):
1850
- if self.__pipes_initialized:
1851
- self._pipe = { 'control': None }
1852
- self._clean = { 'converge': { 'state': { } }, 'last-success': None }
1853
- reset_bokeh_output( )
1854
- reset_resource_manager( )
1855
-
1856
- ###
1857
- ### reset asyncio result future
1858
- ###
1859
- self.__result_future = None
1860
-
1861
- ###
1862
- ### used by data pipe (websocket) initialization function
1863
- ###
1864
- self.__pipes_initialized = False
1865
-
1866
- ###
1867
- ### error or exception result
1868
- ###
1869
- self._error_result = None
1870
-
1871
- ###
1872
- ### iclean results
1873
- ###
1874
- self.__result = None
1875
- self.__result_from_gui = None
1876
-
1877
- '''
1878
- _gen_port_fwd_cmd()
1879
-
1880
- Create an SSH port-forwarding command to create the tunnels necessary for remote connection.
1881
- NOTE: This assumes that the same remote ports are also available locally - which may
1882
- NOT always be true.
1883
- '''
1884
- def _gen_port_fwd_cmd(self):
1885
- hostname = os.uname()[1]
1886
-
1887
- ###
1888
- ### need to add extra cube ports here for multifield imaging
1889
- ###
1890
- ports = [ self._pipe['control'].address[1], self._clean['converge']['pipe'].address[1] ]
1891
-
1892
- for imid, imdetails in self._clean_targets.items( ):
1893
- ports.append( imdetails['gui']['cube']._pipe['image'].address[1] )
1894
- ports.append( imdetails['gui']['cube']._pipe['control'].address[1] )
1895
-
1896
- # Also forward http port if serving webpage
1897
- if not self._is_notebook:
1898
- ports.append(self._http_port)
1899
-
1900
- cmd = 'ssh'
1901
- for port in ports:
1902
- cmd += (' -L ' + str(port) + ':localhost:' + str(port))
1903
-
1904
- cmd += ' ' + str(hostname)
1905
- return cmd
1906
-
1907
- def _residual_path( self, gclean, imid ):
1908
- if self._clean['gclean_paths'] is None:
1909
- raise RuntimeError( f'''gclean paths are not available for {imid}''' )
1910
- for p in self._clean['gclean_paths']:
1911
- if p['name'] == imid:
1912
- return f"{p['imagepath']}/{p['residualname']}"
1913
- raise RuntimeError( f'''gclean residual path not found for {imid}''' )
1914
-
1915
- def _mask_path( self, gclean, imid ):
1916
- if self._clean['gclean_paths'] is None:
1917
- raise RuntimeError( f'''gclean paths are not available for {imid}''' )
1918
- for p in self._clean['gclean_paths']:
1919
- if p['name'] == imid:
1920
- return f"{p['imagepath']}/{p['maskname']}"
1921
- raise RuntimeError( f'''gclean mask path not found for {imid}''' )
1922
1808
 
1923
1809
  def __init__( self, vis, imagename, selectdata=True, field='', spw='', timerange='', uvrange='', antenna='', scan='', observation='', intent='', datacolumn='corrected', imsize=[ int(100) ], cell=[ ], phasecenter='', stokes='I', projection='SIN', startmodel='', specmode='mfs', reffreq='', nchan=int(-1), start='', width='', outframe='LSRK', veltype='radio', restfreq=[ ], interpolation='linear', perchanweightdensity=True, gridder='standard', facets=int(1), psfphasecenter='', wprojplanes=int(1), vptable='', mosweight=True, aterm=True, psterm=False, wbawp=True, conjbeams=False, cfcache='', usepointing=False, computepastep=float(360.0), rotatepastep=float(360.0), pointingoffsetsigdev=[ ], pblimit=float(0.2), normtype='flatnoise', deconvolver='hogbom', scales=[ ], nterms=int(2), smallscalebias=float(0.0), fusedthreshold=float(0.0), largestscale=int(-1), restoration=True, restoringbeam=[ ], pbcor=False, outlierfile='', weighting='natural', robust=float(0.5), noise='1.0Jy', npixels=int(0), uvtaper=[ '' ], niter=int(0), gain=float(0.1), threshold=float(0.0), nsigma=float(0.0), cycleniter=int(-1), cyclefactor=float(1.0), minpsffraction=float(0.05), maxpsffraction=float(0.8), nmajor=int(-1), usemask='user', mask='', pbmask=float(0.0), sidelobethreshold=float(3.0), noisethreshold=float(5.0), lownoisethreshold=float(1.5), negativethreshold=float(0.0), smoothfactor=float(1.0), minbeamfrac=float(0.3), cutthreshold=float(0.01), growiterations=int(75), dogrowprune=True, minpercentchange=float(-1.0), verbose=False, fastnoise=True, restart=True, savemodel='none', calcres=True, calcpsf=True, psfcutoff=float(0.35), parallel=False, iclean_backend="PROD" ):
1924
1810
 
1811
+
1925
1812
  ###
1926
1813
  ### iclean_backend can be used to select alternate backends for interactive clean. This could be used
1927
1814
  ### to enable a backend with extended features or it could be used to select a stub backend designed
@@ -1939,815 +1826,14 @@ class InteractiveClean:
1939
1826
  else:
1940
1827
  raise ImportError(f"Could not locate {iclean_backend} kind of iclean backend")
1941
1828
 
1942
- ###
1943
- ### With Bokeh 3.2.2, the spectrum and convergence plots extend beyond the edge of the
1944
- ### browser window (requiring scrolling) if a width is not specified. It could be that
1945
- ### this should be computed from the width of the tabbed control area at the right of
1946
- ### the image display.
1947
- ###
1948
- self._conv_spect_plot_width = 450
1949
- ###
1950
- ### Create application context (which includes a temporary directory).
1951
- ### This sets the title of the plot.
1952
- ###
1953
- self._app_state = AppContext( 'Interactive Clean' )
1954
-
1955
- ###
1956
- ### Whether or not the Interactive Clean session is running remotely
1957
- ###
1958
- #self._is_remote = remote
1959
- self._is_remote = False
1960
-
1961
- ###
1962
- ### whether or not the session is being run from a jupyter notebook or script
1963
- ###
1964
- self._is_notebook = is_interactive_jupyter()
1965
-
1966
- ##
1967
- ## the http port for serving GUI in webpage if not running in script
1968
- ##
1969
- self._http_port = None
1829
+ self._args = {'vis': vis, 'selectdata': selectdata, 'field': field, 'spw': spw, 'timerange': timerange, 'uvrange': uvrange, 'antenna': antenna, 'scan': scan, 'observation': observation, 'intent': intent, 'datacolumn': datacolumn, 'imagename': imagename, 'imsize': imsize, 'cell': cell, 'phasecenter': phasecenter, 'stokes': stokes, 'projection': projection, 'startmodel': startmodel, 'specmode': specmode, 'reffreq': reffreq, 'nchan': nchan, 'start': start, 'width': width, 'outframe': outframe, 'veltype': veltype, 'restfreq': restfreq, 'interpolation': interpolation, 'perchanweightdensity': perchanweightdensity, 'gridder': gridder, 'facets': facets, 'psfphasecenter': psfphasecenter, 'wprojplanes': wprojplanes, 'vptable': vptable, 'mosweight': mosweight, 'aterm': aterm, 'psterm': psterm, 'wbawp': wbawp, 'conjbeams': conjbeams, 'cfcache': cfcache, 'usepointing': usepointing, 'computepastep': computepastep, 'rotatepastep': rotatepastep, 'pointingoffsetsigdev': pointingoffsetsigdev, 'pblimit': pblimit, 'normtype': normtype, 'deconvolver': deconvolver, 'scales': scales, 'nterms': nterms, 'smallscalebias': smallscalebias, 'fusedthreshold': fusedthreshold, 'largestscale': largestscale, 'restoration': restoration, 'restoringbeam': restoringbeam, 'pbcor': pbcor, 'outlierfile': outlierfile, 'weighting': weighting, 'robust': robust, 'noise': noise, 'npixels': npixels, 'uvtaper': uvtaper, 'niter': niter, 'gain': gain, 'threshold': threshold, 'nsigma': nsigma, 'cycleniter': cycleniter, 'cyclefactor': cyclefactor, 'minpsffraction': minpsffraction, 'maxpsffraction': maxpsffraction, 'nmajor': nmajor, 'usemask': usemask, 'mask': mask, 'pbmask': pbmask, 'sidelobethreshold': sidelobethreshold, 'noisethreshold': noisethreshold, 'lownoisethreshold': lownoisethreshold, 'negativethreshold': negativethreshold, 'smoothfactor': smoothfactor, 'minbeamfrac': minbeamfrac, 'cutthreshold': cutthreshold, 'growiterations': growiterations, 'dogrowprune': dogrowprune, 'minpercentchange': minpercentchange, 'verbose': verbose, 'fastnoise': fastnoise, 'restart': restart, 'savemodel': savemodel, 'calcres': calcres, 'calcpsf': calcpsf, 'psfcutoff': psfcutoff, 'parallel': parallel}
1830
+ self._gclean = self._gclean_module.gclean( **self._args )
1831
+ self._gclean_paths = self._gclean.image_products( )
1832
+ #self._residual_path = self._residual_path(self._clean['gclean'],imid)
1833
+ #self._mask_path = self._mask_path(self._clean['gclean'],imid)
1970
1834
 
1971
- ###
1972
- ### the asyncio future that is used to transmit the result from interactive clean
1973
- ###
1974
- self.__result_future = None
1975
-
1976
- ###
1977
- ### This is used to tell whether the websockets have been initialized, but also to
1978
- ### indicate if __call__ is being called multiple times to allow for resetting Bokeh
1979
- ###
1980
- self.__pipes_initialized = False
1981
-
1982
- ###
1983
- ### State required to manage iteration control
1984
- ###
1985
- self._control = { 'iteration': { } }
1986
-
1987
- ###
1988
- ### color specs
1989
- ###
1990
- self._converge_color = { 'residual': 'black',
1991
- 'flux': 'forestgreen' }
1835
+ self._ui = InteractiveCleanUI(self._gclean, self._args)
1992
1836
 
1993
- ###
1994
- ### widgets shared across image tabs (multifield imaging)
1995
- ###
1996
- self._cube_palette = None
1997
- self._image_bitmask_controls = None
1998
-
1999
- ###
2000
- ### String which indicates the changes applied to the mask to indicte when
2001
- ### the mask has changed... however, THIS IS NO LONGER USED
2002
- ### It could be removed, but it adds minor overhead and would be
2003
- ### DIFFICULT to add back in the future
2004
- ###
2005
- self._last_mask_breadcrumbs = ''
2006
-
2007
- ###
2008
- ### Set up dictionary of javascript code snippets
2009
- ###
2010
- self._initialize_javascript( )
2011
-
2012
- # Create folder for the generated html webpage - needs its own folder to not name conflict (must be 'index.html')
2013
- webpage_dirname = imagename + '_webpage'
2014
- ### Directory is created when an HTTP server is running
2015
- ### (MAX)
2016
- # if not os.path.isdir(webpage_dirname):
2017
- # os.makedirs(webpage_dirname)
2018
- self._webpage_path = os.path.abspath(webpage_dirname)
2019
-
2020
- self._pipe = { 'control': None }
2021
- self._clean = { 'converge': { 'state': { } }, 'last-success': None }
2022
-
2023
- self._clean_targets = { imagename: { 'args': {'vis': vis, 'selectdata': selectdata, 'field': field, 'spw': spw, 'timerange': timerange, 'uvrange': uvrange, 'antenna': antenna, 'scan': scan, 'observation': observation, 'intent': intent, 'datacolumn': datacolumn, 'imagename': imagename, 'imsize': imsize, 'cell': cell, 'phasecenter': phasecenter, 'stokes': stokes, 'projection': projection, 'startmodel': startmodel, 'specmode': specmode, 'reffreq': reffreq, 'nchan': nchan, 'start': start, 'width': width, 'outframe': outframe, 'veltype': veltype, 'restfreq': restfreq, 'interpolation': interpolation, 'perchanweightdensity': perchanweightdensity, 'gridder': gridder, 'facets': facets, 'psfphasecenter': psfphasecenter, 'wprojplanes': wprojplanes, 'vptable': vptable, 'mosweight': mosweight, 'aterm': aterm, 'psterm': psterm, 'wbawp': wbawp, 'conjbeams': conjbeams, 'cfcache': cfcache, 'usepointing': usepointing, 'computepastep': computepastep, 'rotatepastep': rotatepastep, 'pointingoffsetsigdev': pointingoffsetsigdev, 'pblimit': pblimit, 'normtype': normtype, 'deconvolver': deconvolver, 'scales': scales, 'nterms': nterms, 'smallscalebias': smallscalebias, 'fusedthreshold': fusedthreshold, 'largestscale': largestscale, 'restoration': restoration, 'restoringbeam': restoringbeam, 'pbcor': pbcor, 'outlierfile': outlierfile, 'weighting': weighting, 'robust': robust, 'noise': noise, 'npixels': npixels, 'uvtaper': uvtaper, 'niter': niter, 'gain': gain, 'threshold': threshold, 'nsigma': nsigma, 'cycleniter': cycleniter, 'cyclefactor': cyclefactor, 'minpsffraction': minpsffraction, 'maxpsffraction': maxpsffraction, 'nmajor': nmajor, 'usemask': usemask, 'mask': mask, 'pbmask': pbmask, 'sidelobethreshold': sidelobethreshold, 'noisethreshold': noisethreshold, 'lownoisethreshold': lownoisethreshold, 'negativethreshold': negativethreshold, 'smoothfactor': smoothfactor, 'minbeamfrac': minbeamfrac, 'cutthreshold': cutthreshold, 'growiterations': growiterations, 'dogrowprune': dogrowprune, 'minpercentchange': minpercentchange, 'verbose': verbose, 'fastnoise': fastnoise, 'restart': restart, 'savemodel': savemodel, 'calcres': calcres, 'calcpsf': calcpsf, 'psfcutoff': psfcutoff, 'parallel': parallel} } }
2024
- self._initial_clean_params = { }
2025
-
2026
- self._parameters = ImagerParameters( **{ k:v for k,v in self._clean_targets[imagename]['args'].items( )
2027
- if k in ImagerParameters.__init__.__code__.co_varnames[1:] } )
2028
-
2029
- ###
2030
- ### For 6.6.5, outliers are not yet completely implemented so the
2031
- ### 'outlierfile' parameter may not be present.
2032
- ###
2033
- if 'outlierfile' in locals( ) and outlierfile and exists(outlierfile):
2034
- outliers, err = self._parameters.parseOutlierFile(outlierfile)
2035
- if err: raise RuntimeError( f'''outlierfile error: {err}''' )
2036
- for image in outliers:
2037
- if 'impars' in image and 'imagename' in image['impars']:
2038
- name = image['impars']['imagename']
2039
- self._clean_targets[name] = { }
2040
- args = {'vis': vis, 'selectdata': selectdata, 'field': field, 'spw': spw, 'timerange': timerange, 'uvrange': uvrange, 'antenna': antenna, 'scan': scan, 'observation': observation, 'intent': intent, 'datacolumn': datacolumn, 'imagename': imagename, 'imsize': imsize, 'cell': cell, 'phasecenter': phasecenter, 'stokes': stokes, 'projection': projection, 'startmodel': startmodel, 'specmode': specmode, 'reffreq': reffreq, 'nchan': nchan, 'start': start, 'width': width, 'outframe': outframe, 'veltype': veltype, 'restfreq': restfreq, 'interpolation': interpolation, 'perchanweightdensity': perchanweightdensity, 'gridder': gridder, 'facets': facets, 'psfphasecenter': psfphasecenter, 'wprojplanes': wprojplanes, 'vptable': vptable, 'mosweight': mosweight, 'aterm': aterm, 'psterm': psterm, 'wbawp': wbawp, 'conjbeams': conjbeams, 'cfcache': cfcache, 'usepointing': usepointing, 'computepastep': computepastep, 'rotatepastep': rotatepastep, 'pointingoffsetsigdev': pointingoffsetsigdev, 'pblimit': pblimit, 'normtype': normtype, 'deconvolver': deconvolver, 'scales': scales, 'nterms': nterms, 'smallscalebias': smallscalebias, 'fusedthreshold': fusedthreshold, 'largestscale': largestscale, 'restoration': restoration, 'restoringbeam': restoringbeam, 'pbcor': pbcor, 'outlierfile': outlierfile, 'weighting': weighting, 'robust': robust, 'noise': noise, 'npixels': npixels, 'uvtaper': uvtaper, 'niter': niter, 'gain': gain, 'threshold': threshold, 'nsigma': nsigma, 'cycleniter': cycleniter, 'cyclefactor': cyclefactor, 'minpsffraction': minpsffraction, 'maxpsffraction': maxpsffraction, 'nmajor': nmajor, 'usemask': usemask, 'mask': mask, 'pbmask': pbmask, 'sidelobethreshold': sidelobethreshold, 'noisethreshold': noisethreshold, 'lownoisethreshold': lownoisethreshold, 'negativethreshold': negativethreshold, 'smoothfactor': smoothfactor, 'minbeamfrac': minbeamfrac, 'cutthreshold': cutthreshold, 'growiterations': growiterations, 'dogrowprune': dogrowprune, 'minpercentchange': minpercentchange, 'verbose': verbose, 'fastnoise': fastnoise, 'restart': restart, 'savemodel': savemodel, 'calcres': calcres, 'calcpsf': calcpsf, 'psfcutoff': psfcutoff, 'parallel': parallel}
2041
- args['outlierfile'] = '' ### >>HERE>> outlierfile TEMPORARILY handled in _interactiveclean.py
2042
- for _,arg_mods in image.items( ):
2043
- for k,v in arg_mods.items( ):
2044
- if k in args:
2045
- args[k] = v
2046
- self._clean_targets[name]['args'] = args
2047
-
2048
- ###
2049
- ### create clean interface -- final version will have only one gclean object
2050
- ###
2051
- self._init_values = { "deconvolver": deconvolver, ### used by _residual_path( )
2052
- "cycleniter": cycleniter, ### used by _setup( )
2053
- "threshold": threshold, ### used by _setup( )
2054
- "cyclefactor": cyclefactor, ### used by _setup( )
2055
- "gain": gain, ### used by _setup( )
2056
- "nsigma": nsigma, ### used by _setup( )
2057
- "convergence_state": { 'convergence': {}, ### shares state between
2058
- 'cyclethreshold': {} } } ### __init__( ) and _setup( )
2059
-
2060
- self._clean['gclean'] = None
2061
- self._clean['gclean_paths'] = None
2062
- self._clean['imid'] = [ ]
2063
- self._clean['gclean_rest'] = [ ]
2064
- for imid, imdetails in self._clean_targets.items( ):
2065
- self._clean['imid'].append(imid)
2066
-
2067
- ###
2068
- ### Residual path...
2069
- ###
2070
- if 'path' not in imdetails: imdetails['path'] = { }
2071
- if self._clean['gclean'] is None:
2072
-
2073
- self._clean['gclean'] = self._gclean_module.gclean( **imdetails['args'] )
2074
- self._clean['gclean_paths'] = self._clean['gclean'].image_products( )
2075
-
2076
- imdetails['path']['residual'] = self._residual_path(self._clean['gclean'],imid)
2077
- imdetails['path']['mask'] = self._mask_path(self._clean['gclean'],imid)
2078
-
2079
- else:
2080
-
2081
- imdetails['path']['residual'] = self._residual_path(self._clean['gclean'],imid)
2082
- imdetails['path']['mask'] = self._mask_path(self._clean['gclean'],imid)
2083
-
2084
- for idx, (imid, imdetails) in enumerate(self._clean_targets.items( )):
2085
- imdetails['gui'] = { }
2086
-
2087
- imdetails['gui'] = { 'params': { 'iteration': { }, 'automask': { } },
2088
- 'image': { },
2089
- 'image-adjust': { } }
2090
-
2091
- ###
2092
- ### Only the first image should initialize the initial convergence state
2093
- ###
2094
- imdetails['gui']['cube'] = CubeMask( imdetails['path']['residual'], mask=imdetails['path']['mask'], abort=self._abort_handler,
2095
- init_script=CustomJS( args=dict( initial_convergence_state=self._init_values["convergence_state"],
2096
- name=imid ),
2097
- code='''document._casa_convergence_data = initial_convergence_state''' )
2098
- if idx == 0 else None )
2099
-
2100
- ###
2101
- ### Auto Masking Parameters
2102
- ###
2103
- imdetails['params'] = { }
2104
- imdetails['params']['am'] = { }
2105
- imdetails['params']['am']['usemask'] = usemask
2106
- imdetails['params']['am']['noisethreshold'] = noisethreshold
2107
- imdetails['params']['am']['sidelobethreshold'] = sidelobethreshold
2108
- imdetails['params']['am']['lownoisethreshold'] = lownoisethreshold
2109
- imdetails['params']['am']['minbeamfrac'] = minbeamfrac
2110
- imdetails['params']['am']['negativethreshold'] = negativethreshold
2111
- imdetails['params']['am']['dogrowprune'] = dogrowprune
2112
- imdetails['params']['am']['fastnoise'] = fastnoise
2113
-
2114
- def _init_pipes( self ):
2115
- if not self.__pipes_initialized:
2116
- self.__pipes_initialized = True
2117
- self._pipe['control'] = DataPipe( address=find_ws_address( ), abort=self._abort_handler )
2118
- ###
2119
- ### One pipe for updating the convergence plots.
2120
- ###
2121
- self._clean['converge'] = { 'state': None }
2122
- self._clean['converge']['pipe'] = DataPipe( address=find_ws_address( ), abort=self._abort_handler )
2123
- self._clean['converge']['id'] = str(uuid4( ))
2124
-
2125
-
2126
-
2127
- # Get port for serving HTTP server if running in script
2128
- self._http_port = find_ws_address("")[1]
2129
- for imid, imdetails in self._clean_targets.items( ):
2130
- imdetails['gui']['cube']._init_pipes( )
2131
-
2132
- def _create_convergence_gui( self, imdetails, orient='horizontal', sizing_mode='stretch_width', **kw ):
2133
- TOOLTIPS='''<div>
2134
- <div>
2135
- <span style="font-weight: bold;">@type</span>
2136
- <span>@values</span>
2137
- </div>
2138
- <div>
2139
- <span style="font-weight: bold; font-size: 10px">cycle threshold</span>
2140
- <span>@cyclethreshold</span>
2141
- </div>
2142
- <div>
2143
- <span style="font-weight: bold; font-size: 10px">stop</span>
2144
- <span>@stopDesc</span>
2145
- </div>
2146
- </div>'''
2147
-
2148
- hover = HoverTool( tooltips=TOOLTIPS )
2149
- imdetails['gui']['convergence'] = figure( sizing_mode=sizing_mode, y_axis_location="right",
2150
- tools=[ hover ], toolbar_location=None, **kw )
2151
-
2152
- if orient == 'vertical':
2153
- imdetails['gui']['convergence'].yaxis.axis_label='Iteration (cycle threshold dotted red)'
2154
- imdetails['gui']['convergence'].xaxis.axis_label='Peak Residual'
2155
- imdetails['gui']['convergence'].extra_x_ranges = { 'residual_range': DataRange1d( follow='end' ),
2156
- 'flux_range': DataRange1d( follow='end' ) }
2157
-
2158
- imdetails['gui']['convergence'].step( 'values', 'iterations', source=imdetails['converge-data']['cyclethreshold'],
2159
- line_color='red', x_range_name='residual_range', line_dash='dotted', line_width=2 )
2160
- imdetails['gui']['convergence'].line( 'values', 'iterations', source=imdetails['converge-data']['residual'],
2161
- line_color=self._converge_color['residual'], x_range_name='residual_range' )
2162
- imdetails['gui']['convergence'].scatter( 'values', 'iterations', source=imdetails['converge-data']['residual'],
2163
- color=self._converge_color['residual'], x_range_name='residual_range',size=10 )
2164
- imdetails['gui']['convergence'].line( 'values', 'iterations', source=imdetails['converge-data']['flux'],
2165
- line_color=self._converge_color['flux'], x_range_name='flux_range' )
2166
- imdetails['gui']['convergence'].scatter( 'values', 'iterations', source=imdetails['converge-data']['flux'],
2167
- color=self._converge_color['flux'], x_range_name='flux_range', size=10 )
2168
-
2169
- imdetails['gui']['convergence'].add_layout( LinearAxis( x_range_name='flux_range', axis_label='Total Flux',
2170
- axis_line_color=self._converge_color['flux'],
2171
- major_label_text_color=self._converge_color['flux'],
2172
- axis_label_text_color=self._converge_color['flux'],
2173
- major_tick_line_color=self._converge_color['flux'],
2174
- minor_tick_line_color=self._converge_color['flux'] ), 'above')
2175
-
2176
- else:
2177
- imdetails['gui']['convergence'].xaxis.axis_label='Iteration (cycle threshold dotted red)'
2178
- imdetails['gui']['convergence'].yaxis.axis_label='Peak Residual'
2179
- imdetails['gui']['convergence'].extra_y_ranges = { 'residual_range': DataRange1d( follow='end' ),
2180
- 'flux_range': DataRange1d( follow='end' ) }
2181
-
2182
- imdetails['gui']['convergence'].step( 'iterations', 'values', source=imdetails['converge-data']['cyclethreshold'],
2183
- line_color='red', y_range_name='residual_range', line_dash='dotted', line_width=2 )
2184
- imdetails['gui']['convergence'].line( 'iterations', 'values', source=imdetails['converge-data']['residual'],
2185
- line_color=self._converge_color['residual'], y_range_name='residual_range' )
2186
- imdetails['gui']['convergence'].scatter( 'iterations', 'values', source=imdetails['converge-data']['residual'],
2187
- color=self._converge_color['residual'], y_range_name='residual_range',size=10 )
2188
- imdetails['gui']['convergence'].line( 'iterations', 'values', source=imdetails['converge-data']['flux'],
2189
- line_color=self._converge_color['flux'], y_range_name='flux_range' )
2190
- imdetails['gui']['convergence'].scatter( 'iterations', 'values', source=imdetails['converge-data']['flux'],
2191
- color=self._converge_color['flux'], y_range_name='flux_range', size=10 )
2192
-
2193
- imdetails['gui']['convergence'].add_layout( LinearAxis( y_range_name='flux_range', axis_label='Total Flux',
2194
- axis_line_color=self._converge_color['flux'],
2195
- major_label_text_color=self._converge_color['flux'],
2196
- axis_label_text_color=self._converge_color['flux'],
2197
- major_tick_line_color=self._converge_color['flux'],
2198
- minor_tick_line_color=self._converge_color['flux'] ), 'right')
2199
-
2200
-
2201
- def _launch_gui( self ):
2202
- '''create and show GUI
2203
- '''
2204
- ###
2205
- ### Will contain the top level GUI
2206
- ###
2207
- self._fig = { }
2208
-
2209
- ###
2210
- ### Python-side handler for events from the interactive clean control buttons
2211
- ###
2212
- async def clean_handler( msg, self=self ):
2213
- if msg['action'] == 'next' or msg['action'] == 'finish':
2214
-
2215
- if 'mask' in msg['value']:
2216
- ###
2217
- ### >>HERE>> breadcrumbs must be specific to the field they are related to...
2218
- ###
2219
- if 'breadcrumbs' in msg['value'] and msg['value']['breadcrumbs'] is not None and msg['value']['breadcrumbs'] != self._last_mask_breadcrumbs:
2220
- self._last_mask_breadcrumbs = msg['value']['breadcrumbs']
2221
- mask_dir = "%s.mask" % self._imagename
2222
- shutil.rmtree(mask_dir)
2223
- new_mask = imdetails['gui']['cube'].jsmask_to_raw(msg['value']['mask'])
2224
- self._mask_history.append(new_mask)
2225
-
2226
- msg['value']['mask'] = convert_masks(masks=new_mask, coord='pixel', cdesc=imdetails['gui']['cube'].coorddesc())
2227
-
2228
- else:
2229
- ##### seemingly the mask path used to be spliced in?
2230
- #msg['value']['mask'] = self._mask_path
2231
- pass
2232
- else:
2233
- ##### seemingly the mask path used to be spliced in?
2234
- #msg['value']['mask'] = self._mask_path
2235
- pass
2236
-
2237
- ###
2238
- ### In the final implementation, there will only be one gclean object...
2239
- ###
2240
- convergence_state={ 'convergence': {}, 'cyclethreshold': {} }
2241
- err,errmsg = self._clean['gclean'].update( dict( **msg['value']['iteration'],
2242
- **msg['value']['automask'] ) )
2243
-
2244
- iteration_limit = int(msg['value']['iteration']['niter'])
2245
- stopdesc, stopcode, majordone, majorleft, iterleft, self._convergence_data = await self._clean['gclean'].__anext__( )
2246
-
2247
- clean_cmds = self._clean['gclean']._log( )
2248
-
2249
- for key, value in self._convergence_data.items( ):
2250
-
2251
- if len(value['chan']) == 0 or stopcode[0] == -1:
2252
- ### stopcode[0] == -1 indicates an error condition within gclean
2253
- return dict( result='error', stopcode=stopcode, cmd=clean_cmds,
2254
- convergence=None, majordone=majordone,
2255
- majorleft=majorleft, iterleft=iterleft, stopdesc=stopdesc )
2256
-
2257
- convergence_state['convergence'][key] = value['chan']
2258
- convergence_state['cyclethreshold'][key] = value['major']['cyclethreshold']
2259
-
2260
- ### stopcode[0] != 0 indicates that some stopping criteria has been reached
2261
- ### this will also catch errors as well as convergence
2262
- ### (so 'converged' isn't quite right...)
2263
- self._clean['last-success'] = dict( result='converged' if stopcode[0] else 'update', stopcode=stopcode, cmd=clean_cmds,
2264
- convergence=convergence_state['convergence'],
2265
- iterdone=iteration_limit - iterleft, iterleft=iterleft,
2266
- majordone=majordone, majorleft=majorleft, cyclethreshold=convergence_state['cyclethreshold'], stopdesc=stopdesc )
2267
- return self._clean['last-success']
2268
-
2269
- elif msg['action'] == 'stop':
2270
- self.__stop( )
2271
- return dict( result='stopped', update=dict( ) )
2272
- elif msg['action'] == 'status':
2273
- return dict( result="ok", update=dict( ) )
2274
- else:
2275
- print( "got something else: '%s'" % msg['action'] )
2276
-
2277
- ###
2278
- ### set up websockets which will be used for control and convergence updates
2279
- ###
2280
- self._init_pipes( )
2281
-
2282
- ###
2283
- ### Setup id that will be used for messages from each button
2284
- ###
2285
- self._clean_ids = { }
2286
- for btn in "continue", 'finish', 'stop':
2287
- self._clean_ids[btn] = str(uuid4( ))
2288
- #print("%s: %s" % ( btn, self._clean_ids[btn] ) )
2289
- self._pipe['control'].register( self._clean_ids[btn], clean_handler )
2290
-
2291
-
2292
- ###
2293
- ### There is one set of tclean controls for all images/outlier/etc. because
2294
- ### in the final version gclean will handle the iterations for all fields...
2295
- ###
2296
- cwidth = 64
2297
- cheight = 40
2298
- self._control['iteration'] = { }
2299
- self._control['iteration']['continue'] = TipButton( max_width=cwidth, max_height=cheight, name='continue',
2300
- icon=svg_icon(icon_name="iclean-continue", size=18),
2301
- tooltip=Tooltip( content=HTML( '''Stop after <b>one major cycle</b> or when any stopping criteria is met.''' ), position='left') )
2302
- self._control['iteration']['finish'] = TipButton( max_width=cwidth, max_height=cheight, name='finish',
2303
- icon=svg_icon(icon_name="iclean-finish", size=18),
2304
- tooltip=Tooltip( content=HTML( '''<b>Continue</b> until some stopping criteria is met.''' ), position='left') )
2305
- self._control['iteration']['stop'] = TipButton( button_type="danger", max_width=cwidth, max_height=cheight, name='stop',
2306
- icon=svg_icon(icon_name="iclean-stop", size=18),
2307
- tooltip=Tooltip( content=HTML( '''<p>Clicking a <font color="red">red</font> stop button will cause this tab to close and control will return to Python.<p>Clicking an <font color="orange">orange</font> stop button will cause <tt>tclean</tt> to stop after the current major cycle.''' ), position='left' ) )
2308
-
2309
- ###
2310
- ### The single SHARED help button will be supplied by the first CubeMask...
2311
- ###
2312
- help_button = None
2313
- ###
2314
- ### First status line will be reused...
2315
- ###
2316
- status_line = None
2317
-
2318
- ###
2319
- ### Manage the widgets which are shared between tabs...
2320
- ###
2321
- icw = SharedWidgets( )
2322
- toolbars = [ ]
2323
- for imid, imdetails in self._clean_targets.items( ):
2324
- imdetails['gui']['stats'] = imdetails['gui']['cube'].statistics( )
2325
- imdetails['image-channels'] = imdetails['gui']['cube'].shape( )[3]
2326
-
2327
- status_line = imdetails['gui']['stopcode'] = imdetails['gui']['cube'].status_text( "<p>initial residual image</p>" if imdetails['image-channels'] > 1 else "<p>initial <b>single-channel</b> residual image</p>", width=230, reuse=status_line )
2328
-
2329
- ###
2330
- ### Retrieve convergence information
2331
- ###
2332
- def convergence_handler( msg, self=self, imid=imid ):
2333
- if msg['action'] == 'retrieve':
2334
- return { 'result': self._clean['last-success'] }
2335
- else:
2336
- return { 'result': None, 'error': 'unrecognized action' }
2337
-
2338
- self._clean['converge']['pipe'].register( self._clean['converge']['id'], convergence_handler )
2339
-
2340
- ###
2341
- ### Data source that will be used for updating the convergence plot
2342
- ###
2343
- stokes = 0
2344
- convergence = imdetails['converge']['chan'][0][stokes]
2345
- imdetails['converge-data'] = { }
2346
- imdetails['converge-data']['flux'] = ColumnDataSource( data=dict( values=convergence['modelFlux'], iterations=convergence['iterations'],
2347
- cyclethreshold=convergence['cycleThresh'],
2348
- stopDesc=list( map( ImagingDict.get_summaryminor_stopdesc, convergence['stopCode'] ) ),
2349
- type=['flux'] * len(convergence['iterations']) ) )
2350
- imdetails['converge-data']['residual'] = ColumnDataSource( data=dict( values=convergence['peakRes'], iterations=convergence['iterations'],
2351
- cyclethreshold=convergence['cycleThresh'],
2352
- stopDesc=list( map( ImagingDict.get_summaryminor_stopdesc, convergence['stopCode'] ) ),
2353
- type=['residual'] * len(convergence['iterations'])) )
2354
- imdetails['converge-data']['cyclethreshold'] = ColumnDataSource( data=dict( values=convergence['cycleThresh'], iterations=convergence['iterations'] ) )
2355
-
2356
-
2357
- ###
2358
- ### help page for cube interactions
2359
- ###
2360
- if help_button is None:
2361
- help_button = imdetails['gui']['cube'].help( rows=[ '<tr><td><i><b>red</b> stop button</i></td><td>clicking the stop button (when red) will close the dialog and control to python</td></tr>',
2362
- '<tr><td><i><b>orange</b> stop button</i></td><td>clicking the stop button (when orange) will return control to the GUI after the currently executing tclean run completes</td></tr>' ], position='right' )
2363
-
2364
- self._create_convergence_gui( imdetails, orient='horizontal', sizing_mode='stretch_height', width=self._conv_spect_plot_width )
2365
-
2366
- imdetails['gui']['params']['iteration']['nmajor'] = icw.nmajor( title='nmajor', value="%s" % self._initial_clean_params['nmajor'], width=90 )
2367
- imdetails['gui']['params']['iteration']['niter'] = icw.niter( title='niter', value="%s" % self._initial_clean_params['niter'], width=90 )
2368
- imdetails['gui']['params']['iteration']['cycleniter'] = icw.cycleniter( title="cycleniter", value="%s" % self._initial_clean_params['cycleniter'], width=90 )
2369
- imdetails['gui']['params']['iteration']['threshold'] = icw.threshold( title="threshold", value="%s" % self._initial_clean_params['threshold'], width=90 )
2370
- imdetails['gui']['params']['iteration']['cyclefactor'] = icw.cyclefactor( value="%s" % self._initial_clean_params['cyclefactor'], title="cyclefactor", width=90 )
2371
- imdetails['gui']['params']['iteration']['gain'] = icw.gain( title='gain', value="%s" % self._initial_clean_params['gain'], width=90 )
2372
- imdetails['gui']['params']['iteration']['nsigma'] = icw.nsigma( title='nsigma', value="%s" % self._initial_clean_params['nsigma'], width=90 )
2373
-
2374
- if imdetails['params']['am']['usemask'] == 'auto-multithresh':
2375
- ###
2376
- ### Currently automasking tab is only available when the user selects 'auto-multithresh'
2377
- ###
2378
- imdetails['gui']['params']['automask']['active'] = True
2379
- imdetails['gui']['params']['automask']['noisethreshold'] = icw.noisethreshold( title='noisethreshold', value="%s" % imdetails['params']['am']['noisethreshold'], margin=( 5, 25, 5, 5 ), width=90 )
2380
- imdetails['gui']['params']['automask']['sidelobethreshold'] = icw.sidelobethreshold( title='sidelobethreshold', value="%s" % imdetails['params']['am']['sidelobethreshold'], margin=( 5, 25, 5, 5 ), width=90 )
2381
- imdetails['gui']['params']['automask']['lownoisethreshold'] = icw.lownoisethreshold( title='lownoisethreshold', value="%s" % imdetails['params']['am']['lownoisethreshold'], margin=( 5, 25, 5, 5 ), width=90 )
2382
- imdetails['gui']['params']['automask']['minbeamfrac'] = icw.minbeamfrac( title='minbeamfrac', value="%s" % imdetails['params']['am']['minbeamfrac'], width=90 )
2383
- imdetails['gui']['params']['automask']['negativethreshold'] = icw.negativethreshold( title='negativethreshold', value="%s" % imdetails['params']['am']['negativethreshold'], margin=( 5, 25, 5, 5 ), width=90 )
2384
- imdetails['gui']['params']['automask']['dogrowprune'] = icw.dogrowprune( label='dogrowprune', active=imdetails['params']['am']['dogrowprune'], margin=( 15, 25, 5, 5 ) )
2385
- imdetails['gui']['params']['automask']['fastnoise'] = icw.fastnoise( label='fastnoise', active=imdetails['params']['am']['fastnoise'], margin=( 15, 25, 5, 5 ) )
2386
-
2387
-
2388
- imdetails['gui']['image']['src'] = imdetails['gui']['cube'].js_obj( )
2389
- imdetails['gui']['image']['fig'] = imdetails['gui']['cube'].image( grid=False, height_policy='max', width_policy='max',
2390
- channelcb=CustomJS( args=dict( img_state={ 'src': imdetails['gui']['image']['src'],
2391
- 'flux': imdetails['converge-data']['flux'],
2392
- 'residual': imdetails['converge-data']['residual'],
2393
- 'cyclethreshold': imdetails['converge-data']['cyclethreshold'] },
2394
- imid=imid,
2395
- ctrl={ 'converge': self._clean['converge'] },
2396
- stopdescmap=ImagingDict.get_summaryminor_stopdesc( ) ),
2397
- code=self._js['update-converge'] +
2398
- '''update_convergence_single( img_state, document._casa_convergence_data.convergence[imid] )''' ) )
2399
-
2400
- ###
2401
- ### collect toolbars for syncing selection
2402
- ###
2403
- toolbars.append(imdetails['gui']['image']['fig'].toolbar)
2404
-
2405
- ###
2406
- ### spectrum plot must be disabled during iteration due to "tap to change channel" functionality
2407
- ###
2408
- if imdetails['image-channels'] > 1:
2409
- imdetails['gui']['spectrum'] = imdetails['gui']['cube'].spectrum( orient='vertical', sizing_mode='stretch_height', width=self._conv_spect_plot_width )
2410
- imdetails['gui']['slider'] = imdetails['gui']['cube'].slider( show_value=False, title='', margin=(14,5,5,5), sizing_mode="scale_width" )
2411
- imdetails['gui']['goto'] = imdetails['gui']['cube'].goto( )
2412
- else:
2413
- imdetails['gui']['spectrum'] = None
2414
- imdetails['gui']['slider'] = None
2415
- imdetails['gui']['goto'] = None
2416
-
2417
- imdetails['gui']['channel-ctrl'] = imdetails['gui']['cube'].channel_ctrl( )
2418
-
2419
- imdetails['gui']['cursor-pixel-text'] = imdetails['gui']['cube'].pixel_tracking_text( margin=(-3, 5, 3, 30) )
2420
-
2421
- self._image_bitmask_controls = imdetails['gui']['cube'].bitmask_ctrl( reuse=self._image_bitmask_controls, button_type='light' )
2422
-
2423
- if imdetails['params']['am']['usemask'] == 'auto-multithresh':
2424
- imdetails['gui']['auto-masking-panel'] = [ TabPanel( child=column( row( Tip( imdetails['gui']['params']['automask']['noisethreshold'],
2425
- tooltip=Tooltip( content=HTML( 'sets the signal-to-noise threshold above which significant emission is masked during the initial round of mask creation' ),
2426
- position='bottom' ) ),
2427
- Tip( imdetails['gui']['params']['automask']['sidelobethreshold'],
2428
- tooltip=Tooltip( content=HTML( 'sets a threshold based on the sidelobe level above which significant emission is masked during the initial round of mask creation' ),
2429
- position='bottom' ) ),
2430
- Tip( imdetails['gui']['params']['automask']['minbeamfrac'],
2431
- tooltip=Tooltip( content=HTML( 'sets the minimum size a region must be to be retained in the mask' ),
2432
- position='bottom' ) ) ),
2433
- row( Tip( imdetails['gui']['params']['automask']['lownoisethreshold'],
2434
- tooltip=Tooltip( content=HTML( 'sets the threshold into which the initial mask (which is determined by either noisethreshold or sidelobethreshold) is expanded in order to include low signal-to-noise regions in the mask' ),
2435
- position='bottom' ) ),
2436
- Tip( imdetails['gui']['params']['automask']['negativethreshold'],
2437
- tooltip=Tooltip( content=HTML( 'sets the signal-to-noise threshold for absorption features to be masked' ),
2438
- position='bottom' ) ) ),
2439
- row( Tip( imdetails['gui']['params']['automask']['dogrowprune'],
2440
- tooltip=Tooltip( content=HTML( 'allows you to turn off the pruning of the low signal-to-noise mask, which speeds up masking for images and cubes with complex low signal-to-noise emission' ),
2441
- position='bottom' ) ),
2442
- Tip( imdetails['gui']['params']['automask']['fastnoise'],
2443
- tooltip=Tooltip( content=HTML( 'When set to True, a simpler but faster noise calucation is used' ),
2444
- position='bottom' ) ) ) ),
2445
- title='Automask' ) ]
2446
- else:
2447
- imdetails['gui']['auto-masking-panel'] = [ ]
2448
-
2449
-
2450
- ###
2451
- ### synchronize toolbar selections among figures
2452
- ###
2453
- if toolbars:
2454
- for tb in toolbars:
2455
- tb.js_on_change( 'active_changed',
2456
- ###
2457
- ### toolbars must filter out 'tb' to avoid circular references
2458
- ###
2459
- CustomJS( args=dict(toolbars=[t for t in toolbars if t.id != tb.id]),
2460
- code='''casalib.map( (gest,tool) => {
2461
- if ( tool.active ) {
2462
- // a tool which belongs to the toolbar that signaled a change
2463
- // is active for this gesture...
2464
- toolbars.forEach( (other_tb) => {
2465
- let new_active = other_tb.gestures[gest].tools.find(
2466
- (t) => t.name == tool.active.name )
2467
- if ( ! other_tb.gestures[gest].active ) {
2468
- if ( new_active ) {
2469
- other_tb.gestures[gest].active = new_active
2470
- new_active.active = true
2471
- }
2472
- } else if ( other_tb.gestures[gest].active.name != tool.active.name ) {
2473
- if ( new_active ) {
2474
- other_tb.gestures[gest].active.active = false
2475
- new_active.active = true
2476
- other_tb.gestures[gest].active = new_active
2477
- }
2478
- }
2479
- } )
2480
- } else {
2481
- // a tool which belongs to the toolbar that signaled a change
2482
- // is NOT active for this gesture...
2483
- toolbars.forEach( (other_tb) => {
2484
- if ( other_tb.gestures[gest] && other_tb.gestures[gest].active ) {
2485
- other_tb.gestures[gest].active.active = false
2486
- other_tb.gestures[gest].active = null
2487
- }
2488
- } )
2489
- }
2490
- }, cb_obj.gestures )''' ) )
2491
-
2492
- ###
2493
- ### button to display the tclean log -- in the final implmentation, outliers and other multifield imaging should be handled by gclean
2494
- ###
2495
- self._log_button = TipButton( max_width=help_button.width, max_height=help_button.height, name='log',
2496
- icon=svg_icon(icon_name="bp-application-sm", size=25),
2497
- tooltip=Tooltip( content=HTML('''click here to see the <pre>tclean</pre> execution log'''), position="right" ),
2498
- margin=(-1, 0, -10, 0), button_type='light',
2499
- stylesheets=[ InlineStyleSheet( css='''.bk-btn { border: 0px solid #ccc; padding: 0 var(--padding-vertical) var(--padding-horizontal); margin-top: 3px; }''' ) ] )
2500
-
2501
- self._control['iteration']['cb'] = CustomJS( args=dict( images_state={ k: { 'status': v['gui']['stopcode'],
2502
- 'automask': v['gui']['params']['automask'],
2503
- 'iteration': v['gui']['params']['iteration'],
2504
- 'img': v['gui']['image']['fig'],
2505
- 'src': v['gui']['cube'].js_obj( ),
2506
- 'spectrum': v['gui']['spectrum'],
2507
- 'src': v['gui']['image']['src'],
2508
- 'flux': v['converge-data']['flux'],
2509
- 'cyclethreshold': v['converge-data']['cyclethreshold'],
2510
- 'residual': v['converge-data']['residual'],
2511
- 'navi': { 'slider': v['gui']['slider'],
2512
- 'goto': v['gui']['goto'],
2513
- ## it doesn't seem like pixel tracking must be disabled
2514
- ##'tracking': v['gui']['cursor-pixel-text'],
2515
- 'stokes': v['gui']['channel-ctrl'][1] } }
2516
- for k,v in self._clean_targets.items( ) },
2517
- ctrl={ 'converge': self._clean['converge'] },
2518
- clean_ctrl=self._control['iteration'],
2519
- state=dict( mode='interactive', stopped=False, awaiting_stop=False, mask="" ),
2520
- ctrl_pipe=self._pipe['control'],
2521
- ids=self._clean_ids,
2522
- logbutton=self._log_button,
2523
- stopdescmap=ImagingDict.get_summaryminor_stopdesc( )
2524
- ),
2525
- code=self._js['update-converge'] + self._js['clean-refresh'] + self._js['clean-disable'] +
2526
- self._js['clean-enable'] + self._js['clean-status-update'] +
2527
- self._js['iter-gui-update'] + self._js['clean-wait'] +
2528
- '''function invalid_niter( s ) {
2529
- let v = parseInt( s )
2530
- if ( v > 0 ) return ''
2531
- if ( v == 0 ) return 'niter is zero'
2532
- if ( v < 0 ) return 'niter cannot be negative'
2533
- if ( isNaN(v) ) return 'niter must be an integer'
2534
- }
2535
- const itobj = Object.entries(images_state)[0][1].iteration
2536
- if ( ! state.stopped && cb_obj.origin.name == 'finish' ) {
2537
- let invalid = invalid_niter(itobj.niter.value)
2538
- if ( invalid ) update_status( invalid )
2539
- else {
2540
- state.mode = 'continuous'
2541
- update_status( 'Running multiple iterations' )
2542
- disable( false )
2543
- clean_ctrl.stop.button_type = "warning"
2544
- const thevalue = get_update_dictionary( )
2545
- ctrl_pipe.send( ids[cb_obj.origin.name],
2546
- { action: 'finish',
2547
- value: thevalue },
2548
- update_gui )
2549
- }
2550
- }
2551
- if ( ! state.stopped && state.mode === 'interactive' &&
2552
- cb_obj.origin.name === 'continue' ) {
2553
- let invalid = invalid_niter(itobj.niter.value)
2554
- if ( invalid ) update_status( invalid )
2555
- else {
2556
- update_status( 'Running one set of deconvolution iterations' )
2557
- disable( true )
2558
- // only send message for button that was pressed
2559
- // it's unclear whether 'this.origin.' or 'cb_obj.origin.' should be used
2560
- // (or even if 'XXX.origin.' is public)...
2561
- ctrl_pipe.send( ids[cb_obj.origin.name],
2562
- { action: 'next',
2563
- value: get_update_dictionary( ) },
2564
- update_gui )
2565
- }
2566
- }
2567
- if ( state.mode === 'interactive' && cb_obj.origin.name === 'stop' ) {
2568
- if ( confirm( "Are you sure you want to end this interactive clean session and close the GUI?" ) ) {
2569
- disable( true )
2570
- //ctrl_pipe.send( ids[cb_obj.origin.name],
2571
- // { action: 'stop',
2572
- // value: { } },
2573
- // update_gui )
2574
- document._casa_window_closed = true
2575
- /*** this will close the tab >>>>---------+ ***/
2576
- /*** | ***/
2577
- /*** vvvvv----------------------------+ ***/
2578
- document._cube_done( Object.entries(images_state).reduce((acc,[k,v]) => ({ ...acc, [k]: v.src.masks( ) }),{ } ) )
2579
- }
2580
- } else if ( state.mode === 'continuous' &&
2581
- cb_obj.origin.name === 'stop' &&
2582
- ! state.awaiting_stop ) {
2583
- disable( true )
2584
- state.awaiting_stop = true
2585
- ctrl_pipe.send( ids[cb_obj.origin.name],
2586
- { action: 'status',
2587
- value: { } },
2588
- wait_for_tclean_stop )
2589
- }''' )
2590
-
2591
-
2592
- self._control['iteration']['continue'].js_on_click( self._control['iteration']['cb'] )
2593
- self._control['iteration']['finish'].js_on_click( self._control['iteration']['cb'] )
2594
- self._control['iteration']['stop'].js_on_click( self._control['iteration']['cb'] )
2595
-
2596
- self._log_button.js_on_click( CustomJS( args=dict( logbutton=self._log_button ),
2597
- code='''function format_log( elem ) {
2598
- return `<html>
2599
- <head>
2600
- <style type="text/css">
2601
- body {
2602
- counter-reset: section;
2603
- }
2604
- p:not([no-num]):before {
2605
- font-weight: bold;
2606
- counter-increment: section;
2607
- content: "" counter(section) ": ";
2608
- }
2609
- </style>
2610
- </head>
2611
- <body>
2612
- <h1>Interactive Clean History</h1>
2613
- ` + elem.map((x) => x.startsWith('#') ? `<p no-num>${x}</p>` : `<p>${x}</p>`).join('\\n') + '</body>\\n</html>'
2614
- }
2615
- let b = cb_obj.origin
2616
- if ( ! b._window || b._window.closed ) {
2617
- b._window = window.open("about:blank","Interactive Clean Log")
2618
- b._window.document.write(format_log(b._log))
2619
- b._window.document.close( )
2620
- }''' ) )
2621
-
2622
- ###
2623
- ### Setup script that will be called when the user closes the
2624
- ### browser tab that is running interactive clean
2625
- ###
2626
- initial_log = self._clean['gclean']._log( )
2627
-
2628
- self._pipe['control'].init_script=CustomJS( args=dict( ctrl_pipe=self._pipe['control'],
2629
- ids=self._clean_ids,
2630
- logbutton=self._log_button,
2631
- log=initial_log,
2632
- initial_image=list(self._clean_targets.items( ))[0][0]
2633
- ),
2634
- code=self._js['initialize'] +
2635
- '''if ( ! logbutton._log ) {
2636
- /*** store log list with log button for access in other callbacks ***/
2637
- logbutton._log = log
2638
- }''' )
2639
-
2640
- tab_panels = list( map( self._create_image_panel, self._clean_targets.items( ) ) )
2641
-
2642
- for imid, imdetails in self._clean_targets.items( ):
2643
- imdetails['gui']['cube'].connect( )
2644
-
2645
- image_tabs = Tabs( tabs=tab_panels, tabs_location='below', height_policy='max', width_policy='max' )
2646
-
2647
- self._fig['layout'] = column(
2648
- row( help_button,
2649
- self._log_button,
2650
- Spacer( height=self._control['iteration']['stop'].height, sizing_mode="scale_width" ),
2651
- Div( text="<div><b>status:</b></div>" ),
2652
- status_line,
2653
- self._control['iteration']['stop'], self._control['iteration']['continue'], self._control['iteration']['finish'], sizing_mode="scale_width" ),
2654
- row( image_tabs, height_policy='max', width_policy='max' ),
2655
- height_policy='max', width_policy='max' )
2656
-
2657
- ###
2658
- ### Keep track of which image is currently active in document._casa_image_name (which is
2659
- ### initialized in self._js['initialize']). Also, update the current control sub-tab
2660
- ### when the field main-tab is changed. An attempt to manage this all within the
2661
- ### control sub-tabs using a reference to self._image_control_tab_groups from
2662
- ### each control sub-tab failed with:
2663
- ###
2664
- ### bokeh.core.serialization.SerializationError: circular reference
2665
- ###
2666
- image_tabs.js_on_change( 'active', CustomJS( args=dict( names=[ t[0] for t in self._clean_targets.items( ) ],
2667
- itergroups=self._image_control_tab_groups ),
2668
- code='''if ( ! hasprop(document,'_casa_last_control_tab') ) {
2669
- document._casa_last_control_tab = 0
2670
- }
2671
- document._casa_image_name = names[cb_obj.active]
2672
- itergroups[document._casa_image_name].active = document._casa_last_control_tab''' ) )
2673
-
2674
- # Change display type depending on runtime environment
2675
- if self._is_notebook:
2676
- output_notebook()
2677
- else:
2678
- ### Directory is created when an HTTP server is running
2679
- ### (MAX)
2680
- ### output_file(self._imagename+'_webpage/index.html')
2681
- pass
2682
-
2683
- show(self._fig['layout'])
2684
-
2685
- def _create_colormap_adjust( self, imdetails ):
2686
- palette = imdetails['gui']['cube'].palette( reuse=self._cube_palette )
2687
- return column( row( Div(text="<div><b>Colormap:</b></div>",margin=(5,2,5,25)), palette ),
2688
- imdetails['gui']['cube'].colormap_adjust( ), sizing_mode='stretch_both' )
2689
-
2690
-
2691
- def _create_control_image_tab( self, imid, imdetails ):
2692
- result = Tabs( tabs=[ TabPanel(child=column( row( Tip( imdetails['gui']['params']['iteration']['nmajor'],
2693
- tooltip=Tooltip( content=HTML( 'maximum number of major cycles to run before stopping'),
2694
- position='bottom' ) ),
2695
- Tip( imdetails['gui']['params']['iteration']['niter'],
2696
- tooltip=Tooltip( content=HTML( 'number of clean iterations to run' ),
2697
- position='bottom' ) ),
2698
- Tip( imdetails['gui']['params']['iteration']['threshold'],
2699
- tooltip=Tooltip( content=HTML( 'stopping threshold' ),
2700
- position='bottom' ) ) ),
2701
- row( Tip( imdetails['gui']['params']['iteration']['nsigma'],
2702
- tooltip=Tooltip( content=HTML( 'multiplicative factor for rms-based threshold stopping'),
2703
- position='bottom' ) ),
2704
- Tip( imdetails['gui']['params']['iteration']['gain'],
2705
- tooltip=Tooltip( content=HTML( 'fraction of the source flux to subtract out of the residual image'),
2706
- position='bottom' ) ) ),
2707
- row( Tip( imdetails['gui']['params']['iteration']['cycleniter'],
2708
- tooltip=Tooltip( content=HTML( 'maximum number of <b>minor-cycle</b> iterations' ),
2709
- position='bottom' ) ),
2710
- Tip( imdetails['gui']['params']['iteration']['cyclefactor'],
2711
- tooltip=Tooltip( content=HTML( 'scaling on PSF sidelobe level to compute the minor-cycle stopping threshold' ),
2712
- position='bottom_left' ) ), background="lightgray" ),
2713
- imdetails['gui']['convergence'], sizing_mode='stretch_height' ),
2714
- title='Iteration' ) ] +
2715
- ( [ TabPanel( child=imdetails['gui']['spectrum'],
2716
- title='Spectrum' ) ] if imdetails['image-channels'] > 1 else [ ] ) +
2717
- [ TabPanel( child=self._create_colormap_adjust(imdetails),
2718
- title='Colormap' ),
2719
- TabPanel( child=column( *imdetails['gui']['stats'] ),
2720
- title='Statistics' ) ] + imdetails['gui']['auto-masking-panel'],
2721
- width=500, sizing_mode='stretch_height', tabs_location='below' )
2722
-
2723
- if not hasattr(self,'_image_control_tab_groups'):
2724
- self._image_control_tab_groups = { }
2725
-
2726
- self._image_control_tab_groups[imid] = result
2727
- result.js_on_change( 'active', CustomJS( args=dict( ),
2728
- code='''document._casa_last_control_tab = cb_obj.active''' ) )
2729
- return result
2730
-
2731
- def _create_image_panel( self, imagetuple ):
2732
- imid, imdetails = imagetuple
2733
-
2734
-
2735
-
2736
- return TabPanel( child=column( row( *imdetails['gui']['channel-ctrl'], imdetails['gui']['cube'].coord_ctrl( ),
2737
- *self._image_bitmask_controls,
2738
- #Spacer( height=5, height_policy="fixed", sizing_mode="scale_width" ),
2739
- imdetails['gui']['cursor-pixel-text'],
2740
- row( Spacer( sizing_mode='stretch_width' ),
2741
- imdetails['gui']['cube'].tapedeck( size='20px' ) if imdetails['image-channels'] > 1 else Div( ),
2742
- Spacer( height=5, width=350 ), width_policy='max' ),
2743
- width_policy='max' ),
2744
- row( imdetails['gui']['image']['fig'],
2745
- column( row( imdetails['gui']['goto'],
2746
- imdetails['gui']['slider'],
2747
- width_policy='max' ) if imdetails['image-channels'] > 1 else Div( ),
2748
- self._create_control_image_tab(imid, imdetails), height_policy='max' ),
2749
- height_policy='max', width_policy='max' ),
2750
- height_policy='max', width_policy='max' ), title=imid )
2751
1837
 
2752
1838
  def __call__( self ):
2753
1839
  '''Display GUI and process events until the user stops the application.
@@ -2760,510 +1846,6 @@ class InteractiveClean:
2760
1846
  cell='12.0arcsec', specmode='cube',
2761
1847
  interpolation='nearest', ... )( ) )
2762
1848
  '''
2763
-
2764
- self._setup()
2765
-
2766
- # If Interactive Clean is being run remotely, print helper info for port tunneling
2767
- if self._is_remote:
2768
- # Tunnel ports for Jupyter kernel connection
2769
- print("\nImportant: Copy the following line and run in your local terminal to establish port forwarding.\
2770
- You may need to change the last argument to align with your ssh config.\n")
2771
- print(self._gen_port_fwd_cmd())
2772
-
2773
- # TODO: Include?
2774
- # VSCode will auto-forward ports that appear in well-formatted addresses.
2775
- # Printing this line will cause VSCode to autoforward the ports
2776
- # print("Cmd: " + str(repr(self.auto_fwd_ports_vscode())))
2777
- input("\nPress enter when port forwarding is setup...")
2778
-
2779
- async def _run_( ):
2780
- async with self.serve( ) as s:
2781
- await s
2782
-
2783
- if self._is_notebook:
2784
- ic_task = asyncio.create_task(_run_())
2785
- else:
2786
- asyncio.run(_run_( ))
2787
- return self.result( )
2788
-
2789
- def _setup( self ):
2790
- self.__reset( )
2791
-
2792
- def initialize_tclean( gclean ):
2793
-
2794
- stopdesc, stopcode, majordone, majorleft, iterleft, all_converge = next(gclean)
2795
-
2796
- for imid, converge in all_converge.items( ):
2797
- #######################################################################################################
2798
- ### gclean seems to return its internal state making it succeptable to modification... so we'll at ###
2799
- ### least start out with unique dictionaries. ###
2800
- #######################################################################################################
2801
- converge = copy.deepcopy(converge)
2802
-
2803
- imdetails = self._clean_targets[imid]
2804
- imdetails['converge'] = converge
2805
-
2806
- if imdetails['converge'] is None or len(imdetails['converge'].keys()) == 0 or \
2807
- imdetails['converge']['major'] is None or imdetails['converge']['chan'] is None:
2808
- ###
2809
- ### gclean should provide argument checking (https://github.com/casangi/casagui/issues/33)
2810
- ### but currently gclean can be initialized with bad arguments and it is not known until
2811
- ### the initial calls to tclean/deconvolve
2812
- ###
2813
- raise RuntimeError(f'''gclean failure "%s" not returned: {imdetails["converge"]}''' % ('major' if imdetails['converge']['major'] is None else 'chan'))
2814
-
2815
- self._clean['cmds'].extend(self._clean['gclean']._log( ))
2816
-
2817
- self._initial_clean_params['nmajor'] = majorleft
2818
- self._initial_clean_params['niter'] = iterleft
2819
- self._initial_clean_params['cycleniter'] = self._init_values["cycleniter"]
2820
- self._initial_clean_params['threshold'] = self._init_values["threshold"]
2821
- self._initial_clean_params['cyclefactor'] = self._init_values["cyclefactor"]
2822
- self._initial_clean_params['gain'] = self._init_values["gain"]
2823
- self._initial_clean_params['nsigma'] = self._init_values["nsigma"]
2824
-
2825
- self._init_values["convergence_state"]['convergence'][imid] = imdetails['converge']['chan']
2826
- self._init_values["convergence_state"]['cyclethreshold'][imid] = imdetails['converge']['major']['cyclethreshold']
2827
-
2828
- return (stopdesc, stopcode, majordone, majorleft, iterleft)
2829
-
2830
-
2831
- self._clean['cmds'] = []
2832
-
2833
- stopdesc, stopcode, majordone, majorleft, iterleft = initialize_tclean(self._clean['gclean'])
2834
-
2835
-
2836
- self._clean['last-success'] = dict( result='converged' if stopcode[0] else 'update', stopcode=stopcode, cmd=self._clean['cmds'],
2837
- convergence=self._init_values["convergence_state"]['convergence'],
2838
- iterdone=0, iterleft=iterleft,
2839
- majordone=majordone, majorleft=majorleft,
2840
- cyclethreshold=self._init_values["convergence_state"]['cyclethreshold'],
2841
- stopdesc=stopdesc )
2842
-
2843
- ### Must occur AFTER initial "next" call to gclean(s)
2844
- self._init_pipes()
2845
-
2846
- @asynccontextmanager
2847
- async def serve( self ):
2848
- '''This function is intended for developers who would like to embed interactive
2849
- clean as a part of a larger GUI. This embedded use of interactive clean is not
2850
- currently supported and would require the addition of parameters to this function
2851
- as well as changes to the interactive clean implementation. However, this function
2852
- does expose the ``asyncio.Future`` that is used to signal completion of the
2853
- interactive cleaning operation, and it provides the coroutines which must be
2854
- managed by asyncio to make the interactive clean GUI responsive.
2855
-
2856
- Example:
2857
- Create ``iclean`` object, process events and retrieve result::
2858
-
2859
- ic = iclean( vis='refim_point_withline.ms', imagename='test', imsize=512,
2860
- cell='12.0arcsec', specmode='cube', interpolation='nearest', ... )
2861
- async def process_events( ):
2862
- async with ic.serve( ) as state:
2863
- await state[0]
2864
-
2865
- asyncio.run(process_events( ))
2866
- print( "Result:", ic.result( ) )
2867
-
2868
-
2869
- Returns
2870
- -------
2871
- (asyncio.Future, dictionary of coroutines)
2872
- '''
2873
- def start_http_server():
2874
- import http.server
2875
- import socketserver
2876
- PORT = self._http_port
2877
- DIRECTORY=self._webpage_path
2878
-
2879
- class Handler(http.server.SimpleHTTPRequestHandler):
2880
- def __init__(self, *args, **kwargs):
2881
- super().__init__(*args, directory=DIRECTORY, **kwargs)
2882
-
2883
- with socketserver.TCPServer(("", PORT), Handler) as httpd:
2884
- print("\nServing Interactive Clean webpage from local directory: ", DIRECTORY)
2885
- print("Use Control-C to stop Interactive clean.\n")
2886
- print("Copy and paste one of the below URLs into your browser (Chrome or Firefox) to view:")
2887
- print("http://localhost:"+str(PORT))
2888
- print("http://127.0.0.1:"+str(PORT))
2889
-
2890
- httpd.serve_forever()
2891
-
2892
- self._launch_gui( )
2893
-
2894
- async with CMC( *( [ ctx for img in self._clean_targets.keys( ) for ctx in
2895
- [
2896
- self._clean_targets[img]['gui']['cube'].serve(self.__stop),
2897
- ]
2898
- ] + [ websockets.serve( self._pipe['control'].process_messages,
2899
- self._pipe['control'].address[0],
2900
- self._pipe['control'].address[1] ),
2901
- websockets.serve( self._clean['converge']['pipe'].process_messages,
2902
- self._clean['converge']['pipe'].address[0],
2903
- self._clean['converge']['pipe'].address[1] ) ]
2904
- ) ):
2905
- self.__result_future = asyncio.Future( )
2906
- yield self.__result_future
2907
-
2908
- def __retrieve_result( self ):
2909
- '''If InteractiveClean had a return value, it would be filled in as part of the
2910
- GUI dialog between Python and JavaScript and this function would return it'''
2911
- if isinstance(self._error_result,Exception):
2912
- raise self._error_result
2913
- elif self._error_result is not None:
2914
- return self._error_result
2915
- return { k: v['converge'] for k,v in self._clean_targets.items( ) }
2916
-
2917
- def result( self ):
2918
- '''If InteractiveClean had a return value, it would be filled in as part of the
2919
- GUI dialog between Python and JavaScript and this function would return it'''
2920
- if self.__result_future is None:
2921
- raise RuntimeError( 'no interactive clean result is available' )
2922
-
2923
- if self.__result is None:
2924
- ### restore returns full return dictionary
2925
- self.__result_from_gui = self.__result_future.result( )
2926
- self.__result = self._clean['gclean'].restore( )
2927
-
2928
- return self.__result
2929
-
2930
- def masks( self ):
2931
- '''Retrieves the masks which were used with interactive clean.
2932
-
2933
- Returns
2934
- -------
2935
- The standard ``cubevis`` cube region dictionary which contains two elements
2936
- ``masks`` and ``polys``.
2937
-
2938
- The value of the ``masks`` element is a dictionary that is indexed by
2939
- tuples of ``(stokes,chan)`` and the value of each element is a list
2940
- whose elements describe the polygons drawn on the channel represented
2941
- by ``(stokes,chan)``. Each polygon description in this list has a
2942
- polygon index (``p``) and a x/y translation (``d``).
2943
-
2944
- The value of the ``polys`` element is a dictionary that is indexed by
2945
- polygon indexes. The value of each polygon index is a dictionary containing
2946
- ``type`` (whose value is either ``'rect'`` or ``'poly``) and ``geometry``
2947
- (whose value is a dictionary containing ``'xs'`` and ``'ys'`` (which are
2948
- the x and y coordinates that define the polygon).
2949
-
2950
- This can be converted to other formats with ``cubevis.utils.convert_masks``.
2951
- '''
2952
- return copy.deepcopy(self._mask_history) ## don't allow users to change history
2953
-
2954
- def history( self ):
2955
- '''Retrieves the commands used during the interactive clean session.
2956
-
2957
- Returns
2958
- -------
2959
- list[str] tclean calls made during the interactive clean session.
2960
- '''
2961
- return self._clean['gclean']._log( True )
2962
-
2963
- def _initialize_javascript( self ):
2964
- self._js = { ### initialize state
2965
- ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
2966
- ### -- document is used storing state --
2967
- ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
2968
- 'initialize': '''if ( ! document._casa_initialized ) {
2969
- document._casa_image_name = initial_image
2970
- document._casa_initialized = true
2971
- document._casa_window_closed = false
2972
- window.addEventListener( 'beforeunload',
2973
- function (e) {
2974
- // if the window is already closed this message is never
2975
- // delivered (unless interactive clean is called again then
2976
- // the event shows up in the newly created control pipe
2977
- if ( document._casa_window_closed == false ) {
2978
- ctrl_pipe.send( ids['stop'],
2979
- { action: 'stop', value: { } },
2980
- undefined ) } } )
2981
- }''',
2982
-
2983
- ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
2984
- ### -- flux_src._convergence_data is used to store the complete --
2985
- ### -- --
2986
- ### -- The "Insert here ..." code seems to be called when when the stokes plane is changed --
2987
- ### -- but there have been no tclean iterations yet... --
2988
- ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
2989
- 'update-converge': '''function update_convergence_single( target, data ) {
2990
- const pos = target.src.cur_chan
2991
- const imdata = data.get(pos[1]).get(pos[0])
2992
- // chan----------------^^^^^^ ^^^^^^----stokes
2993
- const iterations = imdata.iterations
2994
- const peakRes = imdata.peakRes
2995
- const cyclethreshold = imdata.cycleThresh
2996
- const modelFlux = imdata.modelFlux
2997
- const stopCode = imdata.stopCode
2998
- const stopDesc = imdata.stopCode.map( code => stopdescmap.has(code) ? stopdescmap.get(code): "" )
2999
- target.residual.data = { iterations, cyclethreshold, stopDesc, values: peakRes, type: Array(iterations.length).fill('residual') }
3000
- target.flux.data = { iterations, cyclethreshold, stopDesc, values: modelFlux, type: Array(iterations.length).fill('flux') }
3001
- target.cyclethreshold.data = { iterations, values: cyclethreshold }
3002
- }
3003
-
3004
- function update_convergence( recurse=false ) {
3005
- let convdata
3006
- if ( hasprop(document,'_casa_convergence_data') ) {
3007
- convdata = document._casa_convergence_data
3008
- } else {
3009
- if ( ! recurse ) {
3010
- ctrl.converge.pipe.send( ctrl.converge.id, { action: 'retrieve' },
3011
- (msg) => { if ( hasprop( msg.result, 'convergence' ) ) {
3012
- document._casa_convergence_data = { convergence: msg.result.convergence,
3013
- cyclethreshold: msg.result.cyclethreshold }
3014
- update_convergence(true)
3015
- } } )
3016
- } else { console.log( 'INTERNAL ERROR: fetching convergence data failed' ) }
3017
- return
3018
- }
3019
-
3020
- Object.entries(images_state).map(
3021
- ([k,v],i) => { update_convergence_single(v,convdata.convergence[k]) } )
3022
- }''',
3023
-
3024
- 'clean-refresh': '''function refresh( clean_msg ) {
3025
- const itobj = Object.entries(images_state)[0][1].iteration
3026
- let stokes = 0 // later we will receive the polarity
3027
- // from some widget mechanism...
3028
- if ( clean_msg !== undefined ) {
3029
- if ( 'iterleft' in clean_msg ) {
3030
- itobj.niter.value = '' + clean_msg['iterleft']
3031
- } else if ( clean_msg !== undefined && 'iterdone' in clean_msg ) {
3032
- const remaining = parseInt(itobj.niter.value) - parseInt(clean_msg['iterdone'])
3033
- itobj.niter.value = '' + (remaining < 0 ? 0 : remaining)
3034
- }
3035
-
3036
- if ( 'majorleft' in clean_msg ) {
3037
- itobj.nmajor.value = '' + clean_msg['majorleft']
3038
- } else if ( 'majordone' in clean_msg ) {
3039
- const nm = parseInt(itobj.nmajor.value)
3040
- if ( nm != -1 ) {
3041
- const remaining = nm - parseInt(clean_msg['majordone'])
3042
- itobj.nmajor.value = '' + (remaining < 0 ? 0 : remaining)
3043
- } else itobj.nmajor.value = '' + nm // nmajor == -1 implies do not consider nmajor in stop decision
3044
- }
3045
-
3046
- if ( hasprop(clean_msg,'convergence') && clean_msg.convergence != null ) {
3047
- document._casa_convergence_data = { convergence: clean_msg.convergence,
3048
- cyclethreshold: clean_msg.cyclethreshold }
3049
- }
3050
- }
3051
-
3052
- // All images must be updated... without this no images are updated
3053
- casalib.map( (im,state) => state.src.refresh( msg => {
3054
- if ( 'stats' in msg ) state.src.update_statistics( msg.stats )
3055
- } ), images_state )
3056
- // Update convergence plot...
3057
- update_convergence( )
3058
- }''',
3059
-
3060
- ###
3061
- ### enabling/disabling tools in imdetails['gui']['image']['fig'].toolbar.tools does not seem to not work
3062
- ### imdetails['gui']['image']['fig'].toolbar.tools.tool_name (e.g. "Box Select", "Lasso Select")
3063
- ###
3064
- ### By design, images_state[*].automask.*/images_state[*].iteration.* are singletons which only need
3065
- ### to be disabled once...
3066
- ###
3067
- 'clean-disable': '''function disable( with_stop ) {
3068
- const amobj = Object.entries(images_state)[0][1].automask
3069
- Object.entries(amobj).map(
3070
- ([k,v],i) => { if ( hasprop(v,'disabled') ) v.disabled = true } )
3071
- const itobj = Object.entries(images_state)[0][1].iteration
3072
- Object.entries(itobj).map(
3073
- ([k,v],i) => { if ( hasprop(v,'disabled') ) v.disabled = true } )
3074
- Object.entries(images_state).map(
3075
- ([k,v],i) => {
3076
- v.img.disabled = true
3077
- if ( v.spectrum ) v.spectrum.disabled = true
3078
- v.src.disable_masking( )
3079
- v.src.disable_pixel_update( )
3080
- Object.entries(v.navi).map(
3081
- ([k1,v1],i1) => { if ( hasprop(v1,'disabled') ) v1.disabled = true }
3082
- )
3083
- }
3084
- )
3085
- clean_ctrl.continue.disabled = true
3086
- clean_ctrl.finish.disabled = true
3087
- clean_ctrl.stop.disabled = with_stop
3088
- }''',
3089
-
3090
- 'clean-enable': '''function enable( only_stop ) {
3091
- const amobj = Object.entries(images_state)[0][1].automask
3092
- Object.entries(amobj).map(
3093
- ([k,v],i) => { if ( hasprop(v,'disabled') ) v.disabled = false } )
3094
- const itobj = Object.entries(images_state)[0][1].iteration
3095
- Object.entries(itobj).map(
3096
- ([k,v],i) => { if ( hasprop(v,'disabled') ) v.disabled = false } )
3097
- Object.entries(images_state).map(
3098
- ([k,v],i) => {
3099
- v.img.disabled = false
3100
- if ( v.spectrum ) v.spectrum.disabled = false
3101
- v.src.enable_masking( )
3102
- v.src.enable_pixel_update( )
3103
- Object.entries(v.navi).map(
3104
- ([k1,v1],i) => { if ( hasprop(v1,'disabled') ) v1.disabled = false } )
3105
- } )
3106
-
3107
- clean_ctrl.stop.disabled = false
3108
- if ( ! only_stop ) {
3109
- clean_ctrl.continue.disabled = false
3110
- clean_ctrl.finish.disabled = false
3111
- }
3112
- }''',
3113
-
3114
-
3115
- 'clean-status-update': '''function update_status( status ) {
3116
- const stopstr = [ 'Zero stop code',
3117
- 'Iteration limit hit',
3118
- 'Force stop',
3119
- 'No change in peak residual across two major cycles',
3120
- 'Peak residual increased by 3x from last major cycle',
3121
- 'Peak residual increased by 3x from the minimum',
3122
- 'Zero mask found',
3123
- 'No mask found',
3124
- 'N-sigma or other valid exit criterion',
3125
- 'Stopping criteria encountered',
3126
- 'Unrecognized stop code' ]
3127
- if ( typeof status === 'number' ) {
3128
- images_state[document._casa_image_name]['status'].text =
3129
- '<p>' +
3130
- stopstr[ status < 0 || status >= stopstr.length ?
3131
- stopstr.length - 1 : status ] +
3132
- '</p>'
3133
- } else {
3134
- images_state[document._casa_image_name]['status'].text = `<p>${status}</p>`
3135
- }
3136
- }''',
3137
-
3138
- 'iter-gui-update': '''function get_update_dictionary( ) {
3139
- //const amste = images_state[document._casa_image_name]['automask']
3140
- //const clste = images_state[document._casa_image_name]['iteration']
3141
- // Assumption is that there is ONE set of iteration and automask updates
3142
- // for ALL imaging fields...
3143
- const amobj = Object.entries(images_state)[0][1].automask
3144
- const automask = amobj.active ?
3145
- Object.entries(amobj).reduce(
3146
- (acc,[k1,v1]) => { if ( hasprop(v1,'value') ) acc[k1] = v1.value; return acc },
3147
- { dogrowprune: amobj.dogrowprune.active,
3148
- fastnoise: amobj.fastnoise.active,
3149
- active: true }
3150
- ) : { }
3151
- const itobj = Object.entries(images_state)[0][1].iteration
3152
- const iteration = Object.entries(itobj).reduce(
3153
- (acc,[k1,v1]) => { if ( hasprop(v1,'value') ) acc[k1] = v1.value; return acc },
3154
- { }
3155
- )
3156
-
3157
- const masks = Object.entries(images_state).reduce( (acc,[k,v]) => { acc[k] = v.src.masks( ); return acc }, { } )
3158
- const breadcrumbs = Object.entries(images_state).reduce( (acc,[k,v]) => { acc[k] = v.src.breadcrumbs( ); return acc }, { } )
3159
- return { iteration, automask, masks, breadcrumbs, current_image: document._casa_image_name }
3160
- }
3161
- function update_log( log_lines ) {
3162
- let b = logbutton
3163
- b._log = b._log.concat( log_lines )
3164
- if ( b._window && ! b._window.closed ) {
3165
- for ( const line of log_lines ) {
3166
- const p = b._window.document.createElement('p')
3167
- p.appendChild( b._window.document.createTextNode(line) )
3168
- b._window.document.body.appendChild(p)
3169
- }
3170
- }
3171
- }
3172
- function update_gui( msg ) {
3173
- const itobj = Object.entries(images_state)[0][1].iteration
3174
- if ( msg.result === 'error' ) {
3175
- // ************************************************************************************
3176
- // ******** error occurs and is signaled by _gclean, e.g. exception in gclean ********
3177
- // ************************************************************************************
3178
- state.mode = 'interactive'
3179
- clean_ctrl.stop.button_type = "danger"
3180
- enable(false)
3181
- state.stopped = false
3182
- update_status( msg.stopdesc ? msg.stopdesc : 'An internal error has occurred' )
3183
- if ( 'cmd' in msg ) {
3184
- update_log( msg.cmd )
3185
- }
3186
- } else if ( msg.result === 'no-action' ) {
3187
- update_status( msg.stopdesc ? msg.stopdesc : 'nothing done' )
3188
- enable( false )
3189
- if ( 'cmd' in msg ) {
3190
- update_log( msg.cmd )
3191
- }
3192
- } else if ( msg.result == 'converged' ) {
3193
- state.mode = 'interactive'
3194
- clean_ctrl.stop.button_type = "danger"
3195
- enable(false)
3196
- state.stopped = false
3197
- update_status( msg.stopdesc ? msg.stopdesc : 'stopping criteria reached' )
3198
- if ( 'cmd' in msg ) {
3199
- update_log( msg.cmd )
3200
- }
3201
- refresh( msg )
3202
- } else if ( msg.result === 'update' ) {
3203
- if ( 'cmd' in msg ) {
3204
- update_log( msg.cmd )
3205
- }
3206
- refresh( msg )
3207
- // stopcode[0] == 1: iteration limit hit
3208
- // stopcode[0] == 9: major cycle limit hit
3209
- // *******************************************************************************************
3210
- // ******** perhaps the user should not be locked into exiting after the limit is hit ********
3211
- // *******************************************************************************************
3212
- //state.stopped = state.stopped || (msg.stopcode[0] > 1 && msg.stopcode[0] < 9) || msg.stopcode[0] == 0
3213
- state.stopped = false
3214
- if ( state.mode === 'interactive' && ! state.awaiting_stop ) {
3215
- clean_ctrl.stop.button_type = "danger"
3216
- update_status( msg.stopdesc ? msg.stopdesc : 'stopcode' in msg ? msg.stopcode[0] : -1 )
3217
- if ( ! state.stopped ) {
3218
- enable( false )
3219
- } else {
3220
- disable( false )
3221
- }
3222
- } else if ( state.mode === 'continuous' && ! state.awaiting_stop ) {
3223
- if ( ! state.stopped && itobj.niter.value > 0 && (itobj.nmajor.value > 0 || itobj.nmajor.value == -1) ) {
3224
- // *******************************************************************************************
3225
- // ******** 'niter.value > 0 so continue with one more iteration ********
3226
- // ******** 'nmajor.value' == -1 implies do not consider nmajor in stop consideration ********
3227
- // *******************************************************************************************
3228
- ctrl_pipe.send( ids[cb_obj.origin.name],
3229
- { action: 'finish',
3230
- value: get_update_dictionary( ) },
3231
- update_gui )
3232
- } else if ( ! state.stopped ) {
3233
- // *******************************************************************************************
3234
- // ******** 'niter.value <= 0 so iteration should stop ********
3235
- // *******************************************************************************************
3236
- state.mode = 'interactive'
3237
- clean_ctrl.stop.button_type = "danger"
3238
- enable(false)
3239
- state.stopped = false
3240
- update_status( msg.stopdesc ? msg.stopdesc : 'stopping criteria reached' )
3241
- } else {
3242
- state.mode = 'interactive'
3243
- clean_ctrl.stop.button_type = "danger"
3244
- enable(false)
3245
- state.stopped = false
3246
- update_status( msg.stopdesc ? msg.stopdesc : 'stopcode' in msg ? msg.stopcode[0] : -1 )
3247
- }
3248
- }
3249
- } else if ( msg.result === 'error' ) {
3250
- img_src.drop_breadcrumb('E')
3251
- if ( 'cmd' in msg ) {
3252
- update_log( msg.cmd )
3253
- }
3254
- state.mode = 'interactive'
3255
- clean_ctrl.stop.button_type = "danger"
3256
- state.stopped = false
3257
- update_status( 'stopcode' in msg ? msg.stopcode[0] : -1 )
3258
- enable( false )
3259
- }
3260
- }''',
3261
-
3262
- 'clean-wait': '''function wait_for_tclean_stop( msg ) {
3263
- state.mode = 'interactive'
3264
- clean_ctrl.stop.button_type = "danger"
3265
- enable( false )
3266
- state.awaiting_stop = false
3267
- update_status( 'user requested stop' )
3268
- }''',
3269
- }
1849
+ context = exe.Context( exe.Mode.SYNC )
1850
+ exec_task = self._ui( exe.Setting.CLI, context, "interactive-clean" )
1851
+ return context.execute( exec_task, "interactive-clean" )