cubevis 0.5.18__tar.gz → 0.5.20__tar.gz
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.
- {cubevis-0.5.18 → cubevis-0.5.20}/PKG-INFO +1 -1
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__init__.py +24 -2
- cubevis-0.5.20/cubevis/exe/__init__.py +4 -0
- cubevis-0.5.20/cubevis/exe/_context.py +126 -0
- cubevis-0.5.20/cubevis/exe/_mode.py +7 -0
- cubevis-0.5.20/cubevis/exe/_setting.py +4 -0
- cubevis-0.5.20/cubevis/exe/_task.py +230 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/apps/_createmask.py +2 -2
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/apps/_createregion.py +2 -2
- cubevis-0.5.20/cubevis/private/apps/_interactiveclean.mustache +89 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/apps/_interactiveclean.py +17 -1426
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/toolbox/__init__.py +1 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/toolbox/_cube.py +2 -2
- cubevis-0.5.18/cubevis/private/apps/_interactiveclean.mustache → cubevis-0.5.20/cubevis/toolbox/_interactive_clean_ui.mustache +164 -163
- cubevis-0.5.20/cubevis/toolbox/_interactive_clean_ui.py +1498 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/__init__.py +1 -14
- cubevis-0.5.20/cubevis/utils/_jupyter.py +92 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/pyproject.toml +1 -1
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-outlier/run-iclean.py +2 -1
- {cubevis-0.5.18 → cubevis-0.5.20}/LICENSE +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/LICENSE.rst +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/20px/fast-backward.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/20px/fast-forward.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/20px/step-backward.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/20px/step-forward.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/add-chan.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/add-chan.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/add-cube.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/add-cube.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/drag.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/drag.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/mask-selected.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/mask.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/mask.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/new-layer-sm-selected.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/new-layer-sm-selected.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/new-layer-sm.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/new-layer-sm.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/reset.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/reset.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/sub-chan.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/sub-chan.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/sub-cube.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/sub-cube.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/zoom-to-fit.png +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__icons__/zoom-to-fit.svg +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__js__/bokeh-3.6.1.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__js__/bokeh-tables-3.6.1.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__js__/bokeh-widgets-3.6.1.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__js__/casalib.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/__js__/cubevisjs.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/annotations/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/annotations/_ev_poly_annotation.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/components/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/format/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/format/_time_ticks.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/format/_wcs_ticks.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/models/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/models/_edit_span.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/models/_ev_text_input.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/models/_tip.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/models/_tip_button.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/sources/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/sources/_data_pipe.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/sources/_image_data_source.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/sources/_image_pipe.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/sources/_spectra_data_source.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/sources/_updatable_data_source.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/_initialize.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/_javascript.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/_palette.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/_session.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/js/bokeh-2.4.1.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/js/bokeh-gl-2.4.1.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/js/bokeh-tables-2.4.1.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/js/bokeh-widgets-2.4.1.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/js/casaguijs-v0.0.4.0-b2.4.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/js/casaguijs-v0.0.5.0-b2.4.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/js/casaguijs-v0.0.6.0-b2.4.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/state/js/casalib-v0.0.1.min.js +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/tools/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/tools/_cbreset_tool.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/tools/_drag_tool.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/utils/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/utils/_axes_labels.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/bokeh/utils/_svg_icon.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/data/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/data/casaimage/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/_gclean.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/apps/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/apps/_plotants.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/apps/_plotbandpass.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/casashell/createmask.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/casashell/iclean.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/casatasks/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/casatasks/createmask.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/casatasks/createregion.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/private/casatasks/iclean.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/readme.rst +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/remote/__init__.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/remote/_gclean.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/remote/_local.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/remote/_remote_kernel.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/toolbox/_app_context.py +0 -0
- {cubevis-0.5.18/cubevis/private/apps → cubevis-0.5.20/cubevis/toolbox}/_interactiveclean_wrappers.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/toolbox/_region_list.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_ResourceManager.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_contextmgrchain.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_conversion.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_copydoc.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_docenum.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_import_protected_module.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_logging.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_pkgs.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_regions.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_static.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/cubevis/utils/_tiles.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/readme.rst +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/alma-many-chan/alma-many-chan.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/basic-websockets-demo/client.html +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/basic-websockets-demo/client.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/basic-websockets-demo/server.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/createmask-demo/run-createmask.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/createregion-demo/run-createregion.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/cubemask-demo/image-slider-spectra-done-stats.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/cubemask-demo/image-slider-spectra-done.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/cubemask-demo/image-slider-spectra.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/cubemask-demo/image-slider.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/cubemask-demo/image.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-demo/m100_interactive.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-demo/mask0-iclean.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-demo/run-gclean.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-demo/run-iclean-obj.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-demo/run-iclean.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-demo/vla-sim-jet-iclean.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-first-look/run-fl-cont.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-first-look/run-fl-line.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-outlier/test_outlier.txt +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/iclean-remote/iclean_remote_webserver.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/large-cube/run-largecube.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/svg-test.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/updatable-data-source/direct-plot.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/updatable-data-source/simple-update.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/updatable-data-source/updated-plot.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/uranus-demo/uranus-iclean.py +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/websocket-reconnect/client.html +0 -0
- {cubevis-0.5.18 → cubevis-0.5.20}/tests/manual/websocket-reconnect/server.py +0 -0
|
@@ -29,11 +29,33 @@
|
|
|
29
29
|
used to build GUI applications for astronomy. It also contains some
|
|
30
30
|
applications turn-key applications'''
|
|
31
31
|
|
|
32
|
-
import os as
|
|
32
|
+
import os as _os
|
|
33
|
+
import logging as _logging
|
|
33
34
|
|
|
35
|
+
logger = _logging.getLogger('cubevis')
|
|
36
|
+
_handler = _logging.StreamHandler()
|
|
37
|
+
_formatter = _logging.Formatter('[%(name)s] %(levelname)s: %(message)s')
|
|
38
|
+
_handler.setFormatter(_formatter)
|
|
39
|
+
logger.addHandler(_handler)
|
|
40
|
+
|
|
41
|
+
if _os.getenv('CUBEVIS_DEBUG', '').lower() in ('1', 'true', 'yes', 'on'):
|
|
42
|
+
logger.setLevel(_logging.DEBUG)
|
|
43
|
+
else:
|
|
44
|
+
logger.setLevel(_logging.INFO)
|
|
34
45
|
|
|
35
46
|
from .private.apps import iclean
|
|
36
47
|
|
|
48
|
+
|
|
49
|
+
def set_log_level(level):
|
|
50
|
+
"""Set the logging level for cubevis.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
level: Logging level (e.g., logging.DEBUG, logging.INFO, 'DEBUG', 'INFO')
|
|
54
|
+
"""
|
|
55
|
+
if isinstance(level, str):
|
|
56
|
+
level = getattr(_logging, level.upper())
|
|
57
|
+
logger.setLevel(level)
|
|
58
|
+
|
|
37
59
|
try:
|
|
38
60
|
from .__version__ import __version__
|
|
39
61
|
except ModuleNotFoundError:
|
|
@@ -52,7 +74,7 @@ def xml_interface_defs( ):
|
|
|
52
74
|
'''
|
|
53
75
|
return { }
|
|
54
76
|
|
|
55
|
-
__mustache_interface_templates__ = { 'iclean':
|
|
77
|
+
__mustache_interface_templates__ = { 'iclean': _os.path.join( _os.path.dirname(__file__), "private", "casashell", "iclean.mustache" ) }
|
|
56
78
|
def mustache_interface_templates( ):
|
|
57
79
|
'''This provides a list of mustache files provided by cubevis. It may eventually allow
|
|
58
80
|
casashell to generate all of its bindings at startup time. This would allow casashell
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Optional, Union, Any, Callable
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
|
6
|
+
|
|
7
|
+
from . import Mode, Task
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
class Context:
|
|
12
|
+
"""Unified interface for different execution contexts with lifecycle management."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, mode: Mode, executor: Optional[ThreadPoolExecutor] = None):
|
|
15
|
+
logger.debug( f"\tContext::__init__({mode},{executor} ): {id(self)}" )
|
|
16
|
+
self.mode = mode
|
|
17
|
+
self.executor = executor or ThreadPoolExecutor(max_workers=4)
|
|
18
|
+
self._running_tasks = []
|
|
19
|
+
self._stop_events = {}
|
|
20
|
+
|
|
21
|
+
def create_stop_condition( self, task_id: Optional[str] = None,
|
|
22
|
+
prefer_asyncio: bool = False ):
|
|
23
|
+
"""Create appropriate stop condition for the execution mode."""
|
|
24
|
+
logger.debug( f"\tContext::create_stop_condition({task_id}): {id(self)}" )
|
|
25
|
+
if task_id is None:
|
|
26
|
+
task_id = f"task_{len(self._stop_events)}"
|
|
27
|
+
|
|
28
|
+
if prefer_asyncio or self.mode in [ExecutionMode.ASYNC_RUN, ExecutionMode.ASYNC_TASK]:
|
|
29
|
+
# For async contexts, use asyncio primitives
|
|
30
|
+
event = asyncio.Event()
|
|
31
|
+
else:
|
|
32
|
+
# ====>> if self.mode in [Mode.SYNC, Mode.THREAD]: <<====
|
|
33
|
+
# For threading contexts, always use threading primitives
|
|
34
|
+
condition = threading.Condition()
|
|
35
|
+
condition.stop_requested = False
|
|
36
|
+
|
|
37
|
+
def stop_task():
|
|
38
|
+
with condition:
|
|
39
|
+
condition.stop_requested = True
|
|
40
|
+
condition.notify_all()
|
|
41
|
+
|
|
42
|
+
condition.stop_task = stop_task
|
|
43
|
+
event = condition
|
|
44
|
+
|
|
45
|
+
self._stop_events[task_id] = event
|
|
46
|
+
return event, task_id
|
|
47
|
+
|
|
48
|
+
def stop_task(self, task_id: str):
|
|
49
|
+
"""Signal a task to stop."""
|
|
50
|
+
if task_id in self._stop_events:
|
|
51
|
+
condition_or_event = self._stop_events[task_id]
|
|
52
|
+
if isinstance(condition_or_event, threading.Condition):
|
|
53
|
+
logger.debug( f"\tContext::stop_task({task_id}): {id(self)} - condition" )
|
|
54
|
+
condition_or_event.stop_task()
|
|
55
|
+
elif hasattr(condition_or_event, 'set'): # Both threading.Event and asyncio.Event have set()
|
|
56
|
+
logger.debug( f"\tContext::stop_task({task_id}): {id(self)} - event" )
|
|
57
|
+
condition_or_event.set()
|
|
58
|
+
else:
|
|
59
|
+
# Fallback for other types
|
|
60
|
+
logger.debug(f"Warning: Unknown stop condition type: {type(condition_or_event)}")
|
|
61
|
+
|
|
62
|
+
def stop_all_tasks(self):
|
|
63
|
+
"""Signal all tasks to stop."""
|
|
64
|
+
logger.debug( f"\tContext::stop_all_task( ): {id(self)}" )
|
|
65
|
+
for condition_or_event in self._stop_events.values():
|
|
66
|
+
if isinstance(condition_or_event, threading.Condition):
|
|
67
|
+
condition_or_event.stop_task()
|
|
68
|
+
elif hasattr(condition_or_event, 'set'): # Both threading.Event and asyncio.Event have set()
|
|
69
|
+
condition_or_event.set()
|
|
70
|
+
else:
|
|
71
|
+
logger.debug(f"Warning: Unknown stop condition type: {type(condition_or_event)}")
|
|
72
|
+
|
|
73
|
+
def execute(self, task: Task, task_id: Optional[str] = None) -> Union[Any, Future, asyncio.Task]:
|
|
74
|
+
"""Execute Bokeh task according to the configured mode."""
|
|
75
|
+
|
|
76
|
+
if self.mode == Mode.SYNC:
|
|
77
|
+
logger.debug( f"\tContext::execute({task},{task_id} ): {id(self)} - sync" )
|
|
78
|
+
return task.run_sync()
|
|
79
|
+
|
|
80
|
+
elif self.mode == Mode.ASYNC_RUN:
|
|
81
|
+
logger.debug( f"\tContext::execute({task},{task_id} ): {id(self)} - async" )
|
|
82
|
+
if asyncio.iscoroutinefunction(task.server_func):
|
|
83
|
+
return asyncio.run(task.run_async())
|
|
84
|
+
else:
|
|
85
|
+
async def async_wrapper():
|
|
86
|
+
return await task.run_async()
|
|
87
|
+
return asyncio.run(async_wrapper())
|
|
88
|
+
|
|
89
|
+
elif self.mode == Mode.ASYNC_TASK:
|
|
90
|
+
logger.debug( f"\tContext::execute({task},{task_id} ): {id(self)} - task" )
|
|
91
|
+
try:
|
|
92
|
+
loop = asyncio.get_running_loop()
|
|
93
|
+
async_task = loop.create_task(task.run_async())
|
|
94
|
+
if task_id:
|
|
95
|
+
self._running_tasks.append((task_id, async_task))
|
|
96
|
+
return async_task
|
|
97
|
+
except RuntimeError:
|
|
98
|
+
raise RuntimeError("ASYNC_TASK mode requires an active event loop")
|
|
99
|
+
|
|
100
|
+
elif self.mode == Mode.THREAD:
|
|
101
|
+
logger.debug( f"\tContext::execute({task},{task_id} ): {id(self)} - thread" )
|
|
102
|
+
future = self.executor.submit(task.run_sync)
|
|
103
|
+
if task_id:
|
|
104
|
+
self._running_tasks.append((task_id, future))
|
|
105
|
+
return future
|
|
106
|
+
|
|
107
|
+
async def execute_async(self, task: Task, task_id: Optional[str] = None) -> Any:
|
|
108
|
+
"""Async version that always returns the result."""
|
|
109
|
+
|
|
110
|
+
if self.mode == Mode.SYNC:
|
|
111
|
+
logger.debug( f"\tContext::execute_async({task},{task_id} ): {id(self)} - sync" )
|
|
112
|
+
loop = asyncio.get_running_loop()
|
|
113
|
+
return await loop.run_in_executor(self.executor, task.run_sync)
|
|
114
|
+
|
|
115
|
+
elif self.mode == Mode.ASYNC_RUN:
|
|
116
|
+
logger.debug( f"\tContext::execute_async({task},{task_id} ): {id(self)} - async" )
|
|
117
|
+
return await task.run_async()
|
|
118
|
+
|
|
119
|
+
elif self.mode == Mode.ASYNC_TASK:
|
|
120
|
+
logger.debug( f"\tContext::execute_async({task},{task_id} ): {id(self)} - task" )
|
|
121
|
+
return await task.run_async()
|
|
122
|
+
|
|
123
|
+
elif self.mode == Mode.THREAD:
|
|
124
|
+
logger.debug( f"\tContext::execute_async({task},{task_id} ): {id(self)} - thread" )
|
|
125
|
+
loop = asyncio.get_running_loop()
|
|
126
|
+
return await loop.run_in_executor(self.executor, task.run_sync)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import asyncio
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Callable, Optional, Union, Any
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
class Task:
|
|
9
|
+
"""A long-running Bokeh backend task that runs until completion or stop signal.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self,
|
|
13
|
+
server_func: Callable,
|
|
14
|
+
*args,
|
|
15
|
+
stop_condition: Optional[Union[threading.Event, asyncio.Event, threading.Condition, Callable[[], bool]]] = None,
|
|
16
|
+
**kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Args:
|
|
19
|
+
server_func: Function that starts the Bokeh server (sync or async)
|
|
20
|
+
stop_condition: Event or callable that signals when to stop
|
|
21
|
+
- threading.Event: for thread-based execution
|
|
22
|
+
- asyncio.Event: for async execution
|
|
23
|
+
- threading.Condition: for complex condition-based stopping (most flexible)
|
|
24
|
+
- Callable[[], bool]: function returning True when should stop
|
|
25
|
+
- None: runs until server_func returns naturally
|
|
26
|
+
"""
|
|
27
|
+
logger.debug( f"\tTask::__init__({server_func},{args},{stop_condition},{kwargs}): {id(self)}" )
|
|
28
|
+
self.server_func = server_func
|
|
29
|
+
self.args = args
|
|
30
|
+
self.kwargs = kwargs
|
|
31
|
+
self.stop_condition = stop_condition
|
|
32
|
+
self._result = None
|
|
33
|
+
self._exception = None
|
|
34
|
+
|
|
35
|
+
def _convert_asyncio_event_to_threading(self, asyncio_event):
|
|
36
|
+
"""Convert asyncio.Event to threading.Event for sync contexts."""
|
|
37
|
+
threading_event = threading.Event()
|
|
38
|
+
logger.debug( f"\tTask::_convert_asyncio_event_to_threading({asyncio_event}): {id(self)}" )
|
|
39
|
+
|
|
40
|
+
# Create a bridge between asyncio.Event and threading.Event
|
|
41
|
+
# We'll use a background thread with its own event loop to monitor the asyncio event
|
|
42
|
+
def bridge_events():
|
|
43
|
+
# Create a new event loop for this thread
|
|
44
|
+
try:
|
|
45
|
+
loop = asyncio.new_event_loop()
|
|
46
|
+
asyncio.set_event_loop(loop)
|
|
47
|
+
|
|
48
|
+
async def wait_and_set():
|
|
49
|
+
await asyncio_event.wait()
|
|
50
|
+
threading_event.set()
|
|
51
|
+
|
|
52
|
+
loop.run_until_complete(wait_and_set())
|
|
53
|
+
except Exception as e:
|
|
54
|
+
# If bridging fails, fall back to a simple timeout approach
|
|
55
|
+
print(f"Warning: Event bridging failed: {e}")
|
|
56
|
+
time.sleep(0.1) # Small delay to prevent tight loop
|
|
57
|
+
threading_event.set() # Set it anyway to unblock
|
|
58
|
+
finally:
|
|
59
|
+
try:
|
|
60
|
+
loop.close()
|
|
61
|
+
except:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# Start the bridge in a daemon thread
|
|
65
|
+
bridge_thread = threading.Thread(target=bridge_events, daemon=True)
|
|
66
|
+
bridge_thread.start()
|
|
67
|
+
|
|
68
|
+
return threading_event
|
|
69
|
+
|
|
70
|
+
def run_sync(self) -> Any:
|
|
71
|
+
"""Run synchronously until completion or stop signal."""
|
|
72
|
+
if self.stop_condition is None:
|
|
73
|
+
# Handle async functions in sync context
|
|
74
|
+
if asyncio.iscoroutinefunction(self.server_func):
|
|
75
|
+
logger.debug( f"\tTask::run_sync( ): {id(self)} - asyncio no stop condition {self.server_func}" )
|
|
76
|
+
return asyncio.run(self.server_func(*self.args, **self.kwargs))
|
|
77
|
+
else:
|
|
78
|
+
logger.debug( f"\tTask::run_sync( ): {id(self)} - no stop condition {self.server_func}" )
|
|
79
|
+
return self.server_func(*self.args, **self.kwargs)
|
|
80
|
+
|
|
81
|
+
# Handle asyncio.Event in sync context by converting to threading.Event
|
|
82
|
+
stop_condition = self.stop_condition
|
|
83
|
+
if isinstance(self.stop_condition, asyncio.Event):
|
|
84
|
+
logger.debug( f"\tTask::run_sync( ): {id(self)} - asyncio bridge {self.server_func}" )
|
|
85
|
+
# Convert asyncio.Event to threading.Event for sync execution
|
|
86
|
+
stop_condition = self._convert_asyncio_event_to_threading(self.stop_condition)
|
|
87
|
+
|
|
88
|
+
if callable(stop_condition):
|
|
89
|
+
# Poll-based stopping
|
|
90
|
+
logger.debug( f"\tTask::run_sync( ): {id(self)} - polling {self.server_func}" )
|
|
91
|
+
while not stop_condition():
|
|
92
|
+
try:
|
|
93
|
+
if asyncio.iscoroutinefunction(self.server_func):
|
|
94
|
+
result = asyncio.run(self.server_func(*self.args, **self.kwargs))
|
|
95
|
+
else:
|
|
96
|
+
result = self.server_func(*self.args, **self.kwargs)
|
|
97
|
+
if result is not None: # Server function completed
|
|
98
|
+
return result
|
|
99
|
+
except Exception as e:
|
|
100
|
+
self._exception = e
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
elif isinstance(stop_condition, threading.Event):
|
|
104
|
+
# Event-based stopping for threads
|
|
105
|
+
logger.debug( f"\tTask::run_sync( ): {id(self)} - threading event {self.server_func}" )
|
|
106
|
+
def run_with_check():
|
|
107
|
+
while not stop_condition.is_set():
|
|
108
|
+
try:
|
|
109
|
+
if asyncio.iscoroutinefunction(self.server_func):
|
|
110
|
+
result = asyncio.run(self.server_func(*self.args, **self.kwargs))
|
|
111
|
+
else:
|
|
112
|
+
result = self.server_func(*self.args, **self.kwargs)
|
|
113
|
+
if result is not None:
|
|
114
|
+
return result
|
|
115
|
+
except Exception as e:
|
|
116
|
+
self._exception = e
|
|
117
|
+
raise
|
|
118
|
+
return run_with_check()
|
|
119
|
+
|
|
120
|
+
elif isinstance(stop_condition, threading.Condition):
|
|
121
|
+
logger.debug( f"\tTask::run_sync( ): {id(self)} - threading condition {self.server_func}" )
|
|
122
|
+
# Condition-based stopping - most flexible and efficient
|
|
123
|
+
def run_with_condition():
|
|
124
|
+
with stop_condition:
|
|
125
|
+
while True:
|
|
126
|
+
# Check if we should stop (condition should have a flag or predicate)
|
|
127
|
+
# The condition object should be used by external code to signal stopping
|
|
128
|
+
# We'll pass the condition to the server function so it can wait efficiently
|
|
129
|
+
try:
|
|
130
|
+
# Pass the condition to the server function if it accepts it
|
|
131
|
+
import inspect
|
|
132
|
+
sig = inspect.signature(self.server_func)
|
|
133
|
+
if 'stop_condition' in sig.parameters:
|
|
134
|
+
if asyncio.iscoroutinefunction(self.server_func):
|
|
135
|
+
result = asyncio.run(self.server_func(*self.args, stop_condition=stop_condition, **self.kwargs))
|
|
136
|
+
else:
|
|
137
|
+
result = self.server_func(*self.args, stop_condition=stop_condition, **self.kwargs)
|
|
138
|
+
else:
|
|
139
|
+
if asyncio.iscoroutinefunction(self.server_func):
|
|
140
|
+
result = asyncio.run(self.server_func(*self.args, **self.kwargs))
|
|
141
|
+
else:
|
|
142
|
+
result = self.server_func(*self.args, **self.kwargs)
|
|
143
|
+
|
|
144
|
+
if result is not None:
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
# If function returns None, wait for condition change
|
|
148
|
+
stop_condition.wait()
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
self._exception = e
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
return run_with_condition()
|
|
155
|
+
|
|
156
|
+
else:
|
|
157
|
+
raise ValueError(f"Invalid stop_condition ({type(stop_condition)}) for sync execution")
|
|
158
|
+
|
|
159
|
+
async def run_async(self) -> Any:
|
|
160
|
+
"""Run asynchronously until completion or stop signal."""
|
|
161
|
+
if asyncio.iscoroutinefunction(self.server_func):
|
|
162
|
+
if self.stop_condition is None:
|
|
163
|
+
logger.debug( f"\tTask::run_async( ): {id(self)} - no stop condition {self.server_func}" )
|
|
164
|
+
return await self.server_func(*self.args, **self.kwargs)
|
|
165
|
+
|
|
166
|
+
if callable(self.stop_condition):
|
|
167
|
+
# Poll-based stopping
|
|
168
|
+
logger.debug( f"\tTask::run_async( ): {id(self)} - polling {self.server_func}" )
|
|
169
|
+
while not self.stop_condition():
|
|
170
|
+
try:
|
|
171
|
+
# Run server function as task so we can check stop condition
|
|
172
|
+
task = asyncio.create_task(self.server_func(*self.args, **self.kwargs))
|
|
173
|
+
|
|
174
|
+
# Check stop condition periodically
|
|
175
|
+
while not task.done() and not self.stop_condition():
|
|
176
|
+
await asyncio.sleep(0.1)
|
|
177
|
+
|
|
178
|
+
if self.stop_condition():
|
|
179
|
+
task.cancel()
|
|
180
|
+
try:
|
|
181
|
+
await task
|
|
182
|
+
except asyncio.CancelledError:
|
|
183
|
+
pass
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
return await task
|
|
187
|
+
except Exception as e:
|
|
188
|
+
self._exception = e
|
|
189
|
+
raise
|
|
190
|
+
|
|
191
|
+
elif isinstance(self.stop_condition, asyncio.Event):
|
|
192
|
+
logger.debug( f"\tTask::run_async( ): {id(self)} - asyncio event {self.server_func}" )
|
|
193
|
+
# Event-based stopping for async
|
|
194
|
+
server_task = asyncio.create_task(self.server_func(*self.args, **self.kwargs))
|
|
195
|
+
stop_task = asyncio.create_task(self.stop_condition.wait())
|
|
196
|
+
|
|
197
|
+
done, pending = await asyncio.wait(
|
|
198
|
+
[server_task, stop_task],
|
|
199
|
+
return_when=asyncio.FIRST_COMPLETED
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Cancel remaining tasks
|
|
203
|
+
for task in pending:
|
|
204
|
+
task.cancel()
|
|
205
|
+
try:
|
|
206
|
+
await task
|
|
207
|
+
except asyncio.CancelledError:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
if server_task in done:
|
|
211
|
+
return server_task.result()
|
|
212
|
+
else:
|
|
213
|
+
return None # Stopped by event
|
|
214
|
+
|
|
215
|
+
elif isinstance(self.stop_condition, threading.Condition):
|
|
216
|
+
logger.debug( f"\tTask::run_async( ): {id(self)} - threading condition {self.server_func}" )
|
|
217
|
+
# For async context with threading.Condition, we need to run in executor
|
|
218
|
+
# since threading.Condition is synchronous
|
|
219
|
+
loop = asyncio.get_event_loop()
|
|
220
|
+
return await loop.run_in_executor(None, self.run_sync)
|
|
221
|
+
|
|
222
|
+
else:
|
|
223
|
+
raise ValueError("Invalid stop_condition for async execution")
|
|
224
|
+
|
|
225
|
+
else:
|
|
226
|
+
logger.debug( f"\tTask::run_async( ): {id(self)} - until completion" )
|
|
227
|
+
# Sync function in async context - run in thread pool
|
|
228
|
+
loop = asyncio.get_event_loop()
|
|
229
|
+
return await loop.run_in_executor(None, self.run_sync)
|
|
230
|
+
|
|
@@ -41,7 +41,7 @@ from bokeh.io import reset_output as reset_bokeh_output
|
|
|
41
41
|
from bokeh.io import output_notebook
|
|
42
42
|
from bokeh.models.dom import HTML
|
|
43
43
|
from bokeh.models.ui.tooltips import Tooltip
|
|
44
|
-
from cubevis.utils import resource_manager, reset_resource_manager,
|
|
44
|
+
from cubevis.utils import resource_manager, reset_resource_manager, is_interactive_jupyter
|
|
45
45
|
from cubevis.data import casaimage
|
|
46
46
|
from cubevis.bokeh.models import TipButton, Tip
|
|
47
47
|
from cubevis.utils import ContextMgrChain as CMC
|
|
@@ -395,7 +395,7 @@ class CreateMask:
|
|
|
395
395
|
itergroups[document._casa_image_name].active = document._casa_last_control_tab''' ) )
|
|
396
396
|
|
|
397
397
|
# Change display type depending on runtime environment
|
|
398
|
-
if
|
|
398
|
+
if is_interactive_jupyter( ):
|
|
399
399
|
output_notebook()
|
|
400
400
|
else:
|
|
401
401
|
### Directory is created when an HTTP server is running
|
|
@@ -41,7 +41,7 @@ from bokeh.io import curdoc
|
|
|
41
41
|
from bokeh.io import reset_output as reset_bokeh_output
|
|
42
42
|
from bokeh.models.dom import HTML
|
|
43
43
|
from bokeh.models.ui.tooltips import Tooltip
|
|
44
|
-
from cubevis.utils import resource_manager, reset_resource_manager,
|
|
44
|
+
from cubevis.utils import resource_manager, reset_resource_manager, is_interactive_jupyter
|
|
45
45
|
from cubevis.data import casaimage
|
|
46
46
|
from cubevis.bokeh.models import TipButton, Tip
|
|
47
47
|
from cubevis.utils import ContextMgrChain as CMC
|
|
@@ -448,7 +448,7 @@ class CreateRegion:
|
|
|
448
448
|
itergroups[document._casa_image_name].active = document._casa_last_control_tab''' ) )
|
|
449
449
|
|
|
450
450
|
# Change display type depending on runtime environment
|
|
451
|
-
if
|
|
451
|
+
if is_interactive_jupyter( ):
|
|
452
452
|
output_notebook()
|
|
453
453
|
else:
|
|
454
454
|
### Directory is created when an HTTP server is running
|
|
@@ -0,0 +1,89 @@
|
|
|
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 ``InteractiveClean`` application for interactive control
|
|
30
|
+
of tclean'''
|
|
31
|
+
|
|
32
|
+
from pprint import pprint
|
|
33
|
+
|
|
34
|
+
import sys
|
|
35
|
+
from os.path import exists
|
|
36
|
+
from casatasks.private.imagerhelpers.input_parameters import ImagerParameters
|
|
37
|
+
|
|
38
|
+
from cubevis.utils import find_pkg, load_pkg
|
|
39
|
+
from cubevis.toolbox import InteractiveCleanUI
|
|
40
|
+
from cubevis import exe
|
|
41
|
+
|
|
42
|
+
class InteractiveClean:
|
|
43
|
+
'''InteractiveClean(...) implements interactive clean using Bokeh
|
|
44
|
+
{{docstring}}
|
|
45
|
+
'''
|
|
46
|
+
|
|
47
|
+
def __init__( self, vis, imagename{{# initParams}}, {{name}}={{default}}{{/ initParams}}, iclean_backend="PROD" ):
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
###
|
|
51
|
+
### iclean_backend can be used to select alternate backends for interactive clean. This could be used
|
|
52
|
+
### to enable a backend with extended features or it could be used to select a stub backend designed
|
|
53
|
+
### for testing
|
|
54
|
+
###
|
|
55
|
+
mod_specs = None
|
|
56
|
+
self._gclean_module = None
|
|
57
|
+
if iclean_backend == 'PROD':
|
|
58
|
+
mod_specs = find_pkg( "casatasks.private.imagerhelpers._gclean" )
|
|
59
|
+
else:
|
|
60
|
+
mod_specs = find_pkg( f"_gclean_{iclean_backend}" )
|
|
61
|
+
|
|
62
|
+
if mod_specs:
|
|
63
|
+
self._gclean_module = load_pkg(mod_specs[0])
|
|
64
|
+
else:
|
|
65
|
+
raise ImportError(f"Could not locate {iclean_backend} kind of iclean backend")
|
|
66
|
+
|
|
67
|
+
self._args = {{forwDict}}
|
|
68
|
+
self._gclean = self._gclean_module.gclean( **self._args )
|
|
69
|
+
self._gclean_paths = self._gclean.image_products( )
|
|
70
|
+
#self._residual_path = self._residual_path(self._clean['gclean'],imid)
|
|
71
|
+
#self._mask_path = self._mask_path(self._clean['gclean'],imid)
|
|
72
|
+
|
|
73
|
+
self._ui = InteractiveCleanUI(self._gclean, self._args)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def __call__( self ):
|
|
77
|
+
'''Display GUI and process events until the user stops the application.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
Create ``iclean`` object and display::
|
|
81
|
+
|
|
82
|
+
print( "Result: %s" %
|
|
83
|
+
iclean( vis='refim_point_withline.ms', imagename='test', imsize=512,
|
|
84
|
+
cell='12.0arcsec', specmode='cube',
|
|
85
|
+
interpolation='nearest', ... )( ) )
|
|
86
|
+
'''
|
|
87
|
+
context = exe.Context( exe.Mode.SYNC )
|
|
88
|
+
exec_task = self._ui( exe.Setting.CLI, context, "interactive-clean" )
|
|
89
|
+
return context.execute( exec_task, "interactive-clean" )
|