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

@@ -0,0 +1,1499 @@
1
+ ########################################################################
2
+ #
3
+ #TASK XML> tclean -argfilter=interactive,fullsummary -argfilter:initParams=vis,imagename
4
+ # Copyright (C) 2022,2023,2024,2025
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
+ ########################################################################
29
+ '''implementation of the ``InteractiveCleanUI`` application for interactive control
30
+ of tclean'''
31
+
32
+ ###
33
+ ### Useful for debugging
34
+ ###
35
+ ###from cubevis.bokeh.state import initialize_bokeh
36
+ ###initialize_bokeh( bokehjs_subst=".../bokeh-3.2.2.js" )
37
+ ###
38
+
39
+ from pprint import pprint
40
+
41
+ import os
42
+ import sys
43
+ import copy
44
+ import asyncio
45
+ import shutil
46
+ import websockets
47
+ from os.path import basename, abspath, exists, join
48
+ from uuid import uuid4
49
+ from html import escape as html_escape
50
+ from contextlib import asynccontextmanager
51
+ from bokeh.models import Button, TextInput, Checkbox, Div, LinearAxis, CustomJS, Spacer, Span, HoverTool, DataRange1d, Step, InlineStyleSheet
52
+ from bokeh.events import ModelEvent, MouseEnter
53
+ from bokeh.models import TabPanel, Tabs
54
+ from bokeh.plotting import ColumnDataSource, figure, show
55
+ from bokeh.layouts import column, row, layout
56
+ from bokeh.io import reset_output as reset_bokeh_output, output_notebook
57
+ from bokeh.models.dom import HTML
58
+
59
+ from bokeh.models.ui.tooltips import Tooltip
60
+ from cubevis.bokeh.models import TipButton, Tip, EvTextInput
61
+ from cubevis.utils import resource_manager, reset_resource_manager, is_interactive_jupyter, find_pkg, load_pkg
62
+ from cubevis.utils import ContextMgrChain as CMC
63
+
64
+ # pylint: disable=no-name-in-module
65
+ from casatasks.private.imagerhelpers.imager_return_dict import ImagingDict
66
+
67
+ from casatasks.private.imagerhelpers.input_parameters import ImagerParameters
68
+ # pylint: enable=no-name-in-module
69
+
70
+ from cubevis.utils import find_ws_address, convert_masks
71
+ from cubevis.toolbox import CubeMask, AppContext
72
+ from cubevis.bokeh.utils import svg_icon
73
+ from cubevis.bokeh.sources import DataPipe
74
+ from cubevis.utils import DocEnum
75
+ from cubevis import exe
76
+
77
+ from ._interactiveclean_wrappers import SharedWidgets
78
+
79
+ USE_MULTIPLE_GCLEAN_HACK=False
80
+
81
+ class InteractiveCleanUI:
82
+ '''InteractiveCleanUI(...) implements interactive clean using Bokeh
83
+ '''
84
+ def __stop( self, _=None ):
85
+ self.__result_future.set_result(self.__retrieve_result( ))
86
+
87
+ def _abort_handler( self, err ):
88
+ self._error_result = err
89
+ self.__stop( )
90
+
91
+ def __reset( self ):
92
+ if self.__pipes_initialized:
93
+ self._pipe = { 'control': None }
94
+ self._clean = { 'converge': { 'state': { } }, 'last-success': None }
95
+ reset_bokeh_output( )
96
+ reset_resource_manager( )
97
+
98
+ ###
99
+ ### reset asyncio result future
100
+ ###
101
+ self.__result_future = None
102
+
103
+ ###
104
+ ### used by data pipe (websocket) initialization function
105
+ ###
106
+ self.__pipes_initialized = False
107
+
108
+ ###
109
+ ### error or exception result
110
+ ###
111
+ self._error_result = None
112
+
113
+ ###
114
+ ### iclean results
115
+ ###
116
+ self.__result = None
117
+ self.__result_from_gui = None
118
+
119
+ '''
120
+ _gen_port_fwd_cmd()
121
+
122
+ Create an SSH port-forwarding command to create the tunnels necessary for remote connection.
123
+ NOTE: This assumes that the same remote ports are also available locally - which may
124
+ NOT always be true.
125
+ '''
126
+ def _gen_port_fwd_cmd(self):
127
+ hostname = os.uname()[1]
128
+
129
+ ###
130
+ ### need to add extra cube ports here for multifield imaging
131
+ ###
132
+ ports = [ self._pipe['control'].address[1], self._clean['converge']['pipe'].address[1] ]
133
+
134
+ for imid, imdetails in self._clean_targets.items( ):
135
+ ports.append( imdetails['gui']['cube']._pipe['image'].address[1] )
136
+ ports.append( imdetails['gui']['cube']._pipe['control'].address[1] )
137
+
138
+ # Also forward http port if serving webpage
139
+ if not self._is_notebook:
140
+ ports.append(self._http_port)
141
+
142
+ cmd = 'ssh'
143
+ for port in ports:
144
+ cmd += (' -L ' + str(port) + ':localhost:' + str(port))
145
+
146
+ cmd += ' ' + str(hostname)
147
+ return cmd
148
+
149
+ def _residual_path( self, gclean, imid ):
150
+ if self._clean['gclean_paths'] is None:
151
+ raise RuntimeError( f'''gclean paths are not available for {imid}''' )
152
+ for p in self._clean['gclean_paths']:
153
+ if p['name'] == imid:
154
+ return f"{p['imagepath']}/{p['residualname']}"
155
+ raise RuntimeError( f'''gclean residual path not found for {imid}''' )
156
+
157
+ def _mask_path( self, gclean, imid ):
158
+ if self._clean['gclean_paths'] is None:
159
+ raise RuntimeError( f'''gclean paths are not available for {imid}''' )
160
+ for p in self._clean['gclean_paths']:
161
+ if p['name'] == imid:
162
+ return f"{p['imagepath']}/{p['maskname']}"
163
+ raise RuntimeError( f'''gclean mask path not found for {imid}''' )
164
+
165
+ def __init__( self, gclean, user_args ):
166
+
167
+ ###
168
+ ### With Bokeh 3.2.2, the spectrum and convergence plots extend beyond the edge of the
169
+ ### browser window (requiring scrolling) if a width is not specified. It could be that
170
+ ### this should be computed from the width of the tabbed control area at the right of
171
+ ### the image display.
172
+ ###
173
+ self._conv_spect_plot_width = 450
174
+ ###
175
+ ### Create application context (which includes a temporary directory).
176
+ ### This sets the title of the plot.
177
+ ###
178
+ self._app_state = AppContext( 'Interactive Clean' )
179
+
180
+ ###
181
+ ### Whether or not the Interactive Clean session is running remotely
182
+ ###
183
+ #self._is_remote = remote
184
+ self._is_remote = False
185
+
186
+ ###
187
+ ### whether or not the session is being run from a jupyter notebook or script
188
+ ###
189
+ self._is_notebook = is_interactive_jupyter()
190
+
191
+ ##
192
+ ## the http port for serving GUI in webpage if not running in script
193
+ ##
194
+ self._http_port = None
195
+
196
+ ###
197
+ ### the asyncio future that is used to transmit the result from interactive clean
198
+ ###
199
+ self.__result_future = None
200
+
201
+ ###
202
+ ### This is used to tell whether the websockets have been initialized, but also to
203
+ ### indicate if __call__ is being called multiple times to allow for resetting Bokeh
204
+ ###
205
+ self.__pipes_initialized = False
206
+
207
+ ###
208
+ ### State required to manage iteration control
209
+ ###
210
+ self._control = { 'iteration': { } }
211
+
212
+ ###
213
+ ### color specs
214
+ ###
215
+ self._converge_color = { 'residual': 'black',
216
+ 'flux': 'forestgreen' }
217
+
218
+ ###
219
+ ### widgets shared across image tabs (multifield imaging)
220
+ ###
221
+ self._cube_palette = None
222
+ self._image_bitmask_controls = None
223
+
224
+ ###
225
+ ### String which indicates the changes applied to the mask to indicte when
226
+ ### the mask has changed... however, THIS IS NO LONGER USED
227
+ ### It could be removed, but it adds minor overhead and would be
228
+ ### DIFFICULT to add back in the future
229
+ ###
230
+ self._last_mask_breadcrumbs = ''
231
+
232
+ ###
233
+ ### Set up dictionary of javascript code snippets
234
+ ###
235
+ self._initialize_javascript( )
236
+
237
+ self._pipe = { 'control': None }
238
+ self._clean = { 'converge': { 'state': { } }, 'last-success': None }
239
+
240
+ ###
241
+ ### create clean interface -- final version will have only one gclean object
242
+ ###
243
+ self._init_values = { "deconvolver": user_args['deconvolver'], ### used by _residual_path( )
244
+ "cycleniter": user_args['cycleniter'], ### used by _setup( )
245
+ "threshold": user_args['threshold'], ### used by _setup( )
246
+ "cyclefactor": user_args['cyclefactor'], ### used by _setup( )
247
+ "gain": user_args['gain'], ### used by _setup( )
248
+ "nsigma": user_args['nsigma'], ### used by _setup( )
249
+ "convergence_state": { 'convergence': {}, ### shares state between
250
+ 'cyclethreshold': {} } } ### __init__( ) and _setup( )
251
+
252
+ self._clean['gclean'] = gclean
253
+
254
+ self._clean['gclean_paths'] = { prod['name']: prod for prod in self._clean['gclean'].image_products( ) }
255
+ self._clean['imid'] = [ name for name,prod in self._clean['gclean_paths'].items( ) ]
256
+ self._clean_targets = { id: { } for id in self._clean['imid'] }
257
+ self._clean['gclean_rest'] = [ ]
258
+ self._initial_clean_params = { }
259
+
260
+ imagename = self._clean['imid'][0]
261
+
262
+ # Create folder for the generated html webpage - needs its own folder to not name conflict (must be 'index.html')
263
+ webpage_dirname = imagename + '_webpage'
264
+ ### Directory is created when an HTTP server is running
265
+ ### (MAX)
266
+ # if not os.path.isdir(webpage_dirname):
267
+ # os.makedirs(webpage_dirname)
268
+ self._webpage_path = os.path.abspath(webpage_dirname)
269
+
270
+ for imid, imdetails in self._clean_targets.items( ):
271
+ self._clean['imid'].append(imid)
272
+
273
+ ###
274
+ ### Residual path...
275
+ ###
276
+ if 'path' not in imdetails: imdetails['path'] = { }
277
+ output_dir = self._clean['gclean_paths'][imid]['imagepath']
278
+ imdetails['path']['residual'] = join( output_dir, self._clean['gclean_paths'][imid]['residualname'] )
279
+ imdetails['path']['mask'] = join( output_dir, self._clean['gclean_paths'][imid]['maskname'] )
280
+
281
+ for idx, (imid, imdetails) in enumerate(self._clean_targets.items( )):
282
+ imdetails['gui'] = { }
283
+
284
+ imdetails['gui'] = { 'params': { 'iteration': { }, 'automask': { } },
285
+ 'image': { },
286
+ 'image-adjust': { } }
287
+
288
+ ###
289
+ ### Only the first image should initialize the initial convergence state
290
+ ###
291
+ imdetails['gui']['cube'] = CubeMask( imdetails['path']['residual'], mask=imdetails['path']['mask'], abort=self._abort_handler,
292
+ init_script=CustomJS( args=dict( initial_convergence_state=self._init_values["convergence_state"],
293
+ name=imid ),
294
+ code='''document._casa_convergence_data = initial_convergence_state''' )
295
+ if idx == 0 else None )
296
+
297
+ ###
298
+ ### Auto Masking Parameters
299
+ ###
300
+ imdetails['params'] = { }
301
+ imdetails['params']['am'] = { }
302
+ imdetails['params']['am']['usemask'] = user_args['usemask']
303
+ imdetails['params']['am']['noisethreshold'] = user_args['noisethreshold']
304
+ imdetails['params']['am']['sidelobethreshold'] = user_args['sidelobethreshold']
305
+ imdetails['params']['am']['lownoisethreshold'] = user_args['lownoisethreshold']
306
+ imdetails['params']['am']['minbeamfrac'] = user_args['minbeamfrac']
307
+ imdetails['params']['am']['negativethreshold'] = user_args['negativethreshold']
308
+ imdetails['params']['am']['dogrowprune'] = user_args['dogrowprune']
309
+ imdetails['params']['am']['fastnoise'] = user_args['fastnoise']
310
+
311
+ def _init_pipes( self ):
312
+ if not self.__pipes_initialized:
313
+ self.__pipes_initialized = True
314
+ self._pipe['control'] = DataPipe( address=find_ws_address( ), abort=self._abort_handler )
315
+ ###
316
+ ### One pipe for updating the convergence plots.
317
+ ###
318
+ self._clean['converge'] = { 'state': None }
319
+ self._clean['converge']['pipe'] = DataPipe( address=find_ws_address( ), abort=self._abort_handler )
320
+ self._clean['converge']['id'] = str(uuid4( ))
321
+
322
+
323
+
324
+ # Get port for serving HTTP server if running in script
325
+ self._http_port = find_ws_address("")[1]
326
+ for imid, imdetails in self._clean_targets.items( ):
327
+ imdetails['gui']['cube']._init_pipes( )
328
+
329
+ def _create_convergence_gui( self, imdetails, orient='horizontal', sizing_mode='stretch_width', **kw ):
330
+ TOOLTIPS='''<div>
331
+ <div>
332
+ <span style="font-weight: bold;">@type</span>
333
+ <span>@values</span>
334
+ </div>
335
+ <div>
336
+ <span style="font-weight: bold; font-size: 10px">cycle threshold</span>
337
+ <span>@cyclethreshold</span>
338
+ </div>
339
+ <div>
340
+ <span style="font-weight: bold; font-size: 10px">stop</span>
341
+ <span>@stopDesc</span>
342
+ </div>
343
+ </div>'''
344
+
345
+ hover = HoverTool( tooltips=TOOLTIPS )
346
+ imdetails['gui']['convergence'] = figure( sizing_mode=sizing_mode, y_axis_location="right",
347
+ tools=[ hover ], toolbar_location=None, **kw )
348
+
349
+ if orient == 'vertical':
350
+ imdetails['gui']['convergence'].yaxis.axis_label='Iteration (cycle threshold dotted red)'
351
+ imdetails['gui']['convergence'].xaxis.axis_label='Peak Residual'
352
+ imdetails['gui']['convergence'].extra_x_ranges = { 'residual_range': DataRange1d( follow='end' ),
353
+ 'flux_range': DataRange1d( follow='end' ) }
354
+
355
+ imdetails['gui']['convergence'].step( 'values', 'iterations', source=imdetails['converge-data']['cyclethreshold'],
356
+ line_color='red', x_range_name='residual_range', line_dash='dotted', line_width=2 )
357
+ imdetails['gui']['convergence'].line( 'values', 'iterations', source=imdetails['converge-data']['residual'],
358
+ line_color=self._converge_color['residual'], x_range_name='residual_range' )
359
+ imdetails['gui']['convergence'].scatter( 'values', 'iterations', source=imdetails['converge-data']['residual'],
360
+ color=self._converge_color['residual'], x_range_name='residual_range',size=10 )
361
+ imdetails['gui']['convergence'].line( 'values', 'iterations', source=imdetails['converge-data']['flux'],
362
+ line_color=self._converge_color['flux'], x_range_name='flux_range' )
363
+ imdetails['gui']['convergence'].scatter( 'values', 'iterations', source=imdetails['converge-data']['flux'],
364
+ color=self._converge_color['flux'], x_range_name='flux_range', size=10 )
365
+
366
+ imdetails['gui']['convergence'].add_layout( LinearAxis( x_range_name='flux_range', axis_label='Total Flux',
367
+ axis_line_color=self._converge_color['flux'],
368
+ major_label_text_color=self._converge_color['flux'],
369
+ axis_label_text_color=self._converge_color['flux'],
370
+ major_tick_line_color=self._converge_color['flux'],
371
+ minor_tick_line_color=self._converge_color['flux'] ), 'above')
372
+
373
+ else:
374
+ imdetails['gui']['convergence'].xaxis.axis_label='Iteration (cycle threshold dotted red)'
375
+ imdetails['gui']['convergence'].yaxis.axis_label='Peak Residual'
376
+ imdetails['gui']['convergence'].extra_y_ranges = { 'residual_range': DataRange1d( follow='end' ),
377
+ 'flux_range': DataRange1d( follow='end' ) }
378
+
379
+ imdetails['gui']['convergence'].step( 'iterations', 'values', source=imdetails['converge-data']['cyclethreshold'],
380
+ line_color='red', y_range_name='residual_range', line_dash='dotted', line_width=2 )
381
+ imdetails['gui']['convergence'].line( 'iterations', 'values', source=imdetails['converge-data']['residual'],
382
+ line_color=self._converge_color['residual'], y_range_name='residual_range' )
383
+ imdetails['gui']['convergence'].scatter( 'iterations', 'values', source=imdetails['converge-data']['residual'],
384
+ color=self._converge_color['residual'], y_range_name='residual_range',size=10 )
385
+ imdetails['gui']['convergence'].line( 'iterations', 'values', source=imdetails['converge-data']['flux'],
386
+ line_color=self._converge_color['flux'], y_range_name='flux_range' )
387
+ imdetails['gui']['convergence'].scatter( 'iterations', 'values', source=imdetails['converge-data']['flux'],
388
+ color=self._converge_color['flux'], y_range_name='flux_range', size=10 )
389
+
390
+ imdetails['gui']['convergence'].add_layout( LinearAxis( y_range_name='flux_range', axis_label='Total Flux',
391
+ axis_line_color=self._converge_color['flux'],
392
+ major_label_text_color=self._converge_color['flux'],
393
+ axis_label_text_color=self._converge_color['flux'],
394
+ major_tick_line_color=self._converge_color['flux'],
395
+ minor_tick_line_color=self._converge_color['flux'] ), 'right')
396
+
397
+
398
+ def _launch_gui( self ):
399
+ '''create and show GUI
400
+ '''
401
+ ###
402
+ ### Will contain the top level GUI
403
+ ###
404
+ self._fig = { }
405
+
406
+ ###
407
+ ### Python-side handler for events from the interactive clean control buttons
408
+ ###
409
+ async def clean_handler( msg, self=self ):
410
+ if msg['action'] == 'next' or msg['action'] == 'finish':
411
+
412
+ if 'mask' in msg['value']:
413
+ ###
414
+ ### >>HERE>> breadcrumbs must be specific to the field they are related to...
415
+ ###
416
+ if 'breadcrumbs' in msg['value'] and msg['value']['breadcrumbs'] is not None and msg['value']['breadcrumbs'] != self._last_mask_breadcrumbs:
417
+ self._last_mask_breadcrumbs = msg['value']['breadcrumbs']
418
+ mask_dir = "%s.mask" % self._imagename
419
+ shutil.rmtree(mask_dir)
420
+ new_mask = imdetails['gui']['cube'].jsmask_to_raw(msg['value']['mask'])
421
+ self._mask_history.append(new_mask)
422
+
423
+ msg['value']['mask'] = convert_masks(masks=new_mask, coord='pixel', cdesc=imdetails['gui']['cube'].coorddesc())
424
+
425
+ else:
426
+ ##### seemingly the mask path used to be spliced in?
427
+ #msg['value']['mask'] = self._mask_path
428
+ pass
429
+ else:
430
+ ##### seemingly the mask path used to be spliced in?
431
+ #msg['value']['mask'] = self._mask_path
432
+ pass
433
+
434
+ ###
435
+ ### In the final implementation, there will only be one gclean object...
436
+ ###
437
+ convergence_state={ 'convergence': {}, 'cyclethreshold': {} }
438
+ err,errmsg = self._clean['gclean'].update( dict( **msg['value']['iteration'],
439
+ **msg['value']['automask'] ) )
440
+
441
+ iteration_limit = int(msg['value']['iteration']['niter'])
442
+ stopdesc, stopcode, majordone, majorleft, iterleft, self._convergence_data = await self._clean['gclean'].__anext__( )
443
+
444
+ clean_cmds = self._clean['gclean']._log( )
445
+
446
+ for key, value in self._convergence_data.items( ):
447
+
448
+ if len(value['chan']) == 0 or stopcode[0] == -1:
449
+ ### stopcode[0] == -1 indicates an error condition within gclean
450
+ return dict( result='error', stopcode=stopcode, cmd=clean_cmds,
451
+ convergence=None, majordone=majordone,
452
+ majorleft=majorleft, iterleft=iterleft, stopdesc=stopdesc )
453
+
454
+ convergence_state['convergence'][key] = value['chan']
455
+ convergence_state['cyclethreshold'][key] = value['major']['cyclethreshold']
456
+
457
+ ### stopcode[0] != 0 indicates that some stopping criteria has been reached
458
+ ### this will also catch errors as well as convergence
459
+ ### (so 'converged' isn't quite right...)
460
+ self._clean['last-success'] = dict( result='converged' if stopcode[0] else 'update', stopcode=stopcode, cmd=clean_cmds,
461
+ convergence=convergence_state['convergence'],
462
+ iterdone=iteration_limit - iterleft, iterleft=iterleft,
463
+ majordone=majordone, majorleft=majorleft, cyclethreshold=convergence_state['cyclethreshold'], stopdesc=stopdesc )
464
+ return self._clean['last-success']
465
+
466
+ elif msg['action'] == 'stop':
467
+ self.__stop( )
468
+ return dict( result='stopped', update=dict( ) )
469
+ elif msg['action'] == 'status':
470
+ return dict( result="ok", update=dict( ) )
471
+ else:
472
+ print( "got something else: '%s'" % msg['action'] )
473
+
474
+ ###
475
+ ### set up websockets which will be used for control and convergence updates
476
+ ###
477
+ self._init_pipes( )
478
+
479
+ ###
480
+ ### Setup id that will be used for messages from each button
481
+ ###
482
+ self._clean_ids = { }
483
+ for btn in "continue", 'finish', 'stop':
484
+ self._clean_ids[btn] = str(uuid4( ))
485
+ #print("%s: %s" % ( btn, self._clean_ids[btn] ) )
486
+ self._pipe['control'].register( self._clean_ids[btn], clean_handler )
487
+
488
+
489
+ ###
490
+ ### There is one set of tclean controls for all images/outlier/etc. because
491
+ ### in the final version gclean will handle the iterations for all fields...
492
+ ###
493
+ cwidth = 64
494
+ cheight = 40
495
+ self._control['iteration'] = { }
496
+ self._control['iteration']['continue'] = TipButton( max_width=cwidth, max_height=cheight, name='continue',
497
+ icon=svg_icon(icon_name="iclean-continue", size=18),
498
+ tooltip=Tooltip( content=HTML( '''Stop after <b>one major cycle</b> or when any stopping criteria is met.''' ), position='left') )
499
+ self._control['iteration']['finish'] = TipButton( max_width=cwidth, max_height=cheight, name='finish',
500
+ icon=svg_icon(icon_name="iclean-finish", size=18),
501
+ tooltip=Tooltip( content=HTML( '''<b>Continue</b> until some stopping criteria is met.''' ), position='left') )
502
+ self._control['iteration']['stop'] = TipButton( button_type="danger", max_width=cwidth, max_height=cheight, name='stop',
503
+ icon=svg_icon(icon_name="iclean-stop", size=18),
504
+ 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' ) )
505
+
506
+ ###
507
+ ### The single SHARED help button will be supplied by the first CubeMask...
508
+ ###
509
+ help_button = None
510
+ ###
511
+ ### First status line will be reused...
512
+ ###
513
+ status_line = None
514
+
515
+ ###
516
+ ### Manage the widgets which are shared between tabs...
517
+ ###
518
+ icw = SharedWidgets( )
519
+ toolbars = [ ]
520
+ for imid, imdetails in self._clean_targets.items( ):
521
+ imdetails['gui']['stats'] = imdetails['gui']['cube'].statistics( )
522
+ imdetails['image-channels'] = imdetails['gui']['cube'].shape( )[3]
523
+
524
+ 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 )
525
+
526
+ ###
527
+ ### Retrieve convergence information
528
+ ###
529
+ def convergence_handler( msg, self=self, imid=imid ):
530
+ if msg['action'] == 'retrieve':
531
+ return { 'result': self._clean['last-success'] }
532
+ else:
533
+ return { 'result': None, 'error': 'unrecognized action' }
534
+
535
+ self._clean['converge']['pipe'].register( self._clean['converge']['id'], convergence_handler )
536
+
537
+ ###
538
+ ### Data source that will be used for updating the convergence plot
539
+ ###
540
+ stokes = 0
541
+ convergence = imdetails['converge']['chan'][0][stokes]
542
+ imdetails['converge-data'] = { }
543
+ imdetails['converge-data']['flux'] = ColumnDataSource( data=dict( values=convergence['modelFlux'], iterations=convergence['iterations'],
544
+ cyclethreshold=convergence['cycleThresh'],
545
+ stopDesc=list( map( ImagingDict.get_summaryminor_stopdesc, convergence['stopCode'] ) ),
546
+ type=['flux'] * len(convergence['iterations']) ) )
547
+ imdetails['converge-data']['residual'] = ColumnDataSource( data=dict( values=convergence['peakRes'], iterations=convergence['iterations'],
548
+ cyclethreshold=convergence['cycleThresh'],
549
+ stopDesc=list( map( ImagingDict.get_summaryminor_stopdesc, convergence['stopCode'] ) ),
550
+ type=['residual'] * len(convergence['iterations'])) )
551
+ imdetails['converge-data']['cyclethreshold'] = ColumnDataSource( data=dict( values=convergence['cycleThresh'], iterations=convergence['iterations'] ) )
552
+
553
+
554
+ ###
555
+ ### help page for cube interactions
556
+ ###
557
+ if help_button is None:
558
+ 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>',
559
+ '<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' )
560
+
561
+ self._create_convergence_gui( imdetails, orient='horizontal', sizing_mode='stretch_height', width=self._conv_spect_plot_width )
562
+
563
+ imdetails['gui']['params']['iteration']['nmajor'] = icw.nmajor( title='nmajor', value="%s" % self._initial_clean_params['nmajor'], width=90 )
564
+ imdetails['gui']['params']['iteration']['niter'] = icw.niter( title='niter', value="%s" % self._initial_clean_params['niter'], width=90 )
565
+ imdetails['gui']['params']['iteration']['cycleniter'] = icw.cycleniter( title="cycleniter", value="%s" % self._initial_clean_params['cycleniter'], width=90 )
566
+ imdetails['gui']['params']['iteration']['threshold'] = icw.threshold( title="threshold", value="%s" % self._initial_clean_params['threshold'], width=90 )
567
+ imdetails['gui']['params']['iteration']['cyclefactor'] = icw.cyclefactor( value="%s" % self._initial_clean_params['cyclefactor'], title="cyclefactor", width=90 )
568
+ imdetails['gui']['params']['iteration']['gain'] = icw.gain( title='gain', value="%s" % self._initial_clean_params['gain'], width=90 )
569
+ imdetails['gui']['params']['iteration']['nsigma'] = icw.nsigma( title='nsigma', value="%s" % self._initial_clean_params['nsigma'], width=90 )
570
+
571
+ if imdetails['params']['am']['usemask'] == 'auto-multithresh':
572
+ ###
573
+ ### Currently automasking tab is only available when the user selects 'auto-multithresh'
574
+ ###
575
+ imdetails['gui']['params']['automask']['active'] = True
576
+ imdetails['gui']['params']['automask']['noisethreshold'] = icw.noisethreshold( title='noisethreshold', value="%s" % imdetails['params']['am']['noisethreshold'], margin=( 5, 25, 5, 5 ), width=90 )
577
+ imdetails['gui']['params']['automask']['sidelobethreshold'] = icw.sidelobethreshold( title='sidelobethreshold', value="%s" % imdetails['params']['am']['sidelobethreshold'], margin=( 5, 25, 5, 5 ), width=90 )
578
+ imdetails['gui']['params']['automask']['lownoisethreshold'] = icw.lownoisethreshold( title='lownoisethreshold', value="%s" % imdetails['params']['am']['lownoisethreshold'], margin=( 5, 25, 5, 5 ), width=90 )
579
+ imdetails['gui']['params']['automask']['minbeamfrac'] = icw.minbeamfrac( title='minbeamfrac', value="%s" % imdetails['params']['am']['minbeamfrac'], width=90 )
580
+ imdetails['gui']['params']['automask']['negativethreshold'] = icw.negativethreshold( title='negativethreshold', value="%s" % imdetails['params']['am']['negativethreshold'], margin=( 5, 25, 5, 5 ), width=90 )
581
+ imdetails['gui']['params']['automask']['dogrowprune'] = icw.dogrowprune( label='dogrowprune', active=imdetails['params']['am']['dogrowprune'], margin=( 15, 25, 5, 5 ) )
582
+ imdetails['gui']['params']['automask']['fastnoise'] = icw.fastnoise( label='fastnoise', active=imdetails['params']['am']['fastnoise'], margin=( 15, 25, 5, 5 ) )
583
+
584
+
585
+ imdetails['gui']['image']['src'] = imdetails['gui']['cube'].js_obj( )
586
+ imdetails['gui']['image']['fig'] = imdetails['gui']['cube'].image( grid=False, height_policy='max', width_policy='max',
587
+ channelcb=CustomJS( args=dict( img_state={ 'src': imdetails['gui']['image']['src'],
588
+ 'flux': imdetails['converge-data']['flux'],
589
+ 'residual': imdetails['converge-data']['residual'],
590
+ 'cyclethreshold': imdetails['converge-data']['cyclethreshold'] },
591
+ imid=imid,
592
+ ctrl={ 'converge': self._clean['converge'] },
593
+ stopdescmap=ImagingDict.get_summaryminor_stopdesc( ) ),
594
+ code=self._js['update-converge'] +
595
+ '''update_convergence_single( img_state, document._casa_convergence_data.convergence[imid] )''' ) )
596
+
597
+ ###
598
+ ### collect toolbars for syncing selection
599
+ ###
600
+ toolbars.append(imdetails['gui']['image']['fig'].toolbar)
601
+
602
+ ###
603
+ ### spectrum plot must be disabled during iteration due to "tap to change channel" functionality
604
+ ###
605
+ if imdetails['image-channels'] > 1:
606
+ imdetails['gui']['spectrum'] = imdetails['gui']['cube'].spectrum( orient='vertical', sizing_mode='stretch_height', width=self._conv_spect_plot_width )
607
+ imdetails['gui']['slider'] = imdetails['gui']['cube'].slider( show_value=False, title='', margin=(14,5,5,5), sizing_mode="scale_width" )
608
+ imdetails['gui']['goto'] = imdetails['gui']['cube'].goto( )
609
+ else:
610
+ imdetails['gui']['spectrum'] = None
611
+ imdetails['gui']['slider'] = None
612
+ imdetails['gui']['goto'] = None
613
+
614
+ imdetails['gui']['channel-ctrl'] = imdetails['gui']['cube'].channel_ctrl( )
615
+
616
+ imdetails['gui']['cursor-pixel-text'] = imdetails['gui']['cube'].pixel_tracking_text( margin=(-3, 5, 3, 30) )
617
+
618
+ self._image_bitmask_controls = imdetails['gui']['cube'].bitmask_ctrl( reuse=self._image_bitmask_controls, button_type='light' )
619
+
620
+ if imdetails['params']['am']['usemask'] == 'auto-multithresh':
621
+ imdetails['gui']['auto-masking-panel'] = [ TabPanel( child=column( row( Tip( imdetails['gui']['params']['automask']['noisethreshold'],
622
+ tooltip=Tooltip( content=HTML( 'sets the signal-to-noise threshold above which significant emission is masked during the initial round of mask creation' ),
623
+ position='bottom' ) ),
624
+ Tip( imdetails['gui']['params']['automask']['sidelobethreshold'],
625
+ 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' ),
626
+ position='bottom' ) ),
627
+ Tip( imdetails['gui']['params']['automask']['minbeamfrac'],
628
+ tooltip=Tooltip( content=HTML( 'sets the minimum size a region must be to be retained in the mask' ),
629
+ position='bottom' ) ) ),
630
+ row( Tip( imdetails['gui']['params']['automask']['lownoisethreshold'],
631
+ 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' ),
632
+ position='bottom' ) ),
633
+ Tip( imdetails['gui']['params']['automask']['negativethreshold'],
634
+ tooltip=Tooltip( content=HTML( 'sets the signal-to-noise threshold for absorption features to be masked' ),
635
+ position='bottom' ) ) ),
636
+ row( Tip( imdetails['gui']['params']['automask']['dogrowprune'],
637
+ 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' ),
638
+ position='bottom' ) ),
639
+ Tip( imdetails['gui']['params']['automask']['fastnoise'],
640
+ tooltip=Tooltip( content=HTML( 'When set to True, a simpler but faster noise calucation is used' ),
641
+ position='bottom' ) ) ) ),
642
+ title='Automask' ) ]
643
+ else:
644
+ imdetails['gui']['auto-masking-panel'] = [ ]
645
+
646
+
647
+ ###
648
+ ### synchronize toolbar selections among figures
649
+ ###
650
+ if toolbars:
651
+ for tb in toolbars:
652
+ tb.js_on_change( 'active_changed',
653
+ ###
654
+ ### toolbars must filter out 'tb' to avoid circular references
655
+ ###
656
+ CustomJS( args=dict(toolbars=[t for t in toolbars if t.id != tb.id]),
657
+ code='''casalib.map( (gest,tool) => {
658
+ if ( tool.active ) {
659
+ // a tool which belongs to the toolbar that signaled a change
660
+ // is active for this gesture...
661
+ toolbars.forEach( (other_tb) => {
662
+ let new_active = other_tb.gestures[gest].tools.find(
663
+ (t) => t.name == tool.active.name )
664
+ if ( ! other_tb.gestures[gest].active ) {
665
+ if ( new_active ) {
666
+ other_tb.gestures[gest].active = new_active
667
+ new_active.active = true
668
+ }
669
+ } else if ( other_tb.gestures[gest].active.name != tool.active.name ) {
670
+ if ( new_active ) {
671
+ other_tb.gestures[gest].active.active = false
672
+ new_active.active = true
673
+ other_tb.gestures[gest].active = new_active
674
+ }
675
+ }
676
+ } )
677
+ } else {
678
+ // a tool which belongs to the toolbar that signaled a change
679
+ // is NOT active for this gesture...
680
+ toolbars.forEach( (other_tb) => {
681
+ if ( other_tb.gestures[gest] && other_tb.gestures[gest].active ) {
682
+ other_tb.gestures[gest].active.active = false
683
+ other_tb.gestures[gest].active = null
684
+ }
685
+ } )
686
+ }
687
+ }, cb_obj.gestures )''' ) )
688
+
689
+ ###
690
+ ### button to display the tclean log -- in the final implmentation, outliers and other multifield imaging should be handled by gclean
691
+ ###
692
+ self._log_button = TipButton( max_width=help_button.width, max_height=help_button.height, name='log',
693
+ icon=svg_icon(icon_name="bp-application-sm", size=25),
694
+ tooltip=Tooltip( content=HTML('''click here to see the <pre>tclean</pre> execution log'''), position="right" ),
695
+ margin=(-1, 0, -10, 0), button_type='light',
696
+ stylesheets=[ InlineStyleSheet( css='''.bk-btn { border: 0px solid #ccc; padding: 0 var(--padding-vertical) var(--padding-horizontal); margin-top: 3px; }''' ) ] )
697
+
698
+ self._control['iteration']['cb'] = CustomJS( args=dict( images_state={ k: { 'status': v['gui']['stopcode'],
699
+ 'automask': v['gui']['params']['automask'],
700
+ 'iteration': v['gui']['params']['iteration'],
701
+ 'img': v['gui']['image']['fig'],
702
+ 'src': v['gui']['cube'].js_obj( ),
703
+ 'spectrum': v['gui']['spectrum'],
704
+ 'src': v['gui']['image']['src'],
705
+ 'flux': v['converge-data']['flux'],
706
+ 'cyclethreshold': v['converge-data']['cyclethreshold'],
707
+ 'residual': v['converge-data']['residual'],
708
+ 'navi': { 'slider': v['gui']['slider'],
709
+ 'goto': v['gui']['goto'],
710
+ ## it doesn't seem like pixel tracking must be disabled
711
+ ##'tracking': v['gui']['cursor-pixel-text'],
712
+ 'stokes': v['gui']['channel-ctrl'][1] } }
713
+ for k,v in self._clean_targets.items( ) },
714
+ ctrl={ 'converge': self._clean['converge'] },
715
+ clean_ctrl=self._control['iteration'],
716
+ state=dict( mode='interactive', stopped=False, awaiting_stop=False, mask="" ),
717
+ ctrl_pipe=self._pipe['control'],
718
+ ids=self._clean_ids,
719
+ logbutton=self._log_button,
720
+ stopdescmap=ImagingDict.get_summaryminor_stopdesc( )
721
+ ),
722
+ code=self._js['update-converge'] + self._js['clean-refresh'] + self._js['clean-disable'] +
723
+ self._js['clean-enable'] + self._js['clean-status-update'] +
724
+ self._js['iter-gui-update'] + self._js['clean-wait'] +
725
+ '''function invalid_niter( s ) {
726
+ let v = parseInt( s )
727
+ if ( v > 0 ) return ''
728
+ if ( v == 0 ) return 'niter is zero'
729
+ if ( v < 0 ) return 'niter cannot be negative'
730
+ if ( isNaN(v) ) return 'niter must be an integer'
731
+ }
732
+ const itobj = Object.entries(images_state)[0][1].iteration
733
+ if ( ! state.stopped && cb_obj.origin.name == 'finish' ) {
734
+ let invalid = invalid_niter(itobj.niter.value)
735
+ if ( invalid ) update_status( invalid )
736
+ else {
737
+ state.mode = 'continuous'
738
+ update_status( 'Running multiple iterations' )
739
+ disable( false )
740
+ clean_ctrl.stop.button_type = "warning"
741
+ const thevalue = get_update_dictionary( )
742
+ ctrl_pipe.send( ids[cb_obj.origin.name],
743
+ { action: 'finish',
744
+ value: thevalue },
745
+ update_gui )
746
+ }
747
+ }
748
+ if ( ! state.stopped && state.mode === 'interactive' &&
749
+ cb_obj.origin.name === 'continue' ) {
750
+ let invalid = invalid_niter(itobj.niter.value)
751
+ if ( invalid ) update_status( invalid )
752
+ else {
753
+ update_status( 'Running one set of deconvolution iterations' )
754
+ disable( true )
755
+ // only send message for button that was pressed
756
+ // it's unclear whether 'this.origin.' or 'cb_obj.origin.' should be used
757
+ // (or even if 'XXX.origin.' is public)...
758
+ ctrl_pipe.send( ids[cb_obj.origin.name],
759
+ { action: 'next',
760
+ value: get_update_dictionary( ) },
761
+ update_gui )
762
+ }
763
+ }
764
+ if ( state.mode === 'interactive' && cb_obj.origin.name === 'stop' ) {
765
+ if ( confirm( "Are you sure you want to end this interactive clean session and close the GUI?" ) ) {
766
+ disable( true )
767
+ //ctrl_pipe.send( ids[cb_obj.origin.name],
768
+ // { action: 'stop',
769
+ // value: { } },
770
+ // update_gui )
771
+ document._casa_window_closed = true
772
+ /*** this will close the tab >>>>---------+ ***/
773
+ /*** | ***/
774
+ /*** vvvvv----------------------------+ ***/
775
+ document._cube_done( Object.entries(images_state).reduce((acc,[k,v]) => ({ ...acc, [k]: v.src.masks( ) }),{ } ) )
776
+ }
777
+ } else if ( state.mode === 'continuous' &&
778
+ cb_obj.origin.name === 'stop' &&
779
+ ! state.awaiting_stop ) {
780
+ disable( true )
781
+ state.awaiting_stop = true
782
+ ctrl_pipe.send( ids[cb_obj.origin.name],
783
+ { action: 'status',
784
+ value: { } },
785
+ wait_for_tclean_stop )
786
+ }''' )
787
+
788
+
789
+ self._control['iteration']['continue'].js_on_click( self._control['iteration']['cb'] )
790
+ self._control['iteration']['finish'].js_on_click( self._control['iteration']['cb'] )
791
+ self._control['iteration']['stop'].js_on_click( self._control['iteration']['cb'] )
792
+
793
+ self._log_button.js_on_click( CustomJS( args=dict( logbutton=self._log_button ),
794
+ code='''function format_log( elem ) {
795
+ return `<html>
796
+ <head>
797
+ <style type="text/css">
798
+ body {
799
+ counter-reset: section;
800
+ }
801
+ p:not([no-num]):before {
802
+ font-weight: bold;
803
+ counter-increment: section;
804
+ content: "" counter(section) ": ";
805
+ }
806
+ </style>
807
+ </head>
808
+ <body>
809
+ <h1>Interactive Clean History</h1>
810
+ ` + elem.map((x) => x.startsWith('#') ? `<p no-num>${x}</p>` : `<p>${x}</p>`).join('\\n') + '</body>\\n</html>'
811
+ }
812
+ let b = cb_obj.origin
813
+ if ( ! b._window || b._window.closed ) {
814
+ b._window = window.open("about:blank","Interactive Clean Log")
815
+ b._window.document.write(format_log(b._log))
816
+ b._window.document.close( )
817
+ }''' ) )
818
+
819
+ ###
820
+ ### Setup script that will be called when the user closes the
821
+ ### browser tab that is running interactive clean
822
+ ###
823
+ initial_log = self._clean['gclean']._log( )
824
+
825
+ self._pipe['control'].init_script=CustomJS( args=dict( ctrl_pipe=self._pipe['control'],
826
+ ids=self._clean_ids,
827
+ logbutton=self._log_button,
828
+ log=initial_log,
829
+ initial_image=list(self._clean_targets.items( ))[0][0]
830
+ ),
831
+ code=self._js['initialize'] +
832
+ '''if ( ! logbutton._log ) {
833
+ /*** store log list with log button for access in other callbacks ***/
834
+ logbutton._log = log
835
+ }''' )
836
+
837
+ tab_panels = list( map( self._create_image_panel, self._clean_targets.items( ) ) )
838
+
839
+ for imid, imdetails in self._clean_targets.items( ):
840
+ imdetails['gui']['cube'].connect( )
841
+
842
+ image_tabs = Tabs( tabs=tab_panels, tabs_location='below', height_policy='max', width_policy='max' )
843
+
844
+ self._fig['layout'] = column(
845
+ row( help_button,
846
+ self._log_button,
847
+ Spacer( height=self._control['iteration']['stop'].height, sizing_mode="scale_width" ),
848
+ Div( text="<div><b>status:</b></div>" ),
849
+ status_line,
850
+ self._control['iteration']['stop'], self._control['iteration']['continue'], self._control['iteration']['finish'], sizing_mode="scale_width" ),
851
+ row( image_tabs, height_policy='max', width_policy='max' ),
852
+ height_policy='max', width_policy='max' )
853
+
854
+ ###
855
+ ### Keep track of which image is currently active in document._casa_image_name (which is
856
+ ### initialized in self._js['initialize']). Also, update the current control sub-tab
857
+ ### when the field main-tab is changed. An attempt to manage this all within the
858
+ ### control sub-tabs using a reference to self._image_control_tab_groups from
859
+ ### each control sub-tab failed with:
860
+ ###
861
+ ### bokeh.core.serialization.SerializationError: circular reference
862
+ ###
863
+ image_tabs.js_on_change( 'active', CustomJS( args=dict( names=[ t[0] for t in self._clean_targets.items( ) ],
864
+ itergroups=self._image_control_tab_groups ),
865
+ code='''if ( ! hasprop(document,'_casa_last_control_tab') ) {
866
+ document._casa_last_control_tab = 0
867
+ }
868
+ document._casa_image_name = names[cb_obj.active]
869
+ itergroups[document._casa_image_name].active = document._casa_last_control_tab''' ) )
870
+
871
+ # Change display type depending on runtime environment
872
+ if self._is_notebook:
873
+ output_notebook()
874
+ else:
875
+ ### Directory is created when an HTTP server is running
876
+ ### (MAX)
877
+ ### output_file(self._imagename+'_webpage/index.html')
878
+ pass
879
+
880
+ show(self._fig['layout'])
881
+
882
+ def _create_colormap_adjust( self, imdetails ):
883
+ palette = imdetails['gui']['cube'].palette( reuse=self._cube_palette )
884
+ return column( row( Div(text="<div><b>Colormap:</b></div>",margin=(5,2,5,25)), palette ),
885
+ imdetails['gui']['cube'].colormap_adjust( ), sizing_mode='stretch_both' )
886
+
887
+
888
+ def _create_control_image_tab( self, imid, imdetails ):
889
+ result = Tabs( tabs=[ TabPanel(child=column( row( Tip( imdetails['gui']['params']['iteration']['nmajor'],
890
+ tooltip=Tooltip( content=HTML( 'maximum number of major cycles to run before stopping'),
891
+ position='bottom' ) ),
892
+ Tip( imdetails['gui']['params']['iteration']['niter'],
893
+ tooltip=Tooltip( content=HTML( 'number of clean iterations to run' ),
894
+ position='bottom' ) ),
895
+ Tip( imdetails['gui']['params']['iteration']['threshold'],
896
+ tooltip=Tooltip( content=HTML( 'stopping threshold' ),
897
+ position='bottom' ) ) ),
898
+ row( Tip( imdetails['gui']['params']['iteration']['nsigma'],
899
+ tooltip=Tooltip( content=HTML( 'multiplicative factor for rms-based threshold stopping'),
900
+ position='bottom' ) ),
901
+ Tip( imdetails['gui']['params']['iteration']['gain'],
902
+ tooltip=Tooltip( content=HTML( 'fraction of the source flux to subtract out of the residual image'),
903
+ position='bottom' ) ) ),
904
+ row( Tip( imdetails['gui']['params']['iteration']['cycleniter'],
905
+ tooltip=Tooltip( content=HTML( 'maximum number of <b>minor-cycle</b> iterations' ),
906
+ position='bottom' ) ),
907
+ Tip( imdetails['gui']['params']['iteration']['cyclefactor'],
908
+ tooltip=Tooltip( content=HTML( 'scaling on PSF sidelobe level to compute the minor-cycle stopping threshold' ),
909
+ position='bottom_left' ) ), background="lightgray" ),
910
+ imdetails['gui']['convergence'], sizing_mode='stretch_height' ),
911
+ title='Iteration' ) ] +
912
+ ( [ TabPanel( child=imdetails['gui']['spectrum'],
913
+ title='Spectrum' ) ] if imdetails['image-channels'] > 1 else [ ] ) +
914
+ [ TabPanel( child=self._create_colormap_adjust(imdetails),
915
+ title='Colormap' ),
916
+ TabPanel( child=column( *imdetails['gui']['stats'] ),
917
+ title='Statistics' ) ] + imdetails['gui']['auto-masking-panel'],
918
+ width=500, sizing_mode='stretch_height', tabs_location='below' )
919
+
920
+ if not hasattr(self,'_image_control_tab_groups'):
921
+ self._image_control_tab_groups = { }
922
+
923
+ self._image_control_tab_groups[imid] = result
924
+ result.js_on_change( 'active', CustomJS( args=dict( ),
925
+ code='''document._casa_last_control_tab = cb_obj.active''' ) )
926
+ return result
927
+
928
+ def _create_image_panel( self, imagetuple ):
929
+ imid, imdetails = imagetuple
930
+
931
+
932
+
933
+ return TabPanel( child=column( row( *imdetails['gui']['channel-ctrl'], imdetails['gui']['cube'].coord_ctrl( ),
934
+ *self._image_bitmask_controls,
935
+ #Spacer( height=5, height_policy="fixed", sizing_mode="scale_width" ),
936
+ imdetails['gui']['cursor-pixel-text'],
937
+ row( Spacer( sizing_mode='stretch_width' ),
938
+ imdetails['gui']['cube'].tapedeck( size='20px' ) if imdetails['image-channels'] > 1 else Div( ),
939
+ Spacer( height=5, width=350 ), width_policy='max' ),
940
+ width_policy='max' ),
941
+ row( imdetails['gui']['image']['fig'],
942
+ column( row( imdetails['gui']['goto'],
943
+ imdetails['gui']['slider'],
944
+ width_policy='max' ) if imdetails['image-channels'] > 1 else Div( ),
945
+ self._create_control_image_tab(imid, imdetails), height_policy='max' ),
946
+ height_policy='max', width_policy='max' ),
947
+ height_policy='max', width_policy='max' ), title=imid )
948
+
949
+ def __call__( self, setting, exec_context, id=None ):
950
+ '''Display GUI and process events until the user stops the application.
951
+
952
+ Example:
953
+ Create ``iclean`` object and display::
954
+
955
+ print( "Result: %s" %
956
+ iclean( vis='refim_point_withline.ms', imagename='test', imsize=512,
957
+ cell='12.0arcsec', specmode='cube',
958
+ interpolation='nearest', ... )( ) )
959
+ '''
960
+
961
+ self._setup()
962
+
963
+ # If Interactive Clean is being run remotely, print helper info for port tunneling
964
+ if self._is_remote:
965
+ # Tunnel ports for Jupyter kernel connection
966
+ print("\nImportant: Copy the following line and run in your local terminal to establish port forwarding.\
967
+ You may need to change the last argument to align with your ssh config.\n")
968
+ print(self._gen_port_fwd_cmd())
969
+
970
+ # TODO: Include?
971
+ # VSCode will auto-forward ports that appear in well-formatted addresses.
972
+ # Printing this line will cause VSCode to autoforward the ports
973
+ # print("Cmd: " + str(repr(self.auto_fwd_ports_vscode())))
974
+ input("\nPress enter when port forwarding is setup...")
975
+
976
+ ###
977
+ ### cubevis.exe subpkg supports adding a stop condition to allow for interrupt,
978
+ ### but it is not needed for synchronous execution
979
+ ###
980
+ self._exec = { 'stop-condition': None }
981
+ #self._exec['stop-condition'], self._exec['id'] = exec_context.create_stop_condition(id)
982
+
983
+ return exe.Task( self._task_server )
984
+ # , stop_condition=self._exec['stop-condition'] )
985
+
986
+ async def _task_server( self ):
987
+ """Wrapper for your serve() context manager"""
988
+
989
+ @asynccontextmanager
990
+ async def serve_func( ):
991
+ '''This function is intended for developers who would like to embed interactive
992
+ clean as a part of a larger GUI. This embedded use of interactive clean is not
993
+ currently supported and would require the addition of parameters to this function
994
+ as well as changes to the interactive clean implementation. However, this function
995
+ does expose the ``asyncio.Future`` that is used to signal completion of the
996
+ interactive cleaning operation, and it provides the coroutines which must be
997
+ managed by asyncio to make the interactive clean GUI responsive.
998
+
999
+ Example:
1000
+ Create ``iclean`` object, process events and retrieve result::
1001
+
1002
+ ic = iclean( vis='refim_point_withline.ms', imagename='test', imsize=512,
1003
+ cell='12.0arcsec', specmode='cube', interpolation='nearest', ... )
1004
+ async def process_events( ):
1005
+ async with ic.serve( ) as state:
1006
+ await state[0]
1007
+
1008
+ asyncio.run(process_events( ))
1009
+ print( "Result:", ic.result( ) )
1010
+
1011
+ Returns
1012
+ -------
1013
+ (asyncio.Future, dictionary of coroutines)
1014
+ '''
1015
+ def start_http_server():
1016
+ import http.server
1017
+ import socketserver
1018
+ PORT = self._http_port
1019
+ DIRECTORY=self._webpage_path
1020
+
1021
+ class Handler(http.server.SimpleHTTPRequestHandler):
1022
+ def __init__(self, *args, **kwargs):
1023
+ super().__init__(*args, directory=DIRECTORY, **kwargs)
1024
+
1025
+ with socketserver.TCPServer(("", PORT), Handler) as httpd:
1026
+ print("\nServing Interactive Clean webpage from local directory: ", DIRECTORY)
1027
+ print("Use Control-C to stop Interactive clean.\n")
1028
+ print("Copy and paste one of the below URLs into your browser (Chrome or Firefox) to view:")
1029
+ print("http://localhost:"+str(PORT))
1030
+ print("http://127.0.0.1:"+str(PORT))
1031
+
1032
+ httpd.serve_forever()
1033
+
1034
+ self._launch_gui( )
1035
+
1036
+ async with CMC( *( [ ctx for img in self._clean_targets.keys( ) for ctx in
1037
+ [
1038
+ self._clean_targets[img]['gui']['cube'].serve(self.__stop),
1039
+ ]
1040
+ ] + [ websockets.serve( self._pipe['control'].process_messages,
1041
+ self._pipe['control'].address[0],
1042
+ self._pipe['control'].address[1] ),
1043
+ websockets.serve( self._clean['converge']['pipe'].process_messages,
1044
+ self._clean['converge']['pipe'].address[0],
1045
+ self._clean['converge']['pipe'].address[1] ) ]
1046
+ ) ):
1047
+ self.__result_future = asyncio.Future( )
1048
+ yield self.__result_future
1049
+
1050
+ async with serve_func( ) as result_future:
1051
+ if self._exec['stop-condition'] is None:
1052
+ await result_future
1053
+ return self.result( )
1054
+ else:
1055
+ raise RuntimeError( 'internal error: no stop condition expected' )
1056
+
1057
+ ###
1058
+ ### If stop conditions were used, a mechanism to check the stop
1059
+ ### condition would be required...
1060
+ ###
1061
+ #if isinstance(self._exec['stop-condition'], asyncio.Event):
1062
+ # done, pending = await asyncio.wait(
1063
+ # [ result_future, asyncio.create_task(stop_condition.wait()) ],
1064
+ # return_when=asyncio.FIRST_COMPLETED
1065
+ # )
1066
+ # # Cancel pending tasks
1067
+ # for task in pending:
1068
+ # task.cancel()
1069
+ # try:
1070
+ # await task
1071
+ # except asyncio.CancelledError:
1072
+ # pass
1073
+ #
1074
+ # if result_future in done:
1075
+ # return result_future.result()
1076
+ # else:
1077
+ # return "Stopped by signal"
1078
+ #else:
1079
+ # raise RuntimeError( f"unexpected stop condition type: {type(self._exec['stop-condition'])}" )
1080
+
1081
+ def _setup( self ):
1082
+ self.__reset( )
1083
+
1084
+ def initialize_tclean( gclean ):
1085
+
1086
+ stopdesc, stopcode, majordone, majorleft, iterleft, all_converge = next(gclean)
1087
+
1088
+ for imid, converge in all_converge.items( ):
1089
+ #######################################################################################################
1090
+ ### gclean seems to return its internal state making it succeptable to modification... so we'll at ###
1091
+ ### least start out with unique dictionaries. ###
1092
+ #######################################################################################################
1093
+ converge = copy.deepcopy(converge)
1094
+
1095
+ imdetails = self._clean_targets[imid]
1096
+ imdetails['converge'] = converge
1097
+
1098
+ if imdetails['converge'] is None or len(imdetails['converge'].keys()) == 0 or \
1099
+ imdetails['converge']['major'] is None or imdetails['converge']['chan'] is None:
1100
+ ###
1101
+ ### gclean should provide argument checking (https://github.com/casangi/casagui/issues/33)
1102
+ ### but currently gclean can be initialized with bad arguments and it is not known until
1103
+ ### the initial calls to tclean/deconvolve
1104
+ ###
1105
+ raise RuntimeError(f'''gclean failure "%s" not returned: {imdetails["converge"]}''' % ('major' if imdetails['converge']['major'] is None else 'chan'))
1106
+
1107
+ self._clean['cmds'].extend(self._clean['gclean']._log( ))
1108
+
1109
+ self._initial_clean_params['nmajor'] = majorleft
1110
+ self._initial_clean_params['niter'] = iterleft
1111
+ self._initial_clean_params['cycleniter'] = self._init_values["cycleniter"]
1112
+ self._initial_clean_params['threshold'] = self._init_values["threshold"]
1113
+ self._initial_clean_params['cyclefactor'] = self._init_values["cyclefactor"]
1114
+ self._initial_clean_params['gain'] = self._init_values["gain"]
1115
+ self._initial_clean_params['nsigma'] = self._init_values["nsigma"]
1116
+
1117
+ self._init_values["convergence_state"]['convergence'][imid] = imdetails['converge']['chan']
1118
+ self._init_values["convergence_state"]['cyclethreshold'][imid] = imdetails['converge']['major']['cyclethreshold']
1119
+
1120
+ return (stopdesc, stopcode, majordone, majorleft, iterleft)
1121
+
1122
+
1123
+ self._clean['cmds'] = []
1124
+
1125
+ stopdesc, stopcode, majordone, majorleft, iterleft = initialize_tclean(self._clean['gclean'])
1126
+
1127
+
1128
+ self._clean['last-success'] = dict( result='converged' if stopcode[0] else 'update', stopcode=stopcode, cmd=self._clean['cmds'],
1129
+ convergence=self._init_values["convergence_state"]['convergence'],
1130
+ iterdone=0, iterleft=iterleft,
1131
+ majordone=majordone, majorleft=majorleft,
1132
+ cyclethreshold=self._init_values["convergence_state"]['cyclethreshold'],
1133
+ stopdesc=stopdesc )
1134
+
1135
+ ### Must occur AFTER initial "next" call to gclean(s)
1136
+ self._init_pipes()
1137
+
1138
+ def __retrieve_result( self ):
1139
+ '''If InteractiveCleanUI had a return value, it would be filled in as part of the
1140
+ GUI dialog between Python and JavaScript and this function would return it'''
1141
+ if isinstance(self._error_result,Exception):
1142
+ raise self._error_result
1143
+ elif self._error_result is not None:
1144
+ return self._error_result
1145
+ return { k: v['converge'] for k,v in self._clean_targets.items( ) }
1146
+
1147
+ def result( self ):
1148
+ '''If InteractiveCleanUI had a return value, it would be filled in as part of the
1149
+ GUI dialog between Python and JavaScript and this function would return it'''
1150
+ if self.__result_future is None:
1151
+ raise RuntimeError( 'no interactive clean result is available' )
1152
+
1153
+ if self.__result is None:
1154
+ ### restore returns full return dictionary
1155
+ self.__result_from_gui = self.__result_future.result( )
1156
+ self.__result = self._clean['gclean'].restore( )
1157
+
1158
+ return self.__result
1159
+
1160
+ def masks( self ):
1161
+ '''Retrieves the masks which were used with interactive clean.
1162
+
1163
+ Returns
1164
+ -------
1165
+ The standard ``cubevis`` cube region dictionary which contains two elements
1166
+ ``masks`` and ``polys``.
1167
+
1168
+ The value of the ``masks`` element is a dictionary that is indexed by
1169
+ tuples of ``(stokes,chan)`` and the value of each element is a list
1170
+ whose elements describe the polygons drawn on the channel represented
1171
+ by ``(stokes,chan)``. Each polygon description in this list has a
1172
+ polygon index (``p``) and a x/y translation (``d``).
1173
+
1174
+ The value of the ``polys`` element is a dictionary that is indexed by
1175
+ polygon indexes. The value of each polygon index is a dictionary containing
1176
+ ``type`` (whose value is either ``'rect'`` or ``'poly``) and ``geometry``
1177
+ (whose value is a dictionary containing ``'xs'`` and ``'ys'`` (which are
1178
+ the x and y coordinates that define the polygon).
1179
+
1180
+ This can be converted to other formats with ``cubevis.utils.convert_masks``.
1181
+ '''
1182
+ return copy.deepcopy(self._mask_history) ## don't allow users to change history
1183
+
1184
+ def history( self ):
1185
+ '''Retrieves the commands used during the interactive clean session.
1186
+
1187
+ Returns
1188
+ -------
1189
+ list[str] tclean calls made during the interactive clean session.
1190
+ '''
1191
+ return self._clean['gclean']._log( True )
1192
+
1193
+ def _initialize_javascript( self ):
1194
+ self._js = { ### initialize state
1195
+ ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
1196
+ ### -- document is used storing state --
1197
+ ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
1198
+ 'initialize': '''if ( ! document._casa_initialized ) {
1199
+ document._casa_image_name = initial_image
1200
+ document._casa_initialized = true
1201
+ document._casa_window_closed = false
1202
+ window.addEventListener( 'beforeunload',
1203
+ function (e) {
1204
+ // if the window is already closed this message is never
1205
+ // delivered (unless interactive clean is called again then
1206
+ // the event shows up in the newly created control pipe
1207
+ if ( document._casa_window_closed == false ) {
1208
+ ctrl_pipe.send( ids['stop'],
1209
+ { action: 'stop', value: { } },
1210
+ undefined ) } } )
1211
+ }''',
1212
+
1213
+ ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
1214
+ ### -- flux_src._convergence_data is used to store the complete --
1215
+ ### -- --
1216
+ ### -- The "Insert here ..." code seems to be called when when the stokes plane is changed --
1217
+ ### -- but there have been no tclean iterations yet... --
1218
+ ### --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
1219
+ 'update-converge': '''function update_convergence_single( target, data ) {
1220
+ const pos = target.src.cur_chan
1221
+ const imdata = data.get(pos[1]).get(pos[0])
1222
+ // chan----------------^^^^^^ ^^^^^^----stokes
1223
+ const iterations = imdata.iterations
1224
+ const peakRes = imdata.peakRes
1225
+ const cyclethreshold = imdata.cycleThresh
1226
+ const modelFlux = imdata.modelFlux
1227
+ const stopCode = imdata.stopCode
1228
+ const stopDesc = imdata.stopCode.map( code => stopdescmap.has(code) ? stopdescmap.get(code): "" )
1229
+ target.residual.data = { iterations, cyclethreshold, stopDesc, values: peakRes, type: Array(iterations.length).fill('residual') }
1230
+ target.flux.data = { iterations, cyclethreshold, stopDesc, values: modelFlux, type: Array(iterations.length).fill('flux') }
1231
+ target.cyclethreshold.data = { iterations, values: cyclethreshold }
1232
+ }
1233
+
1234
+ function update_convergence( recurse=false ) {
1235
+ let convdata
1236
+ if ( hasprop(document,'_casa_convergence_data') ) {
1237
+ convdata = document._casa_convergence_data
1238
+ } else {
1239
+ if ( ! recurse ) {
1240
+ ctrl.converge.pipe.send( ctrl.converge.id, { action: 'retrieve' },
1241
+ (msg) => { if ( hasprop( msg.result, 'convergence' ) ) {
1242
+ document._casa_convergence_data = { convergence: msg.result.convergence,
1243
+ cyclethreshold: msg.result.cyclethreshold }
1244
+ update_convergence(true)
1245
+ } } )
1246
+ } else { console.log( 'INTERNAL ERROR: fetching convergence data failed' ) }
1247
+ return
1248
+ }
1249
+
1250
+ Object.entries(images_state).map(
1251
+ ([k,v],i) => { update_convergence_single(v,convdata.convergence[k]) } )
1252
+ }''',
1253
+
1254
+ 'clean-refresh': '''function refresh( clean_msg ) {
1255
+ const itobj = Object.entries(images_state)[0][1].iteration
1256
+ let stokes = 0 // later we will receive the polarity
1257
+ // from some widget mechanism...
1258
+ if ( clean_msg !== undefined ) {
1259
+ if ( 'iterleft' in clean_msg ) {
1260
+ itobj.niter.value = '' + clean_msg['iterleft']
1261
+ } else if ( clean_msg !== undefined && 'iterdone' in clean_msg ) {
1262
+ const remaining = parseInt(itobj.niter.value) - parseInt(clean_msg['iterdone'])
1263
+ itobj.niter.value = '' + (remaining < 0 ? 0 : remaining)
1264
+ }
1265
+
1266
+ if ( 'majorleft' in clean_msg ) {
1267
+ itobj.nmajor.value = '' + clean_msg['majorleft']
1268
+ } else if ( 'majordone' in clean_msg ) {
1269
+ const nm = parseInt(itobj.nmajor.value)
1270
+ if ( nm != -1 ) {
1271
+ const remaining = nm - parseInt(clean_msg['majordone'])
1272
+ itobj.nmajor.value = '' + (remaining < 0 ? 0 : remaining)
1273
+ } else itobj.nmajor.value = '' + nm // nmajor == -1 implies do not consider nmajor in stop decision
1274
+ }
1275
+
1276
+ if ( hasprop(clean_msg,'convergence') && clean_msg.convergence != null ) {
1277
+ document._casa_convergence_data = { convergence: clean_msg.convergence,
1278
+ cyclethreshold: clean_msg.cyclethreshold }
1279
+ }
1280
+ }
1281
+
1282
+ // All images must be updated... without this no images are updated
1283
+ casalib.map( (im,state) => state.src.refresh( msg => {
1284
+ if ( 'stats' in msg ) state.src.update_statistics( msg.stats )
1285
+ } ), images_state )
1286
+ // Update convergence plot...
1287
+ update_convergence( )
1288
+ }''',
1289
+
1290
+ ###
1291
+ ### enabling/disabling tools in imdetails['gui']['image']['fig'].toolbar.tools does not seem to not work
1292
+ ### imdetails['gui']['image']['fig'].toolbar.tools.tool_name (e.g. "Box Select", "Lasso Select")
1293
+ ###
1294
+ ### By design, images_state[*].automask.*/images_state[*].iteration.* are singletons which only need
1295
+ ### to be disabled once...
1296
+ ###
1297
+ 'clean-disable': '''function disable( with_stop ) {
1298
+ const amobj = Object.entries(images_state)[0][1].automask
1299
+ Object.entries(amobj).map(
1300
+ ([k,v],i) => { if ( hasprop(v,'disabled') ) v.disabled = true } )
1301
+ const itobj = Object.entries(images_state)[0][1].iteration
1302
+ Object.entries(itobj).map(
1303
+ ([k,v],i) => { if ( hasprop(v,'disabled') ) v.disabled = true } )
1304
+ Object.entries(images_state).map(
1305
+ ([k,v],i) => {
1306
+ v.img.disabled = true
1307
+ if ( v.spectrum ) v.spectrum.disabled = true
1308
+ v.src.disable_masking( )
1309
+ v.src.disable_pixel_update( )
1310
+ Object.entries(v.navi).map(
1311
+ ([k1,v1],i1) => { if ( hasprop(v1,'disabled') ) v1.disabled = true }
1312
+ )
1313
+ }
1314
+ )
1315
+ clean_ctrl.continue.disabled = true
1316
+ clean_ctrl.finish.disabled = true
1317
+ clean_ctrl.stop.disabled = with_stop
1318
+ }''',
1319
+
1320
+ 'clean-enable': '''function enable( only_stop ) {
1321
+ const amobj = Object.entries(images_state)[0][1].automask
1322
+ Object.entries(amobj).map(
1323
+ ([k,v],i) => { if ( hasprop(v,'disabled') ) v.disabled = false } )
1324
+ const itobj = Object.entries(images_state)[0][1].iteration
1325
+ Object.entries(itobj).map(
1326
+ ([k,v],i) => { if ( hasprop(v,'disabled') ) v.disabled = false } )
1327
+ Object.entries(images_state).map(
1328
+ ([k,v],i) => {
1329
+ v.img.disabled = false
1330
+ if ( v.spectrum ) v.spectrum.disabled = false
1331
+ v.src.enable_masking( )
1332
+ v.src.enable_pixel_update( )
1333
+ Object.entries(v.navi).map(
1334
+ ([k1,v1],i) => { if ( hasprop(v1,'disabled') ) v1.disabled = false } )
1335
+ } )
1336
+
1337
+ clean_ctrl.stop.disabled = false
1338
+ if ( ! only_stop ) {
1339
+ clean_ctrl.continue.disabled = false
1340
+ clean_ctrl.finish.disabled = false
1341
+ }
1342
+ }''',
1343
+
1344
+
1345
+ 'clean-status-update': '''function update_status( status ) {
1346
+ const stopstr = [ 'Zero stop code',
1347
+ 'Iteration limit hit',
1348
+ 'Force stop',
1349
+ 'No change in peak residual across two major cycles',
1350
+ 'Peak residual increased by 3x from last major cycle',
1351
+ 'Peak residual increased by 3x from the minimum',
1352
+ 'Zero mask found',
1353
+ 'No mask found',
1354
+ 'N-sigma or other valid exit criterion',
1355
+ 'Stopping criteria encountered',
1356
+ 'Unrecognized stop code' ]
1357
+ if ( typeof status === 'number' ) {
1358
+ images_state[document._casa_image_name]['status'].text =
1359
+ '<p>' +
1360
+ stopstr[ status < 0 || status >= stopstr.length ?
1361
+ stopstr.length - 1 : status ] +
1362
+ '</p>'
1363
+ } else {
1364
+ images_state[document._casa_image_name]['status'].text = `<p>${status}</p>`
1365
+ }
1366
+ }''',
1367
+
1368
+ 'iter-gui-update': '''function get_update_dictionary( ) {
1369
+ //const amste = images_state[document._casa_image_name]['automask']
1370
+ //const clste = images_state[document._casa_image_name]['iteration']
1371
+ // Assumption is that there is ONE set of iteration and automask updates
1372
+ // for ALL imaging fields...
1373
+ const amobj = Object.entries(images_state)[0][1].automask
1374
+ const automask = amobj.active ?
1375
+ Object.entries(amobj).reduce(
1376
+ (acc,[k1,v1]) => { if ( hasprop(v1,'value') ) acc[k1] = v1.value; return acc },
1377
+ { dogrowprune: amobj.dogrowprune.active,
1378
+ fastnoise: amobj.fastnoise.active,
1379
+ active: true }
1380
+ ) : { }
1381
+ const itobj = Object.entries(images_state)[0][1].iteration
1382
+ const iteration = Object.entries(itobj).reduce(
1383
+ (acc,[k1,v1]) => { if ( hasprop(v1,'value') ) acc[k1] = v1.value; return acc },
1384
+ { }
1385
+ )
1386
+
1387
+ const masks = Object.entries(images_state).reduce( (acc,[k,v]) => { acc[k] = v.src.masks( ); return acc }, { } )
1388
+ const breadcrumbs = Object.entries(images_state).reduce( (acc,[k,v]) => { acc[k] = v.src.breadcrumbs( ); return acc }, { } )
1389
+ return { iteration, automask, masks, breadcrumbs, current_image: document._casa_image_name }
1390
+ }
1391
+ function update_log( log_lines ) {
1392
+ let b = logbutton
1393
+ b._log = b._log.concat( log_lines )
1394
+ if ( b._window && ! b._window.closed ) {
1395
+ for ( const line of log_lines ) {
1396
+ const p = b._window.document.createElement('p')
1397
+ p.appendChild( b._window.document.createTextNode(line) )
1398
+ b._window.document.body.appendChild(p)
1399
+ }
1400
+ }
1401
+ }
1402
+ function update_gui( msg ) {
1403
+ const itobj = Object.entries(images_state)[0][1].iteration
1404
+ if ( msg.result === 'error' ) {
1405
+ // ************************************************************************************
1406
+ // ******** error occurs and is signaled by _gclean, e.g. exception in gclean ********
1407
+ // ************************************************************************************
1408
+ state.mode = 'interactive'
1409
+ clean_ctrl.stop.button_type = "danger"
1410
+ enable(false)
1411
+ state.stopped = false
1412
+ update_status( msg.stopdesc ? msg.stopdesc : 'An internal error has occurred' )
1413
+ if ( 'cmd' in msg ) {
1414
+ update_log( msg.cmd )
1415
+ }
1416
+ } else if ( msg.result === 'no-action' ) {
1417
+ update_status( msg.stopdesc ? msg.stopdesc : 'nothing done' )
1418
+ enable( false )
1419
+ if ( 'cmd' in msg ) {
1420
+ update_log( msg.cmd )
1421
+ }
1422
+ } else if ( msg.result == 'converged' ) {
1423
+ state.mode = 'interactive'
1424
+ clean_ctrl.stop.button_type = "danger"
1425
+ enable(false)
1426
+ state.stopped = false
1427
+ update_status( msg.stopdesc ? msg.stopdesc : 'stopping criteria reached' )
1428
+ if ( 'cmd' in msg ) {
1429
+ update_log( msg.cmd )
1430
+ }
1431
+ refresh( msg )
1432
+ } else if ( msg.result === 'update' ) {
1433
+ if ( 'cmd' in msg ) {
1434
+ update_log( msg.cmd )
1435
+ }
1436
+ refresh( msg )
1437
+ // stopcode[0] == 1: iteration limit hit
1438
+ // stopcode[0] == 9: major cycle limit hit
1439
+ // *******************************************************************************************
1440
+ // ******** perhaps the user should not be locked into exiting after the limit is hit ********
1441
+ // *******************************************************************************************
1442
+ //state.stopped = state.stopped || (msg.stopcode[0] > 1 && msg.stopcode[0] < 9) || msg.stopcode[0] == 0
1443
+ state.stopped = false
1444
+ if ( state.mode === 'interactive' && ! state.awaiting_stop ) {
1445
+ clean_ctrl.stop.button_type = "danger"
1446
+ update_status( msg.stopdesc ? msg.stopdesc : 'stopcode' in msg ? msg.stopcode[0] : -1 )
1447
+ if ( ! state.stopped ) {
1448
+ enable( false )
1449
+ } else {
1450
+ disable( false )
1451
+ }
1452
+ } else if ( state.mode === 'continuous' && ! state.awaiting_stop ) {
1453
+ if ( ! state.stopped && itobj.niter.value > 0 && (itobj.nmajor.value > 0 || itobj.nmajor.value == -1) ) {
1454
+ // *******************************************************************************************
1455
+ // ******** 'niter.value > 0 so continue with one more iteration ********
1456
+ // ******** 'nmajor.value' == -1 implies do not consider nmajor in stop consideration ********
1457
+ // *******************************************************************************************
1458
+ ctrl_pipe.send( ids[cb_obj.origin.name],
1459
+ { action: 'finish',
1460
+ value: get_update_dictionary( ) },
1461
+ update_gui )
1462
+ } else if ( ! state.stopped ) {
1463
+ // *******************************************************************************************
1464
+ // ******** 'niter.value <= 0 so iteration should stop ********
1465
+ // *******************************************************************************************
1466
+ state.mode = 'interactive'
1467
+ clean_ctrl.stop.button_type = "danger"
1468
+ enable(false)
1469
+ state.stopped = false
1470
+ update_status( msg.stopdesc ? msg.stopdesc : 'stopping criteria reached' )
1471
+ } else {
1472
+ state.mode = 'interactive'
1473
+ clean_ctrl.stop.button_type = "danger"
1474
+ enable(false)
1475
+ state.stopped = false
1476
+ update_status( msg.stopdesc ? msg.stopdesc : 'stopcode' in msg ? msg.stopcode[0] : -1 )
1477
+ }
1478
+ }
1479
+ } else if ( msg.result === 'error' ) {
1480
+ img_src.drop_breadcrumb('E')
1481
+ if ( 'cmd' in msg ) {
1482
+ update_log( msg.cmd )
1483
+ }
1484
+ state.mode = 'interactive'
1485
+ clean_ctrl.stop.button_type = "danger"
1486
+ state.stopped = false
1487
+ update_status( 'stopcode' in msg ? msg.stopcode[0] : -1 )
1488
+ enable( false )
1489
+ }
1490
+ }''',
1491
+
1492
+ 'clean-wait': '''function wait_for_tclean_stop( msg ) {
1493
+ state.mode = 'interactive'
1494
+ clean_ctrl.stop.button_type = "danger"
1495
+ enable( false )
1496
+ state.awaiting_stop = false
1497
+ update_status( 'user requested stop' )
1498
+ }''',
1499
+ }