diff --git a/docs/source/interfaces/direct/dns_reduction/DNS Reduction.rst b/docs/source/interfaces/direct/dns_reduction/DNS Reduction.rst index baa9698e519a..8c0c5de1fc3b 100644 --- a/docs/source/interfaces/direct/dns_reduction/DNS Reduction.rst +++ b/docs/source/interfaces/direct/dns_reduction/DNS Reduction.rst @@ -33,7 +33,7 @@ Interface Usage and Description * :ref:`DNS Powder TOF ` * :ref:`DNS Powder Elastic ` -* DNS Single Crystal Elastic +* :ref:`DNS Single Crystal Elastic ` * DNS Simulation Feedback & Comments diff --git a/docs/source/interfaces/direct/dns_reduction/dns_single_crystal_elastic_plotting_tab.rst b/docs/source/interfaces/direct/dns_reduction/dns_single_crystal_elastic_plotting_tab.rst index 0995f369b813..bbb9fa378835 100644 --- a/docs/source/interfaces/direct/dns_reduction/dns_single_crystal_elastic_plotting_tab.rst +++ b/docs/source/interfaces/direct/dns_reduction/dns_single_crystal_elastic_plotting_tab.rst @@ -54,9 +54,9 @@ The buttons below the **Plotting** tab have the following functionality (from le The rest of the buttons have the same functionality as in matplotlib's navigation toolbar. When the mouse cursor is hovered over the plot, the cursor's :math:`(x, y)` coordinates together with the coresponding -:math:`hkl` values will be displayed on the right hand side of matplotlib's control buttons. In addition, the correponding -value of intensity (with an error-bar) of the closest measured data point will be displayed as well. (This does not give the -intensity of the quadrilateral, which could involve interpolation.) +:math:`hkl` values (in relative lattice units, r.l.u.) will be displayed on the right hand side of matplotlib's control buttons. +In addition, the correponding value of intensity (with an error-bar) of the closest measured data point will be displayed as well. +(This does not give the intensity of the quadrilateral, which could involve interpolation.) The **X**, **Y** and **Z** input lines below the navigation buttons allow to manually specify the region to zoom into. The syntax to be used inside these lines is similar to the python's list slicing (or dnsplot's range). For example, **0:2** in the diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_elastic/script_generator/elastic_powder_script_generator_presenter.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_elastic/script_generator/elastic_powder_script_generator_presenter.py index d16e12606fd4..c9b4909186be 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_elastic/script_generator/elastic_powder_script_generator_presenter.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_elastic/script_generator/elastic_powder_script_generator_presenter.py @@ -26,5 +26,5 @@ def get_option_dict(self): self.own_dict["subtract"] = self._plot_list return self.own_dict - def _finish_script_run(self): + def _finish_script_run(self, options=None): self._plot_list = self.model.get_plot_list() diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/dns_gui_main_reduced_menu.ui b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/dns_gui_main_reduced_menu.ui index 84cd009efad3..6a4e1ebb26d2 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/dns_gui_main_reduced_menu.ui +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/dns_gui_main_reduced_menu.ui @@ -30,7 +30,7 @@ 0 0 701 - 25 + 24 @@ -53,6 +53,7 @@ + @@ -98,9 +99,9 @@ Simulation - + - Single Crystal elastic + Single Crystal Elastic @@ -128,9 +129,14 @@ DNS website - + - Single crystal elastic + Single Crystal Elastic + + + + + Single Crystal Elastic diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/dns_modus.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/dns_modus.py index f19c5920d3f9..4b513fbe753b 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/dns_modus.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/dns_modus.py @@ -25,6 +25,13 @@ DNSElasticPowderScriptGeneratorWidget, ) +# single crystal elastic +from mantidqtinterfaces.dns_single_crystal_elastic.options.elastic_single_crystal_options_widget import DNSElasticSCOptionsWidget +from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_widget import DNSElasticSCPlotWidget +from mantidqtinterfaces.dns_single_crystal_elastic.script_generator.elastic_single_crystal_script_generator_widget import ( + DNSElasticSCScriptGeneratorWidget, +) + class DNSModus: """ @@ -48,6 +55,14 @@ def __init__(self, name, parent): "xml_dump", "plot_elastic_powder", ], + "single_crystal_elastic": [ + "paths", + "file_selector", + "elastic_single_crystal_options", + "elastic_single_crystal_script_generator", + "xml_dump", + "plot_elastic_single_crystal", + ], } # Yapf: disable self._widget_map = { @@ -61,6 +76,10 @@ def __init__(self, name, parent): "elastic_powder_options": DNSElasticPowderOptionsWidget, "elastic_powder_script_generator": DNSElasticPowderScriptGeneratorWidget, "plot_elastic_powder": DNSElasticPowderPlotWidget, + # single crystal elastic + "elastic_single_crystal_options": DNSElasticSCOptionsWidget, + "elastic_single_crystal_script_generator": DNSElasticSCScriptGeneratorWidget, + "plot_elastic_single_crystal": DNSElasticSCPlotWidget, } # Yapf: enable diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/file_selector/file_selector_presenter.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/file_selector/file_selector_presenter.py index c275f36f075f..7c2d26a6a32d 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/file_selector/file_selector_presenter.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/file_selector/file_selector_presenter.py @@ -241,8 +241,8 @@ def _format_view(self): self.num_columns = self.model.get_active_model_column_count() self.view.set_first_column_spanned(self.model.get_scan_range()) # expand all only in the case when the total number of files - # to display is less than 151 (more files to expand might take - # significant time to process) + # to display is less than 151 (more files to expand takes + # some time to execute) file_count = self.model.get_number_of_files_in_treeview() if file_count <= 150: self.view.expand_all() diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_presenter.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_presenter.py index c398ac3e6285..bceed0067932 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_presenter.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_presenter.py @@ -23,7 +23,7 @@ def __init__(self, name=None, view=None, parameter_abo=None, modus=None, parent= self.command_line_reader = command_line_reader self._parameter_abo = parameter_abo self.model = self._parameter_abo - self._switch_mode("powder_elastic") + self._switch_mode("single_crystal_elastic") # connect signals self._attach_signal_slots() diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_view.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_view.py index c6acaf2bbe69..98e5573a8854 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_view.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_view.py @@ -45,10 +45,12 @@ def __init__(self, parent=None, app=None, within_mantid=None): self.modus_mapping = { self.ui.actionPowder_Elastic: "powder_elastic", self.ui.actionPowder_TOF: "powder_tof", + self.ui.actionSingle_Crystal_Elastic: "single_crystal_elastic", } self.modus_titles = { "powder_elastic": "DNS Reduction GUI - Powder Elastic", "powder_tof": "DNS Reduction GUI - Powder TOF", + "single_crystal_elastic": "DNS Reduction GUI - Single Crystal Elastic", } for key in self.modus_mapping: key.triggered.connect(self._modus_change) @@ -101,7 +103,7 @@ def _help_button_clicked(self): show_interface_help("direct/dns_reduction/DNS Reduction", QProcess(self)) def add_submenu(self, subview): - for menu in subview.menues: + for menu in subview.menus: submenu = self.menu.insertMenu(self.ui.menuHelp.menuAction(), menu) self.subview_menus.append(submenu) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_widget.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_widget.py index 0a185650fbc4..ed679c003cbf 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_widget.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/main_widget.py @@ -27,7 +27,7 @@ def __init__(self, name=None, parent=None, app=None, within_mantid=None): self.view = DNSReductionGUIView(parent=self, app=app, within_mantid=within_mantid) self.parameter_abo = ParameterAbo() self.model = self.parameter_abo - self.modus = DNSModus(name="powder_elastic", parent=self) + self.modus = DNSModus(name="single_crystal_elastic", parent=self) self.presenter = DNSReductionGUIPresenter( parent=self, view=self.view, modus=self.modus, name=name, parameter_abo=self.parameter_abo ) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/script_generator/common_script_generator_presenter.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/script_generator/common_script_generator_presenter.py index a8b582d49aec..8b760bad70d6 100644 --- a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/script_generator/common_script_generator_presenter.py +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_powder_tof/script_generator/common_script_generator_presenter.py @@ -57,11 +57,11 @@ def _generate_script(self): self.view.show_status_message(error, 30, clear=True) else: self._script_number += 1 - self._finish_script_run() + self._finish_script_run(options) # activate copy script to clipboard button self.view.pB_copy_to_clipboard.setEnabled(True) - def _finish_script_run(self): + def _finish_script_run(self, options): pass def _get_sample_data(self): diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/data_structures/__init__.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/data_structures/__init__.py new file mode 100644 index 000000000000..3a0f4b85f667 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/data_structures/__init__.py @@ -0,0 +1,6 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/data_structures/dns_single_crystal_map.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/data_structures/dns_single_crystal_map.py new file mode 100644 index 000000000000..2d40265ea828 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/data_structures/dns_single_crystal_map.py @@ -0,0 +1,112 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +Class which loads and stores a single DNS datafile in a dictionary. +""" + +import numpy as np +from matplotlib import path +from matplotlib import tri +from mantidqtinterfaces.dns_powder_tof.data_structures.object_dict import ObjectDict +from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_helpers import angle_to_q + + +def _get_mesh(omega, two_theta, z_mesh): + omega_mesh_no_nan, two_theta_mesh_no_nan = np.meshgrid(omega, two_theta) + not_nan_pos = ~np.isnan(z_mesh) + omega_mesh_no_nan = omega_mesh_no_nan[not_nan_pos] + two_theta_mesh_no_nan = two_theta_mesh_no_nan[not_nan_pos] + z_mesh_no_nan = z_mesh[not_nan_pos] + return omega_mesh_no_nan, two_theta_mesh_no_nan, z_mesh_no_nan + + +def _get_unique(omega_mesh, two_theta_mesh): + omega = np.unique(omega_mesh) + two_theta = np.unique(two_theta_mesh) + return omega, two_theta + + +def _get_q_mesh(omega_mesh, two_theta_mesh, wavelength): + q_x, q_y = angle_to_q(two_theta=two_theta_mesh, omega=omega_mesh, wavelength=wavelength) + return q_x, q_y + + +def _get_hkl_mesh(qx_mesh, qy_mesh, dx, dy): + hklx_mesh = qx_mesh * dx / 2.0 / np.pi + hkly_mesh = qy_mesh * dy / 2.0 / np.pi + return hklx_mesh, hkly_mesh + + +class DNSScMap(ObjectDict): + """ + Class for storing data of a single DNS single crystal plot. + """ + + def __init__(self, parameter, two_theta=None, omega=None, z_mesh=None, error_mesh=None): + super().__init__() + # non interpolated data: + omega_mesh, two_theta_mesh, z_mesh = _get_mesh(omega, two_theta, z_mesh) + omega_unique, two_theta_unique = _get_unique(omega_mesh, two_theta_mesh) + qx_mesh, qy_mesh = _get_q_mesh(omega_mesh, two_theta_mesh, parameter["wavelength"]) + hklx_mesh, hkly_mesh = _get_hkl_mesh(qx_mesh, qy_mesh, parameter["dx"], parameter["dy"]) + # setting attributes dictionary keys + self.omega_interpolated = None + self.two_theta = two_theta_unique + self.omega = omega_unique + self.omega_offset = parameter["omega_offset"] + self.hklx_mesh = hklx_mesh + self.hkly_mesh = hkly_mesh + self.z_mesh = z_mesh + self.error_mesh = error_mesh + self.dx = parameter["dx"] + self.dy = parameter["dy"] + self.hkl1 = parameter["hkl1"] + self.hkl2 = parameter["hkl2"] + self.wavelength = parameter["wavelength"] + self.hkl_mesh = [self.hklx_mesh, self.hkly_mesh, self.z_mesh] + + def triangulate(self, mesh_name, switch=False): + plot_x, plot_y, _z = getattr(self, mesh_name) + self.triangulation = tri.Triangulation(plot_x.flatten(), plot_y.flatten()) + return self.triangulation + + def interpolate_triangulation(self, interpolation=0): + if self.triangulation is None: + return None + z = self.z_mesh + triangulator = self.triangulation + return [triangulator, z.flatten()] + + def get_dns_map_border(self, mesh_name): + two_theta = self.two_theta + omega = self.omega + dns_path = np.zeros((2 * two_theta.size + 2 * omega.size, 2)) + hkl_path = np.zeros((2 * two_theta.size + 2 * omega.size, 2)) + dns_path[:, 0] = np.concatenate( + (two_theta[0] * np.ones(omega.size), two_theta, two_theta[-1] * np.ones(omega.size), np.flip(two_theta)) + ) + dns_path[:, 1] = np.concatenate((np.flip(omega), omega[0] * np.ones(two_theta.size), omega, omega[-1] * np.ones(two_theta.size))) + dns_path[:, 0], dns_path[:, 1] = angle_to_q(dns_path[:, 0], dns_path[:, 1], self.wavelength) + if "hkl" in mesh_name: + hkl_path[:, 0] = dns_path[:, 0] * self.dx / 2.0 / np.pi + hkl_path[:, 1] = dns_path[:, 1] * self.dy / 2.0 / np.pi + return path.Path(hkl_path) + return path.Path(dns_path) + + def mask_triangles(self, mesh_name): + x, y, _z = getattr(self, mesh_name) + x = x.flatten() + y = y.flatten() + dns_path = self.get_dns_map_border(mesh_name) + triangles = self.triangulation.triangles + x_y_triangles = np.zeros((len(x[triangles]), 2)) + x_y_triangles[:, 0] = np.mean(x[triangles], axis=1) + x_y_triangles[:, 1] = np.mean(y[triangles], axis=1) + maxi = dns_path.contains_points(x_y_triangles) + self.triangulation.set_mask(np.invert(maxi)) + return self.triangulation diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/__init__.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/__init__.py new file mode 100644 index 000000000000..3a0f4b85f667 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/__init__.py @@ -0,0 +1,6 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/elastic_single_crystal_options_presenter.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/elastic_single_crystal_options_presenter.py new file mode 100644 index 000000000000..a4ad17c718ca --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/elastic_single_crystal_options_presenter.py @@ -0,0 +1,87 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS single crystal elastic options tab presenter of DNS reduction GUI. +""" + +from mantidqtinterfaces.dns_powder_tof.options.common_options_presenter import DNSCommonOptionsPresenter + + +class DNSElasticSCOptionsPresenter(DNSCommonOptionsPresenter): + def __init__(self, name=None, parent=None, view=None, model=None): + super().__init__(parent=parent, name=name, view=view, model=model) + # connect signals + self._attach_signal_slots() + + def _set_auto_two_theta_binning(self): + if self.param_dict["file_selector"]["full_data"]: + sample_data = self.param_dict["file_selector"]["full_data"] + two_theta_params_dict = get_automatic_two_theta_binning(sample_data) + own_options = self.get_option_dict() + for parameter in two_theta_params_dict.keys(): + own_options[parameter] = two_theta_params_dict[parameter] + self.set_view_from_param() + + def _set_auto_omega_binning(self): + if self.param_dict["file_selector"]["full_data"]: + sample_data = self.param_dict["file_selector"]["full_data"] + omega_params_dict = get_automatic_omega_binning(sample_data) + own_options = self.get_option_dict() + for parameter in omega_params_dict.keys(): + own_options[parameter] = omega_params_dict[parameter] + self.set_view_from_param() + + def _get_automatic_binning_state(self): + return self.view._map["automatic_binning"].isChecked() + + def tab_got_focus(self): + auto_binning_is_on = self._get_automatic_binning_state() + if auto_binning_is_on: + self._set_auto_two_theta_binning() + self._set_auto_omega_binning() + + def process_request(self): + own_options = self.get_option_dict() + if own_options["get_wavelength"]: + self._determine_wavelength() + + def get_option_dict(self): + if self.view is not None: + self.own_dict.update(self.view.get_state()) + return self.own_dict + + def _attach_signal_slots(self): + self.view.sig_get_wavelength.connect(self._determine_wavelength) + self.view.sig_auto_binning_clicked.connect(self._set_auto_two_theta_binning) + self.view.sig_auto_binning_clicked.connect(self._set_auto_omega_binning) + + +def get_automatic_two_theta_binning(sample_data): + det_rot = [-x["det_rot"] for x in sample_data] + two_theta_last_det = 115.0 + two_theta_max = max(det_rot) + two_theta_last_det + two_theta_min = min(det_rot) + two_theta_step = 0.5 + number_two_theta_bins = int(round((two_theta_max - two_theta_min) / two_theta_step) + 1) + two_theta_binning_dict = { + "two_theta_min": two_theta_min, + "two_theta_max": two_theta_max, + "two_theta_bin_size": two_theta_step, + "nbins": number_two_theta_bins, + } + return two_theta_binning_dict + + +def get_automatic_omega_binning(sample_data): + omega = [x["sample_rot"] - x["det_rot"] for x in sample_data] + omega_min = min(omega) + omega_max = max(omega) + omega_step = 1.0 + number_omega_bins = int(round((omega_max - omega_min) / omega_step) + 1) + omega_binning_dict = {"omega_min": omega_min, "omega_max": omega_max, "omega_bin_size": omega_step, "omega_nbins": number_omega_bins} + return omega_binning_dict diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/elastic_single_crystal_options_view.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/elastic_single_crystal_options_view.py new file mode 100644 index 000000000000..c660bb8b7d3a --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/elastic_single_crystal_options_view.py @@ -0,0 +1,107 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS single crystal elastic options tab view of DNS reduction GUI. +""" + +from qtpy.QtCore import Signal + +from mantidqt.utils.qt import load_ui + +from mantidqtinterfaces.dns_powder_tof.data_structures.dns_view import DNSView + + +class DNSElasticSCOptionsView(DNSView): + """ + Widget that lets user select reduction options. + """ + + NAME = "Options" + + def __init__(self, parent): + super().__init__(parent) + self._content = load_ui(__file__, "elastic_single_crystal_options.ui", baseinstance=self) + self._map = { + "all_options": self._content, + "wavelength": self._content.dSB_wavelength, + "get_wavelength": self._content.cB_get_wavelength, + "norm_time": self._content.rB_norm_time, + "norm_monitor": self._content.rB_norm_monitor, + "corrections": self._content.gB_corrections, + "det_efficiency": self._content.cB_det_efficiency, + "sum_vana_sf_nsf": self._content.cB_sum_vana_sf_nsf, + "ignore_vana_fields": self._content.cB_ignore_vana_fields, + "flipping_ratio": self._content.cB_flipping_ratio, + "subtract_background_from_sample": self._content.cB_subtract_background_from_sample, + "background_factor": self._content.dSB_background_factor, + "binning": self._content.gB_binning, + "automatic_binning": self._content.cB_automatic_binning, + "two_theta_min": self._content.dSB_two_theta_min, + "two_theta_max": self._content.dSB_two_theta_max, + "two_theta_bin_size": self._content.dSB_two_theta_bin_size, + "omega_min": self._content.dSB_omega_min, + "omega_max": self._content.dSB_omega_max, + "omega_bin_size": self._content.dSB_omega_bin_size, + "lattice_parameters": self._content.gB_lattice_parameter, + "a": self._content.dSB_a, + "b": self._content.dSB_b, + "c": self._content.dSB_c, + "alpha": self._content.dSB_alpha, + "beta": self._content.dSB_beta, + "gamma": self._content.dSB_gamma, + "orientation": self._content.gB_orientation, + "hkl1": self._content.lE_hkl1, + "hkl2": self._content.lE_hkl2, + "omega_offset": self._content.dSB_omega_offset, + "use_dx_dy": self._content.cB_use_dx_dy, + "dx": self._content.dSB_dx, + "dy": self._content.dSB_dy, + } + + self._map["use_dx_dy"].setChecked(True) + self._map["dx"].setValue(3.672) + self._map["dy"].setValue(6.539) + self._map["corrections"].setChecked(False) + self._map["all_options"].setEnabled(False) + # connect signals + self._attach_signal_slots() + + # signals + sig_get_wavelength = Signal() + sig_two_theta_max_changed = Signal() + sig_two_theta_min_changed = Signal() + sig_omega_max_changed = Signal() + sig_omega_min_changed = Signal() + sig_auto_binning_clicked = Signal(int) + + def deactivate_get_wavelength(self): + self._map["get_wavelength"].setCheckState(0) + + def _get_wavelength(self, state): + if state: + self.sig_get_wavelength.emit() + + def _two_theta_min_changed(self): + self.sig_two_theta_min_changed.emit() + + def _two_theta_max_changed(self): + self.sig_two_theta_max_changed.emit() + + def _omega_min_changed(self): + self.sig_omega_min_changed.emit() + + def _omega_max_changed(self): + self.sig_omega_max_changed.emit() + + def _attach_signal_slots(self): + self._map["wavelength"].valueChanged.connect(self.deactivate_get_wavelength) + self._map["get_wavelength"].stateChanged.connect(self._get_wavelength) + self._map["two_theta_min"].valueChanged.connect(self._two_theta_min_changed) + self._map["two_theta_max"].valueChanged.connect(self._two_theta_max_changed) + self._map["omega_min"].valueChanged.connect(self._omega_min_changed) + self._map["omega_max"].valueChanged.connect(self._omega_max_changed) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/elastic_single_crystal_options_widget.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/elastic_single_crystal_options_widget.py new file mode 100644 index 000000000000..c3ed54c89977 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/options/elastic_single_crystal_options_widget.py @@ -0,0 +1,23 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS single crystal elastic options widget of DNS reduction GUI. +""" + +from mantidqtinterfaces.dns_powder_tof.data_structures.dns_widget import DNSWidget +from mantidqtinterfaces.dns_powder_tof.options.common_options_model import DNSCommonOptionsModel +from mantidqtinterfaces.dns_single_crystal_elastic.options.elastic_single_crystal_options_presenter import DNSElasticSCOptionsPresenter +from mantidqtinterfaces.dns_single_crystal_elastic.options.elastic_single_crystal_options_view import DNSElasticSCOptionsView + + +class DNSElasticSCOptionsWidget(DNSWidget): + def __init__(self, name, parent): + super().__init__(name, parent) + self.view = DNSElasticSCOptionsView(parent=parent.view) + self.model = DNSCommonOptionsModel(parent=self) + self.presenter = DNSElasticSCOptionsPresenter(parent=self, view=self.view, model=self.model, name=name) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/__init__.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/__init__.py new file mode 100644 index 000000000000..3a0f4b85f667 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/__init__.py @@ -0,0 +1,6 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_helpers.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_helpers.py new file mode 100644 index 000000000000..7eff3b63f465 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_helpers.py @@ -0,0 +1,21 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +Helper functions for DNS single crystal elastic calculations. +""" + +from numpy import cos, pi, radians, sin + + +def angle_to_q(two_theta, omega, wavelength): # should work with np arrays + tt = radians(two_theta) + w = radians(omega) + two_pi_over_lambda = 2.0 * pi / wavelength + qx = (cos(-w) - cos(-w + tt)) * two_pi_over_lambda + qy = (sin(-w) - sin(-w + tt)) * two_pi_over_lambda + return qx, qy diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_datalist.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_datalist.py new file mode 100644 index 000000000000..fafbceef9f32 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_datalist.py @@ -0,0 +1,75 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +Sub widget for DNS single crystal elastic plot view handling of the list of plot files. +""" + +from qtpy.QtCore import QObject +from qtpy.QtCore import Signal +from mantidqtinterfaces.dns_powder_elastic.data_structures.dns_plot_list import DNSPlotListModel + + +class DNSDatalist(QObject): + """ + Class for manipulations with workspaces available for user selection + and subsequent visualization. + """ + + def __init__(self, parent, view): + super().__init__(parent) + self._view = view + self._parent = parent + self._model = DNSPlotListModel(view) + self._old_item = None + self._model.itemChanged.connect(self._item_clicked) + + sig_datalist_changed = Signal() + + def up(self): + # up arrow click + self._model.itemChanged.disconnect() + self._model.up() + self._model.itemChanged.connect(self._item_clicked) + self.sig_datalist_changed.emit() + + def down(self): + # down arrow click + self._model.itemChanged.disconnect() + self._model.down() + self._model.itemChanged.connect(self._item_clicked) + self.sig_datalist_changed.emit() + + def check_first(self): + # check first available workspace + self._model.itemChanged.disconnect() + self._parent.draw() + self._parent.app.processEvents() + self._model.check_first() + self._model.itemChanged.connect(self._item_clicked) + self.sig_datalist_changed.emit() + + def get_checked_plots(self): + return self._model.get_checked_item_names() + + def get_datalist(self): + return self._model.get_names() + + def _item_clicked(self, item): + self._model.itemChanged.disconnect() + self._model.uncheck_items() + item.setCheckState(2) + self._model.itemChanged.connect(self._item_clicked) + if not self._old_item == item: # prevents double triggering + self.sig_datalist_changed.emit() + self._old_item = item + + def set_datalist(self, datalist): + self._model.itemChanged.disconnect() + self._model.set_items(datalist) + self._view.setModel(self._model) + self._model.itemChanged.connect(self._item_clicked) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_menu.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_menu.py new file mode 100644 index 000000000000..16330bbb7705 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_menu.py @@ -0,0 +1,133 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS single crystal elastic plot tab menu of DNS reduction GUI. +""" + +from mantidqt import icons +from qtpy.QtCore import Signal +from qtpy.QtGui import QPixmap, QIcon +from qtpy.QtWidgets import QActionGroup, QMenu +from mantidqt.widgets.plotconfigdialog.imagestabwidget.view import create_colormap_img +from mantid.plots.utility import get_colormap_names + + +def set_mdi_icons(mapping): + mapping["down"].setIcon(icons.get_icon("mdi.arrow-down")) + mapping["up"].setIcon(icons.get_icon("mdi.arrow-up")) + mapping["grid"].setIcon(icons.get_icon("mdi.grid")) + mapping["linestyle"].setIcon(icons.get_icon("mdi.ray-vertex")) + mapping["crystal_axes"].setIcon(icons.get_icon("mdi.axis-arrow")) + mapping["projections"].setIcon(icons.get_icon("mdi.chart-bell-curve")) + mapping["invert_cb"].setIcon(icons.get_icon("mdi.invert-colors")) + mapping["save_data"].setIcon(icons.get_icon("mdi.database-export")) + + +def set_up_colormap_selector(mapping): + colormap_names = get_colormap_names() + for cmap_name in colormap_names: + qt_img = create_colormap_img(cmap_name) + pixmap = QPixmap.fromImage(qt_img) + mapping["colormap"].addItem(QIcon(pixmap), cmap_name) + mapping["colormap"].setCurrentIndex(colormap_names.index("jet")) + + +class DNSElasticSCPlotOptionsMenu(QMenu): + def __init__(self, parent): + super().__init__("Plot Options") + # adding action + action_omega_offset = self.addAction("Change \u03C9 Offset") + action_dx_dy = self.addAction("Change d-spacings") + + # connections + action_omega_offset.triggered.connect(parent.change_omega_offset) + action_dx_dy.triggered.connect(parent.change_dxdy) + + +class DNSElasticSCPlotViewMenu(QMenu): + def __init__(self): + super().__init__("Plot View") + # adding actions + self._menu_plot_type = PlotTypeMenu(self) + self.addMenu(self._menu_plot_type) + self._menu_axes = AxesMenu(self) + self.addMenu(self._menu_axes) + self._menu_interpolation = InterpolationMenu(self) + self.addMenu(self._menu_interpolation) + self.menus = [self._menu_plot_type, self._menu_axes, self._menu_interpolation] + + sig_replot = Signal(str) + + def get_value(self): + plot_type = self._menu_plot_type.get_value() + axis_type = self._menu_axes.get_value() + axis_type["interpolate"] = self._menu_interpolation.get_value() + axis_type["plot_type"] = plot_type + return axis_type + + +class PlotTypeMenu(QMenu): + def __init__(self, parent): + super().__init__("Plot Type") + self.parent = parent + # adding action + action_triangulation_mesh = self.addAction("Triangulation") + action_triangulation_mesh.setCheckable(True) + action_triangulation_mesh.setChecked(True) + # action group + p_tag = QActionGroup(self) + p_tag.addAction(action_triangulation_mesh) + self.p_tag = p_tag + self.action_triangulation_mesh = action_triangulation_mesh + + def get_value(self): + index = self.p_tag.actions().index(self.p_tag.checkedAction()) + plot_type_list = {0: "triangulation"} + return plot_type_list[index] + + +class AxesMenu(QMenu): + def __init__(self, parent): + super().__init__("Axes") + self.parent = parent + action_hkl = self.addAction("(n_x, n_y)") + self.addSeparator() + # setting checkable and standard option checked + action_hkl.setCheckable(True) + action_hkl.setChecked(True) + # action group + qag = QActionGroup(self) + qag.addAction(action_hkl) + qag.setExclusive(True) + # connect Signals + self.qag = qag + + def get_value(self): + axis_list = {0: "hkl"} + index = self.qag.actions().index(self.qag.checkedAction()) + axis_type = axis_list[index] + return {"type": axis_type, "switch": False, "fix_aspect": False} + + +class InterpolationMenu(QMenu): + def __init__(self, parent): + super().__init__("Interpolation") + self.parent = parent + # adding actions + action_interpolation_off = self.addAction("Off") + # setting checkable and check standard option + action_interpolation_off.setCheckable(True) + action_interpolation_off.setChecked(True) + # action group + i_pag = QActionGroup(self) + i_pag.addAction(action_interpolation_off) + i_pag.setExclusive(True) + self.i_pag = i_pag + + def get_value(self): + return self.i_pag.actions().index(self.i_pag.checkedAction()) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model.py new file mode 100644 index 000000000000..4843eb798098 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_model.py @@ -0,0 +1,73 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS single crystal elastic plot tab model of DNS reduction GUI. +""" + +from mantidqtinterfaces.dns_powder_tof.data_structures.dns_obs_model import DNSObsModel +from mantidqtinterfaces.dns_single_crystal_elastic.data_structures.dns_single_crystal_map import DNSScMap +from mantidqtinterfaces.dns_powder_tof.data_structures.object_dict import ObjectDict + + +class DNSElasticSCPlotModel(DNSObsModel): + """ + Model for DNS plot calculations. + """ + + def __init__(self, parent): + super().__init__(parent) + self._single_crystal_map = None + + self._data = ObjectDict() + self._data.x = None + self._data.y = None + self._data.z = None + self._data.z_min = None + self._data.z_max = None + self._data.pz_min = None + self._data.triang = None + self._data.z_triang = None + + def create_single_crystal_map(self, data_array, options, initial_values=None): + two_theta = data_array["two_theta_array"] + omega = data_array["omega_array"] + z_mesh = data_array["intensity"] + error = data_array["error"] + parameter = { + "wavelength": options["wavelength"], + "dx": options["dx"], + "dy": options["dy"], + "hkl1": options["hkl1"], + "hkl2": options["hkl2"], + "omega_offset": options["omega_offset"], + } + if initial_values is not None: + parameter.update(initial_values) + self._single_crystal_map = DNSScMap(parameter=parameter, two_theta=two_theta, omega=omega, z_mesh=z_mesh, error_mesh=error) + return self._single_crystal_map + + def get_interpolated_triangulation(self, interpolate, axis_type, switch): + mesh_name = axis_type + "_mesh" + self._single_crystal_map.triangulate(mesh_name=mesh_name, switch=switch) + self._single_crystal_map.mask_triangles(mesh_name=mesh_name) + triangulator_refiner, z_refiner = self._single_crystal_map.interpolate_triangulation(interpolate) + self._data.triang = triangulator_refiner + self._data.z_triang = z_refiner + # this is important to get the limits + x, y, z = getattr(self._single_crystal_map, mesh_name) + self._data.x = x + self._data.y = y + self._data.z = z + return triangulator_refiner, z_refiner + + def get_axis_labels(self, axis_type, crystal_axes, switch=False): + hkl1 = self._single_crystal_map.hkl1 + hkl2 = self._single_crystal_map.hkl2 + axis_labels = {"hkl": [f"[{hkl1}] (r.l.u.)", f"[{hkl2}] (r.l.u.)"]} + labels = axis_labels[axis_type] + return labels diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot.py new file mode 100644 index 000000000000..2edf0795cbd7 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_plot.py @@ -0,0 +1,62 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS single crystal elastic plot tab of DNS reduction GUI. +""" + +import matplotlib +from mpl_toolkits.axisartist import Subplot + + +class DNSScPlot: + def __init__(self, parent, figure, grid_helper): + super().__init__() + self._fig = figure + self._fig.clf() + ax1 = Subplot(self._fig, 1, 1, 1, grid_helper=grid_helper) + self._ax = self._fig.add_subplot(ax1) + self._ax.set_visible(False) + self._parent = parent + self._fig = figure + self._plot = None + self._colorbar = None + self._ax_hist = [None, None] + + def on_resize(self, _dummy=None): # connected to canvas resize + self._fig.tight_layout(pad=0.3) + + @staticmethod + def set_fontsize(fontsize): + matplotlib.rcParams.update({"font.size": fontsize}) + + def create_colorbar(self): + self._colorbar = self._fig.colorbar(self._plot, ax=self._ax, extend="max", pad=0.01) + + def set_norm(self, norm): + self._plot.set_norm(norm) + + def set_shading(self, shading): + if self._plot is not None: + self._plot.set_shading(shading) + + def set_zlim(self, zlim): + if self._plot is not None: + self._plot.set_clim(zlim[0], zlim[1]) + self._colorbar.mappable.set_clim(vmin=zlim[0], vmax=zlim[1]) + + def set_cmap(self, cmap): + if self._plot is not None: + self._plot.set_cmap(cmap) + + def set_axis_labels(self, x_label, y_label): + self._ax.set_xlabel(x_label) + self._ax.set_ylabel(y_label) + + def plot_triangulation(self, triang, z, cmap, edge_colors, shading): + self._ax.set_visible(True) + self._plot = self._ax.tripcolor(triang, z, cmap=cmap, edgecolors=edge_colors, shading=shading) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter.py new file mode 100644 index 000000000000..a7fc9e4a49b2 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_presenter.py @@ -0,0 +1,93 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS single crystal elastic plot tab presenter of DNS reduction GUI. +""" + +from mantidqtinterfaces.dns_powder_tof.data_structures.dns_observer import DNSObserver +from mantidqtinterfaces.dns_powder_tof.data_structures.object_dict import ObjectDict +from mantidqtinterfaces.dns_single_crystal_elastic.plot import mpl_helpers + + +class DNSElasticSCPlotPresenter(DNSObserver): + def __init__(self, name=None, parent=None, view=None, model=None): + super().__init__(parent=parent, name=name, view=view, model=model) + # connect signals + self._attach_signal_slots() + # plot parameter + self._plot_param = ObjectDict() + self._plot_param.grid_state = 0 + self._plot_param.grid_helper = None + self._plot_param.colormap_name = "jet" + self._plot_param.lines = 0 + + def _datalist_updated(self, workspaces): + compare = self.view.datalist.get_datalist() + return ( + self.param_dict["elastic_single_crystal_script_generator"]["script_number"] != self._plotted_script_number + or workspaces != compare + ) # check is necessary for simulation + + def _plot(self, initial_values=None): + axis_type = self.view.get_axis_type() + plot_list = self.view.datalist.get_checked_plots() + if not plot_list: + return + self._plot_param.plot_name = plot_list[0] + generated_dict = self.param_dict["elastic_single_crystal_script_generator"] + data_array = generated_dict["data_arrays"][self._plot_param.plot_name] + options = self.param_dict["elastic_single_crystal_options"] + self.model.create_single_crystal_map(data_array, options, initial_values) + self.view.create_subfigure(self._plot_param.grid_helper) + self._want_plot(axis_type["plot_type"]) + self._set_axis_labels() + self.view.single_crystal_plot.create_colorbar() + self.view.single_crystal_plot.on_resize() + self.view.canvas.figure.tight_layout() + self.view.draw() + + def tab_got_focus(self): + workspaces = sorted(self.param_dict["elastic_single_crystal_script_generator"]["plot_list"]) + if self._datalist_updated(workspaces): + self.view.datalist.set_datalist(workspaces) + self._plotted_script_number = self.param_dict["elastic_single_crystal_script_generator"]["script_number"] + self.view.process_events() + self.view.datalist.check_first() + + def _plot_triangulation(self, interpolate, axis_type, switch): + color_map, edge_colors, shading = self._get_plot_styles() + triangulation, z = self.model.get_interpolated_triangulation(interpolate, axis_type, switch) + self.view.single_crystal_plot.plot_triangulation(triangulation, z, color_map, edge_colors, shading) + + def _set_colormap(self): + cmap = self._get_plot_styles()[0] + self.view.single_crystal_plot.set_cmap(cmap) + self.view.draw() + + def _want_plot(self, plot_type): + axis_type = self.view.get_axis_type() + self._plot_triangulation(axis_type["interpolate"], axis_type["type"], axis_type["switch"]) + + def _get_plot_styles(self): + own_dict = self.view.get_state() + shading = "flat" + edge_colors = ["face", "white", "black"][self._plot_param.lines] + colormap_name = own_dict["colormap"] + cmap = mpl_helpers.get_cmap(colormap_name) + return cmap, edge_colors, shading + + def _set_axis_labels(self): + axis_type = self.view.get_axis_type() + own_dict = self.view.get_state() + x_label, y_label = self.model.get_axis_labels(axis_type["type"], own_dict["crystal_axes"]) + self.view.single_crystal_plot.set_axis_labels(x_label, y_label) + + def _attach_signal_slots(self): + self.view.sig_plot.connect(self._plot) + self.view.sig_change_colormap.connect(self._set_colormap) + self._plotted_script_number = 0 diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_view.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_view.py new file mode 100644 index 000000000000..4f9c2b1d0a05 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_view.py @@ -0,0 +1,101 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS single crystal elastic plot tab view of DNS reduction GUI. +""" + +from mantidqt.utils.qt import load_ui +from matplotlib.backends.backend_qt5agg import FigureCanvas +from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar +from matplotlib.figure import Figure +from qtpy.QtCore import Signal +from qtpy.QtWidgets import QSizePolicy +from mantidqtinterfaces.dns_powder_tof.data_structures.dns_view import DNSView +from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_menu import ( + DNSElasticSCPlotViewMenu, + set_mdi_icons, + set_up_colormap_selector, +) +from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_plot import DNSScPlot +from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_datalist import DNSDatalist + + +class DNSElasticSCPlotView(DNSView): + """ + DNS Widget to plot elastic single crystal data + """ + + NAME = "Plotting" + + def __init__(self, parent): + super().__init__(parent) + _content = load_ui(__file__, "elastic_single_crystal_plot.ui", baseinstance=self) + + self._map = { + "datalist": _content.lV_datalist, + "down": _content.tB_down, + "up": _content.tB_up, + "grid": _content.tB_grid, + "linestyle": _content.tB_linestyle, + "crystal_axes": _content.tB_crystal_axes, + "colormap": _content.combB_colormap, + "projections": _content.tB_projections, + "invert_cb": _content.tB_invert_cb, + "save_data": _content.tB_save_data, + "fontsize": _content.sB_fontsize, + } + # change tool button icons to mdi icons + set_mdi_icons(self._map) + # colormap selector + set_up_colormap_selector(self._map) + + # datalist + self.datalist = DNSDatalist(self, self._map["datalist"]) + self._map["down"].clicked.connect(self.datalist.down) + self._map["up"].clicked.connect(self.datalist.up) + self.datalist.sig_datalist_changed.connect(self._plot) + + # connecting signals + self._attach_signal_slots() + + # setting up custom menu for single crystal plot options and views + self.views_menu = DNSElasticSCPlotViewMenu() + self.menus = [] + self.menus.append(self.views_menu) + self.views_menu.sig_replot.connect(self._plot) + canvas = FigureCanvas(Figure()) + canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + toolbar = NavigationToolbar(canvas, self) + _content.plot_head_layout.insertWidget(5, toolbar) + _content.plot_layout.insertWidget(2, canvas) + self.canvas = canvas + self.single_crystal_plot = DNSScPlot(self, self.canvas.figure, None) + self.initial_values = None + + # Signals + sig_plot = Signal() + sig_change_colormap = Signal() + + def _change_colormap(self): + self.sig_change_colormap.emit() + + def _plot(self): + self.sig_plot.emit() + + # gui options + def create_subfigure(self, grid_helper=None): + self.single_crystal_plot = DNSScPlot(self, self.canvas.figure, grid_helper) + + def get_axis_type(self): + return self.views_menu.get_value() + + def draw(self): + self.canvas.draw() + + def _attach_signal_slots(self): + self._map["colormap"].currentIndexChanged.connect(self._change_colormap) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_widget.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_widget.py new file mode 100644 index 000000000000..b118681759b6 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/elastic_single_crystal_plot_widget.py @@ -0,0 +1,23 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS single crystal elastic plot tab widget of DNS reduction GUI. +""" + +from mantidqtinterfaces.dns_powder_tof.data_structures.dns_widget import DNSWidget +from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_model import DNSElasticSCPlotModel +from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_presenter import DNSElasticSCPlotPresenter +from mantidqtinterfaces.dns_single_crystal_elastic.plot.elastic_single_crystal_plot_view import DNSElasticSCPlotView + + +class DNSElasticSCPlotWidget(DNSWidget): + def __init__(self, name, parent): + super().__init__(name, parent) + self.view = DNSElasticSCPlotView(parent=parent.view) + self.model = DNSElasticSCPlotModel(parent=self) + self.presenter = DNSElasticSCPlotPresenter(parent=self, view=self.view, model=self.model, name=name) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/mpl_helpers.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/mpl_helpers.py new file mode 100644 index 000000000000..d0306478d7ac --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/plot/mpl_helpers.py @@ -0,0 +1,12 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +import matplotlib as mpl + + +def get_cmap(colormap_name): + return mpl.colormaps[colormap_name] diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/script_generator/elastic_single_crystal_script_generator_model.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/script_generator/elastic_single_crystal_script_generator_model.py new file mode 100644 index 000000000000..dc6ef08526f1 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/script_generator/elastic_single_crystal_script_generator_model.py @@ -0,0 +1,202 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS script generator model for elastic single crystal data. +""" + +from mantidqtinterfaces.dns_powder_elastic.data_structures.dns_elastic_powder_dataset import DNSElasticDataset +from mantidqtinterfaces.dns_powder_tof.helpers.list_range_converters import get_normalisation +from mantidqtinterfaces.dns_powder_tof.script_generator.common_script_generator_model import DNSScriptGeneratorModel +import numpy as np +from mantid.simpleapi import mtd + +INDENT_SPACE = 4 * " " + + +class DNSElasticSCScriptGeneratorModel(DNSScriptGeneratorModel): + def __init__(self, parent): + super().__init__(parent) + self._data_arrays = {} + self._script = [] + self._plot_list = [] + self._sample_data = None + self._standard_data = None + self._loop = None + self._spacing = None + self._export_path = None + self._ascii = None + self._nexus = None + self._norm = None + + def script_maker(self, options, paths, file_selector=None): + self._script = [] + # shortcuts for options + self._vana_correction = options["corrections"] and options["det_efficiency"] + self._nicr_correction = options["corrections"] and options["flipping_ratio"] + self._sample_background_correction = options["corrections"] and options["subtract_background_from_sample"] + self._background_factor = options["background_factor"] + self._ignore_vana = str(options["ignore_vana_fields"]) + self._sum_sf_nsf = str(options["sum_vana_sf_nsf"]) + self._corrections = self._sample_background_correction or self._vana_correction or self._nicr_correction + self._export_path = paths["export_dir"] + self._ascii = paths["ascii"] and paths["export"] and bool(self._export_path) + self._nexus = paths["nexus"] and paths["export"] and bool(self._export_path) + self._norm = get_normalisation(options) + self._setup_sample_data(paths, file_selector) + self._set_loop() + # validate if input makes sense, otherwise return + # an empty script and error message + error = self._check_errors_in_selected_files(options) + if error: + self._script = [""] + error_message = error + else: + error_message = "" + # starting writing script + self._add_lines_to_script(self._get_header_lines()) + self._add_lines_to_script(self._get_sample_data_lines()) + self._add_lines_to_script(self._get_param_lines(options)) + self._add_lines_to_script(self._get_binning_lines(options)) + self._add_lines_to_script(self._get_load_data_lines()) + save_string = self._get_save_string() + self._add_lines_to_script(self._get_convert_to_matrix_lines(save_string)) + return self._script, error_message + + def _setup_sample_data(self, paths, file_selector): + self._sample_data = DNSElasticDataset(data=file_selector["full_data"], path=paths["data_dir"], is_sample=True) + self._plot_list = self._sample_data.create_subtract() + + def _set_loop(self): + if len(self._sample_data.keys()) == 1: + self._loop = f"for workspace in wss_sample['{list(self._sample_data.keys())[0]}']:" + self._spacing = "\n" + INDENT_SPACE + else: + self._loop = f"for sample, workspacelist in wss_sample.items():\n{INDENT_SPACE}for workspace in workspacelist:" + self._spacing = "\n" + 2 * INDENT_SPACE + + @staticmethod + def _get_header_lines(): + lines = [ + "from mantidqtinterfaces.dns_single_crystal_elastic.scripts.md_single_crystal_elastic import load_all", + "from mantid.simpleapi import ConvertMDHistoToMatrixWorkspace, mtd, SaveAscii, SaveNexus", + "", + ] + return lines + + def _get_sample_data_lines(self): + return [f"sample_data = {self._sample_data.format_dataset()}"] + + def _get_param_lines(self, options): + return [ + "", + f"params = {{'a': {options['a']}, " + f"\n 'b': {options['b']}," + f"\n 'c': {options['c']}," + f"\n 'alpha': {options['alpha']}," + f"\n 'beta': {options['beta']}," + f"\n 'gamma': {options['gamma']}," + f"\n 'hkl1': '{options['hkl1']}'," + f"\n 'hkl2': '{options['hkl2']}'," + f"\n 'omega_offset': {options['omega_offset']}," + f"\n 'norm_to': '{self._norm}'," + f"\n 'dx': '{options['dx']}'," + f"\n 'dy': '{options['dy']}'," + "}", + "", + ] + + def _get_binning_lines(self, options): + two_theta_bin_edge_min = options["two_theta_min"] - options["two_theta_bin_size"] / 2 + two_theta_bin_edge_max = options["two_theta_max"] + options["two_theta_bin_size"] / 2 + two_theta_num_bins = int(round((two_theta_bin_edge_max - two_theta_bin_edge_min) / options["two_theta_bin_size"])) + two_theta_binning = [two_theta_bin_edge_min, two_theta_bin_edge_max, two_theta_num_bins] + omega_bin_edge_min = options["omega_min"] - options["omega_bin_size"] / 2 + omega_bin_edge_max = options["omega_max"] + options["omega_bin_size"] / 2 + omega_num_bins = int(round((omega_bin_edge_max - omega_bin_edge_min) / options["omega_bin_size"])) + omega_binning = [omega_bin_edge_min, omega_bin_edge_max, omega_num_bins] + lines = [ + f"binning = {{'two_theta_binning': {two_theta_binning},\n" + f" 'omega_binning': {omega_binning}}} # min, max, number_of_bins" + ] + return lines + + def _get_load_data_lines(self): + lines = ["wss_sample = load_all(sample_data, binning, params)"] + if self._corrections: + lines += ["wss_standard = load_all(standard_data, binning, params, standard=True)"] + lines += [""] + return lines + + def _get_ascii_save_string(self): + if self._ascii: + return ( + f"{self._spacing}SaveAscii('mat_{{}}'.format(workspace), " + f"'{self._export_path}/{{}}.csv'.format(workspace), WriteSpectrumID=False)" + ) + return "" + + def _get_nexus_save_string(self): + if self._nexus: + return f"{self._spacing}SaveNexus('mat_{{}}'.format(workspace), '{self._export_path}/{{}}.nxs'.format(workspace))" + return "" + + def _get_save_string(self): + ascii_string = self._get_ascii_save_string() + nexus_string = self._get_nexus_save_string() + save_strings = [x for x in [ascii_string, nexus_string] if x] + return "".join(save_strings) + + def _get_convert_to_matrix_lines(self, save_string): + lines = [ + "", + "# convert the output sample MDHistoWorkspaces to MatrixWorkspaces", + "# and save the data into the directory specified for Export", + f"{self._loop}{self._spacing}ConvertMDHistoToMatrixWorkspace(workspace, Outputworkspace='mat_{{}}'.format(workspace), " + f"Normalization='NoNormalization'){save_string}", + "", + ] + if not save_string: + lines.remove("# and save the data into the directory specified for Export") + return lines + + def get_plot_list(self, options): + for plot in self._plot_list: + self._data_arrays[plot] = { + "two_theta_array": self._get_sample_data_two_theta_binning_array(options), + "omega_array": self._get_sample_data_omega_binning_array(options), + "intensity": mtd[plot].getSignalArray(), + "error": np.sqrt(mtd[plot].getErrorSquaredArray()), + } + return self._plot_list, self._data_arrays + + def _get_sample_data_omega_binning_array(self, options): + min = round(options["omega_min"], 1) + max = round(options["omega_max"], 1) + bin_size = options["omega_bin_size"] + binning_array = np.arange(min, max + bin_size, bin_size) + return binning_array + + def _get_sample_data_two_theta_binning_array(self, options): + min = round(options["two_theta_min"], 1) + max = round(options["two_theta_max"], 1) + bin_size = options["two_theta_bin_size"] + binning_array = np.arange(min, max + bin_size, bin_size) + return binning_array + + def _check_errors_in_selected_files(self, options): + """ + If any inconsistencies are found in the files selected for + data reduction, then this method will return a string with + the corresponding error_message. If no inconsistencies are + found then the empty string will be returned. + """ + keys_to_check = ["wavelength", "dx", "dy", "hkl1", "hkl2", "omega_offset"] + for key in keys_to_check: + if options[key] == "" or options[key] == [] or options[key] is None: + return f"Missing input for {key}." + return "" diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/script_generator/elastic_single_crystal_script_generator_presenter.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/script_generator/elastic_single_crystal_script_generator_presenter.py new file mode 100644 index 000000000000..d8e17e91e38f --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/script_generator/elastic_single_crystal_script_generator_presenter.py @@ -0,0 +1,32 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS script generator for elastic single crystal data. +""" + +from mantidqtinterfaces.dns_powder_tof.script_generator.common_script_generator_presenter import DNSScriptGeneratorPresenter + + +class DNSElasticSCScriptGeneratorPresenter(DNSScriptGeneratorPresenter): + def __init__(self, name=None, parent=None, view=None, model=None): + super().__init__(parent=parent, name=name, view=view, model=model) + self._plot_list = [] + self._data_arrays = {} + + def _finish_script_run(self, options): + self._plot_list, self._data_arrays = self.model.get_plot_list(options) + + def get_option_dict(self): + if self.view is not None: + self.own_dict.update(self.view.get_state()) + self.own_dict["script_path"] = self._script_path + self.own_dict["script_number"] = self._script_number + self.own_dict["script_text"] = self._script_text + self.own_dict["plot_list"] = self._plot_list + self.own_dict["data_arrays"] = self._data_arrays + return self.own_dict diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/script_generator/elastic_single_crystal_script_generator_widget.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/script_generator/elastic_single_crystal_script_generator_widget.py new file mode 100644 index 000000000000..09627681d286 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/script_generator/elastic_single_crystal_script_generator_widget.py @@ -0,0 +1,27 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS script generator widget for elastic single crystal data. +""" + +from mantidqtinterfaces.dns_powder_tof.data_structures.dns_widget import DNSWidget +from mantidqtinterfaces.dns_powder_tof.script_generator.common_script_generator_view import DNSScriptGeneratorView +from mantidqtinterfaces.dns_single_crystal_elastic.script_generator.elastic_single_crystal_script_generator_model import ( + DNSElasticSCScriptGeneratorModel, +) +from mantidqtinterfaces.dns_single_crystal_elastic.script_generator.elastic_single_crystal_script_generator_presenter import ( + DNSElasticSCScriptGeneratorPresenter, +) + + +class DNSElasticSCScriptGeneratorWidget(DNSWidget): + def __init__(self, name, parent): + super().__init__(name, parent) + self.view = DNSScriptGeneratorView(parent=parent.view) + self.model = DNSElasticSCScriptGeneratorModel(parent=self) + self.presenter = DNSElasticSCScriptGeneratorPresenter(parent=self, view=self.view, model=self.model, name=name) diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/scripts/__init__.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/scripts/__init__.py new file mode 100644 index 000000000000..3a0f4b85f667 --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/scripts/__init__.py @@ -0,0 +1,6 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + diff --git a/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/scripts/md_single_crystal_elastic.py b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/scripts/md_single_crystal_elastic.py new file mode 100644 index 000000000000..412c3f503b9f --- /dev/null +++ b/qt/python/mantidqtinterfaces/mantidqtinterfaces/dns_single_crystal_elastic/scripts/md_single_crystal_elastic.py @@ -0,0 +1,63 @@ +# Mantid Repository : https://github.com/mantidproject/mantid +# +# Copyright © 2023 ISIS Rutherford Appleton Laboratory UKRI, +# NScD Oak Ridge National Laboratory, European Spallation Source +# & Institut Laue - Langevin +# SPDX - License - Identifier: GPL - 3.0 + + +""" +DNS script helpers for reduction of single crystal elastic data. +""" + +from mantid.simpleapi import BinMD, LoadDNSSCD, mtd + + +def load_all(data_dict, binning, params, standard=False): + """ + Loading of multiple DNS files given in a dictionary to workspaces. + """ + workspace_names = {} + for sample_name, fields in data_dict.items(): + workspace_names[sample_name] = [] + path = data_dict[sample_name]["path"] + for field_name, file_numbers in fields.items(): + if field_name != "path": + workspace_name = "_".join((sample_name, field_name)) + workspace_names[sample_name].append(workspace_name) + load_binned(workspace_name, binning, params, path, file_numbers, standard) + return workspace_names + + +def load_binned(workspace_name, binning, params, path, file_numbers, standard): + """ + Loading of multiple DNS datafiles into a single workspace. + """ + ad0 = f"Theta,{binning['two_theta_binning'][0] / 2.0},{binning['two_theta_binning'][1] / 2.0},{binning['two_theta_binning'][2]}" + ad1 = f"Omega,{binning['omega_binning'][0]},{binning['omega_binning'][1]},{binning['omega_binning'][2]}" + filepaths = [path.replace("*" * len(str(number)), str(number)) for number in list(file_numbers)] + filepaths = ", ".join(filepaths) + norm_name = "_".join((workspace_name, "norm")) + if workspace_name.endswith("_sf"): + field_name = workspace_name[-4:] + else: + field_name = workspace_name[-5:] + LoadDNSSCD( + FileNames=filepaths, + OutputWorkspace=workspace_name, + NormalizationWorkspace=norm_name, + Normalization=params["norm_to"], + a=params["a"], + b=params["b"], + c=params["c"], + alpha=params["alpha"], + beta=params["beta"], + gamma=params["gamma"], + OmegaOffset=params["omega_offset"], + HKL1=params["hkl1"], + HKL2=params["hkl2"], + LoadAs="raw", + SaveHuberTo=f"huber_{field_name}", + ) + BinMD(InputWorkspace=workspace_name, OutputWorkspace=workspace_name, AxisAligned=True, AlignedDim0=ad0, AlignedDim1=ad1) + BinMD(InputWorkspace=norm_name, OutputWorkspace=norm_name, AxisAligned=True, AlignedDim0=ad0, AlignedDim1=ad1) + return mtd[workspace_name]