From 1ff5c47b7a727a0c56cac771c63fd5e94de90ad1 Mon Sep 17 00:00:00 2001 From: x01da Date: Mon, 10 Aug 2026 09:19:46 +0200 Subject: [PATCH] wip widget --- debye_bec/bec_widgets/widgets/client.py | 7 +- .../bec_widgets/widgets/designer_plugins.py | 12 +- .../scan_control_advanced.pyproject | 1 - .../__init__,py | 0 .../widgets/scan_control_xas/qt_widgets.py | 230 ++++++++++++ .../register_scan_control_xas.py} | 4 +- .../scan_control_xas.py} | 326 ++++-------------- .../scan_control_xas.pyproject | 1 + .../scan_control_xas_plugin.py} | 16 +- 9 files changed, 313 insertions(+), 284 deletions(-) delete mode 100644 debye_bec/bec_widgets/widgets/scan_control_advanced/scan_control_advanced.pyproject rename debye_bec/bec_widgets/widgets/{scan_control_advanced => scan_control_xas}/__init__,py (100%) create mode 100644 debye_bec/bec_widgets/widgets/scan_control_xas/qt_widgets.py rename debye_bec/bec_widgets/widgets/{scan_control_advanced/register_scan_control_advanced.py => scan_control_xas/register_scan_control_xas.py} (80%) rename debye_bec/bec_widgets/widgets/{scan_control_advanced/scan_control_advanced.py => scan_control_xas/scan_control_xas.py} (57%) create mode 100644 debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.pyproject rename debye_bec/bec_widgets/widgets/{scan_control_advanced/scan_control_advanced_plugin.py => scan_control_xas/scan_control_xas_plugin.py} (69%) diff --git a/debye_bec/bec_widgets/widgets/client.py b/debye_bec/bec_widgets/widgets/client.py index 436a68e..af97cf7 100644 --- a/debye_bec/bec_widgets/widgets/client.py +++ b/debye_bec/bec_widgets/widgets/client.py @@ -4,7 +4,6 @@ from __future__ import annotations from bec_lib.logger import bec_logger - from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout logger = bec_logger.logger @@ -15,7 +14,7 @@ logger = bec_logger.logger _Widgets = { "DataViewer": "DataViewer", "DigitalTwin": "DigitalTwin", - "ScanControlAdvanced": "ScanControlAdvanced", + "ScanControlXAS": "ScanControlXAS", } @@ -67,8 +66,8 @@ class DigitalTwin(RPCBase): """ -class ScanControlAdvanced(RPCBase): - _IMPORT_MODULE = "debye_bec.bec_widgets.widgets.scan_control_advanced.scan_control_advanced" +class ScanControlXAS(RPCBase): + _IMPORT_MODULE = "debye_bec.bec_widgets.widgets.scan_control_xas.scan_control_xas" @rpc_call def attach(self): diff --git a/debye_bec/bec_widgets/widgets/designer_plugins.py b/debye_bec/bec_widgets/widgets/designer_plugins.py index 43dffd4..b098ee3 100644 --- a/debye_bec/bec_widgets/widgets/designer_plugins.py +++ b/debye_bec/bec_widgets/widgets/designer_plugins.py @@ -7,14 +7,10 @@ from __future__ import annotations designer_plugins = { "DataViewer": ("debye_bec.bec_widgets.widgets.data_viewer.data_viewer", "DataViewer"), "DigitalTwin": ("debye_bec.bec_widgets.widgets.digital_twin.digital_twin", "DigitalTwin"), - "ScanControlAdvanced": ( - "debye_bec.bec_widgets.widgets.scan_control_advanced.scan_control_advanced", - "ScanControlAdvanced", + "ScanControlXAS": ( + "debye_bec.bec_widgets.widgets.scan_control_xas.scan_control_xas", + "ScanControlXAS", ), } -widget_icons = { - "DataViewer": "find_in_page", - "DigitalTwin": "lightbulb", - "ScanControlAdvanced": "tune", -} +widget_icons = {"DataViewer": "find_in_page", "DigitalTwin": "lightbulb", "ScanControlXAS": "tune"} diff --git a/debye_bec/bec_widgets/widgets/scan_control_advanced/scan_control_advanced.pyproject b/debye_bec/bec_widgets/widgets/scan_control_advanced/scan_control_advanced.pyproject deleted file mode 100644 index 480639d..0000000 --- a/debye_bec/bec_widgets/widgets/scan_control_advanced/scan_control_advanced.pyproject +++ /dev/null @@ -1 +0,0 @@ -{'files': ['scan_control_advanced.py']} \ No newline at end of file diff --git a/debye_bec/bec_widgets/widgets/scan_control_advanced/__init__,py b/debye_bec/bec_widgets/widgets/scan_control_xas/__init__,py similarity index 100% rename from debye_bec/bec_widgets/widgets/scan_control_advanced/__init__,py rename to debye_bec/bec_widgets/widgets/scan_control_xas/__init__,py diff --git a/debye_bec/bec_widgets/widgets/scan_control_xas/qt_widgets.py b/debye_bec/bec_widgets/widgets/scan_control_xas/qt_widgets.py new file mode 100644 index 0000000..d16d011 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/scan_control_xas/qt_widgets.py @@ -0,0 +1,230 @@ +from typing import Literal, Optional + +from bec_widgets.utils.colors import get_accent_colors +from bec_widgets.utils.error_popups import SafeSlot +from qtpy.QtCore import Signal + +# pylint: disable=E0611 +from qtpy.QtWidgets import ( + QApplication, + QComboBox, + QDialog, + QGridLayout, + QHBoxLayout, + QLabel, + QPushButton, + QSizePolicy, + QVBoxLayout, + QWidget, +) + +LABEL_WIDTH = 118 +ROW_MARGINS = (4, 0, 4, 0) +ROW_SPACING = 6 + + +class ComboBox(QWidget): + def __init__(self, identifier="", label="", enums=None): + super().__init__() + + layout = QVBoxLayout(self) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) + + self.identifier = identifier + + self.label = QLabel(label) + self.label.setFixedWidth(LABEL_WIDTH) + self.label.setWordWrap(True) + self.label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) + layout.addWidget(self.label) + + self.value = QComboBox() + self.value.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + + for entry in enums or []: + self.value.addItem(entry) + + layout.addWidget(self.value) + + def set_current_text(self, text): + self.value.setCurrentText(text) + + def currentText(self) -> str: + return self.value.currentText() + + def has_focus(self) -> bool: + return QApplication.focusWidget() is self.value.view() + + def activated_connect(self, func): + """Connect a function to the Enter/Return key press.""" + self.value.activated.connect(func) + + def setDisabled(self, disable): + self.value.setDisabled(disable) + + +class TextIndicator(QWidget): + def __init__(self, identifier="", label="", text=""): + super().__init__() + + layout = QVBoxLayout(self) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) + + self.identifier = identifier + + self.label = QLabel(label) + self.label.setFixedWidth(LABEL_WIDTH) + self.label.setWordWrap(True) + self.label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) + layout.addWidget(self.label) + + self.value = QLabel(text) + self.value.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + + layout.addWidget(self.value) + + def setText(self, text): + self.value.setText(text) + + def text(self) -> str: + return self.value.text() + + +ELEMENTS = { + # period 4 + "Sc": (4, 3), + "Ti": (4, 4), + "V": (4, 5), + "Cr": (4, 6), + "Mn": (4, 7), + "Fe": (4, 8), + "Co": (4, 9), + "Ni": (4, 10), + "Cu": (4, 11), + "Zn": (4, 12), + "Ga": (4, 13), + "Ge": (4, 14), + "As": (4, 15), + "Se": (4, 16), + "Br": (4, 17), + "Kr": (4, 18), + # period 5 + "Rb": (5, 1), + "Sr": (5, 2), + "Y": (5, 3), + "Zr": (5, 4), + "Nb": (5, 5), + "Mo": (5, 6), + "Tc": (5, 7), + "Ru": (5, 8), + "Rh": (5, 9), + "Pd": (5, 10), + "Ag": (5, 11), + "Cd": (5, 12), + "In": (5, 13), + "Sn": (5, 14), + "Sb": (5, 15), + "Te": (5, 16), + "I": (5, 17), + "Xe": (5, 18), + # period 6 + "Cs": (6, 1), + "Ba": (6, 2), + "La": (8, 3), + "Ce": (8, 4), + "Pr": (8, 5), + "Nd": (8, 6), + "Pm": (8, 7), + "Sm": (8, 8), + "Eu": (8, 9), + "Gd": (8, 10), + "Tb": (8, 11), + "Dy": (8, 12), + "Ho": (8, 13), + "Er": (8, 14), + "Tm": (8, 15), + "Pb": (6, 14), + "Bi": (6, 15), + "Po": (6, 16), + "At": (6, 17), + "Rn": (6, 18), + # period 7 + "Fr": (7, 1), + "Ra": (7, 2), + "Ac": (9, 3), + "Th": (9, 4), + "Pa": (9, 5), + "U": (9, 6), +} + + +class PeriodicTableDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.selected = None + + layout = QGridLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(1) + + for element, (row, col) in ELEMENTS.items(): + button = QPushButton(element) + button.setFixedSize(42, 32) + button.clicked.connect(lambda checked=False, e=element: self.select(e)) + layout.addWidget(button, row - 1, col - 1) + + @SafeSlot() + def select(self, element): + self.selected = element + self.accept() + + +class ElementSelector(QWidget): + + dialogClosed = Signal() + + def __init__(self): + super().__init__() + self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + layout = QVBoxLayout(self) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) + self.label = QLabel("Element") + layout_selection = QHBoxLayout(self) + self.button = QPushButton("Select") + self.button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + self.button.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" + ) + layout_selection.addWidget(self.button) + layout.addWidget(self.label) + layout.addLayout(layout_selection) + self.current_element = "" + + self.button.clicked.connect(self.open_table) + + def open_table(self): + dialog = PeriodicTableDialog(self) + if dialog.exec_(): + self.current_element = dialog.selected + self.dialogClosed.emit() + + def currentElement(self): + return self.current_element + + def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None): + """ + Apply the theme + + Args: + theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None. + """ + if theme is None: + app = QApplication.instance() + theme = app.theme.theme # type: ignore + + self.button.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" + ) diff --git a/debye_bec/bec_widgets/widgets/scan_control_advanced/register_scan_control_advanced.py b/debye_bec/bec_widgets/widgets/scan_control_xas/register_scan_control_xas.py similarity index 80% rename from debye_bec/bec_widgets/widgets/scan_control_advanced/register_scan_control_advanced.py rename to debye_bec/bec_widgets/widgets/scan_control_xas/register_scan_control_xas.py index 70bf187..c3fb9e7 100644 --- a/debye_bec/bec_widgets/widgets/scan_control_advanced/register_scan_control_advanced.py +++ b/debye_bec/bec_widgets/widgets/scan_control_xas/register_scan_control_xas.py @@ -6,9 +6,9 @@ def main(): # pragma: no cover return from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection - from .scan_control_advanced_plugin import ScanControlAdvancedPlugin + from .scan_control_xas_plugin import ScanControlXASPlugin - QPyDesignerCustomWidgetCollection.addCustomWidget(ScanControlAdvancedPlugin()) + QPyDesignerCustomWidgetCollection.addCustomWidget(ScanControlXASPlugin()) if __name__ == "__main__": # pragma: no cover diff --git a/debye_bec/bec_widgets/widgets/scan_control_advanced/scan_control_advanced.py b/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.py similarity index 57% rename from debye_bec/bec_widgets/widgets/scan_control_advanced/scan_control_advanced.py rename to debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.py index ddf3659..4b7f4bf 100644 --- a/debye_bec/bec_widgets/widgets/scan_control_advanced/scan_control_advanced.py +++ b/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.py @@ -1,5 +1,8 @@ +""" +Scan Control XAS: Custom BEC widget for Scan Control XAS scans. +""" + import sys -from functools import partial from typing import Literal, Optional import numpy as np @@ -8,43 +11,41 @@ import xraydb from bec_lib import bec_logger from bec_lib.endpoints import MessageEndpoints from bec_widgets.utils.bec_dispatcher import BECDispatcher -from bec_widgets.utils.colors import Colors, apply_theme, get_accent_colors +from bec_widgets.utils.colors import Colors, apply_theme from bec_widgets.utils.error_popups import SafeSlot from bec_widgets.widgets.control.scan_control.scan_control import ScanControl -from qtpy.QtCore import Qt, Signal +from qtpy.QtCore import Qt # pylint: disable=E0611 from qtpy.QtWidgets import ( QApplication, - QComboBox, - QDialog, QFrame, - QGridLayout, QGroupBox, QHBoxLayout, - QLabel, - QPushButton, QScrollArea, QSizePolicy, QVBoxLayout, QWidget, ) +# pylint: disable=E0402 from ....devices.mo1_bragg.mo1_bragg_utils import compute_spline +from .qt_widgets import ComboBox, ElementSelector, TextIndicator logger = bec_logger.logger -SHOW_MOTION_PROFILE = [ +ALLOWED_SCANS = [ "xas_simple_scan", "xas_simple_scan_with_xrd", "xas_advanced_scan", "xas_advanced_scan_with_xrd", ] -# TODO: Theme change edge marker. Show edge energy next to edge selector. Bugfix edge selector - -class ScanControlAdvanced(ScanControl): +class ScanControlXAS(ScanControl): + """ + Main widget of Scan Control XAS + """ def __init__(self, *args, **kwargs): self._motion_profile_group = QGroupBox("Motion Profile Preview") @@ -52,51 +53,51 @@ class ScanControlAdvanced(ScanControl): self.motion_profile_widget = MotionProfileWidget(self) motion_profile_layout.addWidget(self.motion_profile_widget) super().__init__( - allowed_scans=SHOW_MOTION_PROFILE, default_scan="xas_simple_scan", *args, **kwargs + allowed_scans=ALLOWED_SCANS, default_scan="xas_simple_scan", *args, **kwargs ) - self.show_motion_profile(self._selected_scan) + self._show_motion_profile(self._selected_scan) self.d_spacing = 0 self.bec_dispatcher.connect_slot( - self.update_d_spacing, MessageEndpoints.device_readback("mo1_bragg") + self._update_d_spacing, MessageEndpoints.device_readback("mo1_bragg") ) @SafeSlot(dict, dict) - def update_d_spacing(self, msg: dict, metadata: dict): + def _update_d_spacing(self, msg: dict, _: dict): d_spacing = msg["signals"].get("mo1_bragg_crystal_current_d_spacing")["value"] if d_spacing != self.d_spacing: self.d_spacing = d_spacing self.motion_profile_widget.update_plot(d_spacing=d_spacing) def _add_metadata_form(self): - self.layout.addWidget(self._motion_profile_group, stretch=1) + self.layout.addWidget(self._motion_profile_group, stretch=1) # type: ignore super()._add_metadata_form() - self.connect_scan_parameter_signals() + self._connect_scan_parameter_signals() - def on_scan_selection_changed(self, scan_name: str): - super().on_scan_selection_changed(scan_name) - self.show_motion_profile(scan_name) - if scan_name in SHOW_MOTION_PROFILE: - self.connect_scan_parameter_signals() - self.scan_parameter_changed() + def _on_scan_selection_changed(self, scan_name: str): + super()._on_scan_selection_changed(scan_name) + self._show_motion_profile(scan_name) + if scan_name in ALLOWED_SCANS: + self._connect_scan_parameter_signals() + self._scan_parameter_changed() - def show_motion_profile(self, scan_name): - if scan_name in SHOW_MOTION_PROFILE: + def _show_motion_profile(self, scan_name): + if scan_name in ALLOWED_SCANS: self.motion_profile_widget.setVisible(True) else: self.motion_profile_widget.setVisible(False) - def connect_scan_parameter_signals(self): + def _connect_scan_parameter_signals(self): for box in self.kwarg_boxes: for widget in box.findChildren(QWidget): try: if hasattr(widget, "valueChanged"): widget.valueChanged.connect( - self.scan_parameter_changed, Qt.ConnectionType.UniqueConnection + self._scan_parameter_changed, Qt.ConnectionType.UniqueConnection ) elif hasattr(widget, "textChanged"): widget.textChanged.connect( - self.scan_parameter_changed, Qt.ConnectionType.UniqueConnection + self._scan_parameter_changed, Qt.ConnectionType.UniqueConnection ) except TypeError: # Raised if a connection would not be unique anymore, i.e. if @@ -104,7 +105,8 @@ class ScanControlAdvanced(ScanControl): # the same kwarg_boxes pass - def scan_parameter_changed(self, *_): + @SafeSlot() + def _scan_parameter_changed(self, *_): # logger.info(f"Fe K edge: {xraydb.xray_edge("Fe", "K", energy_only=True)}") params = self.get_scan_parameters()[1] # logger.info(f"Scan parameters: {params}") @@ -127,147 +129,11 @@ H = 6.62606957e-34 E = 1.602176634e-19 C = 299792458 -ELEMENTS = { - # period 4 - "Sc": (4, 3), - "Ti": (4, 4), - "V": (4, 5), - "Cr": (4, 6), - "Mn": (4, 7), - "Fe": (4, 8), - "Co": (4, 9), - "Ni": (4, 10), - "Cu": (4, 11), - "Zn": (4, 12), - "Ga": (4, 13), - "Ge": (4, 14), - "As": (4, 15), - "Se": (4, 16), - "Br": (4, 17), - "Kr": (4, 18), - # period 5 - "Rb": (5, 1), - "Sr": (5, 2), - "Y": (5, 3), - "Zr": (5, 4), - "Nb": (5, 5), - "Mo": (5, 6), - "Tc": (5, 7), - "Ru": (5, 8), - "Rh": (5, 9), - "Pd": (5, 10), - "Ag": (5, 11), - "Cd": (5, 12), - "In": (5, 13), - "Sn": (5, 14), - "Sb": (5, 15), - "Te": (5, 16), - "I": (5, 17), - "Xe": (5, 18), - # period 6 - "Cs": (6, 1), - "Ba": (6, 2), - "La": (8, 3), - "Ce": (8, 4), - "Pr": (8, 5), - "Nd": (8, 6), - "Pm": (8, 7), - "Sm": (8, 8), - "Eu": (8, 9), - "Gd": (8, 10), - "Tb": (8, 11), - "Dy": (8, 12), - "Ho": (8, 13), - "Er": (8, 14), - "Tm": (8, 15), - "Pb": (6, 14), - "Bi": (6, 15), - "Po": (6, 16), - "At": (6, 17), - "Rn": (6, 18), - # period 7 - "Fr": (7, 1), - "Ra": (7, 2), - "Ac": (9, 3), - "Th": (9, 4), - "Pa": (9, 5), - "U": (9, 6), -} - - -class PeriodicTableDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - self.selected = None - - layout = QGridLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(1) - - for element, (row, col) in ELEMENTS.items(): - button = QPushButton(element) - button.setFixedSize(42, 32) - button.clicked.connect(lambda checked=False, e=element: self.select(e)) - layout.addWidget(button, row - 1, col - 1) - - def select(self, element): - self.selected = element - self.accept() - - -class ElementSelector(QWidget): - - dialogClosed = Signal() - - def __init__(self): - super().__init__() - self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - layout = QVBoxLayout(self) - layout.setContentsMargins(*ROW_MARGINS) - layout.setSpacing(ROW_SPACING) - self.label = QLabel("Element") - layout_selection = QHBoxLayout(self) - self.button = QPushButton("Select") - self.button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - self.button.setStyleSheet( - f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" - ) - # self.selection = QLabel("") - layout_selection.addWidget(self.button) - # layout_selection.addWidget(self.selection) - layout.addWidget(self.label) - layout.addLayout(layout_selection) - self.current_element = "" - - self.button.clicked.connect(self.open_table) - - def open_table(self): - dialog = PeriodicTableDialog(self) - if dialog.exec_(): - self.current_element = dialog.selected - # self.selection.setText(dialog.selected) - self.dialogClosed.emit() - - def currentElement(self): - return self.current_element - - def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None): - """ - Apply the theme - - Args: - theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None. - """ - if theme is None: - app = QApplication.instance() - theme = app.theme.theme # type: ignore - - self.button.setStyleSheet( - f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" - ) - class MotionProfileWidget(QWidget): + """ + Additional widget specifically for XAS scans + """ def __init__(self, parent): super().__init__() @@ -298,8 +164,8 @@ class MotionProfileWidget(QWidget): ) scroll = QScrollArea() scroll.setWidgetResizable(True) - scroll.setFrameShape(QFrame.NoFrame) - scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll.setFrameShape(QFrame.Shape.NoFrame) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) scroll.setWidget(plot_container) self.edge_selection = QHBoxLayout() @@ -318,8 +184,7 @@ class MotionProfileWidget(QWidget): self.element.dialogClosed.connect(self._update_edge_selection) self.edge.activated_connect(self._update_edge_selection) - @staticmethod - def _make_plot(left_label: str, bottom_label: str) -> pg.PlotWidget: + def _make_plot(self, left_label: str, bottom_label: str) -> pg.PlotWidget: plot = pg.PlotWidget() plot.setLabel("left", left_label) plot.setLabel("bottom", bottom_label) @@ -329,7 +194,7 @@ class MotionProfileWidget(QWidget): plot.getAxis("left").enableAutoSIPrefix(False) plot.getAxis("bottom").enableAutoSIPrefix(False) plot.setMinimumHeight(180) - plot.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding) + plot.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding) return plot def _add_edge_marker(self, plot, x_arr, y_arr, energy_arr, edge_marker): @@ -353,7 +218,7 @@ class MotionProfileWidget(QWidget): plot.addItem(scatter, ignoreBounds=True) text = f"{self.element.currentElement()}, {self.edge.currentText()}-edge, {self.edge_energy:0.1f} eV" - label = pg.TextItem(text, anchor=(0.5, 1.5), color="r") + label = pg.TextItem(text, anchor=(0, 1.5), color="r") label.setPos(x_marker, y_marker) plot.addItem(label, ignoreBounds=True) @@ -375,7 +240,18 @@ class MotionProfileWidget(QWidget): self.update_plot() @SafeSlot() - def update_plot(self, scan_name=None, scan_parameters=None, d_spacing=None, *_, **__): + def update_plot(self, *_, scan_name=None, scan_parameters=None, d_spacing=None, **__): + """Update the plots. Parameters which are not defined will default to None, + in which case it will use the internal values if available. + + Args: + scan_name(str): Scan name, e.g. "xas_simple_scan" + Defaults to None + scan_parameters(dict): Scan parameters from ScanControl + Defaults to None + d_spacing(float): d-spacing in Angstrom + Defaults to None + """ if scan_name is not None: self.scan_name = scan_name if scan_parameters is not None: @@ -396,15 +272,17 @@ class MotionProfileWidget(QWidget): self.plot_pos.clear() self.plot_vel.clear() - start_angle = self.energy_to_angle(self.scan_parameters["stop"]) - stop_angle = self.energy_to_angle(self.scan_parameters["start"]) + start_angle = self._energy_to_angle(self.scan_parameters["stop"]) + stop_angle = self._energy_to_angle(self.scan_parameters["start"]) + if start_angle == stop_angle: + return if "xas_simple_scan" in self.scan_name: amp = (stop_angle - start_angle) / 2 two_pi_f = 2 * np.pi * 1 / (2 * self.scan_parameters["scan_time"]) pos = (start_angle + stop_angle) / 2 + amp * np.cos(x_time * two_pi_f) vel = two_pi_f * amp * np.sin(x_time * two_pi_f) - energy = self.angle_to_energy(pos) - vel_e = self.velocity_to_energy_per_s(pos, vel) + energy = self._angle_to_energy(pos) + vel_e = self._velocity_to_energy_per_s(pos, vel) self.plot_pos.plot(x_time, energy) self._add_edge_marker(self.plot_pos, x_time, energy, energy, "_") @@ -420,7 +298,7 @@ class MotionProfileWidget(QWidget): return if self.scan_parameters["scan_time"] == 0: return - e_kink_deg = self.energy_to_angle(self.scan_parameters["e_kink"]) + e_kink_deg = self._energy_to_angle(self.scan_parameters["e_kink"]) pos, vel, t = compute_spline( start_angle, stop_angle, @@ -429,8 +307,8 @@ class MotionProfileWidget(QWidget): self.scan_parameters["scan_time"], ) x_time = np.cumsum(t) / 1000 - energy = self.angle_to_energy(pos) - vel_e = self.velocity_to_energy_per_s(pos, vel) + energy = self._angle_to_energy(pos) + vel_e = self._velocity_to_energy_per_s(pos, vel) time_flipped = -np.flip(x_time) + x_time[-1] energy_flipped = np.flip(energy) @@ -470,7 +348,7 @@ class MotionProfileWidget(QWidget): curve.setPen(pg.mkPen(color=color, width=2)) self.element.apply_theme(theme) - def energy_to_angle(self, energy: int | float): + def _energy_to_angle(self, energy: int | float): if self.d_spacing is None: return 0 if energy <= 0: @@ -479,7 +357,7 @@ class MotionProfileWidget(QWidget): val = wl / (2 * self.d_spacing * 1e-10) return np.asin(val) / np.pi * 180 - def angle_to_energy(self, angle: np.ndarray | int | float): + def _angle_to_energy(self, angle: np.ndarray | int | float): if self.d_spacing is None: return 0 if isinstance(angle, np.ndarray): @@ -491,7 +369,7 @@ class MotionProfileWidget(QWidget): wl = 2 * self.d_spacing * 1e-10 * np.sin(angle / 180 * np.pi) return C * H / (E * wl) - def velocity_to_energy_per_s(self, angle, velocity): + def _velocity_to_energy_per_s(self, angle, velocity): if self.d_spacing is None: return 0 return ( @@ -503,84 +381,10 @@ class MotionProfileWidget(QWidget): ) * velocity -LABEL_WIDTH = 118 -ROW_MARGINS = (4, 0, 4, 0) -ROW_SPACING = 6 - - -class ComboBox(QWidget): - def __init__(self, identifier="", label="", enums=None): - super().__init__() - - layout = QVBoxLayout(self) - layout.setContentsMargins(*ROW_MARGINS) - layout.setSpacing(ROW_SPACING) - - self.identifier = identifier - - self.label = QLabel(label) - self.label.setFixedWidth(LABEL_WIDTH) - self.label.setWordWrap(True) - self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - layout.addWidget(self.label) - - self.value = QComboBox() - self.value.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - for entry in enums or []: - self.value.addItem(entry) - - layout.addWidget(self.value) - - def set_current_text(self, text): - self.value.setCurrentText(text) - - def currentText(self) -> str: - return self.value.currentText() - - def has_focus(self) -> bool: - return QApplication.focusWidget() is self.value.view() - - def activated_connect(self, func): - """Connect a function to the Enter/Return key press.""" - self.value.activated.connect(func) - - def setDisabled(self, disable): - self.value.setDisabled(disable) - - -class TextIndicator(QWidget): - def __init__(self, identifier="", label="", text=""): - super().__init__() - - layout = QVBoxLayout(self) - layout.setContentsMargins(*ROW_MARGINS) - layout.setSpacing(ROW_SPACING) - - self.identifier = identifier - - self.label = QLabel(label) - self.label.setFixedWidth(LABEL_WIDTH) - self.label.setWordWrap(True) - self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - layout.addWidget(self.label) - - self.value = QLabel(text) - self.value.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - layout.addWidget(self.value) - - def setText(self, text): - self.value.setText(text) - - def text(self) -> str: - return self.value.text() - - if __name__ == "__main__": app = QApplication(sys.argv) apply_theme("light") dispatcher = BECDispatcher(gui_id="scan_control_advanced") - win = ScanControlAdvanced() + win = ScanControlXAS() win.show() sys.exit(app.exec_()) diff --git a/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.pyproject b/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.pyproject new file mode 100644 index 0000000..8ba607b --- /dev/null +++ b/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.pyproject @@ -0,0 +1 @@ +{'files': ['scan_control_xas.py']} \ No newline at end of file diff --git a/debye_bec/bec_widgets/widgets/scan_control_advanced/scan_control_advanced_plugin.py b/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas_plugin.py similarity index 69% rename from debye_bec/bec_widgets/widgets/scan_control_advanced/scan_control_advanced_plugin.py rename to debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas_plugin.py index 78c1b32..0647be1 100644 --- a/debye_bec/bec_widgets/widgets/scan_control_advanced/scan_control_advanced_plugin.py +++ b/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas_plugin.py @@ -5,17 +5,17 @@ from bec_widgets.utils.bec_designer import designer_material_icon from qtpy.QtDesigner import QDesignerCustomWidgetInterface from qtpy.QtWidgets import QWidget -from .scan_control_advanced import ScanControlAdvanced +from .scan_control_xas import ScanControlXAS DOM_XML = """ - + """ -class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no cover +class ScanControlXASPlugin(QDesignerCustomWidgetInterface): # pragma: no cover def __init__(self): super().__init__() self._form_editor = None @@ -23,7 +23,7 @@ class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no c def createWidget(self, parent): if parent is None: return QWidget() - t = ScanControlAdvanced(parent) + t = ScanControlXAS(parent) return t def domXml(self): @@ -33,10 +33,10 @@ class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no c return "" def icon(self): - return designer_material_icon(ScanControlAdvanced.ICON_NAME) + return designer_material_icon(ScanControlXAS.ICON_NAME) def includeFile(self): - return "scan_control_advanced" + return "scan_control_xas" def initialize(self, form_editor): self._form_editor = form_editor @@ -48,10 +48,10 @@ class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no c return self._form_editor is not None def name(self): - return "ScanControlAdvanced" + return "ScanControlXAS" def toolTip(self): - return "ScanControlAdvanced" + return "ScanControlXAS" def whatsThis(self): return self.toolTip()