wip widget

This commit is contained in:
x01da
2026-08-10 21:27:07 +02:00
parent e6e3104410
commit 1ff5c47b7a
9 changed files with 313 additions and 284 deletions
+3 -4
View File
@@ -4,7 +4,6 @@
from __future__ import annotations from __future__ import annotations
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
logger = bec_logger.logger logger = bec_logger.logger
@@ -15,7 +14,7 @@ logger = bec_logger.logger
_Widgets = { _Widgets = {
"DataViewer": "DataViewer", "DataViewer": "DataViewer",
"DigitalTwin": "DigitalTwin", "DigitalTwin": "DigitalTwin",
"ScanControlAdvanced": "ScanControlAdvanced", "ScanControlXAS": "ScanControlXAS",
} }
@@ -67,8 +66,8 @@ class DigitalTwin(RPCBase):
""" """
class ScanControlAdvanced(RPCBase): class ScanControlXAS(RPCBase):
_IMPORT_MODULE = "debye_bec.bec_widgets.widgets.scan_control_advanced.scan_control_advanced" _IMPORT_MODULE = "debye_bec.bec_widgets.widgets.scan_control_xas.scan_control_xas"
@rpc_call @rpc_call
def attach(self): def attach(self):
@@ -7,14 +7,10 @@ from __future__ import annotations
designer_plugins = { designer_plugins = {
"DataViewer": ("debye_bec.bec_widgets.widgets.data_viewer.data_viewer", "DataViewer"), "DataViewer": ("debye_bec.bec_widgets.widgets.data_viewer.data_viewer", "DataViewer"),
"DigitalTwin": ("debye_bec.bec_widgets.widgets.digital_twin.digital_twin", "DigitalTwin"), "DigitalTwin": ("debye_bec.bec_widgets.widgets.digital_twin.digital_twin", "DigitalTwin"),
"ScanControlAdvanced": ( "ScanControlXAS": (
"debye_bec.bec_widgets.widgets.scan_control_advanced.scan_control_advanced", "debye_bec.bec_widgets.widgets.scan_control_xas.scan_control_xas",
"ScanControlAdvanced", "ScanControlXAS",
), ),
} }
widget_icons = { widget_icons = {"DataViewer": "find_in_page", "DigitalTwin": "lightbulb", "ScanControlXAS": "tune"}
"DataViewer": "find_in_page",
"DigitalTwin": "lightbulb",
"ScanControlAdvanced": "tune",
}
@@ -1 +0,0 @@
{'files': ['scan_control_advanced.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;}}"
)
@@ -6,9 +6,9 @@ def main(): # pragma: no cover
return return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 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 if __name__ == "__main__": # pragma: no cover
@@ -1,5 +1,8 @@
"""
Scan Control XAS: Custom BEC widget for Scan Control XAS scans.
"""
import sys import sys
from functools import partial
from typing import Literal, Optional from typing import Literal, Optional
import numpy as np import numpy as np
@@ -8,43 +11,41 @@ import xraydb
from bec_lib import bec_logger from bec_lib import bec_logger
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from bec_widgets.utils.bec_dispatcher import BECDispatcher 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.utils.error_popups import SafeSlot
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl 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 # pylint: disable=E0611
from qtpy.QtWidgets import ( from qtpy.QtWidgets import (
QApplication, QApplication,
QComboBox,
QDialog,
QFrame, QFrame,
QGridLayout,
QGroupBox, QGroupBox,
QHBoxLayout, QHBoxLayout,
QLabel,
QPushButton,
QScrollArea, QScrollArea,
QSizePolicy, QSizePolicy,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
) )
# pylint: disable=E0402
from ....devices.mo1_bragg.mo1_bragg_utils import compute_spline from ....devices.mo1_bragg.mo1_bragg_utils import compute_spline
from .qt_widgets import ComboBox, ElementSelector, TextIndicator
logger = bec_logger.logger logger = bec_logger.logger
SHOW_MOTION_PROFILE = [ ALLOWED_SCANS = [
"xas_simple_scan", "xas_simple_scan",
"xas_simple_scan_with_xrd", "xas_simple_scan_with_xrd",
"xas_advanced_scan", "xas_advanced_scan",
"xas_advanced_scan_with_xrd", "xas_advanced_scan_with_xrd",
] ]
# TODO: Theme change edge marker. Show edge energy next to edge selector. Bugfix edge selector
class ScanControlXAS(ScanControl):
class ScanControlAdvanced(ScanControl): """
Main widget of Scan Control XAS
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._motion_profile_group = QGroupBox("Motion Profile Preview") self._motion_profile_group = QGroupBox("Motion Profile Preview")
@@ -52,51 +53,51 @@ class ScanControlAdvanced(ScanControl):
self.motion_profile_widget = MotionProfileWidget(self) self.motion_profile_widget = MotionProfileWidget(self)
motion_profile_layout.addWidget(self.motion_profile_widget) motion_profile_layout.addWidget(self.motion_profile_widget)
super().__init__( 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.d_spacing = 0
self.bec_dispatcher.connect_slot( 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) @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"] d_spacing = msg["signals"].get("mo1_bragg_crystal_current_d_spacing")["value"]
if d_spacing != self.d_spacing: if d_spacing != self.d_spacing:
self.d_spacing = d_spacing self.d_spacing = d_spacing
self.motion_profile_widget.update_plot(d_spacing=d_spacing) self.motion_profile_widget.update_plot(d_spacing=d_spacing)
def _add_metadata_form(self): 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() super()._add_metadata_form()
self.connect_scan_parameter_signals() self._connect_scan_parameter_signals()
def on_scan_selection_changed(self, scan_name: str): def _on_scan_selection_changed(self, scan_name: str):
super().on_scan_selection_changed(scan_name) super()._on_scan_selection_changed(scan_name)
self.show_motion_profile(scan_name) self._show_motion_profile(scan_name)
if scan_name in SHOW_MOTION_PROFILE: if scan_name in ALLOWED_SCANS:
self.connect_scan_parameter_signals() self._connect_scan_parameter_signals()
self.scan_parameter_changed() self._scan_parameter_changed()
def show_motion_profile(self, scan_name): def _show_motion_profile(self, scan_name):
if scan_name in SHOW_MOTION_PROFILE: if scan_name in ALLOWED_SCANS:
self.motion_profile_widget.setVisible(True) self.motion_profile_widget.setVisible(True)
else: else:
self.motion_profile_widget.setVisible(False) 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 box in self.kwarg_boxes:
for widget in box.findChildren(QWidget): for widget in box.findChildren(QWidget):
try: try:
if hasattr(widget, "valueChanged"): if hasattr(widget, "valueChanged"):
widget.valueChanged.connect( widget.valueChanged.connect(
self.scan_parameter_changed, Qt.ConnectionType.UniqueConnection self._scan_parameter_changed, Qt.ConnectionType.UniqueConnection
) )
elif hasattr(widget, "textChanged"): elif hasattr(widget, "textChanged"):
widget.textChanged.connect( widget.textChanged.connect(
self.scan_parameter_changed, Qt.ConnectionType.UniqueConnection self._scan_parameter_changed, Qt.ConnectionType.UniqueConnection
) )
except TypeError: except TypeError:
# Raised if a connection would not be unique anymore, i.e. if # Raised if a connection would not be unique anymore, i.e. if
@@ -104,7 +105,8 @@ class ScanControlAdvanced(ScanControl):
# the same kwarg_boxes # the same kwarg_boxes
pass 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)}") # logger.info(f"Fe K edge: {xraydb.xray_edge("Fe", "K", energy_only=True)}")
params = self.get_scan_parameters()[1] params = self.get_scan_parameters()[1]
# logger.info(f"Scan parameters: {params}") # logger.info(f"Scan parameters: {params}")
@@ -127,147 +129,11 @@ H = 6.62606957e-34
E = 1.602176634e-19 E = 1.602176634e-19
C = 299792458 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): class MotionProfileWidget(QWidget):
"""
Additional widget specifically for XAS scans
"""
def __init__(self, parent): def __init__(self, parent):
super().__init__() super().__init__()
@@ -298,8 +164,8 @@ class MotionProfileWidget(QWidget):
) )
scroll = QScrollArea() scroll = QScrollArea()
scroll.setWidgetResizable(True) scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.NoFrame) scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
scroll.setWidget(plot_container) scroll.setWidget(plot_container)
self.edge_selection = QHBoxLayout() self.edge_selection = QHBoxLayout()
@@ -318,8 +184,7 @@ class MotionProfileWidget(QWidget):
self.element.dialogClosed.connect(self._update_edge_selection) self.element.dialogClosed.connect(self._update_edge_selection)
self.edge.activated_connect(self._update_edge_selection) self.edge.activated_connect(self._update_edge_selection)
@staticmethod def _make_plot(self, left_label: str, bottom_label: str) -> pg.PlotWidget:
def _make_plot(left_label: str, bottom_label: str) -> pg.PlotWidget:
plot = pg.PlotWidget() plot = pg.PlotWidget()
plot.setLabel("left", left_label) plot.setLabel("left", left_label)
plot.setLabel("bottom", bottom_label) plot.setLabel("bottom", bottom_label)
@@ -329,7 +194,7 @@ class MotionProfileWidget(QWidget):
plot.getAxis("left").enableAutoSIPrefix(False) plot.getAxis("left").enableAutoSIPrefix(False)
plot.getAxis("bottom").enableAutoSIPrefix(False) plot.getAxis("bottom").enableAutoSIPrefix(False)
plot.setMinimumHeight(180) plot.setMinimumHeight(180)
plot.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding) plot.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding)
return plot return plot
def _add_edge_marker(self, plot, x_arr, y_arr, energy_arr, edge_marker): 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) plot.addItem(scatter, ignoreBounds=True)
text = f"{self.element.currentElement()}, {self.edge.currentText()}-edge, {self.edge_energy:0.1f} eV" 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) label.setPos(x_marker, y_marker)
plot.addItem(label, ignoreBounds=True) plot.addItem(label, ignoreBounds=True)
@@ -375,7 +240,18 @@ class MotionProfileWidget(QWidget):
self.update_plot() self.update_plot()
@SafeSlot() @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: if scan_name is not None:
self.scan_name = scan_name self.scan_name = scan_name
if scan_parameters is not None: if scan_parameters is not None:
@@ -396,15 +272,17 @@ class MotionProfileWidget(QWidget):
self.plot_pos.clear() self.plot_pos.clear()
self.plot_vel.clear() self.plot_vel.clear()
start_angle = self.energy_to_angle(self.scan_parameters["stop"]) start_angle = self._energy_to_angle(self.scan_parameters["stop"])
stop_angle = self.energy_to_angle(self.scan_parameters["start"]) stop_angle = self._energy_to_angle(self.scan_parameters["start"])
if start_angle == stop_angle:
return
if "xas_simple_scan" in self.scan_name: if "xas_simple_scan" in self.scan_name:
amp = (stop_angle - start_angle) / 2 amp = (stop_angle - start_angle) / 2
two_pi_f = 2 * np.pi * 1 / (2 * self.scan_parameters["scan_time"]) 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) 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) vel = two_pi_f * amp * np.sin(x_time * two_pi_f)
energy = self.angle_to_energy(pos) energy = self._angle_to_energy(pos)
vel_e = self.velocity_to_energy_per_s(pos, vel) vel_e = self._velocity_to_energy_per_s(pos, vel)
self.plot_pos.plot(x_time, energy) self.plot_pos.plot(x_time, energy)
self._add_edge_marker(self.plot_pos, x_time, energy, energy, "_") self._add_edge_marker(self.plot_pos, x_time, energy, energy, "_")
@@ -420,7 +298,7 @@ class MotionProfileWidget(QWidget):
return return
if self.scan_parameters["scan_time"] == 0: if self.scan_parameters["scan_time"] == 0:
return 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( pos, vel, t = compute_spline(
start_angle, start_angle,
stop_angle, stop_angle,
@@ -429,8 +307,8 @@ class MotionProfileWidget(QWidget):
self.scan_parameters["scan_time"], self.scan_parameters["scan_time"],
) )
x_time = np.cumsum(t) / 1000 x_time = np.cumsum(t) / 1000
energy = self.angle_to_energy(pos) energy = self._angle_to_energy(pos)
vel_e = self.velocity_to_energy_per_s(pos, vel) vel_e = self._velocity_to_energy_per_s(pos, vel)
time_flipped = -np.flip(x_time) + x_time[-1] time_flipped = -np.flip(x_time) + x_time[-1]
energy_flipped = np.flip(energy) energy_flipped = np.flip(energy)
@@ -470,7 +348,7 @@ class MotionProfileWidget(QWidget):
curve.setPen(pg.mkPen(color=color, width=2)) curve.setPen(pg.mkPen(color=color, width=2))
self.element.apply_theme(theme) 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: if self.d_spacing is None:
return 0 return 0
if energy <= 0: if energy <= 0:
@@ -479,7 +357,7 @@ class MotionProfileWidget(QWidget):
val = wl / (2 * self.d_spacing * 1e-10) val = wl / (2 * self.d_spacing * 1e-10)
return np.asin(val) / np.pi * 180 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: if self.d_spacing is None:
return 0 return 0
if isinstance(angle, np.ndarray): 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) wl = 2 * self.d_spacing * 1e-10 * np.sin(angle / 180 * np.pi)
return C * H / (E * wl) 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: if self.d_spacing is None:
return 0 return 0
return ( return (
@@ -503,84 +381,10 @@ class MotionProfileWidget(QWidget):
) * velocity ) * 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__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
apply_theme("light") apply_theme("light")
dispatcher = BECDispatcher(gui_id="scan_control_advanced") dispatcher = BECDispatcher(gui_id="scan_control_advanced")
win = ScanControlAdvanced() win = ScanControlXAS()
win.show() win.show()
sys.exit(app.exec_()) sys.exit(app.exec_())
@@ -0,0 +1 @@
{'files': ['scan_control_xas.py']}
@@ -5,17 +5,17 @@ from bec_widgets.utils.bec_designer import designer_material_icon
from qtpy.QtDesigner import QDesignerCustomWidgetInterface from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget from qtpy.QtWidgets import QWidget
from .scan_control_advanced import ScanControlAdvanced from .scan_control_xas import ScanControlXAS
DOM_XML = """ DOM_XML = """
<ui language='c++'> <ui language='c++'>
<widget class='ScanControlAdvanced' name='scan_control_advanced'> <widget class='ScanControlXAS' name='scan_control_xas'>
</widget> </widget>
</ui> </ui>
""" """
class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no cover class ScanControlXASPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._form_editor = None self._form_editor = None
@@ -23,7 +23,7 @@ class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no c
def createWidget(self, parent): def createWidget(self, parent):
if parent is None: if parent is None:
return QWidget() return QWidget()
t = ScanControlAdvanced(parent) t = ScanControlXAS(parent)
return t return t
def domXml(self): def domXml(self):
@@ -33,10 +33,10 @@ class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no c
return "" return ""
def icon(self): def icon(self):
return designer_material_icon(ScanControlAdvanced.ICON_NAME) return designer_material_icon(ScanControlXAS.ICON_NAME)
def includeFile(self): def includeFile(self):
return "scan_control_advanced" return "scan_control_xas"
def initialize(self, form_editor): def initialize(self, form_editor):
self._form_editor = form_editor self._form_editor = form_editor
@@ -48,10 +48,10 @@ class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no c
return self._form_editor is not None return self._form_editor is not None
def name(self): def name(self):
return "ScanControlAdvanced" return "ScanControlXAS"
def toolTip(self): def toolTip(self):
return "ScanControlAdvanced" return "ScanControlXAS"
def whatsThis(self): def whatsThis(self):
return self.toolTip() return self.toolTip()