Skip to content

Commit

Permalink
Switch Time of Flight units in Spectrum Viewer (#2151)
Browse files Browse the repository at this point in the history
  • Loading branch information
samtygier-stfc committed Apr 10, 2024
2 parents fdb12ae + 4eb97a0 commit 5b827de
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#2151: Neutron wavelength, energy and tof units can be selected in the Spectrum Viewer
28 changes: 28 additions & 0 deletions mantidimaging/core/utility/unit_conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright (C) 2024 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
from __future__ import annotations

import numpy as np


class UnitConversion:
# target_to_camera_dist = 56 m taken from https://scripts.iucr.org/cgi-bin/paper?S1600576719001730
neutron_mass: float = 1.674927211e-27 # [kg]
planck_h: float = 6.62606896e-34 # [JHz-1]
angstrom: float = 1e-10 # [m]
mega_electro_volt: float = 1.60217662e-19 / 1e6

def __init__(self, data_to_convert: np.ndarray, target_to_camera_dist: float = 56) -> None:
self.tof_data_to_convert = data_to_convert
self.target_to_camera_dist = target_to_camera_dist
self.velocity = self.target_to_camera_dist / self.tof_data_to_convert

def tof_seconds_to_wavelength(self) -> np.ndarray:
wavelength = self.planck_h / (self.neutron_mass * self.velocity)
wavelength_angstroms = wavelength / self.angstrom
return wavelength_angstroms

def tof_seconds_to_energy(self) -> np.ndarray:
energy = self.neutron_mass * self.velocity / 2
energy_evs = energy / self.mega_electro_volt
return energy_evs
38 changes: 38 additions & 0 deletions mantidimaging/gui/windows/spectrum_viewer/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from mantidimaging.core.io.instrument_log import LogColumn
from mantidimaging.core.utility.sensible_roi import SensibleROI
from mantidimaging.core.utility.progress_reporting import Progress
from mantidimaging.core.utility.unit_conversion import UnitConversion

if TYPE_CHECKING:
from mantidimaging.gui.windows.spectrum_viewer.presenter import SpectrumViewerWindowPresenter
Expand All @@ -32,6 +33,13 @@ class SpecType(Enum):
SAMPLE_NORMED = 3


class ToFUnitMode(Enum):
IMAGE_NUMBER = 1
TOF_US = 2
WAVELENGTH = 3
ENERGY = 4


class ErrorMode(Enum):
STANDARD_DEVIATION = "Standard Deviation"
PROPAGATED = "Propagated"
Expand All @@ -55,14 +63,24 @@ class SpectrumViewerWindowModel:
_stack: ImageStack | None = None
_normalise_stack: ImageStack | None = None
tof_range: tuple[int, int] = (0, 0)
tof_plot_range: tuple[float, float] | tuple[int, int] = (0, 0)
_roi_ranges: dict[str, SensibleROI]
tof_mode: ToFUnitMode
tof_data: np.ndarray | None = None
tof_range_full: tuple[int, int] = (0, 0)

def __init__(self, presenter: SpectrumViewerWindowPresenter):
self.presenter = presenter
self._roi_id_counter = 0
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

def roi_name_generator(self) -> str:
"""
Returns a new Unique ID for newly created ROIs
Expand Down Expand Up @@ -94,6 +112,8 @@ def set_stack(self, stack: ImageStack | None) -> None:
return
self._roi_id_counter = 0
self.tof_range = (0, stack.data.shape[0] - 1)
self.tof_range_full = self.tof_range
self.tof_data = self.get_stack_time_of_flight()
self.set_new_roi(ROI_ALL)

def set_new_roi(self, name: str) -> None:
Expand Down Expand Up @@ -441,3 +461,21 @@ def remove_all_roi(self) -> None:
Remove all ROIs from the model
"""
self._roi_ranges = {}

def set_relevant_tof_units(self) -> None:
if self._stack is not None:
self.tof_data = self.get_stack_time_of_flight()
if self.tof_mode == ToFUnitMode.IMAGE_NUMBER or self.tof_data is None:
self.tof_plot_range = (0, self._stack.data.shape[0] - 1)
self.tof_range = (0, self._stack.data.shape[0] - 1)
self.tof_data = np.arange(self.tof_range[0], self.tof_range[1] + 1)
else:
units = UnitConversion(self.tof_data)
if self.tof_mode == ToFUnitMode.TOF_US:
self.tof_data = self.tof_data * 1e6
elif self.tof_mode == ToFUnitMode.WAVELENGTH:
self.tof_data = units.tof_seconds_to_wavelength()
elif self.tof_mode == ToFUnitMode.ENERGY:
self.tof_data = 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)
51 changes: 48 additions & 3 deletions mantidimaging/gui/windows/spectrum_viewer/presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
from typing import TYPE_CHECKING

