Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch Time of Flight units in Spectrum Viewer #2151

Merged
merged 12 commits into from
Apr 10, 2024
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:
samtygier-stfc marked this conversation as resolved.
Show resolved Hide resolved
# 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
39 changes: 39 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,25 @@ 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
#self.presenter.view.units_menu.setEnabled(False)
MikeSullivan7 marked this conversation as resolved.
Show resolved Hide resolved
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 +113,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 +462,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)
60 changes: 57 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.model.set_relevant_tof_units()
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.model.set_relevant_tof_units()
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):
if self.model.tof_data is None:
self.view.tof_mode_select_group.setEnabled(False)
else:
self.view.tof_mode_select_group.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)

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,25 @@ 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]
self.model.set_relevant_tof_units()
tof_mode = self.model.tof_mode
tof_axis_label = ""
if tof_mode == ToFUnitMode.IMAGE_NUMBER:
tof_axis_label = "Image index"
if tof_mode == ToFUnitMode.TOF_US:
tof_axis_label = "Time of Flight (\u03BC s)"
if tof_mode == ToFUnitMode.WAVELENGTH:
tof_axis_label = "Neutron Wavelength (\u212B)"
if tof_mode == ToFUnitMode.ENERGY:
tof_axis_label = "Neutron Energy (MeV)"
samtygier-stfc marked this conversation as resolved.
Show resolved Hide resolved
self.view.spectrum_widget.spectrum_plot_widget.set_tof_axis_label(tof_axis_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 @@ -272,31 +271,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.sigRegionChanged.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: {round(range_min, 3)} - {round(range_max, 3)}')
MikeSullivan7 marked this conversation as resolved.
Show resolved Hide resolved

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
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,7 @@ 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.presenter = SpectrumViewerWindowPresenter(self.view, self.main_window)

def test_get_dataset_id_for_stack_no_stack_id(self):
Expand Down Expand Up @@ -158,7 +159,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 @@ -136,7 +136,7 @@ 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.spectrum_plot_widget.set_tof_range_label(0, 100)
self.assertEqual(self.spectrum_plot_widget._tof_range_label.text, "ToF range: 0 - 100")

def test_WHEN_rename_roi_called_THEN_roi_renamed(self):
Expand Down
31 changes: 28 additions & 3 deletions mantidimaging/gui/windows/spectrum_viewer/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QFileDialog, QPushButton, QLabel, QAbstractItemView, QHeaderView, \
QTabWidget, QComboBox, QSpinBox, QTableWidget, QTableWidgetItem, QGroupBox
QTabWidget, QComboBox, QSpinBox, QTableWidget, QTableWidgetItem, QGroupBox, QActionGroup, QAction
from PyQt5.QtCore import QSignalBlocker, Qt

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
from .model import ROI_RITS, ToFUnitMode
from .presenter import SpectrumViewerWindowPresenter, ExportMode
from mantidimaging.gui.widgets import RemovableRowTableView
from .spectrum_widget import SpectrumWidget
Expand Down Expand Up @@ -81,6 +81,27 @@ def __init__(self, main_window: MainWindowView):
self.spectrum_widget.roi_changed.connect(self.presenter.handle_roi_moved)
self.spectrum_widget.roiColorChangeRequested.connect(self.presenter.change_roi_colour)

self.spectrum_right_click_menu = self.spectrum_widget.spectrum_plot_widget.spectrum.vb.menu
self.units_menu = self.spectrum_right_click_menu.addMenu("Units")
self.tof_mode_select_group = QActionGroup(self)

self.allowed_modes = {
"Image Index": ToFUnitMode.IMAGE_NUMBER,
"Wavelength": ToFUnitMode.WAVELENGTH,
"Energy": ToFUnitMode.ENERGY,
"us": ToFUnitMode.TOF_US
}
MikeSullivan7 marked this conversation as resolved.
Show resolved Hide resolved
for mode in self.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)
if mode == "Image Index":
action.setChecked(True)
if self.presenter.model.tof_data is None:
self.tof_mode_select_group.setEnabled(False)

self._current_dataset_id = None
self.sampleStackSelector.stack_selected_uuid.connect(self.presenter.handle_sample_change)
self.sampleStackSelector.stack_selected_uuid.connect(self.presenter.handle_button_enabled)
Expand All @@ -103,6 +124,7 @@ def __init__(self, main_window: MainWindowView):

self.sampleStackSelector.select_eligible_stack()
self.try_to_select_relevant_normalise_stack("Flat")
self.presenter.handle_tof_unit_change()

self.exportButton.clicked.connect(self.presenter.handle_export_csv)
self.exportButtonRITS.clicked.connect(self.presenter.handle_rits_export)
Expand Down Expand Up @@ -400,7 +422,10 @@ def set_roi_alpha(self, alpha: float, roi_name: str) -> None:
def show_visible_spectrums(self):
for key, value in self.spectrum_widget.spectrum_data_dict.items():
if value is not None and key in self.spectrum_widget.roi_dict:
self.spectrum_widget.spectrum.plot(value, name=key, pen=self.spectrum_widget.roi_dict[key].colour)
self.spectrum_widget.spectrum.plot(self.presenter.model.tof_data,
value,
name=key,
pen=self.spectrum_widget.roi_dict[key].colour)

def add_roi_table_row(self, name: str, colour: tuple[int, int, int]):
"""
Expand Down
Loading