Source code for quicknxs.views.offspec_slice_dialog

"""Dialog to let the user configure off-specular Qz slice parameters."""

from mantid.simpleapi import logger
from matplotlib.lines import Line2D
from numpy import float64
from numpy.typing import NDArray
from qtpy import QtCore, QtWidgets

from quicknxs.presenters.data_manager import DataManager
from quicknxs.views import load_ui
from quicknxs.views.widgets import MPLWidget


[docs] class OffSpecSliceDialog(QtWidgets.QDialog): """Dialog to define off-specular Qz slice parameters with plot preview.""" INTENSITY_MIN = 1e-6 # starting value for the color scale INTENSITY_MAX = 1.0 # ending value for the color scale drawing = False def __init__(self, parent, data_manager: DataManager): QtWidgets.QDialog.__init__(self, parent) self.ui = load_ui("ui_offspec_slice_dialog.ui", base_instance=self) self.data_manager = data_manager self.rect_region = None # Load saved values from settings self.load_settings() # Connect signals to update slice width display self.ui.slice_qz_min.valueChanged.connect(self.update_slice_width) self.ui.slice_qz_max.valueChanged.connect(self.update_slice_width) # Connect signals to update plot region self.ui.slice_qz_min.valueChanged.connect(self.update_region) self.ui.slice_qz_max.valueChanged.connect(self.update_region) # Connect radio buttons to redraw plot self.ui.kizmkfzVSqz.toggled.connect(self.on_coordinate_system_changed) self.ui.qxVSqz.toggled.connect(self.on_coordinate_system_changed) self.ui.kizVSkfz.toggled.connect(self.on_coordinate_system_changed) # Set initial visibility of Qz range controls self.update_qz_range_visibility() # Update slice width display on initialization self.update_slice_width() # Draw the initial plot (deferred to avoid blocking) QtCore.QTimer.singleShot(0, self.draw_plot)
[docs] def on_coordinate_system_changed(self): """Handle coordinate system change - update visibility and redraw plot.""" self.update_qz_range_visibility() self.draw_plot()
[docs] def update_qz_range_visibility(self): """Hide Qz range controls when ki_z vs kf_z is selected (no Qz axis).""" # ki_z vs kf_z doesn't have a Qz axis, so hide the Qz range controls has_qz_axis = not self.ui.kizVSkfz.isChecked() # Hide/show the Qz spinboxes self.ui.slice_qz_min.setVisible(has_qz_axis) self.ui.slice_qz_max.setVisible(has_qz_axis) # Hide/show the Qz labels (label_2 is "Qz min:", label_3 is "Qz max:") if hasattr(self.ui, "label_2"): self.ui.label_2.setVisible(has_qz_axis) if hasattr(self.ui, "label_3"): self.ui.label_3.setVisible(has_qz_axis) # Also hide/show the slice width display self.ui.slice_width_label.setVisible(has_qz_axis) if hasattr(self.ui, "label_4"): # "Slice width:" label self.ui.label_4.setVisible(has_qz_axis)
[docs] def draw_plot(self): """Draw the off-specular data with the configured region overlay.""" if self.drawing: return self.drawing = True # Skip drawing if widget isn't visible (e.g., in tests) if not self.isVisible(): self.drawing = False return plot = self.ui.plot plot.clear() plot.set_xticks_fontsize(8) plot.set_yticks_fontsize(8) # Initialize limits qz_min, qz_max = 0.5, -0.1 qx_min, qx_max = -0.001, 0.001 ki_z_min, ki_z_max = 0.1, -0.1 kf_z_min, kf_z_max = 0.1, -0.1 k_diff_min, k_diff_max = 0.01, -0.01 # Get first state from reduction_states if not self.data_manager.reduction_states: self.drawing = False return first_state = self.data_manager.reduction_states[0] # Plot data from all runs in the reduction list for item in self.data_manager.reduction_list: # Check if off_spec data exists if first_state not in item.cross_sections: continue if item.cross_sections[first_state].off_spec is None: continue offspec = item.cross_sections[first_state].off_spec Qx, Qz, ki_z, kf_z, I, _ = (offspec.Qx, offspec.Qz, offspec.ki_z, offspec.kf_z, offspec.S, offspec.dS) n_total = len(I[0]) # P_0 and P_N are the number of points to cut in TOF on each side p_0 = item.cross_sections[first_state].configuration.cut_first_n_points p_n = n_total - item.cross_sections[first_state].configuration.cut_last_n_points Qx = Qx[:, p_0:p_n] Qz = Qz[:, p_0:p_n] ki_z = ki_z[:, p_0:p_n] kf_z = kf_z[:, p_0:p_n] I = I[:, p_0:p_n] # Extend the X and Y limits of the plotting area try: qz_max = max(Qz[I > 0].max(), qz_max) qz_min = min(Qz[I > 0].min(), qz_min) qx_min = min(qx_min, Qx[I > 0].min()) qx_max = max(qx_max, Qx[I > 0].max()) ki_z_min = min(ki_z_min, ki_z[I > 0].min()) ki_z_max = max(ki_z_max, ki_z[I > 0].max()) kf_z_min = min(kf_z_min, kf_z[I > 0].min()) kf_z_max = max(kf_z_max, kf_z[I > 0].max()) k_diff_min = min(k_diff_min, (ki_z - kf_z)[I > 0].min()) k_diff_max = max(k_diff_max, (ki_z - kf_z)[I > 0].max()) except Exception as exception: logger.error(f"Error extending plotting limits: {exception}") self._paint_intensities(ki_z, kf_z, Qx, Qz, I, plot) # Set plot limits and labels based on selected axis type if self.ui.kizmkfzVSqz.isChecked(): plot.canvas.ax.set_xlim([k_diff_min, k_diff_max]) plot.canvas.ax.set_ylim([qz_min, qz_max]) plot.set_xlabel("k$_{i,z}$-k$_{f,z}$ [Å$^{-1}$]", fontsize=14) plot.set_ylabel("Q$_z$ [Å$^{-1}$]", fontsize=14) elif self.ui.qxVSqz.isChecked(): plot.canvas.ax.set_xlim([qx_min, qx_max]) plot.canvas.ax.set_ylim([qz_min, qz_max]) plot.set_xlabel("Q$_x$ [Å$^{-1}$]", fontsize=14) plot.set_ylabel("Q$_z$ [Å$^{-1}$]", fontsize=14) elif self.ui.kizVSkfz.isChecked(): plot.canvas.ax.set_xlim([ki_z_min, ki_z_max]) plot.canvas.ax.set_ylim([kf_z_min, kf_z_max]) plot.set_xlabel("k$_{i,z}$ [Å$^{-1}$]", fontsize=14) plot.set_ylabel("k$_{f,z}$ [Å$^{-1}$]", fontsize=14) # Draw the region rectangle (create it initially, then update_region will modify it) # Only draw if there's a Qz axis if not self.ui.kizVSkfz.isChecked(): qz_min_val = self.ui.slice_qz_min.value() qz_max_val = self.ui.slice_qz_max.value() xlim = plot.canvas.ax.get_xlim() x_min_val, x_max_val = xlim[0], xlim[1] self.rect_region = Line2D( [x_min_val, x_max_val, x_max_val, x_min_val, x_min_val], [qz_min_val, qz_min_val, qz_max_val, qz_max_val, qz_min_val], ) plot.canvas.ax.add_line(self.rect_region) # Show the plot if plot.cplot is not None: plot.cplot.set_clim([self.INTENSITY_MIN, self.INTENSITY_MAX]) try: plot.draw() except Exception: pass # Ignore drawing errors in headless environments self.drawing = False
def _paint_intensities( self, ki_z: NDArray[float64], kf_z: NDArray[float64], Qx: NDArray[float64], Qz: NDArray[float64], I: NDArray[float64], plot: MPLWidget, ): """Color-paint the intensities versus appropriate X and Y coordinates.""" common_args = { "log": True, "imin": self.INTENSITY_MIN, "imax": self.INTENSITY_MAX, "cmap": "jet", "shading": "gouraud", } if self.ui.kizmkfzVSqz.isChecked(): x, y = (ki_z - kf_z), Qz elif self.ui.qxVSqz.isChecked(): x, y = Qx, Qz elif self.ui.kizVSkfz.isChecked(): x, y = ki_z, kf_z else: x, y = (ki_z - kf_z), Qz # Default plot.pcolormesh(x, y, I, **common_args)
[docs] def update_region(self): """Update the horizontal lines showing the Qz slice region.""" if self.drawing: return # Only update if plot has been initialized with data and rectangle exists if not self.data_manager.reduction_states or self.rect_region is None: return # Don't update region for ki_z vs kf_z (no Qz axis) if self.ui.kizVSkfz.isChecked(): return qz_min = self.ui.slice_qz_min.value() qz_max = self.ui.slice_qz_max.value() # Get x-axis limits from plot xlim = self.ui.plot.canvas.ax.get_xlim() x_min, x_max = xlim[0], xlim[1] # Update rectangle data (matches smooth_dialog/binned_dialog pattern) self.rect_region.set_data([x_min, x_max, x_max, x_min, x_min], [qz_min, qz_min, qz_max, qz_max, qz_min]) try: self.ui.plot.draw() except Exception: pass # Ignore drawing errors in headless environments
[docs] def load_settings(self): """Load parameter values from QSettings.""" settings = QtCore.QSettings(".quicknxs") # Load Qz range if settings.contains("offspec_slice/qz_min"): self.ui.slice_qz_min.setValue(float(settings.value("offspec_slice/qz_min"))) if settings.contains("offspec_slice/qz_max"): self.ui.slice_qz_max.setValue(float(settings.value("offspec_slice/qz_max")))
[docs] def save_settings(self): """Save parameter values to QSettings.""" settings = QtCore.QSettings(".quicknxs") # Save Qz range settings.setValue("offspec_slice/qz_min", self.ui.slice_qz_min.value()) settings.setValue("offspec_slice/qz_max", self.ui.slice_qz_max.value())
[docs] def update_slice_width(self): """Calculate and display the Qz slice width based on current settings.""" qz_min = self.ui.slice_qz_min.value() qz_max = self.ui.slice_qz_max.value() width = qz_max - qz_min self.ui.slice_width_label.setText(f"{width:8.6f} 1/A")
[docs] def get_parameters(self): """ Get the slice parameters as a dictionary. Returns ------- dict Dictionary containing off-specular slice parameters """ return { "off_spec_slice_qz_min": self.ui.slice_qz_min.value(), "off_spec_slice_qz_max": self.ui.slice_qz_max.value(), }
[docs] def accept(self): """Override accept to save settings before closing.""" self.save_settings() super().accept()