From 3b78869cf4285e3c983c9f784534a9ac957e6e38 Mon Sep 17 00:00:00 2001 From: Mathieu Doucet Date: Tue, 7 May 2024 11:40:15 -0400 Subject: [PATCH 01/16] add dead time --- reflectivity_ui/interfaces/configuration.py | 6 ++ .../interfaces/data_handling/data_set.py | 2 +- .../interfaces/data_handling/instrument.py | 91 ++++++++++++++++++- 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/reflectivity_ui/interfaces/configuration.py b/reflectivity_ui/interfaces/configuration.py index 8134733..6c21e27 100644 --- a/reflectivity_ui/interfaces/configuration.py +++ b/reflectivity_ui/interfaces/configuration.py @@ -38,6 +38,12 @@ class Configuration(object): polynomial_stitching_degree = 3 polynomial_stitching_points = 3 + # Dead time options + apply_deadtime = True + paralyzable_deadtime = True + deadtime_value = 4.2 + deadtime_tof_step = 100 + def __init__(self, settings=None): self.instrument = Instrument() # Number of TOF bins diff --git a/reflectivity_ui/interfaces/data_handling/data_set.py b/reflectivity_ui/interfaces/data_handling/data_set.py index bdbd135..cff7feb 100644 --- a/reflectivity_ui/interfaces/data_handling/data_set.py +++ b/reflectivity_ui/interfaces/data_handling/data_set.py @@ -382,7 +382,7 @@ def load(self, update_parameters=True, progress=None): progress(5, "Filtering data...", out_of=100.0) try: - xs_list = self.configuration.instrument.load_data(self.file_path) + xs_list = self.configuration.instrument.load_data(self.file_path, self.configuration) logging.info("%s loaded: %s xs", self.file_path, len(xs_list)) except RuntimeError as run_err: logging.error("Could not load file(s) {}\n {}".format(str(self.file_path), run_err)) diff --git a/reflectivity_ui/interfaces/data_handling/instrument.py b/reflectivity_ui/interfaces/data_handling/instrument.py index 0158cdf..d697c1a 100644 --- a/reflectivity_ui/interfaces/data_handling/instrument.py +++ b/reflectivity_ui/interfaces/data_handling/instrument.py @@ -7,6 +7,7 @@ # local +from reflectivity_ui.interfaces.data_handling import DeadTimeCorrection from reflectivity_ui.interfaces.data_handling.filepath import FilePath # 3rd party @@ -66,6 +67,58 @@ def get_cross_section_label(ws, entry_name): return "%s%s" % (pol_label, ana_label) +def mantid_algorithm_exec(algorithm_class, **kwargs): + algorithm_instance = algorithm_class() + assert algorithm_instance.PyInit, "str(algorithm_class) is not a Mantid Python algorithm" + algorithm_instance.PyInit() + for name, value in kwargs.items(): + algorithm_instance.setProperty(name, value) + algorithm_instance.PyExec() + if "OutputWorkspace" in kwargs: + return algorithm_instance.getProperty("OutputWorkspace").value + + +def get_dead_time_correction(ws, configuration, error_ws=None): + """ + Compute dead time correction to be applied to the reflectivity curve. + The method will also try to load the error events from each of the + data files to ensure that we properly estimate the dead time correction. + :param ws: workspace with raw data to compute correction for + :param configuration: reduction parameters + :param error_ws: workspace with error events + """ + tof_min = ws.getTofMin() + tof_max = ws.getTofMax() + + run_number = ws.getRun().getProperty("run_number").value + corr_ws = mantid_algorithm_exec( + DeadTimeCorrection.SingleReadoutDeadTimeCorrection, + InputWorkspace=ws, + InputErrorEventsWorkspace=error_ws, + Paralyzable=configuration.paralyzable_deadtime, + DeadTime=configuration.deadtime_value, + TOFStep=configuration.deadtime_tof_step, + TOFRange=[tof_min, tof_max], + OutputWorkspace="corr", + ) + corr_ws = api.Rebin(corr_ws, [tof_min, 10, tof_max]) + return corr_ws + + +def apply_dead_time_correction(ws, configuration, error_ws=None): + """ + Apply dead time correction, and ensure that it is done only once + per workspace. + :param ws: workspace with raw data to compute correction for + :param template_data: reduction parameters + """ + if "dead_time_applied" not in ws.getRun(): + corr_ws = get_dead_time_correction(ws, configuration, error_ws=error_ws) + ws = api.Multiply(ws, corr_ws, OutputWorkspace=str(ws)) + api.AddSampleLog(Workspace=ws, LogName="dead_time_applied", LogText="1", LogType="Number") + return ws + + class Instrument(object): """ Instrument class. Holds the data handling that is unique to a specific instrument. @@ -140,7 +193,7 @@ def dummy_filter_cross_sections(ws: EventWorkspace, name_prefix: str = None) -> return cross_sections - def load_data(self, file_path): + def load_data(self, file_path, configuration=None): r""" # type: (unicode) -> WorkspaceGroup @brief Load one or more data sets according to the needs ot the instrument. @@ -152,6 +205,7 @@ def load_data(self, file_path): """ fp_instance = FilePath(file_path) xs_list = list() + err_list = list() temp_workspace_root_name = "".join(random.sample(string.ascii_letters, 12)) # random string of 12 characters workspace_root_name = fp_instance.run_numbers(string_representation="short") for path in fp_instance.single_paths: @@ -169,7 +223,32 @@ def load_data(self, file_path): if len(_path_xs_list) == 1 and not "cross_section_id" in _path_xs_list[0].getRun(): logging.warning("Could not filter data, using getDI") ws = api.LoadEventNexus(Filename=path, OutputWorkspace="raw_events") - path_xs_list = self.dummy_filter_cross_sections(ws, name_prefix=temp_workspace_root_name) + _path_xs_list = self.dummy_filter_cross_sections(ws, name_prefix=temp_workspace_root_name) + if configuration is not None and configuration.apply_deadtime: + err_ws = api.LoadErrorEventsNexus(path) + _err_list = api.MRFilterCrossSections( + InputWorkspace=err_ws, + PolState=self.pol_state, + AnaState=self.ana_state, + PolVeto=self.pol_veto, + AnaVeto=self.ana_veto, + CrossSectionWorkspaces="%s_err_entry" % temp_workspace_root_name, + ) + path_xs_list = [] + for ws in _path_xs_list: + xs_name = ws.getRun()["cross_section_id"].value + if not xs_name == "unfiltered": + # Find the related workspace in with error events + is_found = False + for err_ws in _err_list: + if err_ws.getRun()["cross_section_id"].value == xs_name: + is_found = True + _ws = apply_dead_time_correction(ws, configuration, error_ws=err_ws) + path_xs_list.append(_ws) + if not is_found: + print("Could not find eeror events for [%s]" % xs_name) + _ws = apply_dead_time_correction(ws, configuration, error_ws=None) + path_xs_list.append(_ws) else: path_xs_list = [ ws for ws in _path_xs_list if not ws.getRun()["cross_section_id"].value == "unfiltered" @@ -177,6 +256,7 @@ def load_data(self, file_path): else: ws = api.LoadEventNexus(Filename=path, OutputWorkspace="raw_events") path_xs_list = self.dummy_filter_cross_sections(ws, name_prefix=temp_workspace_root_name) + if len(xs_list) == 0: # initialize xs_list with the cross sections of the first data file xs_list = path_xs_list for ws in xs_list: # replace the temporary names with the run number(s) @@ -184,7 +264,12 @@ def load_data(self, file_path): api.RenameWorkspace(str(ws), name_new) else: for i, ws in enumerate(xs_list): - api.Plus(LHSWorkspace=str(ws), RHSWorkspace=str(path_xs_list[i]), OutputWorkspace=str(ws)) + api.Plus( + LHSWorkspace=str(ws), + RHSWorkspace=str(path_xs_list[i]), + OutputWorkspace=str(ws), + ) + # Insert a log indicating which run numbers contributed to this cross-section for ws in xs_list: api.AddSampleLog( From 6bd105e2086f0d7cd935952620513943f8c4b1e7 Mon Sep 17 00:00:00 2001 From: Mathieu Doucet Date: Tue, 7 May 2024 11:45:35 -0400 Subject: [PATCH 02/16] add dead time algo --- .../data_handling/DeadTimeCorrection.py | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py diff --git a/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py b/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py new file mode 100644 index 0000000..48d07a1 --- /dev/null +++ b/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py @@ -0,0 +1,126 @@ +""" +Dead time correction algorithm for single-readout detectors. +""" + +import numpy as np +import scipy +from mantid.api import ( + AlgorithmFactory, + IEventWorkspaceProperty, + MatrixWorkspaceProperty, + PropertyMode, + PythonAlgorithm, +) +from mantid.kernel import Direction, FloatArrayLengthValidator, FloatArrayProperty +from mantid.simpleapi import Rebin, SumSpectra, logger + + +class SingleReadoutDeadTimeCorrection(PythonAlgorithm): + def category(self): + return "Reflectometry\\SNS" + + def name(self): + return "SingleReadoutDeadTimeCorrection" + + def version(self): + return 1 + + def summary(self): + return "Single read-out dead time correction calculation" + + def PyInit(self): + self.declareProperty( + IEventWorkspaceProperty("InputWorkspace", "", Direction.Input), + "Input workspace use to compute dead time correction", + ) + self.declareProperty( + IEventWorkspaceProperty("InputErrorEventsWorkspace", "", Direction.Input, PropertyMode.Optional), + "Input workspace with error events use to compute dead time correction", + ) + self.declareProperty("DeadTime", 4.2, doc="Dead time in microseconds") + self.declareProperty( + "TOFStep", + 100.0, + doc="TOF bins to compute deadtime correction for, in microseconds", + ) + self.declareProperty( + "Paralyzable", + False, + doc="If true, paralyzable correction will be applied, non-paralyzing otherwise", + ) + self.declareProperty( + FloatArrayProperty( + "TOFRange", + [0.0, 0.0], + FloatArrayLengthValidator(2), + direction=Direction.Input, + ), + "TOF range to use", + ) + self.declareProperty( + MatrixWorkspaceProperty("OutputWorkspace", "", Direction.Output), + "Output workspace", + ) + + def PyExec(self): + # Event data must include error events (all triggers on the detector) + ws_event_data = self.getProperty("InputWorkspace").value + ws_error_events = self.getProperty("InputErrorEventsWorkspace").value + dead_time = self.getProperty("DeadTime").value + tof_step = self.getProperty("TOFStep").value + paralyzing = self.getProperty("Paralyzable").value + output_workspace = self.getPropertyValue("OutputWorkspace") + + # Rebin the data according to the tof_step we want to compute the correction with + tof_min, tof_max = self.getProperty("TOFRange").value + if tof_min == 0 and tof_max == 0: + tof_min = ws_event_data.getTofMin() + tof_max = ws_event_data.getTofMax() + logger.notice("TOF range: %f %f" % (tof_min, tof_max)) + _ws_sc = Rebin( + InputWorkspace=ws_event_data, + Params="%s,%s,%s" % (tof_min, tof_step, tof_max), + PreserveEvents=False, + ) + + # Get the total number of counts on the detector for each TOF bin per pulse + counts_ws = SumSpectra(_ws_sc, OutputWorkspace=output_workspace) + + # If we have error events, add them since those are also detector triggers + if ws_error_events is not None: + _errors = Rebin( + InputWorkspace=ws_error_events, + Params="%s,%s,%s" % (tof_min, tof_step, tof_max), + PreserveEvents=False, + ) + counts_ws += _errors + + t_series = np.asarray(_ws_sc.getRun()["proton_charge"].value) + non_zero = t_series > 0 + n_pulses = np.count_nonzero(non_zero) + rate = counts_ws.readY(0) / n_pulses + + # Compute the dead time correction for each TOF bin + if paralyzing: + true_rate = -scipy.special.lambertw(-rate * dead_time / tof_step).real / dead_time + corr = true_rate / (rate / tof_step) + # If we have no events, set the correction to 1 otherwise we will get a nan + # from the equation above. + corr[rate == 0] = 1 + else: + corr = 1 / (1 - rate * dead_time / tof_step) + + if np.min(corr) < 0: + error = "Corrupted dead time correction:\n" + " Reflected: %s\n" % corr + logger.error(error) + + counts_ws.setY(0, corr) + + # We don't compute an error on the dead time correction, so set it to zero + counts_ws.setE(0, 0 * corr) + counts_ws.setDistribution(True) + + self.setProperty("OutputWorkspace", counts_ws) + + +AlgorithmFactory.subscribe(SingleReadoutDeadTimeCorrection) \ No newline at end of file From 0b25153664eeb67cb0bd64e195d966b53c07ff05 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 15:48:07 +0000 Subject: [PATCH 03/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py b/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py index 48d07a1..a337340 100644 --- a/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py +++ b/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py @@ -123,4 +123,4 @@ def PyExec(self): self.setProperty("OutputWorkspace", counts_ws) -AlgorithmFactory.subscribe(SingleReadoutDeadTimeCorrection) \ No newline at end of file +AlgorithmFactory.subscribe(SingleReadoutDeadTimeCorrection) From 9024b4003139f40af835e8a953b9f0582763b835 Mon Sep 17 00:00:00 2001 From: Mat Doucet Date: Fri, 10 May 2024 14:28:00 -0400 Subject: [PATCH 04/16] Update DeadTimeCorrection.py --- .../interfaces/data_handling/DeadTimeCorrection.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py b/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py index a337340..7d5824e 100644 --- a/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py +++ b/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py @@ -98,6 +98,11 @@ def PyExec(self): t_series = np.asarray(_ws_sc.getRun()["proton_charge"].value) non_zero = t_series > 0 n_pulses = np.count_nonzero(non_zero) + + # If we skip pulses, we need to account for them when computing the + # instantaneous rate + chopper_speed = _ws_sc.getRun()["SpeedRequest1"].value[0] + n_pulses = n_pulses * chopper_speed / 60.0 rate = counts_ws.readY(0) / n_pulses # Compute the dead time correction for each TOF bin From b665b75f72ff886a7aab9f77a5d43562b56ea798 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 18:28:18 +0000 Subject: [PATCH 05/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py b/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py index 7d5824e..e2d9bc8 100644 --- a/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py +++ b/reflectivity_ui/interfaces/data_handling/DeadTimeCorrection.py @@ -99,7 +99,7 @@ def PyExec(self): non_zero = t_series > 0 n_pulses = np.count_nonzero(non_zero) - # If we skip pulses, we need to account for them when computing the + # If we skip pulses, we need to account for them when computing the # instantaneous rate chopper_speed = _ws_sc.getRun()["SpeedRequest1"].value[0] n_pulses = n_pulses * chopper_speed / 60.0 From 5da5650983f31d9012f5dfe1fbbe2a54abf1632b Mon Sep 17 00:00:00 2001 From: Carson Sears Date: Wed, 26 Jun 2024 15:53:07 -0500 Subject: [PATCH 06/16] add UI elements for dead time settings --- reflectivity_ui/interfaces/configuration.py | 12 + .../interfaces/event_handlers/main_handler.py | 10 + reflectivity_ui/interfaces/main_window.py | 20 ++ reflectivity_ui/ui/deadtime_entry.py | 41 +++ reflectivity_ui/ui/deadtime_settings.py | 61 +++++ reflectivity_ui/ui/deadtime_settings.ui | 240 ++++++++++++++++++ reflectivity_ui/ui/ui_main_window.ui | 15 +- 7 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 reflectivity_ui/ui/deadtime_entry.py create mode 100644 reflectivity_ui/ui/deadtime_settings.py create mode 100644 reflectivity_ui/ui/deadtime_settings.ui diff --git a/reflectivity_ui/interfaces/configuration.py b/reflectivity_ui/interfaces/configuration.py index 6c21e27..d250f84 100644 --- a/reflectivity_ui/interfaces/configuration.py +++ b/reflectivity_ui/interfaces/configuration.py @@ -233,6 +233,12 @@ def to_q_settings(self, settings): settings.setValue("do_final_rebin", self.do_final_rebin) settings.setValue("final_rebin_step", self.final_rebin_step) + # Dead time options + settings.setValue("apply_deadtime", self.apply_deadtime) + settings.setValue("paralyzable_deadtime", self.paralyzable_deadtime) + settings.setValue("deadtime_value", self.deadtime_value) + settings.setValue("deadtime_tof_step", self.deadtime_tof_step) + # Off-specular options settings.setValue("off_spec_x_axis", self.off_spec_x_axis) settings.setValue("off_spec_slice", self.off_spec_slice) @@ -331,6 +337,12 @@ def _verify_true(parameter, default): Configuration.do_final_rebin = _verify_true("do_final_rebin", self.do_final_rebin) Configuration.final_rebin_step = float(settings.value("final_rebin_step", self.final_rebin_step)) + # Dead time options + Configuration.apply_deadtime = _verify_true("apply_deadtime", self.apply_deadtime) + Configuration.paralyzable_deadtime = _verify_true("paralyzable_deadtime", self.paralyzable_deadtime) + Configuration.deadtime_value = float(settings.value("deadtime_value", self.deadtime_value)) + Configuration.deadtime_tof_step = float(settings.value("deadtime_tof_step", self.deadtime_tof_step)) + # Off-specular options self.off_spec_x_axis = int(settings.value("off_spec_x_axis", self.off_spec_x_axis)) self.off_spec_slice = _verify_true("off_spec_slice", self.off_spec_slice) diff --git a/reflectivity_ui/interfaces/event_handlers/main_handler.py b/reflectivity_ui/interfaces/event_handlers/main_handler.py index a75638c..cb9ef81 100644 --- a/reflectivity_ui/interfaces/event_handlers/main_handler.py +++ b/reflectivity_ui/interfaces/event_handlers/main_handler.py @@ -1221,6 +1221,11 @@ def get_configuration(self): Configuration.do_final_rebin = self.ui.final_rebin_checkbox.isChecked() Configuration.final_rebin_step = self.ui.q_rebin_spinbox.value() + Configuration.apply_deadtime = self.ui.deadtime_entry.applyCheckBox.isChecked() + Configuration.paralyzable_deadtime = self.main_window.deadtime_settings.paralyzable + Configuration.deadtime_value = self.main_window.deadtime_settings.dead_time + Configuration.deadtime_tof_step = self.main_window.deadtime_settings.tof_step + # UI elements configuration.normalize_x_tof = self.ui.normalizeXTof.isChecked() configuration.x_wl_map = self.ui.xLamda.isChecked() @@ -1330,6 +1335,11 @@ def populate_from_configuration(self, configuration=None): self.ui.final_rebin_checkbox.setChecked(configuration.do_final_rebin) self.ui.q_rebin_spinbox.setValue(configuration.final_rebin_step) + self.ui.deadtime_entry.applyCheckBox.setChecked(configuration.apply_deadtime) + self.main_window.deadtime_settings.paralyzable = configuration.paralyzable_deadtime + self.main_window.deadtime_settings.dead_time = configuration.deadtime_value + self.main_window.deadtime_settings.tof_step = configuration.deadtime_tof_step + # UI elements self.ui.normalizeXTof.setChecked(configuration.normalize_x_tof) self.ui.xLamda.setChecked(configuration.x_wl_map) diff --git a/reflectivity_ui/interfaces/main_window.py b/reflectivity_ui/interfaces/main_window.py index 59db953..a2337f1 100644 --- a/reflectivity_ui/interfaces/main_window.py +++ b/reflectivity_ui/interfaces/main_window.py @@ -16,6 +16,7 @@ from reflectivity_ui.interfaces.event_handlers.plot_handler import PlotHandler from reflectivity_ui.interfaces.event_handlers.main_handler import MainHandler from reflectivity_ui.interfaces import load_ui +from reflectivity_ui.ui.deadtime_settings import DeadTimeSettingsModel, DeadTimeSettingsView # 3rd-party from PyQt5 import QtCore, QtWidgets @@ -68,6 +69,7 @@ def __init__(self): # Object managers self.data_manager = DataManager(self.settings.value("current_directory", os.path.expanduser("~"))) self.plot_manager = PlotManager(self) + self.deadtime_settings = DeadTimeSettingsModel() r"""Setting `auto_change_active = True` bypasses execution of: - MainWindow.file_open_from_list() @@ -94,6 +96,13 @@ def __init__(self): self.initiate_reflectivity_plot.connect(self.plot_manager.plot_refl) + self.ui.deadtime_entry.applyCheckBox.stateChanged.connect(self.apply_deadtime_update) + self.ui.deadtime_entry.settingsButton.clicked.connect(self.open_deadtime_settings) + + def apply_deadtime_update(self, state): + """TODO: Figure out if this is needed""" + pass + def closeEvent(self, event): """Close UI event""" self.file_handler.get_configuration() @@ -528,3 +537,14 @@ def open_polarization_window(self): def open_rawdata_dialog(self): return NotImplemented + + def open_deadtime_settings(self): + r"""Show the dialog for dead-time options. Update attribue deadtime options upon closing the dialog.""" + view = DeadTimeSettingsView(parent=self) + view.set_state( + self.deadtime_settings.paralyzable, self.deadtime_settings.dead_time, self.deadtime_settings.tof_step + ) + view.exec_() + # update the dead time settings of the Main GUI after user has closed the dialog + for option in ["paralyzable", "dead_time", "tof_step"]: + setattr(self.deadtime_settings, option, view.options[option]) diff --git a/reflectivity_ui/ui/deadtime_entry.py b/reflectivity_ui/ui/deadtime_entry.py new file mode 100644 index 0000000..6408706 --- /dev/null +++ b/reflectivity_ui/ui/deadtime_entry.py @@ -0,0 +1,41 @@ +# third party imports +from qtpy.QtWidgets import QGroupBox, QHBoxLayout, QCheckBox, QPushButton + + +class DeadTimeEntryPoint(QGroupBox): + def __init__(self, title='Dead Time Correction'): + super().__init__(title) + self.initUI() + + def initUI(self): + # Set the stylesheet for the group box to have a border + self.setStyleSheet( + "QGroupBox {" + " border: 1px solid gray;" + " border-radius: 5px;" + " margin-top: 1ex;" # space above the group box + "} " + "QGroupBox::title {" + " subcontrol-origin: margin;" + " subcontrol-position: top center;" # align the title to the center + " padding: 0 3px;" + "}" + ) + + self.applyCheckBox = QCheckBox('Apply', self) + self.applyCheckBox.stateChanged.connect(self.toggleSettingsButton) + self.settingsButton = QPushButton('Settings', self) + self.settingsButton.setEnabled(self.applyCheckBox.isChecked()) # enabled if we use the correction + + # Create a horizontal layout for the checkbox and settings button + hbox = QHBoxLayout() + hbox.addWidget(self.applyCheckBox) + hbox.addWidget(self.settingsButton) + hbox.addStretch(1) # This adds a stretchable space after the button (optional) + + # Set the layout for the group box + self.setLayout(hbox) + + def toggleSettingsButton(self, state): + # Enable the settings button if the checkbox is checked, disable otherwise + self.settingsButton.setEnabled(state) diff --git a/reflectivity_ui/ui/deadtime_settings.py b/reflectivity_ui/ui/deadtime_settings.py new file mode 100644 index 0000000..1c7620c --- /dev/null +++ b/reflectivity_ui/ui/deadtime_settings.py @@ -0,0 +1,61 @@ +# third-party imports +from qtpy.QtWidgets import QDialog, QWidget +from xml.dom.minidom import Document, Element +from typing import Any, Callable, Dict +import os + +from qtpy.uic import loadUi + + +class DeadTimeSettingsModel(): + """Stores all options for the dead time correction. These are global options""" + + apply_deadtime: bool = False + paralyzable: bool = True + dead_time: float = 4.2 + tof_step: float = 100.0 + +class DeadTimeSettingsView(QDialog): + """ + Dialog to choose the dead time correction options. + """ + + def __init__(self, parent: QWidget): + super().__init__(parent) + filepath = os.path.join(os.path.dirname(__file__), "deadtime_settings.ui") + self.ui = loadUi(filepath, baseinstance=self) + self.options = self.get_state_from_form() + + def set_state(self, paralyzable, dead_time, tof_step): + """ + Store options and populate the form + :param apply_correction: If True, dead time correction will be applied + :param paralyzable: If True, a paralyzable correction will be used + :param dead_time: Value of the dead time in micro second + :param tof_step: TOF binning in micro second + """ + self.ui.use_paralyzable.setChecked(paralyzable) + self.ui.dead_time_value.setValue(dead_time) + self.ui.dead_time_tof.setValue(tof_step) + self.options = self.get_state_from_form() + + def get_state_from_form(self) -> dict: + r"""Read the options from the form. + + Returns + ------- + Dictionary whose keys must match fields of class `DeadTimeSettingsModel` + """ + return { + 'paralyzable': self.ui.use_paralyzable.isChecked(), + 'dead_time': self.ui.dead_time_value.value(), + 'tof_step': self.ui.dead_time_tof.value(), + } + + def accept(self): + """ + Read in the options on the form when the OK button is + clicked. + """ + self.options = self.get_state_from_form() + self.close() diff --git a/reflectivity_ui/ui/deadtime_settings.ui b/reflectivity_ui/ui/deadtime_settings.ui new file mode 100644 index 0000000..b4c7a9b --- /dev/null +++ b/reflectivity_ui/ui/deadtime_settings.ui @@ -0,0 +1,240 @@ + + + Dialog + + + Qt::NonModal + + + + 0 + 0 + 428 + 150 + + + + + 0 + 0 + + + + + 439 + 150 + + + + Dead Time Settings + + + true + + + + + + + + + + + + + true + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Use two background regions to estimate the background under the peak</p></body></html> + + + Use paralyzable dead time + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Dead time value [us] + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 130 + 0 + + + + + 130 + 16777215 + + + + 4.200000000000000 + + + + + + + + + + + TOF binning use for correction [us] + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 130 + 0 + + + + + 0 + 100 + + + + 0 + + + 10000000.000000000000000 + + + 50.000000000000000 + + + 100.000000000000000 + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + + buttonBox + rejected() + Dialog + close() + + + 235 + 187 + + + 235 + 106 + + + + + buttonBox + accepted() + Dialog + accept() + + + 235 + 187 + + + 235 + 106 + + + + + + _update_model() + + diff --git a/reflectivity_ui/ui/ui_main_window.ui b/reflectivity_ui/ui/ui_main_window.ui index 4a46fc2..6cf1313 100644 --- a/reflectivity_ui/ui/ui_main_window.ui +++ b/reflectivity_ui/ui/ui_main_window.ui @@ -469,7 +469,14 @@ - + + + + Dead Time Correction + + + + Normalize the total reflectivity plateau to unity when stitching @@ -5106,6 +5113,12 @@
reflectivity_ui/ui/compare_plots.h
1 + + DeadTimeEntryPoint + QGroupBox +
reflectivity_ui/ui/deadtime_entry.h
+ 1 +
numberSearchEntry From 5d5badb658811f3814b9339ea2da3a1cb3854c50 Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Fri, 28 Jun 2024 13:06:59 -0400 Subject: [PATCH 07/16] Store deadtime settings in global configuration and att unit tests --- .../interfaces/data_handling/instrument.py | 2 +- .../interfaces/event_handlers/main_handler.py | 6 -- reflectivity_ui/interfaces/main_window.py | 16 +---- reflectivity_ui/ui/deadtime_settings.py | 52 +++++----------- test/ui/test_deadtime_entry.py | 42 +++++++++++++ test/ui/test_deadtime_settings.py | 62 +++++++++++++++++++ 6 files changed, 122 insertions(+), 58 deletions(-) create mode 100644 test/ui/test_deadtime_entry.py create mode 100644 test/ui/test_deadtime_settings.py diff --git a/reflectivity_ui/interfaces/data_handling/instrument.py b/reflectivity_ui/interfaces/data_handling/instrument.py index d697c1a..2278eaf 100644 --- a/reflectivity_ui/interfaces/data_handling/instrument.py +++ b/reflectivity_ui/interfaces/data_handling/instrument.py @@ -246,7 +246,7 @@ def load_data(self, file_path, configuration=None): _ws = apply_dead_time_correction(ws, configuration, error_ws=err_ws) path_xs_list.append(_ws) if not is_found: - print("Could not find eeror events for [%s]" % xs_name) + print("Could not find error events for [%s]" % xs_name) _ws = apply_dead_time_correction(ws, configuration, error_ws=None) path_xs_list.append(_ws) else: diff --git a/reflectivity_ui/interfaces/event_handlers/main_handler.py b/reflectivity_ui/interfaces/event_handlers/main_handler.py index cb9ef81..4dd8e06 100644 --- a/reflectivity_ui/interfaces/event_handlers/main_handler.py +++ b/reflectivity_ui/interfaces/event_handlers/main_handler.py @@ -1222,9 +1222,6 @@ def get_configuration(self): Configuration.final_rebin_step = self.ui.q_rebin_spinbox.value() Configuration.apply_deadtime = self.ui.deadtime_entry.applyCheckBox.isChecked() - Configuration.paralyzable_deadtime = self.main_window.deadtime_settings.paralyzable - Configuration.deadtime_value = self.main_window.deadtime_settings.dead_time - Configuration.deadtime_tof_step = self.main_window.deadtime_settings.tof_step # UI elements configuration.normalize_x_tof = self.ui.normalizeXTof.isChecked() @@ -1336,9 +1333,6 @@ def populate_from_configuration(self, configuration=None): self.ui.q_rebin_spinbox.setValue(configuration.final_rebin_step) self.ui.deadtime_entry.applyCheckBox.setChecked(configuration.apply_deadtime) - self.main_window.deadtime_settings.paralyzable = configuration.paralyzable_deadtime - self.main_window.deadtime_settings.dead_time = configuration.deadtime_value - self.main_window.deadtime_settings.tof_step = configuration.deadtime_tof_step # UI elements self.ui.normalizeXTof.setChecked(configuration.normalize_x_tof) diff --git a/reflectivity_ui/interfaces/main_window.py b/reflectivity_ui/interfaces/main_window.py index a2337f1..b98dd4e 100644 --- a/reflectivity_ui/interfaces/main_window.py +++ b/reflectivity_ui/interfaces/main_window.py @@ -16,7 +16,7 @@ from reflectivity_ui.interfaces.event_handlers.plot_handler import PlotHandler from reflectivity_ui.interfaces.event_handlers.main_handler import MainHandler from reflectivity_ui.interfaces import load_ui -from reflectivity_ui.ui.deadtime_settings import DeadTimeSettingsModel, DeadTimeSettingsView +from reflectivity_ui.ui.deadtime_settings import DeadTimeSettingsView # 3rd-party from PyQt5 import QtCore, QtWidgets @@ -69,7 +69,6 @@ def __init__(self): # Object managers self.data_manager = DataManager(self.settings.value("current_directory", os.path.expanduser("~"))) self.plot_manager = PlotManager(self) - self.deadtime_settings = DeadTimeSettingsModel() r"""Setting `auto_change_active = True` bypasses execution of: - MainWindow.file_open_from_list() @@ -96,13 +95,8 @@ def __init__(self): self.initiate_reflectivity_plot.connect(self.plot_manager.plot_refl) - self.ui.deadtime_entry.applyCheckBox.stateChanged.connect(self.apply_deadtime_update) self.ui.deadtime_entry.settingsButton.clicked.connect(self.open_deadtime_settings) - def apply_deadtime_update(self, state): - """TODO: Figure out if this is needed""" - pass - def closeEvent(self, event): """Close UI event""" self.file_handler.get_configuration() @@ -537,14 +531,8 @@ def open_polarization_window(self): def open_rawdata_dialog(self): return NotImplemented - + def open_deadtime_settings(self): r"""Show the dialog for dead-time options. Update attribue deadtime options upon closing the dialog.""" view = DeadTimeSettingsView(parent=self) - view.set_state( - self.deadtime_settings.paralyzable, self.deadtime_settings.dead_time, self.deadtime_settings.tof_step - ) view.exec_() - # update the dead time settings of the Main GUI after user has closed the dialog - for option in ["paralyzable", "dead_time", "tof_step"]: - setattr(self.deadtime_settings, option, view.options[option]) diff --git a/reflectivity_ui/ui/deadtime_settings.py b/reflectivity_ui/ui/deadtime_settings.py index 1c7620c..10e68cd 100644 --- a/reflectivity_ui/ui/deadtime_settings.py +++ b/reflectivity_ui/ui/deadtime_settings.py @@ -1,19 +1,13 @@ +# package imports +from reflectivity_ui.interfaces.configuration import Configuration + # third-party imports from qtpy.QtWidgets import QDialog, QWidget -from xml.dom.minidom import Document, Element -from typing import Any, Callable, Dict -import os - from qtpy.uic import loadUi +# standard imports +import os -class DeadTimeSettingsModel(): - """Stores all options for the dead time correction. These are global options""" - - apply_deadtime: bool = False - paralyzable: bool = True - dead_time: float = 4.2 - tof_step: float = 100.0 class DeadTimeSettingsView(QDialog): """ @@ -24,38 +18,22 @@ def __init__(self, parent: QWidget): super().__init__(parent) filepath = os.path.join(os.path.dirname(__file__), "deadtime_settings.ui") self.ui = loadUi(filepath, baseinstance=self) - self.options = self.get_state_from_form() + self.set_state_from_global_config() - def set_state(self, paralyzable, dead_time, tof_step): - """ - Store options and populate the form - :param apply_correction: If True, dead time correction will be applied - :param paralyzable: If True, a paralyzable correction will be used - :param dead_time: Value of the dead time in micro second - :param tof_step: TOF binning in micro second + def set_state_from_global_config(self): """ - self.ui.use_paralyzable.setChecked(paralyzable) - self.ui.dead_time_value.setValue(dead_time) - self.ui.dead_time_tof.setValue(tof_step) - self.options = self.get_state_from_form() - - def get_state_from_form(self) -> dict: - r"""Read the options from the form. - - Returns - ------- - Dictionary whose keys must match fields of class `DeadTimeSettingsModel` + Populate the form with the current global configuration """ - return { - 'paralyzable': self.ui.use_paralyzable.isChecked(), - 'dead_time': self.ui.dead_time_value.value(), - 'tof_step': self.ui.dead_time_tof.value(), - } + self.ui.use_paralyzable.setChecked(Configuration.paralyzable_deadtime) + self.ui.dead_time_value.setValue(Configuration.deadtime_value) + self.ui.dead_time_tof.setValue(Configuration.deadtime_tof_step) def accept(self): """ Read in the options on the form when the OK button is - clicked. + clicked and update the global configuration. """ - self.options = self.get_state_from_form() + Configuration.paralyzable_deadtime = self.ui.use_paralyzable.isChecked() + Configuration.deadtime_value = self.ui.dead_time_value.value() + Configuration.deadtime_tof_step = self.ui.dead_time_tof.value() self.close() diff --git a/test/ui/test_deadtime_entry.py b/test/ui/test_deadtime_entry.py new file mode 100644 index 0000000..e54faf5 --- /dev/null +++ b/test/ui/test_deadtime_entry.py @@ -0,0 +1,42 @@ +# third party imports +import pytest +from qtpy.QtCore import Qt # type: ignore + +# local imports +from reflectivity_ui.ui.deadtime_entry import DeadTimeEntryPoint # Make sure to import your class correctly + + +@pytest.fixture +def dead_time_entry_point(qtbot): + widget = DeadTimeEntryPoint() + qtbot.addWidget(widget) + return widget + + +def test_initial_state(dead_time_entry_point): + assert not dead_time_entry_point.applyCheckBox.isChecked() + assert not dead_time_entry_point.settingsButton.isEnabled() + + +def test_checkbox_interaction(dead_time_entry_point, qtbot): + # Simulate checking the checkbox + qtbot.mouseClick(dead_time_entry_point.applyCheckBox, Qt.LeftButton) + # Test if the checkbox is checked + assert dead_time_entry_point.applyCheckBox.isChecked() + # Test if the settings button is now enabled + assert dead_time_entry_point.settingsButton.isEnabled() + + +def test_uncheck_checkbox(dead_time_entry_point, qtbot): + # First, check the checkbox + qtbot.mouseClick(dead_time_entry_point.applyCheckBox, Qt.LeftButton) + # Now, uncheck it + qtbot.mouseClick(dead_time_entry_point.applyCheckBox, Qt.LeftButton) + # Test if the checkbox is unchecked + assert not dead_time_entry_point.applyCheckBox.isChecked() + # Test if the settings button is now disabled + assert not dead_time_entry_point.settingsButton.isEnabled() + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/test/ui/test_deadtime_settings.py b/test/ui/test_deadtime_settings.py new file mode 100644 index 0000000..c601c32 --- /dev/null +++ b/test/ui/test_deadtime_settings.py @@ -0,0 +1,62 @@ +# local imports +from reflectivity_ui.interfaces.configuration import Configuration +from reflectivity_ui.interfaces.main_window import MainWindow + +# standard imports +import unittest.mock as mock + +# third-party imports +import pytest +from qtpy import QtCore, QtWidgets + + +def test_show_deadtime_settings_default_values(qtbot): + """Test showing the deadtime settings dialog and keeping the default values""" + main_window = MainWindow() + qtbot.addWidget(main_window) + + def display_deadtime_settings(): + # open the deadtime settings dialog and close without changing values + dialog = QtWidgets.QApplication.activeModalWidget() + # press Enter to accept (click Ok) + qtbot.keyClick(dialog, QtCore.Qt.Key_Enter, delay=1) + + QtCore.QTimer.singleShot(200, display_deadtime_settings) + main_window.open_deadtime_settings() + + assert Configuration.paralyzable_deadtime == True + assert Configuration.deadtime_value == 4.2 + assert Configuration.deadtime_tof_step == 100 + + +def test_show_deadtime_settings_updated_values(qtbot): + """Test showing the deadtime settings dialog and updating the values""" + new_paralyzable = False + new_dead_time = 5.0 + new_tof_step = 200 + + main_window = MainWindow() + qtbot.addWidget(main_window) + + def update_deadtime_settings(): + # update the values in the deadtime settings dialog and close the dialog + dialog = QtWidgets.QApplication.activeModalWidget() + paralyzable_checkbox = dialog.findChild(QtWidgets.QCheckBox) + paralyzable_checkbox.setChecked(new_paralyzable) + deadtime_spinbox = dialog.findChild(QtWidgets.QDoubleSpinBox, "dead_time_value") + deadtime_spinbox.setValue(new_dead_time) + tof_spinbox = dialog.findChild(QtWidgets.QDoubleSpinBox, "dead_time_tof") + tof_spinbox.setValue(new_tof_step) + # press Enter to accept (click Ok) + qtbot.keyClick(dialog, QtCore.Qt.Key_Enter, delay=1) + + QtCore.QTimer.singleShot(200, update_deadtime_settings) + main_window.open_deadtime_settings() + + assert Configuration.paralyzable_deadtime == new_paralyzable + assert Configuration.deadtime_value == new_dead_time + assert Configuration.deadtime_tof_step == new_tof_step + + +if __name__ == "__main__": + pytest.main([__file__]) From 6162ae8f93363a20cee71483cb786de1b1a285dc Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Tue, 2 Jul 2024 21:18:08 -0400 Subject: [PATCH 08/16] reload files upon change in dead time settings --- reflectivity_ui/interfaces/data_manager.py | 19 +++++++++ .../interfaces/event_handlers/main_handler.py | 7 ++++ reflectivity_ui/interfaces/main_window.py | 18 +++++--- reflectivity_ui/ui/deadtime_entry.py | 42 +++++++++++++++++-- reflectivity_ui/ui/deadtime_settings.py | 37 ++++++++++++++-- 5 files changed, 112 insertions(+), 11 deletions(-) diff --git a/reflectivity_ui/interfaces/data_manager.py b/reflectivity_ui/interfaces/data_manager.py index 3d26883..6dac8dd 100644 --- a/reflectivity_ui/interfaces/data_manager.py +++ b/reflectivity_ui/interfaces/data_manager.py @@ -768,3 +768,22 @@ def current_event_files(self): h5_file_list = glob.glob(os.path.join(self.current_directory, "*.nxs.h5")) event_file_list.extend(h5_file_list) return sorted([os.path.basename(name) for name in event_file_list]) + + def reload_cached_files(self, progress=None): + """ + Force reload of all files cached by the data manager + """ + n_loaded = 0 + n_total = self.get_cachesize() + for nexus_data in self._cache: + file_path = nexus_data.file_path + # keep configuration + xs_main = nexus_data.cross_sections[nexus_data.main_cross_section] + conf = xs_main.configuration + self.load(file_path, conf, force=True, update_parameters=False) + n_loaded += 1 + if progress: + progress.set_value(n_loaded, message="%s loaded" % os.path.basename(file_path), out_of=n_total) + + if progress: + progress.set_value(n_total, message="Done", out_of=n_total) diff --git a/reflectivity_ui/interfaces/event_handlers/main_handler.py b/reflectivity_ui/interfaces/event_handlers/main_handler.py index 4dd8e06..0e702f1 100644 --- a/reflectivity_ui/interfaces/event_handlers/main_handler.py +++ b/reflectivity_ui/interfaces/event_handlers/main_handler.py @@ -1446,6 +1446,13 @@ def strip_overlap(self): self.main_window.initiate_reflectivity_plot.emit(False) + def reload_all_files(self): + """ + Reload all loaded (cached) files + """ + prog = ProgressReporter(progress_bar=self.progress_bar, status_bar=self.status_bar_handler) + self._data_manager.reload_cached_files(prog) + def report_message(self, message, informative_message=None, detailed_message=None, pop_up=False, is_error=False): r""" Report an error. diff --git a/reflectivity_ui/interfaces/main_window.py b/reflectivity_ui/interfaces/main_window.py index b98dd4e..c84a2aa 100644 --- a/reflectivity_ui/interfaces/main_window.py +++ b/reflectivity_ui/interfaces/main_window.py @@ -96,6 +96,7 @@ def __init__(self): self.initiate_reflectivity_plot.connect(self.plot_manager.plot_refl) self.ui.deadtime_entry.settingsButton.clicked.connect(self.open_deadtime_settings) + self.ui.deadtime_entry.reload_files_signal.connect(self.reload_all_files) def closeEvent(self, event): """Close UI event""" @@ -517,6 +518,18 @@ def update_offspec_qz_bin_width(self, value=None): width = (off_spec_y_max - off_spec_y_min) / off_spec_nybins self.ui.offspec_qz_bin_width_label.setText("%8.6f 1/A" % width) + def open_deadtime_settings(self): + r"""Show the dialog for dead-time options. Update global configuration parameters upon + closing the dialog.""" + view = DeadTimeSettingsView(parent=self) + view.reload_files_signal.connect(self.reload_all_files) + view.exec_() + + def reload_all_files(self): + r"""Reload all previously loaded files upon change in loading configuration""" + self.file_handler.reload_all_files() + self.file_loaded() + # Un-used UI signals # pylint: disable=missing-docstring, multiple-statements, no-self-use def change_gisans_colorscale(self): @@ -531,8 +544,3 @@ def open_polarization_window(self): def open_rawdata_dialog(self): return NotImplemented - - def open_deadtime_settings(self): - r"""Show the dialog for dead-time options. Update attribue deadtime options upon closing the dialog.""" - view = DeadTimeSettingsView(parent=self) - view.exec_() diff --git a/reflectivity_ui/ui/deadtime_entry.py b/reflectivity_ui/ui/deadtime_entry.py index 6408706..a2b5f25 100644 --- a/reflectivity_ui/ui/deadtime_entry.py +++ b/reflectivity_ui/ui/deadtime_entry.py @@ -1,9 +1,17 @@ +# package imports +from reflectivity_ui.interfaces.configuration import Configuration +from reflectivity_ui.interfaces.event_handlers.widgets import AcceptRejectDialog + # third party imports +from qtpy.QtCore import Signal from qtpy.QtWidgets import QGroupBox, QHBoxLayout, QCheckBox, QPushButton class DeadTimeEntryPoint(QGroupBox): - def __init__(self, title='Dead Time Correction'): + + reload_files_signal = Signal() + + def __init__(self, title="Dead Time Correction"): super().__init__(title) self.initUI() @@ -22,9 +30,9 @@ def initUI(self): "}" ) - self.applyCheckBox = QCheckBox('Apply', self) + self.applyCheckBox = self.VerifyChangeCheckBox("Apply", self) self.applyCheckBox.stateChanged.connect(self.toggleSettingsButton) - self.settingsButton = QPushButton('Settings', self) + self.settingsButton = QPushButton("Settings", self) self.settingsButton.setEnabled(self.applyCheckBox.isChecked()) # enabled if we use the correction # Create a horizontal layout for the checkbox and settings button @@ -36,6 +44,34 @@ def initUI(self): # Set the layout for the group box self.setLayout(hbox) + class VerifyChangeCheckBox(QCheckBox): + """ + Checkbox that intercepts the state change to ask user to confirm the change in + dead-time settings, since it requires reloading all files + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def ask_user_ok_to_reload_files(self): + """Shows dialog asking user to confirm reloading all files""" + message = "Change dead-time settings and reload all files?" + dialog = AcceptRejectDialog(self, title="Reload files", message=message) + proceed = dialog.exec_() + return proceed + + def mousePressEvent(self, event): + # Ask user to confirm before changing the state + if self.ask_user_ok_to_reload_files(): + # Manually toggle the checkbox state + self.setChecked(not self.isChecked()) + # Ignore the original event since it is handled above + event.ignore() + def toggleSettingsButton(self, state): # Enable the settings button if the checkbox is checked, disable otherwise self.settingsButton.setEnabled(state) + # Update the global configuration state + Configuration.apply_deadtime = state + # Trigger reloading all files to apply the new dead-time settings + self.reload_files_signal.emit() diff --git a/reflectivity_ui/ui/deadtime_settings.py b/reflectivity_ui/ui/deadtime_settings.py index 10e68cd..0de7af8 100644 --- a/reflectivity_ui/ui/deadtime_settings.py +++ b/reflectivity_ui/ui/deadtime_settings.py @@ -1,7 +1,9 @@ # package imports from reflectivity_ui.interfaces.configuration import Configuration +from reflectivity_ui.interfaces.event_handlers.widgets import AcceptRejectDialog # third-party imports +from qtpy.QtCore import Signal from qtpy.QtWidgets import QDialog, QWidget from qtpy.uic import loadUi @@ -14,6 +16,8 @@ class DeadTimeSettingsView(QDialog): Dialog to choose the dead time correction options. """ + reload_files_signal = Signal() + def __init__(self, parent: QWidget): super().__init__(parent) filepath = os.path.join(os.path.dirname(__file__), "deadtime_settings.ui") @@ -28,12 +32,39 @@ def set_state_from_global_config(self): self.ui.dead_time_value.setValue(Configuration.deadtime_value) self.ui.dead_time_tof.setValue(Configuration.deadtime_tof_step) + def check_values_changed(self): + """ + Check if the dialog settings entries have been changed by the user + + Returns + ------- + bool: - True if dialog values are different from the global configuration + - False if dialog values are the same as the global configuration + """ + if not self.ui.use_paralyzable.isChecked() == Configuration.paralyzable_deadtime: + return True + if not self.ui.dead_time_value.value() == Configuration.deadtime_value: + return True + if not self.ui.dead_time_tof.value() == Configuration.deadtime_tof_step: + return True + return False + + def ask_user_ok_to_reload_files(self): + """Shows dialog asking user to confirm reloading all files""" + message = "Change dead-time settings and reload all files?" + dialog = AcceptRejectDialog(self, title="Reload files", message=message) + proceed = dialog.exec_() + return proceed + def accept(self): """ Read in the options on the form when the OK button is clicked and update the global configuration. """ - Configuration.paralyzable_deadtime = self.ui.use_paralyzable.isChecked() - Configuration.deadtime_value = self.ui.dead_time_value.value() - Configuration.deadtime_tof_step = self.ui.dead_time_tof.value() + if self.check_values_changed() and self.ask_user_ok_to_reload_files(): + Configuration.paralyzable_deadtime = self.ui.use_paralyzable.isChecked() + Configuration.deadtime_value = self.ui.dead_time_value.value() + Configuration.deadtime_tof_step = self.ui.dead_time_tof.value() + # trigger reloading all files to apply the new dead-time settings + self.reload_files_signal.emit() self.close() From e2b05b3d0fef27f58098138e8a516a3890285295 Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Wed, 3 Jul 2024 06:42:41 -0400 Subject: [PATCH 09/16] test that changing deadtime settings triggers reloading files --- reflectivity_ui/interfaces/configuration.py | 4 ++ reflectivity_ui/interfaces/main_window.py | 5 ++- test/ui/test_deadtime_entry.py | 41 +++++++++++++++++++-- test/ui/test_deadtime_settings.py | 13 ++++++- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/reflectivity_ui/interfaces/configuration.py b/reflectivity_ui/interfaces/configuration.py index d250f84..4557dfc 100644 --- a/reflectivity_ui/interfaces/configuration.py +++ b/reflectivity_ui/interfaces/configuration.py @@ -395,3 +395,7 @@ def setup_default_values(cls): cls.polynomial_stitching = False cls.polynomial_stitching_degree = 3 cls.polynomial_stitching_points = 3 + cls.apply_deadtime = True + cls.paralyzable_deadtime = True + cls.deadtime_value = 4.2 + cls.deadtime_tof_step = 100 diff --git a/reflectivity_ui/interfaces/main_window.py b/reflectivity_ui/interfaces/main_window.py index c84a2aa..c9ba386 100644 --- a/reflectivity_ui/interfaces/main_window.py +++ b/reflectivity_ui/interfaces/main_window.py @@ -527,8 +527,9 @@ def open_deadtime_settings(self): def reload_all_files(self): r"""Reload all previously loaded files upon change in loading configuration""" - self.file_handler.reload_all_files() - self.file_loaded() + if self.data_manager.get_cachesize() > 0: + self.file_handler.reload_all_files() + self.file_loaded() # Un-used UI signals # pylint: disable=missing-docstring, multiple-statements, no-self-use diff --git a/test/ui/test_deadtime_entry.py b/test/ui/test_deadtime_entry.py index e54faf5..ab80b66 100644 --- a/test/ui/test_deadtime_entry.py +++ b/test/ui/test_deadtime_entry.py @@ -1,9 +1,13 @@ # third party imports import pytest -from qtpy.QtCore import Qt # type: ignore +from qtpy.QtCore import Qt, QTimer # type: ignore +from qtpy.QtWidgets import QApplication, QDialogButtonBox # local imports +from reflectivity_ui.interfaces.configuration import Configuration +from reflectivity_ui.interfaces.main_window import MainWindow from reflectivity_ui.ui.deadtime_entry import DeadTimeEntryPoint # Make sure to import your class correctly +from test.ui import ui_utilities @pytest.fixture @@ -18,7 +22,12 @@ def test_initial_state(dead_time_entry_point): assert not dead_time_entry_point.settingsButton.isEnabled() -def test_checkbox_interaction(dead_time_entry_point, qtbot): +def test_checkbox_interaction(mocker, dead_time_entry_point, qtbot): + # Mock modal dialog + mocker.patch( + "reflectivity_ui.ui.deadtime_entry.DeadTimeEntryPoint.VerifyChangeCheckBox.ask_user_ok_to_reload_files", + return_value=True, + ) # Simulate checking the checkbox qtbot.mouseClick(dead_time_entry_point.applyCheckBox, Qt.LeftButton) # Test if the checkbox is checked @@ -27,7 +36,12 @@ def test_checkbox_interaction(dead_time_entry_point, qtbot): assert dead_time_entry_point.settingsButton.isEnabled() -def test_uncheck_checkbox(dead_time_entry_point, qtbot): +def test_uncheck_checkbox(mocker, dead_time_entry_point, qtbot): + # Mock modal dialog + mocker.patch( + "reflectivity_ui.ui.deadtime_entry.DeadTimeEntryPoint.VerifyChangeCheckBox.ask_user_ok_to_reload_files", + return_value=True, + ) # First, check the checkbox qtbot.mouseClick(dead_time_entry_point.applyCheckBox, Qt.LeftButton) # Now, uncheck it @@ -38,5 +52,26 @@ def test_uncheck_checkbox(dead_time_entry_point, qtbot): assert not dead_time_entry_point.settingsButton.isEnabled() +@pytest.mark.datarepo +def test_checkbox_change_reload_files(mocker, qtbot): + # Mock modal dialog + mocker.patch( + "reflectivity_ui.ui.deadtime_entry.DeadTimeEntryPoint.VerifyChangeCheckBox.ask_user_ok_to_reload_files", + return_value=True, + ) + mock_reload_files = mocker.patch("reflectivity_ui.interfaces.main_window.MainWindow.reload_all_files") + # Initialize main window + main_window = MainWindow() + qtbot.addWidget(main_window) + # Add run to the reduction table + ui_utilities.setText(main_window.numberSearchEntry, str(40785), press_enter=True) + ui_utilities.set_current_file_by_run_number(main_window, 40785) + main_window.actionAddPlot.triggered.emit() + # Simulate checking the deadtime settings checkbox + qtbot.mouseClick(main_window.ui.deadtime_entry.applyCheckBox, Qt.LeftButton) + # Test if file reload was triggered + mock_reload_files.assert_called_once() + + if __name__ == "__main__": pytest.main([__file__]) diff --git a/test/ui/test_deadtime_settings.py b/test/ui/test_deadtime_settings.py index c601c32..4aeca7e 100644 --- a/test/ui/test_deadtime_settings.py +++ b/test/ui/test_deadtime_settings.py @@ -10,10 +10,12 @@ from qtpy import QtCore, QtWidgets -def test_show_deadtime_settings_default_values(qtbot): +def test_show_deadtime_settings_default_values(mocker, qtbot): """Test showing the deadtime settings dialog and keeping the default values""" + mock_reload_files = mocker.patch("reflectivity_ui.interfaces.main_window.MainWindow.reload_all_files") main_window = MainWindow() qtbot.addWidget(main_window) + Configuration.setup_default_values() def display_deadtime_settings(): # open the deadtime settings dialog and close without changing values @@ -27,10 +29,16 @@ def display_deadtime_settings(): assert Configuration.paralyzable_deadtime == True assert Configuration.deadtime_value == 4.2 assert Configuration.deadtime_tof_step == 100 + mock_reload_files.assert_not_called() -def test_show_deadtime_settings_updated_values(qtbot): +def test_show_deadtime_settings_updated_values(mocker, qtbot): """Test showing the deadtime settings dialog and updating the values""" + mocker.patch( + "reflectivity_ui.ui.deadtime_settings.DeadTimeSettingsView.ask_user_ok_to_reload_files", return_value=True + ) + mock_reload_files = mocker.patch("reflectivity_ui.interfaces.main_window.MainWindow.reload_all_files") + new_paralyzable = False new_dead_time = 5.0 new_tof_step = 200 @@ -56,6 +64,7 @@ def update_deadtime_settings(): assert Configuration.paralyzable_deadtime == new_paralyzable assert Configuration.deadtime_value == new_dead_time assert Configuration.deadtime_tof_step == new_tof_step + mock_reload_files.assert_called_once() if __name__ == "__main__": From 914caa7444807dd8ade9208cd78bdb363c66d602 Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Fri, 26 Jul 2024 11:16:39 -0400 Subject: [PATCH 10/16] Address review comments - dead-time correction should be turned off by default - reload only files in tables (not all cached files) - keep the run settings when reloading files - recalculate reflectivity when reloading files --- reflectivity_ui/interfaces/configuration.py | 4 +- reflectivity_ui/interfaces/data_manager.py | 69 ++++++++++++------- .../interfaces/event_handlers/main_handler.py | 47 ++++++++++++- reflectivity_ui/interfaces/main_window.py | 4 +- .../interfaces/test_data_manager.py | 18 +++++ 5 files changed, 111 insertions(+), 31 deletions(-) diff --git a/reflectivity_ui/interfaces/configuration.py b/reflectivity_ui/interfaces/configuration.py index 4557dfc..0d5e83e 100644 --- a/reflectivity_ui/interfaces/configuration.py +++ b/reflectivity_ui/interfaces/configuration.py @@ -39,7 +39,7 @@ class Configuration(object): polynomial_stitching_points = 3 # Dead time options - apply_deadtime = True + apply_deadtime = False paralyzable_deadtime = True deadtime_value = 4.2 deadtime_tof_step = 100 @@ -395,7 +395,7 @@ def setup_default_values(cls): cls.polynomial_stitching = False cls.polynomial_stitching_degree = 3 cls.polynomial_stitching_points = 3 - cls.apply_deadtime = True + cls.apply_deadtime = False cls.paralyzable_deadtime = True cls.deadtime_value = 4.2 cls.deadtime_tof_step = 100 diff --git a/reflectivity_ui/interfaces/data_manager.py b/reflectivity_ui/interfaces/data_manager.py index 6dac8dd..cdeb188 100644 --- a/reflectivity_ui/interfaces/data_manager.py +++ b/reflectivity_ui/interfaces/data_manager.py @@ -3,7 +3,6 @@ Data manager. Holds information about the current data location and manages the data cache. """ - import glob import sys import os @@ -68,6 +67,18 @@ def get_cachesize(self): def clear_cache(self): self._cache = [] + def clear_cached_unused_data(self): + """ + Delete cached files that are not in the reduction list or direct beam list + """ + + def is_used_in_reduction(f: NexusData): + return (self.find_data_in_reduction_list(f) is not None) or ( + self.find_data_in_direct_beam_list(f) is not None + ) + + self._cache[:] = [file for file in self._cache if is_used_in_reduction(file)] + def set_active_data_from_reduction_list(self, index): """ Set a data set in the reduction list as the active @@ -703,18 +714,38 @@ def load_data_from_reduced_file(self, file_path, configuration=None, progress=No Ask the main event handler to update the UI once we are done. :param str file_path: reduced file to load :param Configuration configuration: configuration to base the loaded data on + :param ProgressReporter progress: progress reporter """ t_0 = time.time() db_files, data_files = quicknxs_io.read_reduced_file(file_path, configuration) logging.info("Reduced file loaded: %s sec", time.time() - t_0) - n_loaded = 0 n_total = len(db_files) + len(data_files) if progress and n_total > 0: progress.set_value(1, message="Loaded %s" % os.path.basename(file_path), out_of=n_total) + self.load_direct_beam_and_data_files(db_files, data_files, configuration, progress, t_0) + logging.info("DONE: %s sec", time.time() - t_0) + + def load_direct_beam_and_data_files( + self, db_files, data_files, configuration=None, progress=None, force=False, t_0=None + ): + """ + Load direct beam and data files and add them to the direct beam list and reduction + list, respectively + :param list db_files: list of (run_number, run_file, conf) for direct beam files + :param list data_files: list of (run_number, run_file, conf) for data files + :param Configuration configuration: configuration to base the loaded data on + :param ProgressReporter progress: progress reporter + :param bool force: + :param float t_0: start time for logging data loading time + """ + if not t_0: + t_0 = time.time() + n_loaded = 0 + n_total = len(db_files) + len(data_files) for r_id, run_file, conf in db_files: t_i = time.time() if os.path.isfile(run_file): - is_from_cache = self.load(run_file, conf, update_parameters=False) + is_from_cache = self.load(run_file, conf, force=force, update_parameters=False) if is_from_cache: configuration.normalization = None self._nexus_data.update_configuration(conf) @@ -727,7 +758,6 @@ def load_data_from_reduced_file(self, file_path, configuration=None, progress=No if progress: progress.set_value(n_loaded, message="ERROR: %s does not exist" % run_file, out_of=n_total) n_loaded += 1 - for r_id, run_file, conf in data_files: t_i = time.time() do_files_exist = [] @@ -735,7 +765,7 @@ def load_data_from_reduced_file(self, file_path, configuration=None, progress=No do_files_exist.append((os.path.isfile(name))) if all(do_files_exist): - is_from_cache = self.load(run_file, conf, update_parameters=False) + is_from_cache = self.load(run_file, conf, force=force, update_parameters=False) if is_from_cache: configuration.normalization = None self._nexus_data.update_configuration(conf) @@ -751,12 +781,9 @@ def load_data_from_reduced_file(self, file_path, configuration=None, progress=No if progress: progress.set_value(n_loaded, message="ERROR: %s does not exist" % run_file, out_of=n_total) n_loaded += 1 - if progress: progress.set_value(n_total, message="Done", out_of=n_total) - logging.info("DONE: %s sec", time.time() - t_0) - @property def current_event_files(self): # type: () -> List[str] @@ -769,21 +796,15 @@ def current_event_files(self): event_file_list.extend(h5_file_list) return sorted([os.path.basename(name) for name in event_file_list]) - def reload_cached_files(self, progress=None): + def reload_files(self, configuration=None, progress=None): """ - Force reload of all files cached by the data manager + Force reload of files in the reduction list and direct beam list """ - n_loaded = 0 - n_total = self.get_cachesize() - for nexus_data in self._cache: - file_path = nexus_data.file_path - # keep configuration - xs_main = nexus_data.cross_sections[nexus_data.main_cross_section] - conf = xs_main.configuration - self.load(file_path, conf, force=True, update_parameters=False) - n_loaded += 1 - if progress: - progress.set_value(n_loaded, message="%s loaded" % os.path.basename(file_path), out_of=n_total) - - if progress: - progress.set_value(n_total, message="Done", out_of=n_total) + # Get files to reload + db_files = [(nexus.number, nexus.file_path, nexus.configuration) for nexus in self.direct_beam_list] + data_files = [(nexus.number, nexus.file_path, nexus.configuration) for nexus in self.reduction_list] + # Clear the lists + self.reduction_list.clear() + self.direct_beam_list.clear() + # Reload files and add to reduction and direct beam lists + self.load_direct_beam_and_data_files(db_files, data_files, configuration, progress, True) diff --git a/reflectivity_ui/interfaces/event_handlers/main_handler.py b/reflectivity_ui/interfaces/event_handlers/main_handler.py index 0e702f1..f8a3383 100644 --- a/reflectivity_ui/interfaces/event_handlers/main_handler.py +++ b/reflectivity_ui/interfaces/event_handlers/main_handler.py @@ -1448,10 +1448,53 @@ def strip_overlap(self): def reload_all_files(self): """ - Reload all loaded (cached) files + Reload all files upon change in loading configuration + + To speed up reloading, the file cache is first cleared of files that are not used in the + reduction list or direct beam list. """ + if self.data_manager.get_cachesize() == 0: + return + + # Store the active (plotted) run index + active_idx = self._data_manager.find_active_data_id() + if active_idx is not None: + is_active_data_direct_beam = False + else: + is_active_data_direct_beam = True + active_idx = self._data_manager.find_active_direct_beam_id() + + # Reload files + self._data_manager.clear_cached_unused_data() + configuration = self.get_configuration() prog = ProgressReporter(progress_bar=self.progress_bar, status_bar=self.status_bar_handler) - self._data_manager.reload_cached_files(prog) + self._data_manager.reload_files(configuration, prog) + + # Update the tables in the UI + self.main_window.auto_change_active = True + + self.ui.normalizeTable.setRowCount(len(self._data_manager.direct_beam_list)) + for idx, _ in enumerate(self._data_manager.direct_beam_list): + self._data_manager.set_active_data_from_direct_beam_list(idx) + self.update_direct_beam_table(idx, self._data_manager.active_channel) + self.ui.reductionTable.setRowCount(len(self._data_manager.reduction_list)) + for idx, _ in enumerate(self._data_manager.reduction_list): + self._data_manager.set_active_data_from_reduction_list(idx) + self.update_reduction_table(idx, self._data_manager.active_channel) + + direct_beam_ids = [str(r.number) for r in self._data_manager.direct_beam_list] + self.ui.normalization_list_label.setText(", ".join(direct_beam_ids)) + + # Restore the active run + if is_active_data_direct_beam: + self._data_manager.set_active_data_from_direct_beam_list(active_idx) + else: + self._data_manager.set_active_data_from_reduction_list(active_idx) + + # Update plots + self.file_loaded() + + self.main_window.auto_change_active = False def report_message(self, message, informative_message=None, detailed_message=None, pop_up=False, is_error=False): r""" diff --git a/reflectivity_ui/interfaces/main_window.py b/reflectivity_ui/interfaces/main_window.py index c9ba386..7c2c2ee 100644 --- a/reflectivity_ui/interfaces/main_window.py +++ b/reflectivity_ui/interfaces/main_window.py @@ -527,9 +527,7 @@ def open_deadtime_settings(self): def reload_all_files(self): r"""Reload all previously loaded files upon change in loading configuration""" - if self.data_manager.get_cachesize() > 0: - self.file_handler.reload_all_files() - self.file_loaded() + self.file_handler.reload_all_files() # Un-used UI signals # pylint: disable=missing-docstring, multiple-statements, no-self-use diff --git a/test/unit/reflectivity_ui/interfaces/test_data_manager.py b/test/unit/reflectivity_ui/interfaces/test_data_manager.py index c090b45..e6a8e3a 100644 --- a/test/unit/reflectivity_ui/interfaces/test_data_manager.py +++ b/test/unit/reflectivity_ui/interfaces/test_data_manager.py @@ -75,6 +75,24 @@ def test_load_reduced(self, data_server): manager = DataManager(data_server.directory) manager.load_data_from_reduced_file(data_server.path_to("REF_M_29160_Specular_++.dat")) + def test_clear_cached_unused_data(self, data_server): + """Test helper function clear_cached_unused_data""" + manager = DataManager(data_server.directory) + manager.load(data_server.path_to("REF_M_42112"), Configuration()) + manager.add_active_to_reduction() + manager.load(data_server.path_to("REF_M_42113"), Configuration()) + manager.add_active_to_reduction() + manager.load(data_server.path_to("REF_M_42099"), Configuration()) + manager.add_active_to_normalization() + assert manager.get_cachesize() == 3 + # Load files without adding them to reduction or normalization + manager.load(data_server.path_to("REF_M_42100"), Configuration()) + manager.load(data_server.path_to("REF_M_42116"), Configuration()) + assert manager.get_cachesize() == 5 + # Delete unused files from cache + manager.clear_cached_unused_data() + assert manager.get_cachesize() == 3 + if __name__ == "__main__": pytest.main([__file__]) From 9f9c7e53b44d51f2625c5618284931706ef78376 Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Fri, 2 Aug 2024 13:28:39 -0400 Subject: [PATCH 11/16] fix typo --- reflectivity_ui/interfaces/event_handlers/main_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflectivity_ui/interfaces/event_handlers/main_handler.py b/reflectivity_ui/interfaces/event_handlers/main_handler.py index f8a3383..62087fe 100644 --- a/reflectivity_ui/interfaces/event_handlers/main_handler.py +++ b/reflectivity_ui/interfaces/event_handlers/main_handler.py @@ -1453,7 +1453,7 @@ def reload_all_files(self): To speed up reloading, the file cache is first cleared of files that are not used in the reduction list or direct beam list. """ - if self.data_manager.get_cachesize() == 0: + if self._data_manager.get_cachesize() == 0: return # Store the active (plotted) run index From 729d259a29d21600444646985ab2031c1b43e34a Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Tue, 6 Aug 2024 11:29:49 -0400 Subject: [PATCH 12/16] fix bug where reduction table configuration would change when updating dead-time settings and reloading files --- reflectivity_ui/interfaces/data_manager.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/reflectivity_ui/interfaces/data_manager.py b/reflectivity_ui/interfaces/data_manager.py index cdeb188..1d04b06 100644 --- a/reflectivity_ui/interfaces/data_manager.py +++ b/reflectivity_ui/interfaces/data_manager.py @@ -800,9 +800,14 @@ def reload_files(self, configuration=None, progress=None): """ Force reload of files in the reduction list and direct beam list """ + + def _get_nexus_conf(nexus_data): + """Returns the configuration for the main cross-section of the run""" + return nexus_data.cross_sections[nexus_data.main_cross_section].configuration + # Get files to reload - db_files = [(nexus.number, nexus.file_path, nexus.configuration) for nexus in self.direct_beam_list] - data_files = [(nexus.number, nexus.file_path, nexus.configuration) for nexus in self.reduction_list] + db_files = [(nexus.number, nexus.file_path, _get_nexus_conf(nexus)) for nexus in self.direct_beam_list] + data_files = [(nexus.number, nexus.file_path, _get_nexus_conf(nexus)) for nexus in self.reduction_list] # Clear the lists self.reduction_list.clear() self.direct_beam_list.clear() From bdd2e9e93c1b28b310b5dbe25e35c97752abb8a4 Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Wed, 7 Aug 2024 09:16:27 -0400 Subject: [PATCH 13/16] add used documentation --- docs/user/dead_time_correction.rst | 107 +++++++++++++++++++++++++++++ docs/user/index.rst | 5 ++ 2 files changed, 112 insertions(+) create mode 100644 docs/user/dead_time_correction.rst diff --git a/docs/user/dead_time_correction.rst b/docs/user/dead_time_correction.rst new file mode 100644 index 0000000..eecd52a --- /dev/null +++ b/docs/user/dead_time_correction.rst @@ -0,0 +1,107 @@ +.. _dead_time_correction: + +SingleReadoutDeadTimeCorrection +=============================== + +Dead time is the time after an event that a detector is not able to detect another event. +For a paralyzable detector, an event that happens during the dead time restarts the dead time. For +a non-paralyzable detector, the event is simply lost and does not cause additional dead time. + +Dead-time correction corrects for detector dead time by weighing the events according to: + +.. math:: N = M \frac{1}{(1-\mathrm{rate} \cdot (\frac{t_{\mathrm{dead}}}{t_{\mathrm{bin}}}))} + +for non-paralyzable detectors and + +.. math:: N = M \frac{\mathrm{Re} (\mathrm{W}(-\mathrm{rate} \cdot (\frac{t_{\mathrm{dead}}}{t_{\mathrm{bin}}})) )}{\frac{\mathrm{rate}}{t_{\mathrm{bin}}}} + +for paralyzable detectors, where + +| :math:`N` = true count +| :math:`M` = measured count +| :math:`t_{\mathrm{dead}}` = dead time +| :math:`t_{\mathrm{bin}}` = TOF bin width +| :math:`\mathrm{rate}` = measured count rate +| :math:`\mathrm{W}` = Lambert W function + +The class ``SingleReadoutDeadTimeCorrection`` is a Mantid-style algorithm for computing the +dead-time correction for an event workspace. One can optionally include error events in the +dead-time computation. + +Properties +---------- + +.. list-table:: + :widths: 20 20 20 20 20 + :header-rows: 1 + + * - Name + - Direction + - Type + - Default + - Description + * - InputWorkspace + - Input + - EventWorkspace + - Mandatory + - Input workspace used to compute dead-time correction + * - InputErrorEventsWorkspace + - Input + - EventWorkspace + - + - Input workspace with error events used to compute dead-time correction + * - DeadTime + - Input + - number + - 4.2 + - Dead time in microseconds + * - TOFStep + - Input + - number + - 100.0 + - TOF bins to compute dead-time correction, in microseconds + * - Paralyzable + - Input + - boolean + - False + - If True, paralyzable correction will be applied, non-paralyzable otherwise + * - TOFRange + - Input + - dbl list + - [0.0, 0.0] + - TOF range to use to compute dead-time correction + * - OutputWorkspace + - Output + - MatrixWorkspace + - Mandatory + - Output workspace containing the dead-time correction factor for each TOF bin + +Usage +----- +Example using ``SingleReadoutDeadTimeCorrection`` + +.. code-block:: python + + import mantid.simpleapi as api + from reflectivity_ui.interfaces.data_handling import DeadTimeCorrection + from reflectivity_ui.interfaces.data_handling.instrument import mantid_algorithm_exec + # Load events + path = "/home/u5z/projects/reflectivity_ui/test/data/reflectivity_ui-data/REF_M_42112.nxs.h5" + ws = api.LoadEventNexus(Filename=path, OutputWorkspace="raw_events") + # Load error events + err_ws = api.LoadErrorEventsNexus(path) + # Compute dead-time correction + tof_min = ws.getTofMin() + tof_max = ws.getTofMax() + corr_ws = mantid_algorithm_exec( + DeadTimeCorrection.SingleReadoutDeadTimeCorrection, + InputWorkspace=ws, + InputErrorEventsWorkspace=err_ws, + Paralyzable=False, + DeadTime=4.2, + TOFStep=100.0, + TOFRange=[tof_min, tof_max], + OutputWorkspace="corr", + ) + # Apply dead-time correction + ws = api.Multiply(ws, corr_ws, OutputWorkspace=str(ws)) diff --git a/docs/user/index.rst b/docs/user/index.rst index a71203c..62ae412 100644 --- a/docs/user/index.rst +++ b/docs/user/index.rst @@ -4,3 +4,8 @@ User Guide ========== TODO + +.. toctree:: + :maxdepth: 2 + + dead_time_correction From c36c1216a76cd7e890822b18e69cb2c906232eda Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Thu, 8 Aug 2024 08:24:40 -0400 Subject: [PATCH 14/16] add unit tests for dead-time correction --- docs/developer/documentation.rst | 13 +++++++++ docs/developer/environment.rst | 1 + docs/developer/index.rst | 1 + docs/user/dead_time_correction.rst | 10 +++++-- test/conftest.py | 5 ++++ .../test_dead_time_correction.py | 28 ++++++++++++++++++ .../data_handling/test_instrument.py | 29 +++++++++++++++++++ 7 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 docs/developer/documentation.rst create mode 100644 test/unit/reflectivity_ui/interfaces/data_handling/test_dead_time_correction.py create mode 100644 test/unit/reflectivity_ui/interfaces/data_handling/test_instrument.py diff --git a/docs/developer/documentation.rst b/docs/developer/documentation.rst new file mode 100644 index 0000000..502c09c --- /dev/null +++ b/docs/developer/documentation.rst @@ -0,0 +1,13 @@ +============= +Documentation +============= + +To build the documentation as HTML locally:: + + cd docs + make html + +To test doctest-style code snippets in the documentation using `doctest `_:: + + cd docs + make doctest diff --git a/docs/developer/environment.rst b/docs/developer/environment.rst index afa57c1..56891b6 100644 --- a/docs/developer/environment.rst +++ b/docs/developer/environment.rst @@ -31,6 +31,7 @@ To setup a local development environment, the developers should follow the steps The ``environment.yml`` contains all of the dependencies for both the developer and the build servers. Update file ``environment.yml`` if dependencies are added to the package. +.. _test-data: Test Data --------- diff --git a/docs/developer/index.rst b/docs/developer/index.rst index 08f5de7..01a3b56 100644 --- a/docs/developer/index.rst +++ b/docs/developer/index.rst @@ -11,3 +11,4 @@ Development Guide integration_test ui_test release + documentation diff --git a/docs/user/dead_time_correction.rst b/docs/user/dead_time_correction.rst index eecd52a..5070cc8 100644 --- a/docs/user/dead_time_correction.rst +++ b/docs/user/dead_time_correction.rst @@ -78,15 +78,19 @@ Properties Usage ----- -Example using ``SingleReadoutDeadTimeCorrection`` +Example using ``SingleReadoutDeadTimeCorrection`` (requires :any:`test-data` files) -.. code-block:: python +.. testcode:: import mantid.simpleapi as api + import os + import sys + from pathlib import Path from reflectivity_ui.interfaces.data_handling import DeadTimeCorrection from reflectivity_ui.interfaces.data_handling.instrument import mantid_algorithm_exec # Load events - path = "/home/u5z/projects/reflectivity_ui/test/data/reflectivity_ui-data/REF_M_42112.nxs.h5" + path = Path().resolve().parent / "test" / "data" / "reflectivity_ui-data" / "REF_M_42112.nxs.h5" + path = path.as_posix() ws = api.LoadEventNexus(Filename=path, OutputWorkspace="raw_events") # Load error events err_ws = api.LoadErrorEventsNexus(path) diff --git a/test/conftest.py b/test/conftest.py index 9728539..002beac 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -45,6 +45,11 @@ def h5_path(self): r"""Directory where to find h5 data files""" return self._h5_path + @property + def h5_full_path(self): + r"""Full path to directory where to find h5 data files""" + return os.path.join(self.directory, self.h5_path) + def path_to(self, basename): r"""Absolute path to a data file. If it doesn't exist, try to find it in the remote repository""" # looking in test/data diff --git a/test/unit/reflectivity_ui/interfaces/data_handling/test_dead_time_correction.py b/test/unit/reflectivity_ui/interfaces/data_handling/test_dead_time_correction.py new file mode 100644 index 0000000..5c40b94 --- /dev/null +++ b/test/unit/reflectivity_ui/interfaces/data_handling/test_dead_time_correction.py @@ -0,0 +1,28 @@ +# package imports +from reflectivity_ui.interfaces.data_handling.DeadTimeCorrection import SingleReadoutDeadTimeCorrection + +# 3rd-party imports +import mantid.simpleapi as api +import pytest +from mantid.kernel import amend_config + +# standard imports + + +@pytest.mark.datarepo +@pytest.mark.parametrize("is_paralyzable", [False, True]) +def test_deadtime(is_paralyzable, data_server): + """Test of the dead-time correction algorithm SingleReadoutDeadTimeCorrection""" + with amend_config(data_dir=data_server.h5_full_path): + ws = api.Load("REF_M_42112") + + algo = SingleReadoutDeadTimeCorrection() + algo.PyInit() + algo.setProperty("InputWorkspace", ws) + algo.setProperty("OutputWorkspace", "dead_time_corr") + algo.setProperty("Paralyzable", is_paralyzable) + algo.PyExec() + corr_ws = algo.getProperty("OutputWorkspace").value + corr = corr_ws.readY(0) + for c in corr: + assert 1.0 <= c < 1.001, "value not between 1.0 and 1.001" diff --git a/test/unit/reflectivity_ui/interfaces/data_handling/test_instrument.py b/test/unit/reflectivity_ui/interfaces/data_handling/test_instrument.py new file mode 100644 index 0000000..55bcaed --- /dev/null +++ b/test/unit/reflectivity_ui/interfaces/data_handling/test_instrument.py @@ -0,0 +1,29 @@ +# package imports +from reflectivity_ui.interfaces.configuration import Configuration + +# 3rd party imports +import pytest + + +@pytest.mark.datarepo +def test_load_data_deadtime(data_server): + """Test load data with and without dead-time correction""" + conf = Configuration() + file_path = data_server.path_to("REF_M_42112") + corrected_events = [52283.51, 42028.15, 66880.96, 43405.89] + + # load with dead-time correction + conf.apply_deadtime = True + ws_list = conf.instrument.load_data(file_path, conf) + assert len(ws_list) == 4 + for iws, ws in enumerate(ws_list): + assert "dead_time_applied" in ws.getRun() + assert ws.extractY().sum() == pytest.approx(corrected_events[iws]) + + # load without dead-time correction + conf.apply_deadtime = False + ws_list = conf.instrument.load_data(file_path, conf) + assert len(ws_list) == 4 + for ws in ws_list: + assert "dead_time_applied" not in ws.getRun() + assert ws.extractY().sum() == ws.getNumberEvents() From 2076606e1422b62bdfbb58f10bc6b6b15767586d Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Thu, 8 Aug 2024 12:42:43 -0400 Subject: [PATCH 15/16] add docstrings and test of function reload_all_files --- reflectivity_ui/interfaces/configuration.py | 1 - .../interfaces/data_handling/instrument.py | 13 ++++++- .../event_handlers/test_main_handler.py | 34 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/reflectivity_ui/interfaces/configuration.py b/reflectivity_ui/interfaces/configuration.py index 0d5e83e..d149019 100644 --- a/reflectivity_ui/interfaces/configuration.py +++ b/reflectivity_ui/interfaces/configuration.py @@ -37,7 +37,6 @@ class Configuration(object): polynomial_stitching = False polynomial_stitching_degree = 3 polynomial_stitching_points = 3 - # Dead time options apply_deadtime = False paralyzable_deadtime = True diff --git a/reflectivity_ui/interfaces/data_handling/instrument.py b/reflectivity_ui/interfaces/data_handling/instrument.py index 2278eaf..d0699d6 100644 --- a/reflectivity_ui/interfaces/data_handling/instrument.py +++ b/reflectivity_ui/interfaces/data_handling/instrument.py @@ -11,7 +11,7 @@ from reflectivity_ui.interfaces.data_handling.filepath import FilePath # 3rd party -from mantid.api import WorkspaceGroup +from mantid.api import WorkspaceGroup, PythonAlgorithm from mantid.dataobjects import EventWorkspace import numpy as np import mantid.simpleapi as api @@ -68,6 +68,14 @@ def get_cross_section_label(ws, entry_name): def mantid_algorithm_exec(algorithm_class, **kwargs): + """ + Helper function for executing a Mantid-style algorithm + + :param PythonAlgorithm algorithm_class: the algorithm class to execute + :param kwargs: keyword arguments + :returns Workspace: if ``OutputWorkspace`` is passed as a keyword argument, the value of the + algorithm property ``OutputWorkspace`` will be returned + """ algorithm_instance = algorithm_class() assert algorithm_instance.PyInit, "str(algorithm_class) is not a Mantid Python algorithm" algorithm_instance.PyInit() @@ -225,7 +233,9 @@ def load_data(self, file_path, configuration=None): ws = api.LoadEventNexus(Filename=path, OutputWorkspace="raw_events") _path_xs_list = self.dummy_filter_cross_sections(ws, name_prefix=temp_workspace_root_name) if configuration is not None and configuration.apply_deadtime: + # Load error events from the bank_error_events entry err_ws = api.LoadErrorEventsNexus(path) + # Split error events by cross-section for compatibility with normal events _err_list = api.MRFilterCrossSections( InputWorkspace=err_ws, PolState=self.pol_state, @@ -235,6 +245,7 @@ def load_data(self, file_path, configuration=None): CrossSectionWorkspaces="%s_err_entry" % temp_workspace_root_name, ) path_xs_list = [] + # Apply dead-time correction for each cross-section workspace for ws in _path_xs_list: xs_name = ws.getRun()["cross_section_id"].value if not xs_name == "unfiltered": diff --git a/test/unit/reflectivity_ui/interfaces/event_handlers/test_main_handler.py b/test/unit/reflectivity_ui/interfaces/event_handlers/test_main_handler.py index 25ca796..cbb9ce6 100644 --- a/test/unit/reflectivity_ui/interfaces/event_handlers/test_main_handler.py +++ b/test/unit/reflectivity_ui/interfaces/event_handlers/test_main_handler.py @@ -17,6 +17,8 @@ import os import sys +from test.ui import ui_utilities + this_module_path = sys.modules[__name__].__file__ @@ -37,6 +39,7 @@ class TestMainHandler(object): handler = MainHandler(application) @pytest.mark.datarepo + @pytest.mark.skip(reason="WIP") def test_congruency_fail_report(self, data_server): # Selected subset of log names with an invalid one message = self.handler._congruency_fail_report( @@ -144,6 +147,37 @@ def dialog_click_button(button_type): assert answer is False +@pytest.mark.datarepo +def test_reload_all_files(qtbot): + """Test function reload_all_files""" + main_window = MainWindow() + handler = MainHandler(main_window) + data_manager = main_window.data_manager + qtbot.addWidget(main_window) + selected_row = 0 + + # Add one direct beam run and two data runs + ui_utilities.setText(main_window.numberSearchEntry, str(40786), press_enter=True) + ui_utilities.set_current_file_by_run_number(main_window, 40786) + main_window.actionNorm.triggered.emit() + ui_utilities.set_current_file_by_run_number(main_window, 40785) + main_window.actionAddPlot.triggered.emit() + ui_utilities.set_current_file_by_run_number(main_window, 40782) + main_window.actionAddPlot.triggered.emit() + + # Select/plot the first data run + main_window.reduction_cell_activated(selected_row, 0) + + # Test that reloading all files does not change the active run + assert main_window.ui.reductionTable.rowCount() == 2 + assert len(data_manager.reduction_list) == 2 + assert data_manager._nexus_data == data_manager.reduction_list[selected_row] + handler.reload_all_files() + assert main_window.ui.reductionTable.rowCount() == 2 + assert len(main_window.data_manager.reduction_list) == 2 + assert data_manager._nexus_data == data_manager.reduction_list[selected_row] + + def _get_nexus_data(): """Data for testing""" config = Configuration() From 9b869a711a871603fda88fbc5798cbd29b98b2ad Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Fri, 9 Aug 2024 08:58:23 -0400 Subject: [PATCH 16/16] add release note and unit test for mantid_algorithm_exec --- docs/releasenotes/index.rst | 2 +- docs/user/dead_time_correction.rst | 2 +- .../interfaces/data_handling/instrument.py | 3 +- .../test_dead_time_correction.py | 15 ++++----- .../data_handling/test_instrument.py | 32 +++++++++++++++++++ 5 files changed, 42 insertions(+), 12 deletions(-) diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index a058463..2a1ace4 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -12,7 +12,7 @@ Notes for major and minor releases. Notes for Patch releases are deferred. **Of interest to the User**: -- PR #XYZ: one-liner description +- PR #95: Optional dead-time correction (disabled by default) **Of interest to the Developer:** diff --git a/docs/user/dead_time_correction.rst b/docs/user/dead_time_correction.rst index 5070cc8..00af2a9 100644 --- a/docs/user/dead_time_correction.rst +++ b/docs/user/dead_time_correction.rst @@ -22,7 +22,7 @@ for paralyzable detectors, where | :math:`t_{\mathrm{dead}}` = dead time | :math:`t_{\mathrm{bin}}` = TOF bin width | :math:`\mathrm{rate}` = measured count rate -| :math:`\mathrm{W}` = Lambert W function +| :math:`\mathrm{W}` = `Lambert W function `_ The class ``SingleReadoutDeadTimeCorrection`` is a Mantid-style algorithm for computing the dead-time correction for an event workspace. One can optionally include error events in the diff --git a/reflectivity_ui/interfaces/data_handling/instrument.py b/reflectivity_ui/interfaces/data_handling/instrument.py index d0699d6..d75a485 100644 --- a/reflectivity_ui/interfaces/data_handling/instrument.py +++ b/reflectivity_ui/interfaces/data_handling/instrument.py @@ -77,7 +77,7 @@ def mantid_algorithm_exec(algorithm_class, **kwargs): algorithm property ``OutputWorkspace`` will be returned """ algorithm_instance = algorithm_class() - assert algorithm_instance.PyInit, "str(algorithm_class) is not a Mantid Python algorithm" + assert hasattr(algorithm_instance, "PyInit"), f"{algorithm_class} is not a Mantid Python algorithm" algorithm_instance.PyInit() for name, value in kwargs.items(): algorithm_instance.setProperty(name, value) @@ -98,7 +98,6 @@ def get_dead_time_correction(ws, configuration, error_ws=None): tof_min = ws.getTofMin() tof_max = ws.getTofMax() - run_number = ws.getRun().getProperty("run_number").value corr_ws = mantid_algorithm_exec( DeadTimeCorrection.SingleReadoutDeadTimeCorrection, InputWorkspace=ws, diff --git a/test/unit/reflectivity_ui/interfaces/data_handling/test_dead_time_correction.py b/test/unit/reflectivity_ui/interfaces/data_handling/test_dead_time_correction.py index 5c40b94..c773263 100644 --- a/test/unit/reflectivity_ui/interfaces/data_handling/test_dead_time_correction.py +++ b/test/unit/reflectivity_ui/interfaces/data_handling/test_dead_time_correction.py @@ -1,5 +1,6 @@ # package imports from reflectivity_ui.interfaces.data_handling.DeadTimeCorrection import SingleReadoutDeadTimeCorrection +from reflectivity_ui.interfaces.data_handling.instrument import mantid_algorithm_exec # 3rd-party imports import mantid.simpleapi as api @@ -15,14 +16,12 @@ def test_deadtime(is_paralyzable, data_server): """Test of the dead-time correction algorithm SingleReadoutDeadTimeCorrection""" with amend_config(data_dir=data_server.h5_full_path): ws = api.Load("REF_M_42112") - - algo = SingleReadoutDeadTimeCorrection() - algo.PyInit() - algo.setProperty("InputWorkspace", ws) - algo.setProperty("OutputWorkspace", "dead_time_corr") - algo.setProperty("Paralyzable", is_paralyzable) - algo.PyExec() - corr_ws = algo.getProperty("OutputWorkspace").value + corr_ws = mantid_algorithm_exec( + SingleReadoutDeadTimeCorrection, + InputWorkspace=ws, + Paralyzable=is_paralyzable, + OutputWorkspace="dead_time_corr", + ) corr = corr_ws.readY(0) for c in corr: assert 1.0 <= c < 1.001, "value not between 1.0 and 1.001" diff --git a/test/unit/reflectivity_ui/interfaces/data_handling/test_instrument.py b/test/unit/reflectivity_ui/interfaces/data_handling/test_instrument.py index 55bcaed..1849d40 100644 --- a/test/unit/reflectivity_ui/interfaces/data_handling/test_instrument.py +++ b/test/unit/reflectivity_ui/interfaces/data_handling/test_instrument.py @@ -1,7 +1,11 @@ # package imports from reflectivity_ui.interfaces.configuration import Configuration +from reflectivity_ui.interfaces.data_handling.instrument import mantid_algorithm_exec # 3rd party imports +from mantid.api import MatrixWorkspaceProperty, PythonAlgorithm +from mantid.kernel import Direction +from mantid.simpleapi import CreateSingleValuedWorkspace import pytest @@ -27,3 +31,31 @@ def test_load_data_deadtime(data_server): for ws in ws_list: assert "dead_time_applied" not in ws.getRun() assert ws.extractY().sum() == ws.getNumberEvents() + + +def test_mantid_algorithm_exec(): + """Test helper function mantid_algorithm_exec""" + # test wrong type of class + class TestNotMantidAlgo: + pass + + with pytest.raises(AssertionError, match="is not a Mantid Python algorithm"): + mantid_algorithm_exec(TestNotMantidAlgo) + # test Mantid Python algorithm + + class TestMantidAlgo(PythonAlgorithm): + def PyInit(self): + self.declareProperty("Value", 8, "Value in workspace") + self.declareProperty( + MatrixWorkspaceProperty("OutputWorkspace", "", Direction.Output), + "Output workspace", + ) + + def PyExec(self): + value = self.getProperty("Value").value + ws = CreateSingleValuedWorkspace(value) + self.setProperty("OutputWorkspace", ws) + + custom_value = 4 + ws_out = mantid_algorithm_exec(TestMantidAlgo, Value=custom_value, OutputWorkspace="output") + assert ws_out.readY(0)[0] == custom_value