from logging import getLogger

import numpy as np
from PyQt5.QtCore import QSignalBlocker

from mantidimaging.core.data.dataset import StrictDataset
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
from mantidimaging.gui.windows.spectrum_viewer.model import SpectrumViewerWindowModel, SpecType, ROI_RITS, ErrorMode, \
ToFUnitMode

if TYPE_CHECKING:
from mantidimaging.gui.windows.spectrum_viewer.view import SpectrumViewerWindowView # pragma: no cover
Expand Down Expand Up @@ -62,6 +67,8 @@ def handle_stack_changed(self) -> None:
except RuntimeError:
norm_stack = None
self.model.set_normalise_stack(norm_stack)
self.reset_units_menu()
self.handle_tof_unit_change()
self.show_new_sample()
self.redraw_all_rois()

Expand All @@ -81,9 +88,12 @@ def handle_sample_change(self, uuid: UUID | None) -> None:
if uuid is None:
self.model.set_stack(None)
self.view.clear()
self.view.tof_mode_select_group.setEnabled(False)
return

self.model.set_stack(self.main_window.get_stack(uuid))
self.reset_units_menu()
self.handle_tof_unit_change()
normalise_uuid = self.view.get_normalise_stack()
if normalise_uuid is not None:
try:
Expand All @@ -98,6 +108,19 @@ def handle_sample_change(self, uuid: UUID | None) -> None:
self.show_new_sample()
self.view.on_visibility_change()

def reset_units_menu(self):
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.view.tof_mode_select_group.setEnabled(False)
else:
self.view.tof_mode_select_group.setEnabled(True)

def handle_normalise_stack_change(self, normalise_uuid: UUID | None) -> None:
if normalise_uuid == self.current_norm_stack_uuid:
return
Expand Down Expand Up @@ -135,13 +158,22 @@ def show_new_sample(self) -> None:
averaged_image = self.model.get_averaged_image()
assert averaged_image is not None
self.view.set_image(averaged_image)
self.view.spectrum_widget.spectrum_plot_widget.add_range(*self.model.tof_range)
self.view.spectrum_widget.spectrum_plot_widget.add_range(*self.model.tof_plot_range)
self.view.spectrum_widget.spectrum_plot_widget.set_image_index_range_label(*self.model.tof_range)
self.view.auto_range_image()
if self.view.get_roi_properties_spinboxes():
self.view.set_roi_properties()

def handle_range_slide_moved(self, tof_range) -> None:
self.model.tof_range = tof_range
self.model.tof_plot_range = tof_range
if self.model.tof_mode == ToFUnitMode.IMAGE_NUMBER:
self.model.tof_range = (int(tof_range[0]), int(tof_range[1]))
else:
image_index_min = np.abs(self.model.tof_data - tof_range[0]).argmin()
image_index_max = np.abs(self.model.tof_data - tof_range[1]).argmin()
self.model.tof_range = tuple(sorted((image_index_min, image_index_max)))
self.view.spectrum_widget.spectrum_plot_widget.set_image_index_range_label(*self.model.tof_range)
self.view.spectrum_widget.spectrum_plot_widget.set_tof_range_label(*self.model.tof_plot_range)
averaged_image = self.model.get_averaged_image()
assert averaged_image is not None
self.view.set_image(averaged_image, autoLevels=False)
Expand Down Expand Up @@ -312,3 +344,16 @@ def do_remove_roi(self, roi_name: str | None = None) -> None:
def handle_export_tab_change(self, index: int) -> None:
self.export_mode = ExportMode(index)
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()
self.view.spectrum_widget.spectrum_plot_widget.set_tof_axis_label(
self.view.allowed_modes[selected_mode]["label"])
self.view.spectrum_widget.spectrum.clearPlots()
self.view.spectrum_widget.spectrum.update()
self.view.show_visible_spectrums()
self.view.spectrum_widget.spectrum_plot_widget.add_range(*self.model.tof_plot_range)
self.view.spectrum_widget.spectrum_plot_widget.set_image_index_range_label(*self.model.tof_range)
self.view.auto_range_image()
28 changes: 18 additions & 10 deletions mantidimaging/gui/windows/spectrum_viewer/spectrum_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from PyQt5.QtCore import pyqtSignal, Qt, QSignalBlocker
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QColorDialog, QAction, QMenu, QSplitter, QWidget, QVBoxLayout
from pyqtgraph import ROI, GraphicsLayoutWidget, LinearRegionItem, PlotItem, mkPen
from pyqtgraph import ROI, GraphicsLayoutWidget, LinearRegionItem, PlotItem, mkPen, ViewBox

