From c94cbebf346ecb2c84755e45ee90f41a436539a5 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 29 Jul 2025 11:34:31 -0500 Subject: [PATCH 1/7] chore: move thread_pool.py to tools --- src/navigate/controller/controller.py | 2 +- src/navigate/{controller/thread_pool.py => tools/threads.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/navigate/{controller/thread_pool.py => tools/threads.py} (100%) diff --git a/src/navigate/controller/controller.py b/src/navigate/controller/controller.py index bb4965563..c127c639d 100644 --- a/src/navigate/controller/controller.py +++ b/src/navigate/controller/controller.py @@ -68,7 +68,7 @@ # AdaptiveOpticsPopupController, ) -from navigate.controller.thread_pool import SynchronizedThreadPool +from navigate.tools.threads import SynchronizedThreadPool # Local Model Imports from navigate.model.model import Model diff --git a/src/navigate/controller/thread_pool.py b/src/navigate/tools/threads.py similarity index 100% rename from src/navigate/controller/thread_pool.py rename to src/navigate/tools/threads.py From 21f116b7b75bb43f6ac577848dbed39746d38dd4 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 29 Jul 2025 13:07:33 -0500 Subject: [PATCH 2/7] feat: add warning propagation to signal thread --- src/navigate/model/model.py | 13 ++++++++++--- src/navigate/tools/threads.py | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/navigate/model/model.py b/src/navigate/model/model.py index d5f3ffeaf..9e9ecd757 100644 --- a/src/navigate/model/model.py +++ b/src/navigate/model/model.py @@ -74,6 +74,7 @@ from navigate.tools.common_dict_tools import update_stage_dict from navigate.tools.common_functions import load_module_from_file, VariableWithLock from navigate.tools.file_functions import load_yaml_file, save_yaml_file +from navigate.tools.threads import ThreadWithWarning from navigate.model.microscope import Microscope from navigate.config.config import get_navigate_path from navigate.model.plugins_model import PluginsModel @@ -587,9 +588,13 @@ def run_command( self.data_buffer_saving_flags = None if self.imaging_mode == "live": - self.signal_thread = threading.Thread(target=self.run_live_acquisition) + self.signal_thread = ThreadWithWarning(target=self.run_live_acquisition, + warning_queue=self.event_queue, + logger=self.logger) else: - self.signal_thread = threading.Thread(target=self.run_acquisition) + self.signal_thread = ThreadWithWarning(target=self.run_acquisition, + warning_queue=self.event_queue, + logger=self.logger) self.signal_thread.name = f"{self.imaging_mode} signal" @@ -685,7 +690,9 @@ def run_command( self, self.acquisition_modes_feature_setting[self.imaging_mode] ) self.stop_send_signal = False - self.signal_thread = threading.Thread(target=self.run_live_acquisition) + self.signal_thread = ThreadWithWarning(target=self.run_live_acquisition, + warning_queue=self.event_queue, + logger=self.logger) self.signal_thread.name = "Waveform Popup Signal" self.signal_thread.start() diff --git a/src/navigate/tools/threads.py b/src/navigate/tools/threads.py index 33bf94ef9..0e2925aaf 100644 --- a/src/navigate/tools/threads.py +++ b/src/navigate/tools/threads.py @@ -468,3 +468,29 @@ def __exit__(self, exc_type, exc_val, exc_tb): The traceback of the exception. """ self.waitlistLock.release() + + +class ThreadWithWarning(threading.Thread): + """A custom thread class that raises a warning to the user if any error is raised.""" + + def __init__(self, *args, **kwargs): + """Initialize the ThreadWithWarning.""" + if "warning_queue" in kwargs: + self._warning_queue = kwargs["warning_queue"] + del kwargs["warning_queue"] + self._logger = logger + if "logger" in kwargs: + self._logger = kwargs["logger"] + del kwargs["logger"] + super().__init__(*args, **kwargs) + + def run(self): + """Run the thread and handle warnings.""" + try: + super().run() + except Exception as e: + self._logger.error(f"Error in thread {self.name}: {e}") + if hasattr(self, "_warning_queue"): + self._warning_queue.put(("warning", str(e))) + raise e + From 421383730475d6d157470d99cde4cfa28b24d8ad Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 29 Jul 2025 13:08:00 -0500 Subject: [PATCH 3/7] feat: add warning propagation to signal container --- src/navigate/model/features/feature_container.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/navigate/model/features/feature_container.py b/src/navigate/model/features/feature_container.py index 91122b8bc..ec9b1cc73 100644 --- a/src/navigate/model/features/feature_container.py +++ b/src/navigate/model/features/feature_container.py @@ -488,7 +488,7 @@ class SignalContainer(Container): track of the remaining executions. """ - def __init__(self, root=None, cleanup_list=[], number_of_execution=1): + def __init__(self, root=None, cleanup_list=[], warning_queue=None, number_of_execution=1): """Initialize the SignalContainer object. Parameters: @@ -509,6 +509,9 @@ def __init__(self, root=None, cleanup_list=[], number_of_execution=1): #: int: The remaining number of executions of the control sequence. self.remaining_number_of_execution = number_of_execution + #: Queue: A queue for warning messages related to the control sequence. + self.warning_queue = warning_queue + def reset(self): """Reset the container's state, including the current node and end flag. @@ -556,8 +559,12 @@ def run(self, *args, wait_response=False): while self.curr_node: try: result, is_end = self.curr_node.run(*args, wait_response=wait_response) - except Exception: + except Exception as e: logger.debug(f"SignalContainer - {traceback.format_exc()}") + if self.warning_queue: + self.warning_queue.put( + ("warning", f"Warning: review details below.\n{str(e)}") + ) self.end_flag = True self.cleanup() return @@ -1031,7 +1038,7 @@ def build_feature_tree(feature_list, continue_list, break_list): for node in break_list: if node[0] == "child": node[1].child, node[2].child = create_node({"name": DummyFeature}) - return SignalContainer(signal_root, signal_cleanup_list), DataContainer( + return SignalContainer(signal_root, signal_cleanup_list, model.event_queue), DataContainer( data_root, data_cleanup_list ) From 912629c38e76e07756d57496c65378c21f94646b Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 29 Jul 2025 13:55:26 -0500 Subject: [PATCH 4/7] Revert "chore: move thread_pool.py to tools" This reverts commit c94cbebf346ecb2c84755e45ee90f41a436539a5. --- src/navigate/controller/controller.py | 2 +- src/navigate/{tools/threads.py => controller/thread_pool.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/navigate/{tools/threads.py => controller/thread_pool.py} (100%) diff --git a/src/navigate/controller/controller.py b/src/navigate/controller/controller.py index c127c639d..bb4965563 100644 --- a/src/navigate/controller/controller.py +++ b/src/navigate/controller/controller.py @@ -68,7 +68,7 @@ # AdaptiveOpticsPopupController, ) -from navigate.tools.threads import SynchronizedThreadPool +from navigate.controller.thread_pool import SynchronizedThreadPool # Local Model Imports from navigate.model.model import Model diff --git a/src/navigate/tools/threads.py b/src/navigate/controller/thread_pool.py similarity index 100% rename from src/navigate/tools/threads.py rename to src/navigate/controller/thread_pool.py From 80bf973d02b9ee18a99e7d159bcc2d1f5a81e45f Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 29 Jul 2025 14:35:43 -0500 Subject: [PATCH 5/7] add UserVisibleException --- src/navigate/model/utils/exceptions.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/navigate/model/utils/exceptions.py diff --git a/src/navigate/model/utils/exceptions.py b/src/navigate/model/utils/exceptions.py new file mode 100644 index 000000000..947f4cda0 --- /dev/null +++ b/src/navigate/model/utils/exceptions.py @@ -0,0 +1,35 @@ +# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center. +# All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted for academic and research use only (subject to the +# limitations in the disclaimer below) provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# * Neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +class UserVisibleException(Exception): + """Base class for exceptions that should be visible to the user.""" + def __init__(self, message: str): + super().__init__(message) From 37fd45a2e5dee0b6dc518cc8a1faa684a369b297 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 29 Jul 2025 14:37:29 -0500 Subject: [PATCH 6/7] move threads.py and filter exceptions --- src/navigate/controller/thread_pool.py | 26 -------- .../model/features/feature_container.py | 5 +- src/navigate/model/model.py | 2 +- src/navigate/model/utils/threads.py | 65 +++++++++++++++++++ 4 files changed, 69 insertions(+), 29 deletions(-) create mode 100644 src/navigate/model/utils/threads.py diff --git a/src/navigate/controller/thread_pool.py b/src/navigate/controller/thread_pool.py index 0e2925aaf..33bf94ef9 100644 --- a/src/navigate/controller/thread_pool.py +++ b/src/navigate/controller/thread_pool.py @@ -468,29 +468,3 @@ def __exit__(self, exc_type, exc_val, exc_tb): The traceback of the exception. """ self.waitlistLock.release() - - -class ThreadWithWarning(threading.Thread): - """A custom thread class that raises a warning to the user if any error is raised.""" - - def __init__(self, *args, **kwargs): - """Initialize the ThreadWithWarning.""" - if "warning_queue" in kwargs: - self._warning_queue = kwargs["warning_queue"] - del kwargs["warning_queue"] - self._logger = logger - if "logger" in kwargs: - self._logger = kwargs["logger"] - del kwargs["logger"] - super().__init__(*args, **kwargs) - - def run(self): - """Run the thread and handle warnings.""" - try: - super().run() - except Exception as e: - self._logger.error(f"Error in thread {self.name}: {e}") - if hasattr(self, "_warning_queue"): - self._warning_queue.put(("warning", str(e))) - raise e - diff --git a/src/navigate/model/features/feature_container.py b/src/navigate/model/features/feature_container.py index ec9b1cc73..04b76c50b 100644 --- a/src/navigate/model/features/feature_container.py +++ b/src/navigate/model/features/feature_container.py @@ -38,6 +38,7 @@ # Third Party Imports # Local Imports +from navigate.model.utils.exceptions import UserVisibleException p = __name__.split(".")[1] @@ -561,7 +562,7 @@ def run(self, *args, wait_response=False): result, is_end = self.curr_node.run(*args, wait_response=wait_response) except Exception as e: logger.debug(f"SignalContainer - {traceback.format_exc()}") - if self.warning_queue: + if self.warning_queue and isinstance(e, UserVisibleException): self.warning_queue.put( ("warning", f"Warning: review details below.\n{str(e)}") ) @@ -1038,7 +1039,7 @@ def build_feature_tree(feature_list, continue_list, break_list): for node in break_list: if node[0] == "child": node[1].child, node[2].child = create_node({"name": DummyFeature}) - return SignalContainer(signal_root, signal_cleanup_list, model.event_queue), DataContainer( + return SignalContainer(signal_root, signal_cleanup_list, getattr(model, "event_queue", None)), DataContainer( data_root, data_cleanup_list ) diff --git a/src/navigate/model/model.py b/src/navigate/model/model.py index 9e9ecd757..7d81d520a 100644 --- a/src/navigate/model/model.py +++ b/src/navigate/model/model.py @@ -70,11 +70,11 @@ SharedList, load_dynamic_parameter_functions, ) +from navigate.model.utils.threads import ThreadWithWarning from navigate.log_files.log_functions import log_setup from navigate.tools.common_dict_tools import update_stage_dict from navigate.tools.common_functions import load_module_from_file, VariableWithLock from navigate.tools.file_functions import load_yaml_file, save_yaml_file -from navigate.tools.threads import ThreadWithWarning from navigate.model.microscope import Microscope from navigate.config.config import get_navigate_path from navigate.model.plugins_model import PluginsModel diff --git a/src/navigate/model/utils/threads.py b/src/navigate/model/utils/threads.py new file mode 100644 index 000000000..8a9018732 --- /dev/null +++ b/src/navigate/model/utils/threads.py @@ -0,0 +1,65 @@ +# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center. +# All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted for academic and research use only (subject to the +# limitations in the disclaimer below) provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# * Neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +# Standard Library Imports +import threading +import logging +# Local Imports +from navigate.model.utils.exceptions import UserVisibleException + +# Logger Setup +p = __name__.split(".")[1] +logger = logging.getLogger(p) + +class ThreadWithWarning(threading.Thread): + """A custom thread class that raises a warning to the user if any error is raised.""" + + def __init__(self, *args, **kwargs): + """Initialize the ThreadWithWarning.""" + if "warning_queue" in kwargs: + self._warning_queue = kwargs["warning_queue"] + del kwargs["warning_queue"] + self._logger = logger + if "logger" in kwargs: + self._logger = kwargs["logger"] + del kwargs["logger"] + super().__init__(*args, **kwargs) + + def run(self): + """Run the thread and handle warnings.""" + try: + super().run() + except Exception as e: + self._logger.error(f"Error in thread {self.name}: {e}") + if hasattr(self, "_warning_queue") and isinstance(e, UserVisibleException): + self._warning_queue.put(("warning", str(e))) + raise e + From c1523a60863a1cc774afd6586619de047ef8f911 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 29 Jul 2025 21:58:33 -0500 Subject: [PATCH 7/7] update function parameter description --- src/navigate/model/features/feature_container.py | 2 ++ src/navigate/model/utils/exceptions.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/navigate/model/features/feature_container.py b/src/navigate/model/features/feature_container.py index 04b76c50b..777653d0a 100644 --- a/src/navigate/model/features/feature_container.py +++ b/src/navigate/model/features/feature_container.py @@ -499,6 +499,8 @@ def __init__(self, root=None, cleanup_list=[], warning_queue=None, number_of_exe cleanup_list : list of TreeNode, optional A list of nodes containing 'cleanup' functions to be executed when the container is closed. Default is an empty list. + warning_queue : Queue, optional + A queue for warning messages. Default is None. number_of_execution : int, optional The number of times the control sequence should be executed. Default is 1. """ diff --git a/src/navigate/model/utils/exceptions.py b/src/navigate/model/utils/exceptions.py index 947f4cda0..05b1a41d1 100644 --- a/src/navigate/model/utils/exceptions.py +++ b/src/navigate/model/utils/exceptions.py @@ -32,4 +32,10 @@ class UserVisibleException(Exception): """Base class for exceptions that should be visible to the user.""" def __init__(self, message: str): + """Initialize the exception with a user-friendly message. + Parameters + ---------- + message : str + A message that describes the exception in a user-friendly way. + """ super().__init__(message)