From 1e4e8be548678d442af9660025f11a9615fc8629 Mon Sep 17 00:00:00 2001 From: Marie Backman Date: Wed, 24 Apr 2024 07:42:59 -0400 Subject: [PATCH] - add ConfigurationHandler to handle updating the config state upon signals from the UI (#78) - remove obsolete remove_files.py --- .../event_handlers/configuration_handler.py | 83 ++++++++++++++++++ reflectivity_ui/interfaces/main_window.py | 2 + test/data/remote_files.json | 11 --- test/data/remote_files.py | 29 ------- .../ui/test_reflectivity_extraction_global.py | 84 +++++++++++++++++++ 5 files changed, 169 insertions(+), 40 deletions(-) create mode 100644 reflectivity_ui/interfaces/event_handlers/configuration_handler.py delete mode 100644 test/data/remote_files.json delete mode 100644 test/data/remote_files.py create mode 100644 test/ui/test_reflectivity_extraction_global.py diff --git a/reflectivity_ui/interfaces/event_handlers/configuration_handler.py b/reflectivity_ui/interfaces/event_handlers/configuration_handler.py new file mode 100644 index 00000000..4fc3c45c --- /dev/null +++ b/reflectivity_ui/interfaces/event_handlers/configuration_handler.py @@ -0,0 +1,83 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QSpinBox, QCheckBox, QDoubleSpinBox + +from reflectivity_ui.interfaces.configuration import Configuration + + +class ConfigurationHandler: + """ + Handles updating the configuration state upon changes in the UI + """ + + def __init__(self, main_window): + self.main_window = main_window + self.ui = main_window.ui + self.connect_config_events() + + def config_setter_factory(self, config_name: str, is_checkbox: bool): + """ + Generate anonymous functions to serve as callback when any of the global configurations + (`Configuration` class variables) are updated in the UI. + + Each callback will be associated to one configuration parameter. Upon invoked, + the `Configuration` class variable value will be updated. + + Parameters + ---------- + config_name: str + Name of the Configuration variable to update + is_checkbox: bool + True if the widget type is QCheckBox + """ + + def config_setter(value: int | float): + if is_checkbox: + bool_value = value == Qt.CheckState.Checked + setattr(Configuration, config_name, bool_value) + else: + setattr(Configuration, config_name, value) + + return config_setter + + def connect_config_events(self): + widget_names = [ + "final_rebin_checkbox", + "q_rebin_spinbox", + "normalize_to_unity_checkbox", + "normalization_q_cutoff_spinbox", + "global_fit_checkbox", + "polynomial_stitching_degree_spinbox", + "polynomial_stitching_points_spinbox", + "polynomial_stitching_checkbox", + "fanReflectivity", + "sample_size_spinbox", + "bandwidth_spinbox", + ] + config_names = [ + "do_final_rebin", + "final_rebin_step", + "normalize_to_unity", + "total_reflectivity_q_cutoff", + "global_stitching", + "polynomial_stitching_degree", + "polynomial_stitching_points", + "polynomial_stitching", + "use_constant_q", + "sample_size", + "wl_bandwidth", + ] + + for widget_name, config_name in zip(widget_names, config_names): + # get the widget signal to connect + widget = getattr(self.ui, widget_name) + is_checkbox = False + if isinstance(widget, (QSpinBox, QDoubleSpinBox)): + signal_name = "valueChanged" + elif isinstance(widget, QCheckBox): + is_checkbox = True + signal_name = "stateChanged" + else: + raise ValueError(f"{type(widget)} not supported by ConfigurationHandler") + signal = getattr(widget, signal_name) + # connect the signal to the updater + signal.connect(self.config_setter_factory(config_name, is_checkbox)) diff --git a/reflectivity_ui/interfaces/main_window.py b/reflectivity_ui/interfaces/main_window.py index 864b5dab..90e13613 100644 --- a/reflectivity_ui/interfaces/main_window.py +++ b/reflectivity_ui/interfaces/main_window.py @@ -12,6 +12,7 @@ from .smooth_dialog import SmoothDialog import reflectivity_ui from reflectivity_ui.interfaces.data_handling.filepath import FilePath +from reflectivity_ui.interfaces.event_handlers.configuration_handler import ConfigurationHandler 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 @@ -66,6 +67,7 @@ def __init__(self): # Event handlers self.plot_handler = PlotHandler(self) self.file_handler = MainHandler(self) + self.config_handler = ConfigurationHandler(self) self.ui.compare_widget.data_manager = self.data_manager # Initialization for specific instrument diff --git a/test/data/remote_files.json b/test/data/remote_files.json deleted file mode 100644 index ee9cd488..00000000 --- a/test/data/remote_files.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "files": { - "REF_M_24949": "214df921d4fa70ff5a33c4eb6f8284ad", - "REF_M_29160": "58d6698e1d6bf98e0315687cb980d333", - "REF_M_24945_event.nxs": "7ba1bfbe17b865e0eab7f196ecb09cb0", - "REF_M_24949_event.nxs": "214df921d4fa70ff5a33c4eb6f8284ad", - "REF_M_38198.nxs.h5": "8ce4720d860200b72994047db2d2c927", - "REF_M_38199.nxs.h5": "6a1cceb3ed89913d1d68b3d73156ff44" - }, - "address": "http://198.74.56.37/ftp/external-data/MD5" -} diff --git a/test/data/remote_files.py b/test/data/remote_files.py deleted file mode 100644 index e9eee779..00000000 --- a/test/data/remote_files.py +++ /dev/null @@ -1,29 +0,0 @@ -import json -import os -import sys -import urllib.error -import urllib.parse -import urllib.request - - -this_module_path = sys.modules[__name__].__file__ - - -def fetch_remote_files(): - r"""Fetch remote files before any test runs""" - data_dir = os.path.dirname(this_module_path) - remote_info_file = os.path.join(data_dir, "remote_files.json") - remote_info = json.load(open(remote_info_file, "r")) - - remote_address = remote_info["address"] - remote_files = remote_info["files"] - - for basename, md5 in remote_files.items(): - file_path = os.path.join(data_dir, basename) - if not os.path.isfile(file_path): - print("Fetching data file " + basename) - urllib.request.urlretrieve(os.path.join(remote_address, md5), file_path) - - -if __name__ == "__main__": - fetch_remote_files() diff --git a/test/ui/test_reflectivity_extraction_global.py b/test/ui/test_reflectivity_extraction_global.py new file mode 100644 index 00000000..42650672 --- /dev/null +++ b/test/ui/test_reflectivity_extraction_global.py @@ -0,0 +1,84 @@ +# local imports +from reflectivity_ui.interfaces.configuration import Configuration +from reflectivity_ui.interfaces.data_handling.data_set import NexusData, CrossSectionData +from reflectivity_ui.interfaces.main_window import MainWindow + +# third party imports +import pytest + +# standard library imports + + +def _initialize_test_data(main_window): + """Add one run with two cross-sections to the data manager""" + config = Configuration() + nexus_data = NexusData("file/path", config) + off_off = CrossSectionData("Off_Off", config) + on_off = CrossSectionData("On_Off", config) + nexus_data.cross_sections["Off_Off"] = off_off + nexus_data.cross_sections["On_Off"] = on_off + main_window.data_manager._nexus_data = nexus_data + main_window.data_manager.set_channel(0) + main_window.data_manager.add_active_to_reduction() + + +def _assert_configuration_value(main_window, param_name, gold_value): + """Check parameter value through the data hierarchy""" + assert getattr(main_window.data_manager.active_channel.configuration, param_name) is gold_value + for nexus_data in main_window.data_manager.reduction_list: + assert getattr(nexus_data.configuration, param_name) is gold_value + for xs_data in nexus_data.cross_sections.values(): + assert getattr(xs_data.configuration, param_name) is gold_value + + +def _assert_configuration_float_value(main_window, param_name, gold_value): + """Check float parameter value through the data hierarchy""" + assert getattr(main_window.data_manager.active_channel.configuration, param_name) == pytest.approx(gold_value) + for nexus_data in main_window.data_manager.reduction_list: + assert getattr(nexus_data.configuration, param_name) == pytest.approx(gold_value) + for xs_data in nexus_data.cross_sections.values(): + assert getattr(xs_data.configuration, param_name) == pytest.approx(gold_value) + + +@pytest.mark.parametrize( + "widget, config_param", + [ + ("final_rebin_checkbox", "do_final_rebin"), + ("normalize_to_unity_checkbox", "normalize_to_unity"), + ("global_fit_checkbox", "global_stitching"), + ("polynomial_stitching_checkbox", "polynomial_stitching"), + ("fanReflectivity", "use_constant_q"), + ], +) +def test_global_checkboxes(qtbot, widget, config_param): + """Test that UI global config checkbox changes get propagated to all configuration levels""" + main_window = MainWindow() + qtbot.addWidget(main_window) + _initialize_test_data(main_window) + + getattr(main_window.ui, widget).setChecked(True) + _assert_configuration_value(main_window, config_param, True) + + getattr(main_window.ui, widget).setChecked(False) + _assert_configuration_value(main_window, config_param, False) + + +@pytest.mark.parametrize( + "widget, config_param, gold_value", + [ + ("q_rebin_spinbox", "final_rebin_step", 0.01), + ("normalization_q_cutoff_spinbox", "total_reflectivity_q_cutoff", 0.02), + ("polynomial_stitching_degree_spinbox", "polynomial_stitching_degree", 2), + ("polynomial_stitching_points_spinbox", "polynomial_stitching_points", 5), + ("sample_size_spinbox", "sample_size", 15.0), + ("bandwidth_spinbox", "wl_bandwidth", 2.5), + ], +) +def test_global_spinboxes(qtbot, widget, config_param, gold_value): + """Test that UI global config spinbox changes get propagated to all configuration levels""" + main_window = MainWindow() + qtbot.addWidget(main_window) + _initialize_test_data(main_window) + + getattr(main_window.ui, widget).setValue(gold_value) + _assert_configuration_float_value(main_window, config_param, gold_value)