From c9758180ebfbc76f5a778329c417ee194db726da Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 4 Jul 2025 17:04:11 +0100 Subject: [PATCH 1/6] refactor to use TaskWorkerThread for model.calc_mean_chunk --- .../gui/windows/live_viewer/model.py | 19 +++++++++- .../gui/windows/live_viewer/presenter.py | 38 ++++++------------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index e9ce4d9b7a0..893c1bbbe26 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -214,25 +214,40 @@ def clear_mean_partial(self) -> None: self.mean = np.full(len(self.images), np.nan) self.mean_readable = np.full(len(self.images), 1) - def calc_mean_chunk(self, chunk_size: int) -> None: + def calc_mean_chunk(self, chunk_size: int = 100) -> None: if self.images is not None: + print("calc_mean_chunk 0") nanInds = np.argwhere(np.isnan(self.mean)) if self.roi: + print("calc_mean_chunk 0") left, top, right, bottom = self.roi else: + print("calc_mean_chunk 0") left, top, right, bottom = (0, 0, -1, -1) if nanInds.size > 0: for ind in nanInds[-1:-chunk_size:-1]: try: buffer_data = self.image_cache.load_image(self.images[ind[0]]) + #print(f"calc_mean_chunk try: {buffer_data=}") buffer_mean = np.mean(buffer_data[top:bottom, left:right]) + #print(f"calc_mean_chunk try: {buffer_mean=}") mean_readable = 1 except ImageLoadFailError: - mean_readable = 0 + #print(f"Failed to load {self.images[ind[0]]}") buffer_mean = np.nan + if np.isnan(buffer_mean): + print(f"{self.images[ind[0]].image_name=}") + print(f"{buffer_data[top:bottom, left:right]=}") + print(f"{np.isnan(buffer_data[top:bottom, left:right]).any()=}") + + mean_readable = 0 self.mean_paths[self.images[ind[0]].image_path] = (buffer_mean, mean_readable) np.put(self.mean, ind, buffer_mean) np.put(self.mean_readable, ind, mean_readable) + print(f"calc_mean_chunk: {ind=}") + print(f"calc_mean_chunk: {buffer_mean=}") + print(f"calc_mean_chunk: {mean_readable=}") + print(f"calc_mean_chunk: {buffer_mean * mean_readable=}") class ImageWatcher(QObject): diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 1537e6a9501..3acebedfc33 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -15,6 +15,7 @@ from tifffile import tifffile from tifffile.tifffile import TiffFileError +from mantidimaging.gui.dialogs.async_task import TaskWorkerThread from mantidimaging.gui.mvp_base import BasePresenter from mantidimaging.gui.windows.live_viewer.model import LiveViewerWindowModel, Image_Data from mantidimaging.core.operations.loader import load_filter_packages @@ -30,18 +31,6 @@ IMAGE_lIST_UPDATE_TIME = 100 -class Worker(QObject): - finished = pyqtSignal() - - def __init__(self, presenter: LiveViewerWindowPresenter): - super().__init__() - self.presenter = presenter - - def run(self): - self.presenter.model.calc_mean_chunk(100) - self.finished.emit() - - class LiveViewerWindowPresenter(BasePresenter): """ The presenter for the Live Viewer window. @@ -52,8 +41,6 @@ class LiveViewerWindowPresenter(BasePresenter): view: LiveViewerWindowView model: LiveViewerWindowModel op_func: Callable - thread: QThread - worker: Worker old_image_list_paths: list[Path] = [] def __init__(self, view: LiveViewerWindowView, main_window: MainWindowView): @@ -243,25 +230,24 @@ def handle_roi_moved(self) -> None: self.model.clear_mean_partial() self.model.roi = roi self.set_roi_enabled(False) - self.run_mean_chunk_calc() - - def run_mean_chunk_calc(self) -> None: - self.thread = QThread() - self.worker = Worker(self) - self.worker.moveToThread(self.thread) - self.thread.started.connect(self.worker.run) - self.worker.finished.connect(self.thread.quit) - self.worker.finished.connect(self.worker.deleteLater) - self.thread.finished.connect(self.thread.deleteLater) - self.thread.finished.connect(self.thread_cleanup) + self.thread = TaskWorkerThread() + self.thread.kwargs = {"chunk_size": 100} + self.thread.task_function = self.model.calc_mean_chunk + + self.thread.finished.connect(lambda: self.thread_cleanup(self.thread)) self.thread.start() - def thread_cleanup(self) -> None: + def thread_cleanup(self, thread: TaskWorkerThread) -> None: + print(f"thread_cleanup 0: {np.isnan(self.model.mean * self.model.mean_readable).any()}") + if thread.error is not None: + raise thread.error self.update_intensity_with_mean() self.set_roi_enabled(True) if np.isnan(self.model.mean).any() and self.model.mean_readable.all(): + print("thread_cleanup 1") self.try_next_mean_chunk() if not np.isnan(self.model.mean * self.model.mean_readable).any(): + print("thread_cleanup 2") self.try_next_mean_chunk() def handle_notify_roi_moved(self) -> None: From d303100371d2a1b9430e5194a34e0501aed866e6 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 8 Jul 2025 13:31:08 +0100 Subject: [PATCH 2/6] switch to using masked array and refactor logic to distinguish corruption/uncalculated nans --- .../gui/windows/live_viewer/model.py | 45 +++++-------------- .../gui/windows/live_viewer/presenter.py | 14 +++--- mantidimaging/gui/windows/live_viewer/view.py | 2 +- 3 files changed, 16 insertions(+), 45 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index 893c1bbbe26..d2e8e808c6e 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -151,9 +151,7 @@ def __init__(self, presenter: LiveViewerWindowPresenter): self._dataset_path: Path | None = None self.image_watcher: ImageWatcher | None = None self.images: list[Image_Data] = [] - self.mean: np.ndarray = np.empty(0) - self.mean_readable: np.ndarray = np.empty(0) - self.mean_paths: dict[Path, tuple[float, int]] = {} + self.mean_nan_mask: np.ma.MaskedArray = np.ma.empty(0) self.roi: SensibleROI | None = None self.image_cache = ImageCache(max_cache_size=100) self.mean_calc_finished = False @@ -191,63 +189,40 @@ def close(self) -> None: self.image_watcher = None self.presenter = None # type: ignore # Model instance to be destroyed -type can be inconsistent - def add_mean(self, image_path: Path, image_array: np.ndarray | None) -> None: + def add_mean(self, image_array: np.ndarray | None) -> None: if image_array is None: mean_to_add = np.nan - mean_readable = 0 elif self.roi is not None: left, top, right, bottom = self.roi mean_to_add = np.mean(image_array[top:bottom, left:right]) - mean_readable = 1 else: mean_to_add = np.mean(image_array) - mean_readable = 1 - self.update_mean_dict(image_path, mean_to_add, mean_readable) + self.update_mean(mean_to_add) - def update_mean_dict(self, image_path: Path, mean_to_add: float, mean_readable: int) -> None: - self.mean_paths[image_path] = (mean_to_add, mean_readable) - self.mean = np.array(list(self.mean_paths.values()))[:, 0] - self.mean_readable = np.array(list(self.mean_paths.values()))[:, 1] + def update_mean(self, mean_to_add: float) -> None: + self.mean_nan_mask = np.append(self.mean_nan_mask, np.array(mean_to_add)) def clear_mean_partial(self) -> None: - self.mean_paths = dict.fromkeys(self.mean_paths, (np.nan, 1)) - self.mean = np.full(len(self.images), np.nan) - self.mean_readable = np.full(len(self.images), 1) + self.mean_nan_mask = np.ma.asarray(np.full(len(self.images), np.nan)) def calc_mean_chunk(self, chunk_size: int = 100) -> None: if self.images is not None: - print("calc_mean_chunk 0") - nanInds = np.argwhere(np.isnan(self.mean)) + nanInds = np.argwhere(np.isnan(self.mean_nan_mask)) if self.roi: - print("calc_mean_chunk 0") left, top, right, bottom = self.roi else: - print("calc_mean_chunk 0") left, top, right, bottom = (0, 0, -1, -1) if nanInds.size > 0: for ind in nanInds[-1:-chunk_size:-1]: try: buffer_data = self.image_cache.load_image(self.images[ind[0]]) - #print(f"calc_mean_chunk try: {buffer_data=}") buffer_mean = np.mean(buffer_data[top:bottom, left:right]) - #print(f"calc_mean_chunk try: {buffer_mean=}") - mean_readable = 1 except ImageLoadFailError: - #print(f"Failed to load {self.images[ind[0]]}") buffer_mean = np.nan if np.isnan(buffer_mean): - print(f"{self.images[ind[0]].image_name=}") - print(f"{buffer_data[top:bottom, left:right]=}") - print(f"{np.isnan(buffer_data[top:bottom, left:right]).any()=}") - - mean_readable = 0 - self.mean_paths[self.images[ind[0]].image_path] = (buffer_mean, mean_readable) - np.put(self.mean, ind, buffer_mean) - np.put(self.mean_readable, ind, mean_readable) - print(f"calc_mean_chunk: {ind=}") - print(f"calc_mean_chunk: {buffer_mean=}") - print(f"calc_mean_chunk: {mean_readable=}") - print(f"calc_mean_chunk: {buffer_mean * mean_readable=}") + self.mean_nan_mask[ind[0]] = np.ma.masked + else: + np.put(self.mean_nan_mask, ind, buffer_mean) class ImageWatcher(QObject): diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 3acebedfc33..c09d51170d5 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -7,7 +7,7 @@ from collections.abc import Callable from logging import getLogger import numpy as np -from PyQt5.QtCore import pyqtSignal, QObject, QThread, QTimer +from PyQt5.QtCore import QTimer from astropy.io import fits from astropy.utils.exceptions import AstropyUserWarning @@ -238,32 +238,28 @@ def handle_roi_moved(self) -> None: self.thread.start() def thread_cleanup(self, thread: TaskWorkerThread) -> None: - print(f"thread_cleanup 0: {np.isnan(self.model.mean * self.model.mean_readable).any()}") if thread.error is not None: raise thread.error self.update_intensity_with_mean() self.set_roi_enabled(True) - if np.isnan(self.model.mean).any() and self.model.mean_readable.all(): - print("thread_cleanup 1") - self.try_next_mean_chunk() - if not np.isnan(self.model.mean * self.model.mean_readable).any(): - print("thread_cleanup 2") + if np.isnan(self.model.mean_nan_mask).any(): self.try_next_mean_chunk() def handle_notify_roi_moved(self) -> None: self.model.clear_mean_partial() + if not self.handle_roi_change_timer.isActive(): self.handle_roi_change_timer.start(10) def update_intensity_with_mean(self) -> None: self.view.intensity_profile.clearPlots() - self.view.intensity_profile.plot(self.model.mean) + self.view.intensity_profile.plot(self.model.mean_nan_mask) def set_roi_enabled(self, enable: bool): if self.view.live_viewer.roi_object is not None: self.view.live_viewer.roi_object.blockSignals(not enable) def try_next_mean_chunk(self) -> None: - if np.isnan(self.model.mean).any(): + if np.isnan(self.model.mean_nan_mask).any(): if not self.handle_roi_change_timer.isActive(): self.handle_roi_change_timer.start(10) diff --git a/mantidimaging/gui/windows/live_viewer/view.py b/mantidimaging/gui/windows/live_viewer/view.py index b59eba4f811..212087764a1 100644 --- a/mantidimaging/gui/windows/live_viewer/view.py +++ b/mantidimaging/gui/windows/live_viewer/view.py @@ -136,7 +136,7 @@ def set_intensity_visibility(self) -> None: self.presenter.model.clear_mean_partial() if self.presenter.model.images: self.presenter.handle_roi_moved() - self.presenter.update_intensity(self.presenter.model.mean) + self.presenter.update_intensity(self.presenter.model.mean_nan_mask) else: self.live_viewer.set_roi_visibility_flags(False) self.splitter.setSizes([widget_height, 0]) From e5fe351a4feae837741bf5112aae16fe380711f9 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 8 Jul 2025 14:07:50 +0100 Subject: [PATCH 3/6] refactor presenter.update_image_list to used the masked array and to trigger a new calculation thread when needed --- mantidimaging/gui/windows/live_viewer/model.py | 2 +- mantidimaging/gui/windows/live_viewer/presenter.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/model.py b/mantidimaging/gui/windows/live_viewer/model.py index d2e8e808c6e..f64b29cac62 100644 --- a/mantidimaging/gui/windows/live_viewer/model.py +++ b/mantidimaging/gui/windows/live_viewer/model.py @@ -200,7 +200,7 @@ def add_mean(self, image_array: np.ndarray | None) -> None: self.update_mean(mean_to_add) def update_mean(self, mean_to_add: float) -> None: - self.mean_nan_mask = np.append(self.mean_nan_mask, np.array(mean_to_add)) + self.mean_nan_mask = np.ma.append(self.mean_nan_mask, np.array(mean_to_add)) def clear_mean_partial(self) -> None: self.mean_nan_mask = np.ma.asarray(np.full(len(self.images), np.nan)) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index c09d51170d5..7b2c9f3461d 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -100,11 +100,11 @@ def update_image_list(self) -> None: images_list_paths = [image.image_path for image in images_list] if self.old_image_list_paths == images_list_paths[:-1]: self.try_add_mean(images_list[-1]) - self.update_intensity(self.model.mean) + self.update_intensity(self.model.mean_nan_mask) self.old_image_list_paths = images_list_paths else: self.model.clear_mean_partial() - self.run_mean_chunk_calc() + self.handle_roi_moved() self.view.set_image_range((0, len(images_list) - 1)) self.view.set_image_index(len(images_list) - 1) self.view.set_load_as_dataset_enabled(True) @@ -115,11 +115,11 @@ def notify_update_image_list(self) -> None: def try_add_mean(self, image: Image_Data) -> None: try: image_data = self.model.image_cache.load_image(image) - self.model.add_mean(image.image_path, image_data) + self.model.add_mean(image_data) self.update_intensity_with_mean() except ImageLoadFailError as error: logger.error(error.message) - self.model.add_mean(image.image_path, None) + self.model.add_mean(None) def select_image(self, index: int) -> None: if not self.model.images: From 5d46fc692962f1920f417bd53125bb2ebcda66af Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Tue, 8 Jul 2025 18:15:18 +0100 Subject: [PATCH 4/6] live viewer presenter fix changing to masked arrays Live Viewer system test fix --- mantidimaging/gui/test/gui_system_liveviewer_test.py | 12 ++++++------ .../gui/windows/live_viewer/test/presenter_test.py | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/mantidimaging/gui/test/gui_system_liveviewer_test.py b/mantidimaging/gui/test/gui_system_liveviewer_test.py index 06d6358ccb5..0461659952f 100644 --- a/mantidimaging/gui/test/gui_system_liveviewer_test.py +++ b/mantidimaging/gui/test/gui_system_liveviewer_test.py @@ -41,8 +41,8 @@ def test_open_close_intensity_profile(self): self.live_viewer_window.intensity_action.trigger() QTest.qWait(SHORT_DELAY) QTest.qWait(SHOW_DELAY) - wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean).any(), max_retry=600) - self.assertFalse(np.isnan(self.live_viewer_window.presenter.model.mean).any()) + wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean_nan_mask).any(), max_retry=600) + self.assertFalse(np.isnan(self.live_viewer_window.presenter.model.mean_nan_mask).any()) self.assertNotEqual(self.live_viewer_window.splitter.sizes()[1], 0) self.assertTrue(self.live_viewer_window.intensity_profile.isVisible()) self.live_viewer_window.intensity_action.trigger() @@ -53,14 +53,14 @@ def test_roi_resized(self): QTest.qWait(SHORT_DELAY * 4) self.live_viewer_window.intensity_action.trigger() QTest.qWait(SHORT_DELAY) - wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean).any(), max_retry=600) - old_mean = self.live_viewer_window.presenter.model.mean + wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean_nan_mask).any(), max_retry=600) + old_mean = self.live_viewer_window.presenter.model.mean_nan_mask roi = self.live_viewer_window.live_viewer.roi_object handle_index = 0 new_position = (10, 20) roi.movePoint(handle_index, new_position) self.live_viewer_window.presenter.model.clear_mean_partial() - wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean).any(), max_retry=600) + wait_until(lambda: not np.isnan(self.live_viewer_window.presenter.model.mean_nan_mask).any(), max_retry=600) QTest.qWait(SHORT_DELAY) assert_raises(AssertionError, np.testing.assert_array_equal, old_mean, - self.live_viewer_window.presenter.model.mean) + self.live_viewer_window.presenter.model.mean_nan_mask) diff --git a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py index 4a23ca2995c..711517dc451 100644 --- a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py @@ -21,6 +21,7 @@ def setUp(self): self.main_window = mock.create_autospec(MainWindowView, instance=True) self.model = mock.create_autospec(LiveViewerWindowModel, instance=True) self.model.image_cache = mock.create_autospec(ImageCache, instance=True) + self.model.mean_nan_mask = mock.create_autospec(np.ma.MaskedArray, instance=True) with mock.patch("mantidimaging.gui.windows.live_viewer.presenter.LiveViewerWindowModel") as mock_model: mock_model.return_value = self.model @@ -70,14 +71,14 @@ def test_WHEN_handle_notify_roi_moved_THEN_timer_started(self): self.presenter.handle_roi_change_timer.start.assert_called_once() def test_WHEN_nans_in_mean_THEN_handle_roi_change_timer_start(self): - self.model.mean = np.array([1, 2, 3, np.nan]) + self.model.mean_nan_mask = np.ma.asarray([1, 2, 3, np.nan]) self.presenter.handle_roi_change_timer = mock.Mock() self.presenter.handle_roi_change_timer.isActive.return_value = False self.presenter.try_next_mean_chunk() self.presenter.handle_roi_change_timer.start.assert_called_once_with(10) def test_WHEN_no_nans_in_mean_THEN_handle_roi_change_timer_not_started(self): - self.model.mean = np.array([1, 2, 3, 4]) + self.model.mean_nan_mask = np.ma.asarray([1, 2, 3, 4]) self.presenter.handle_roi_change_timer = mock.Mock() self.presenter.handle_roi_change_timer.isActive.return_value = False self.presenter.try_next_mean_chunk() From 727a49ab241e44ca99218bb98f7d770c566fc56d Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Fri, 25 Jul 2025 15:34:20 +0100 Subject: [PATCH 5/6] suggested changes LV presenter test fix --- .../gui/windows/live_viewer/presenter.py | 16 ++++++++++++---- .../windows/live_viewer/test/presenter_test.py | 8 +++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/mantidimaging/gui/windows/live_viewer/presenter.py b/mantidimaging/gui/windows/live_viewer/presenter.py index 7b2c9f3461d..7d356c89729 100644 --- a/mantidimaging/gui/windows/live_viewer/presenter.py +++ b/mantidimaging/gui/windows/live_viewer/presenter.py @@ -29,6 +29,7 @@ logger = getLogger(__name__) IMAGE_lIST_UPDATE_TIME = 100 +CHUNK_SIZE = 10 class LiveViewerWindowPresenter(BasePresenter): @@ -42,6 +43,8 @@ class LiveViewerWindowPresenter(BasePresenter): model: LiveViewerWindowModel op_func: Callable old_image_list_paths: list[Path] = [] + try_next_mean_chunk_count: int = 0 + try_next_mean_chunk_max_retry: int = 100 def __init__(self, view: LiveViewerWindowView, main_window: MainWindowView): super().__init__(view) @@ -228,10 +231,11 @@ def handle_roi_moved(self) -> None: roi = self.view.live_viewer.get_roi() if roi != self.model.roi: self.model.clear_mean_partial() + self.try_next_mean_chunk_count = 0 self.model.roi = roi self.set_roi_enabled(False) self.thread = TaskWorkerThread() - self.thread.kwargs = {"chunk_size": 100} + self.thread.kwargs = {"chunk_size": CHUNK_SIZE} self.thread.task_function = self.model.calc_mean_chunk self.thread.finished.connect(lambda: self.thread_cleanup(self.thread)) @@ -239,6 +243,7 @@ def handle_roi_moved(self) -> None: def thread_cleanup(self, thread: TaskWorkerThread) -> None: if thread.error is not None: + logger.error("Error during background processing: %s", thread.error) raise thread.error self.update_intensity_with_mean() self.set_roi_enabled(True) @@ -260,6 +265,9 @@ def set_roi_enabled(self, enable: bool): self.view.live_viewer.roi_object.blockSignals(not enable) def try_next_mean_chunk(self) -> None: - if np.isnan(self.model.mean_nan_mask).any(): - if not self.handle_roi_change_timer.isActive(): - self.handle_roi_change_timer.start(10) + self.try_next_mean_chunk_count += 1 + if self.try_next_mean_chunk_count > (len(self.model.images) / CHUNK_SIZE) + self.try_next_mean_chunk_max_retry: + logger.warning("Too many retries for mean calculation. Aborting.") + return + if not self.handle_roi_change_timer.isActive(): + self.handle_roi_change_timer.start(10) diff --git a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py index 711517dc451..1a3ad9950a7 100644 --- a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py @@ -74,6 +74,7 @@ def test_WHEN_nans_in_mean_THEN_handle_roi_change_timer_start(self): self.model.mean_nan_mask = np.ma.asarray([1, 2, 3, np.nan]) self.presenter.handle_roi_change_timer = mock.Mock() self.presenter.handle_roi_change_timer.isActive.return_value = False + self.model.images = np.empty(4) self.presenter.try_next_mean_chunk() self.presenter.handle_roi_change_timer.start.assert_called_once_with(10) @@ -81,5 +82,10 @@ def test_WHEN_no_nans_in_mean_THEN_handle_roi_change_timer_not_started(self): self.model.mean_nan_mask = np.ma.asarray([1, 2, 3, 4]) self.presenter.handle_roi_change_timer = mock.Mock() self.presenter.handle_roi_change_timer.isActive.return_value = False - self.presenter.try_next_mean_chunk() + self.model.images = np.empty(4) + mock_thread = mock.Mock() + mock_thread.error = None + self.presenter.set_roi_enabled = mock.Mock() + self.presenter.update_intensity_with_mean = mock.Mock() + self.presenter.thread_cleanup(mock_thread) self.presenter.handle_roi_change_timer.start.assert_not_called() From f5eb5342a49c0fa676468d4ebff46ed8a60c2636 Mon Sep 17 00:00:00 2001 From: Mike Sullivan Date: Mon, 28 Jul 2025 13:04:33 +0100 Subject: [PATCH 6/6] added test_WHEN_thread_error_raised_THEN_error_caught_and_logged test_WHEN_thread_error_raised_THEN_error_caught --- .../gui/windows/live_viewer/test/presenter_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py index 1a3ad9950a7..19d6552e3fc 100644 --- a/mantidimaging/gui/windows/live_viewer/test/presenter_test.py +++ b/mantidimaging/gui/windows/live_viewer/test/presenter_test.py @@ -89,3 +89,16 @@ def test_WHEN_no_nans_in_mean_THEN_handle_roi_change_timer_not_started(self): self.presenter.update_intensity_with_mean = mock.Mock() self.presenter.thread_cleanup(mock_thread) self.presenter.handle_roi_change_timer.start.assert_not_called() + + def test_WHEN_thread_error_raised_THEN_error_caught_and_logged(self): + self.model.mean_nan_mask = np.ma.asarray([1, 2, 3, 4]) + self.presenter.handle_roi_change_timer = mock.Mock() + self.presenter.handle_roi_change_timer.isActive.return_value = False + self.model.images = np.empty(4) + mock_thread = mock.Mock() + mock_thread.error = ValueError() + self.presenter.set_roi_enabled = mock.Mock() + self.presenter.update_intensity_with_mean = mock.Mock() + with self.assertRaises(ValueError): + self.presenter.thread_cleanup(mock_thread) + self.presenter.handle_roi_change_timer.start.assert_not_called()