from mantidimaging.core.utility.close_enough_point import CloseEnoughPoint
from mantidimaging.core.utility.sensible_roi import SensibleROI
Expand Down Expand Up @@ -121,7 +121,6 @@ def __init__(self) -> None:
self.image = self.image_widget.image
self.spectrum_plot_widget = SpectrumPlotWidget()
self.spectrum = self.spectrum_plot_widget.spectrum

self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.image_widget)
self.splitter.addWidget(self.spectrum_plot_widget)
Expand Down Expand Up @@ -269,31 +268,40 @@ class SpectrumPlotWidget(GraphicsLayoutWidget):
def __init__(self) -> None:
super().__init__()

self.spectrum = self.addPlot()
self.vb = ViewBox()
self.spectrum = self.addPlot(viewbox=self.vb)
self.nextRow()
self._tof_range_label = self.addLabel()
self.nextRow()
self._image_index_range_label = self.addLabel()
self.range_control = LinearRegionItem()
self.range_control.sigRegionChangeFinished.connect(self._handle_tof_range_changed)
self.ci.layout.setRowStretchFactor(0, 1)

def get_tof_range(self) -> tuple[int, int]:
def get_tof_range(self) -> tuple[float, float]:
r_min, r_max = self.range_control.getRegion()
return int(r_min), int(r_max)
return r_min, r_max

def _handle_tof_range_changed(self) -> None:
tof_range = self.get_tof_range()
self._set_tof_range_label(tof_range[0], tof_range[1])
self.set_tof_range_label(tof_range[0], tof_range[1])
self.range_changed.emit(tof_range)

def add_range(self, range_min: int, range_max: int) -> None:
def add_range(self, range_min: int | float, range_max: int | float) -> None:
with QSignalBlocker(self.range_control):
self.range_control.setBounds((range_min, range_max))
self.range_control.setRegion((range_min, range_max))
self.spectrum.addItem(self.range_control)
self._set_tof_range_label(range_min, range_max)
self.set_tof_range_label(range_min, range_max)

def set_tof_range_label(self, range_min: float, range_max: float) -> None:
self._tof_range_label.setText(f'ToF range: {range_min:.3f} - {range_max:.3f}')

def set_image_index_range_label(self, range_min: int, range_max: int) -> None:
self._image_index_range_label.setText(f'Image index range: {range_min} - {range_max}')

def _set_tof_range_label(self, range_min: int, range_max: int) -> None:
self._tof_range_label.setText(f'ToF range: {range_min} - {range_max}')
def set_tof_axis_label(self, tof_axis_label: str) -> None:
self.spectrum.setLabel('bottom', text=tof_axis_label)


class SpectrumProjectionWidget(GraphicsLayoutWidget):
Expand Down
11 changes: 9 additions & 2 deletions mantidimaging/gui/windows/spectrum_viewer/test/presenter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pathlib import Path
from unittest import mock

from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QPushButton, QActionGroup
from parameterized import parameterized

from mantidimaging.core.data.dataset import StrictDataset, MixedDataset
Expand Down Expand Up @@ -34,6 +34,8 @@ def setUp(self) -> None:
self.view.exportButton = mock.create_autospec(QPushButton)
self.view.exportButtonRITS = mock.create_autospec(QPushButton)
self.view.addBtn = mock.create_autospec(QPushButton)
self.view.tof_mode_select_group = mock.create_autospec(QActionGroup)
self.view.allowed_modes = mock.create_autospec(dict)
self.presenter = SpectrumViewerWindowPresenter(self.view, self.main_window)

def test_get_dataset_id_for_stack_no_stack_id(self):
Expand All @@ -53,6 +55,7 @@ def test_handle_sample_change_has_flat_before(self):
self.presenter.main_window.get_stack = mock.Mock(return_value=generate_images())
self.presenter.show_new_sample = mock.Mock()
self.view.try_to_select_relevant_normalise_stack = mock.Mock()
self.presenter.handle_tof_unit_change = mock.Mock()

