Skip to content

Commit

Permalink
Merge 5210976 into b6087e8
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeSullivan7 committed Apr 5, 2024
2 parents b6087e8 + 5210976 commit 582a347
Show file tree
Hide file tree
Showing 9 changed files with 305 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
52 changes: 52 additions & 0 deletions mantidimaging/core/utility/unit_conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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
target_to_camera_dist: float = 56 # [m]
data_offset: float = 0 # [us]
tof_data_to_convert: np.ndarray
velocity: np.ndarray

def __init__(self, data_to_convert: np.ndarray | None = None) -> None:
if data_to_convert is not None:
self.set_data_to_convert(data_to_convert)

def tof_seconds_to_wavelength(self) -> np.ndarray:
self.check_data()
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:
self.check_data()
energy = self.neutron_mass * self.velocity / 2
energy_evs = energy / self.mega_electro_volt
return energy_evs

def tof_seconds_to_us(self) -> np.ndarray:
self.check_data()
return (self.tof_data_to_convert + self.data_offset) * 1e6

def set_target_to_camera_dist(self, target_to_camera_dist: float) -> None:
self.target_to_camera_dist = target_to_camera_dist

def set_data_to_convert(self, data_to_convert: np.ndarray) -> None:
self.tof_data_to_convert = data_to_convert

def check_data(self) -> None:
if self.tof_data_to_convert is None:
raise TypeError("Data is not present")
else:
self.velocity = self.target_to_camera_dist / (self.tof_data_to_convert + self.data_offset)

def set_data_offset(self, data_offset: float) -> None:
self.data_offset = data_offset * 1e-6
77 changes: 74 additions & 3 deletions mantidimaging/gui/ui/spectrum_viewer.ui
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>905</width>
<height>752</height>
<width>1001</width>
<height>766</height>
</rect>
</property>
<property name="windowTitle">
Expand Down Expand Up @@ -391,7 +391,78 @@
</widget>
</item>
<item>
<spacer name="PropertiesSpacer">
<widget class="QGroupBox" name="tofPropertiesGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>84</height>
</size>
</property>
<property name="title">
<string>Time of Flight Properties</string>
</property>
<widget class="QLabel" name="label_3">
<property name="geometry">
<rect>
<x>10</x>
<y>31</y>
<width>101</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>Flight path:</string>
</property>
</widget>
<widget class="QLabel" name="label_4">
<property name="geometry">
<rect>
<x>10</x>
<y>50</y>
<width>91</width>
<height>20</height>
</rect>
</property>
<property name="text">
<string>Time delay: </string>
</property>
</widget>
<widget class="QDoubleSpinBox" name="flightPathSpinBox">
<property name="geometry">
<rect>
<x>80</x>
<y>30</y>
<width>211</width>
<height>19</height>
</rect>
</property>
<property name="suffix">
<string/>
</property>
</widget>
<widget class="QDoubleSpinBox" name="timeDelaySpinBox">
<property name="geometry">
<rect>
<x>80</x>
<y>50</y>
<width>211</width>
<height>19</height>
</rect>
</property>
<property name="suffix">
<string/>
</property>
</widget>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
Expand Down
40 changes: 40 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,26 @@ 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

self.units = UnitConversion()

def roi_name_generator(self) -> str:
"""
Returns a new Unique ID for newly created ROIs
Expand Down Expand Up @@ -94,6 +114,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 +463,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:
self.units.set_data_to_convert(self.tof_data)
if self.tof_mode == ToFUnitMode.TOF_US:
self.tof_data = self.units.tof_seconds_to_us()
elif self.tof_mode == ToFUnitMode.WAVELENGTH:
self.tof_data = self.units.tof_seconds_to_wavelength()
elif self.tof_mode == ToFUnitMode.ENERGY:
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)
76 changes: 73 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,21 @@ 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)
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)

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 +160,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 +346,39 @@ 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)"
self.view.spectrum_widget.spectrum_plot_widget.set_tof_axis_label(tof_axis_label)
self.refresh_spectrum_plot()

def refresh_spectrum_plot(self) -> None:
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()

def handle_flight_path_change(self) -> None:
self.model.units.set_target_to_camera_dist(self.view.flightPathSpinBox.value())
self.model.set_relevant_tof_units()
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.set_data_offset(self.view.timeDelaySpinBox.value())
self.model.set_relevant_tof_units()
self.refresh_spectrum_plot()
Loading

0 comments on commit 582a347

Please sign in to comment.