diff --git a/docs/release_notes/next/dev-2177-spectrum-viewer-ToF-tests b/docs/release_notes/next/dev-2177-spectrum-viewer-ToF-tests new file mode 100644 index 00000000000..3fe6257f6d3 --- /dev/null +++ b/docs/release_notes/next/dev-2177-spectrum-viewer-ToF-tests @@ -0,0 +1 @@ +#2177: Added unit tests for the Time of Flight user properties in the Spectrum Viewer \ No newline at end of file diff --git a/mantidimaging/gui/windows/spectrum_viewer/model.py b/mantidimaging/gui/windows/spectrum_viewer/model.py index e3e0d37e3ae..67d5ca15240 100644 --- a/mantidimaging/gui/windows/spectrum_viewer/model.py +++ b/mantidimaging/gui/windows/spectrum_viewer/model.py @@ -4,7 +4,7 @@ import csv from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict import numpy as np from math import ceil @@ -52,6 +52,31 @@ def get_by_value(cls, value: str) -> ErrorMode: raise ValueError(f"Unknown error mode: {value}") +class AllowedModesTypedDict(TypedDict): + mode: ToFUnitMode + label: str + + +allowed_modes: dict[str, AllowedModesTypedDict] = { + "Image Index": { + "mode": ToFUnitMode.IMAGE_NUMBER, + "label": "Image index" + }, + "Wavelength": { + "mode": ToFUnitMode.WAVELENGTH, + "label": "Neutron Wavelength (\u212B)" + }, + "Energy": { + "mode": ToFUnitMode.ENERGY, + "label": "Neutron Energy (MeV)" + }, + "Time of Flight (\u03BCs)": { + "mode": ToFUnitMode.TOF_US, + "label": "Time of Flight (\u03BCs)" + } +} + + class SpectrumViewerWindowModel: """ The model for the spectrum viewer window. @@ -75,12 +100,6 @@ def __init__(self, presenter: SpectrumViewerWindowPresenter): self._roi_ranges = {} self.special_roi_list = [ROI_ALL] - self.tof_data = self.get_stack_time_of_flight() - if self.tof_data is None: - self.tof_mode = ToFUnitMode.IMAGE_NUMBER - else: - self.tof_mode = ToFUnitMode.WAVELENGTH - self.units = UnitConversion() def roi_name_generator(self) -> str: @@ -481,3 +500,15 @@ def set_relevant_tof_units(self) -> None: self.tof_data = self.units.tof_seconds_to_energy() self.tof_plot_range = (self.tof_data.min(), self.tof_data.max()) self.tof_range = (0, self.tof_data.size) + + def set_tof_unit_mode_for_stack(self) -> None: + if self.get_stack_time_of_flight() is None or self.tof_data is None: + self.tof_mode = ToFUnitMode.IMAGE_NUMBER + self.presenter.change_selected_menu_option("Image Index") + elif self.tof_mode == ToFUnitMode.ENERGY: + self.presenter.change_selected_menu_option("Energy") + elif self.tof_mode == ToFUnitMode.TOF_US: + self.presenter.change_selected_menu_option("Time of Flight (\u03BCs)") + else: + self.tof_mode = ToFUnitMode.WAVELENGTH + self.presenter.change_selected_menu_option("Wavelength") diff --git a/mantidimaging/gui/windows/spectrum_viewer/presenter.py b/mantidimaging/gui/windows/spectrum_viewer/presenter.py index a3f2e6b5cb3..a082c0f7a7c 100644 --- a/mantidimaging/gui/windows/spectrum_viewer/presenter.py +++ b/mantidimaging/gui/windows/spectrum_viewer/presenter.py @@ -15,7 +15,7 @@ from mantidimaging.gui.dialogs.async_task import start_async_task_view, TaskWorkerThread from mantidimaging.gui.mvp_base import BasePresenter from mantidimaging.gui.windows.spectrum_viewer.model import SpectrumViewerWindowModel, SpecType, ROI_RITS, ErrorMode, \ - ToFUnitMode + ToFUnitMode, allowed_modes if TYPE_CHECKING: from mantidimaging.gui.windows.spectrum_viewer.view import SpectrumViewerWindowView # pragma: no cover @@ -67,7 +67,10 @@ def handle_stack_changed(self) -> None: except RuntimeError: norm_stack = None self.model.set_normalise_stack(norm_stack) + + self.model.set_tof_unit_mode_for_stack() self.reset_units_menu() + self.handle_tof_unit_change() self.show_new_sample() self.redraw_all_rois() @@ -92,7 +95,9 @@ def handle_sample_change(self, uuid: UUID | None) -> None: return self.model.set_stack(self.main_window.get_stack(uuid)) + self.model.set_tof_unit_mode_for_stack() self.reset_units_menu() + self.handle_tof_unit_change() normalise_uuid = self.view.get_normalise_stack() if normalise_uuid is not None: @@ -112,20 +117,12 @@ def reset_units_menu(self): if self.model.tof_data is None: self.view.tof_mode_select_group.setEnabled(False) self.view.tofPropertiesGroupBox.setEnabled(False) - else: - self.view.tof_mode_select_group.setEnabled(True) - self.view.tofPropertiesGroupBox.setEnabled(True) - self.model.tof_mode = ToFUnitMode.IMAGE_NUMBER - for action in self.view.tof_mode_select_group.actions(): - with QSignalBlocker(action): - if action.objectName() == 'Image Index': - action.setChecked(True) - else: - action.setChecked(False) - if self.model.tof_data is None: + self.model.tof_mode = ToFUnitMode.IMAGE_NUMBER + self.change_selected_menu_option("Image Index") self.view.tof_mode_select_group.setEnabled(False) else: self.view.tof_mode_select_group.setEnabled(True) + self.view.tofPropertiesGroupBox.setEnabled(True) def handle_normalise_stack_change(self, normalise_uuid: UUID | None) -> None: if normalise_uuid == self.current_norm_stack_uuid: @@ -352,13 +349,15 @@ def handle_export_tab_change(self, index: int) -> None: self.view.on_visibility_change() def handle_tof_unit_change(self) -> None: - selected_mode = self.view.tof_mode_select_group.checkedAction().text() - self.model.tof_mode = self.view.allowed_modes[selected_mode]["mode"] self.model.set_relevant_tof_units() - tof_axis_label = self.view.allowed_modes[selected_mode]["label"] + tof_axis_label = allowed_modes[self.view.tof_units_mode]["label"] self.view.spectrum_widget.spectrum_plot_widget.set_tof_axis_label(tof_axis_label) self.refresh_spectrum_plot() + def handle_tof_unit_change_via_menu(self) -> None: + self.model.tof_mode = allowed_modes[self.view.tof_units_mode]["mode"] + self.handle_tof_unit_change() + def refresh_spectrum_plot(self) -> None: self.view.spectrum_widget.spectrum.clearPlots() self.view.spectrum_widget.spectrum.update() @@ -373,7 +372,14 @@ def handle_flight_path_change(self) -> None: self.refresh_spectrum_plot() def handle_time_delay_change(self) -> None: - self.model.tof_data = self.model.get_stack_time_of_flight() self.model.units.data_offset = self.view.timeDelaySpinBox.value() * 1e-6 self.model.set_relevant_tof_units() self.refresh_spectrum_plot() + + def change_selected_menu_option(self, opt): + for action in self.view.tof_mode_select_group.actions(): + with QSignalBlocker(action): + if action.objectName() == opt: + action.setChecked(True) + else: + action.setChecked(False) diff --git a/mantidimaging/gui/windows/spectrum_viewer/test/presenter_test.py b/mantidimaging/gui/windows/spectrum_viewer/test/presenter_test.py index 01d21810e64..7af33b70aca 100644 --- a/mantidimaging/gui/windows/spectrum_viewer/test/presenter_test.py +++ b/mantidimaging/gui/windows/spectrum_viewer/test/presenter_test.py @@ -12,7 +12,7 @@ from mantidimaging.core.data.dataset import StrictDataset, MixedDataset from mantidimaging.gui.windows.main import MainWindowView from mantidimaging.gui.windows.spectrum_viewer import SpectrumViewerWindowView, SpectrumViewerWindowPresenter -from mantidimaging.gui.windows.spectrum_viewer.model import ErrorMode +from mantidimaging.gui.windows.spectrum_viewer.model import ErrorMode, ToFUnitMode from mantidimaging.gui.windows.spectrum_viewer.spectrum_widget import SpectrumWidget, SpectrumPlotWidget from mantidimaging.test_helpers import mock_versions, start_qapplication from mantidimaging.test_helpers.unit_test_helper import generate_images @@ -173,6 +173,7 @@ def test_gui_changes_tof_range(self): image_stack = generate_images([30, 11, 12]) new_tof_range = (10, 20) self.presenter.model.set_stack(image_stack) + self.presenter.model.tof_mode = ToFUnitMode.IMAGE_NUMBER self.presenter.handle_range_slide_moved(new_tof_range) self.assertEqual(self.presenter.model.tof_range, new_tof_range) @@ -271,3 +272,36 @@ def test_WHEN_do_remove_roi_called_with_no_arguments_THEN_all_rois_removed(self) self.assertEqual(["all", "roi", "roi_1", "roi_2"], self.presenter.model.get_list_of_roi_names()) self.presenter.do_remove_roi() self.assertEqual([], self.presenter.model.get_list_of_roi_names()) + + @parameterized.expand([("Image Index", ToFUnitMode.IMAGE_NUMBER), ("Wavelength", ToFUnitMode.WAVELENGTH), + ("Energy", ToFUnitMode.ENERGY), ("Time of Flight (\u03BCs)", ToFUnitMode.TOF_US)]) + def test_WHEN_tof_unit_selected_THEN_model_mode_changes(self, mode_text, expected_mode): + self.view.tof_units_mode = mode_text + self.presenter.refresh_spectrum_plot = mock.Mock() + self.presenter.handle_tof_unit_change_via_menu() + self.assertEqual(self.presenter.model.tof_mode, expected_mode) + + @mock.patch("mantidimaging.gui.windows.spectrum_viewer.model.SpectrumViewerWindowModel.get_stack_time_of_flight") + def test_WHEN_no_spectrum_data_THEN_mode_is_image_index(self, get_stack_time_of_flight): + self.presenter.model.set_stack(generate_images()) + self.presenter.get_dataset_id_for_stack = mock.Mock(return_value=uuid.uuid4()) + self.presenter.main_window.get_stack = mock.Mock(return_value=generate_images()) + get_stack_time_of_flight.return_value = None + self.view.tof_units_mode = "Wavelength" + self.presenter.refresh_spectrum_plot = mock.Mock() + self.presenter.handle_sample_change(uuid.uuid4()) + self.assertEqual(self.presenter.model.tof_mode, ToFUnitMode.IMAGE_NUMBER) + + def test_WHEN_tof_flight_path_changed_THEN_unit_conversion_flight_path_set(self): + self.view.flightPathSpinBox = mock.Mock() + self.view.flightPathSpinBox.value.return_value = 10 + self.presenter.refresh_spectrum_plot = mock.Mock() + self.presenter.handle_flight_path_change() + self.assertEqual(self.presenter.model.units.target_to_camera_dist, 10) + + def test_WHEN_tof_delay_changed_THEN_unit_conversion_delay_set(self): + self.view.timeDelaySpinBox = mock.Mock() + self.view.timeDelaySpinBox.value.return_value = 400 + self.presenter.refresh_spectrum_plot = mock.Mock() + self.presenter.handle_time_delay_change() + self.assertEqual(self.presenter.model.units.data_offset, 400 * 1e-6) diff --git a/mantidimaging/gui/windows/spectrum_viewer/test/spectrum_test.py b/mantidimaging/gui/windows/spectrum_viewer/test/spectrum_test.py index 20ff64f7d12..ee6aa6b31a4 100644 --- a/mantidimaging/gui/windows/spectrum_viewer/test/spectrum_test.py +++ b/mantidimaging/gui/windows/spectrum_viewer/test/spectrum_test.py @@ -167,3 +167,7 @@ def test_WHEN_rename_roi_called_with_default_roi_THEN_roi_name_not_changed(self) self.assertIn("roi_1", self.spectrum_widget.roi_dict.keys()) self.spectrum_widget.rename_roi("roi_1", "roi") self.assertIn("roi_1", self.spectrum_widget.roi_dict) + + def test_WHEN_tof_axis_label_changed_THEN_axis_label_set(self): + self.spectrum_plot_widget.set_tof_axis_label("test") + self.assertEqual(self.spectrum_plot_widget.spectrum.getAxis('bottom').labelText, "test") diff --git a/mantidimaging/gui/windows/spectrum_viewer/view.py b/mantidimaging/gui/windows/spectrum_viewer/view.py index 51354077f11..b82238b0a85 100644 --- a/mantidimaging/gui/windows/spectrum_viewer/view.py +++ b/mantidimaging/gui/windows/spectrum_viewer/view.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, TypedDict +from typing import TYPE_CHECKING from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QFileDialog, QPushButton, QLabel, QAbstractItemView, QHeaderView, \ @@ -13,7 +13,7 @@ from mantidimaging.core.utility import finder from mantidimaging.gui.mvp_base import BaseMainWindowView from mantidimaging.gui.widgets.dataset_selector import DatasetSelectorWidgetView -from .model import ROI_RITS, ToFUnitMode +from .model import ROI_RITS, allowed_modes from .presenter import SpectrumViewerWindowPresenter, ExportMode from mantidimaging.gui.widgets import RemovableRowTableView from .spectrum_widget import SpectrumWidget @@ -27,11 +27,6 @@ from uuid import UUID -class AllowedModesTypedDict(TypedDict): - mode: ToFUnitMode - label: str - - class SpectrumViewerWindowView(BaseMainWindowView): tableView: RemovableRowTableView sampleStackSelector: DatasetSelectorWidgetView @@ -94,30 +89,12 @@ def __init__(self, main_window: MainWindowView): self.units_menu = self.spectrum_right_click_menu.addMenu("Units") self.tof_mode_select_group = QActionGroup(self) - self.allowed_modes: dict[str, AllowedModesTypedDict] = { - "Image Index": { - "mode": ToFUnitMode.IMAGE_NUMBER, - "label": "Image index" - }, - "Wavelength": { - "mode": ToFUnitMode.WAVELENGTH, - "label": "Neutron Wavelength (\u212B)" - }, - "Energy": { - "mode": ToFUnitMode.ENERGY, - "label": "Neutron Energy (MeV)" - }, - "Time of Flight (\u03BCs)": { - "mode": ToFUnitMode.TOF_US, - "label": "Time of Flight (\u03BCs)" - } - } - for mode in self.allowed_modes.keys(): + for mode in allowed_modes.keys(): action = QAction(mode, self.tof_mode_select_group) action.setCheckable(True) action.setObjectName(mode) self.units_menu.addAction(action) - action.triggered.connect(self.presenter.handle_tof_unit_change) + action.triggered.connect(self.presenter.handle_tof_unit_change_via_menu) if mode == "Image Index": action.setChecked(True) if self.presenter.model.tof_data is None: @@ -527,6 +504,10 @@ def set_binning_visibility(self) -> None: self.bin_step_label.setHidden(hide_binning) self.bin_step_spinBox.setHidden(hide_binning) + @property + def tof_units_mode(self) -> str: + return self.tof_mode_select_group.checkedAction().text() + def set_roi_properties(self) -> None: if self.presenter.export_mode == ExportMode.IMAGE_MODE: self.current_roi = ROI_RITS