self.presenter.handle_sample_change(uuid.uuid4())
self.view.try_to_select_relevant_normalise_stack.assert_called_once_with('Flat_before')
Expand All @@ -67,6 +70,7 @@ def test_handle_sample_change_has_flat_after(self):
self.presenter.show_new_sample = mock.Mock()
self.view.try_to_select_relevant_normalise_stack = mock.Mock()

self.presenter.handle_tof_unit_change = mock.Mock()
self.presenter.handle_sample_change(uuid.uuid4())
self.view.try_to_select_relevant_normalise_stack.assert_called_once_with('Flat_after')
self.presenter.show_new_sample.assert_called_once()
Expand All @@ -88,6 +92,7 @@ def test_handle_sample_change_dataset_unchanged(self):
self.presenter.main_window.get_dataset = mock.Mock()
self.presenter.main_window.get_stack = mock.Mock(return_value=generate_images())
self.presenter.show_new_sample = mock.Mock()
self.presenter.handle_tof_unit_change = mock.Mock()

self.presenter.handle_sample_change(uuid.uuid4())
self.presenter.main_window.get_dataset.assert_not_called()
Expand All @@ -100,6 +105,7 @@ def test_handle_sample_change_to_MixedDataset(self):
self.presenter.main_window.get_stack = mock.Mock(return_value=generate_images())
self.presenter.show_new_sample = mock.Mock()
self.view.try_to_select_relevant_normalise_stack = mock.Mock()
self.presenter.handle_tof_unit_change = mock.Mock()

self.presenter.handle_sample_change(uuid.uuid4())
self.presenter.main_window.get_dataset.assert_called_once()
Expand All @@ -112,6 +118,7 @@ def test_handle_sample_change_no_flat(self):
self.presenter.main_window.get_stack = mock.Mock(return_value=generate_images())
self.presenter.show_new_sample = mock.Mock()
self.view.try_to_select_relevant_normalise_stack = mock.Mock()
self.presenter.handle_tof_unit_change = mock.Mock()

self.presenter.handle_sample_change(uuid.uuid4())
self.presenter.main_window.get_dataset.assert_called_once()
Expand Down Expand Up @@ -158,7 +165,7 @@ def test_WHEN_has_stack_has_bad_norm_THEN_buttons_set(self, normalise_issue, has

def test_WHEN_show_sample_call_THEN_add_range_set(self):
self.presenter.model.set_stack(generate_images([10, 5, 5]))
self.presenter.model.tof_range = (0, 9)
self.presenter.model.tof_plot_range = (0, 9)
self.presenter.show_new_sample()
self.view.spectrum_widget.spectrum_plot_widget.add_range.assert_called_once_with(0, 9)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ def test_WHEN_set_roi_alpha_called_THEN_set_roi_visibility_flags_called(self, _,
def test_WHEN_add_range_called_THEN_region_and_label_set_correctly(self, _, range_min, range_max):
self.spectrum_plot_widget.add_range(range_min, range_max)
self.assertEqual(self.spectrum_plot_widget.range_control.getRegion(), (range_min, range_max))
self.assertEqual(self.spectrum_plot_widget._tof_range_label.text, f"ToF range: {range_min} - {range_max}")
self.assertEqual(self.spectrum_plot_widget._tof_range_label.text,
f"ToF range: {range_min:.3f} - {range_max:.3f}")

def test_WHEN_get_roi_called_THEN_SensibleROI_returned(self):
spectrum_roi = SpectrumROI("roi",
Expand Down Expand Up @@ -136,8 +137,8 @@ def test_WHEN_remove_roi_called_THEN_roi_removed_from_roi_dict(self):
self.assertNotIn(spectrum_roi, self.spectrum_widget.image.vb.addedItems)

def test_WHEN_set_tof_range_called_THEN_range_control_set_correctly(self):
self.spectrum_plot_widget._set_tof_range_label(0, 100)
self.assertEqual(self.spectrum_plot_widget._tof_range_label.text, "ToF range: 0 - 100")
self.spectrum_plot_widget.set_tof_range_label(0, 100)
self.assertEqual(self.spectrum_plot_widget._tof_range_label.text, "ToF range: 0.000 - 100.000")

def test_WHEN_rename_roi_called_THEN_roi_renamed(self):
spectrum_roi = SpectrumROI("roi_1",
Expand Down
Loading

0 comments on commit 5b827de

Please sign in to comment.