wip from beamline, 22.05.2026
CI for debye_bec / test (push) Successful in 1m2s

This commit is contained in:
x01da
2026-05-22 10:50:55 +02:00
parent c998c06b41
commit 8be85f7a9a
5 changed files with 170 additions and 31 deletions
@@ -416,3 +416,16 @@ def pipe_geometries() -> list[dict[str, np.ndarray]]:
}
)
return pipes
def table_to_smpl_pos(table: str) -> float:
"""
Return the sample position based on the table name.
Args:
table (str): Table name, e.g. ES1 or ES2
"""
if table in bl.tables:
return bl.tables[table]["smpl"]
raise ValueError(f"Table {table} not found in beamline parameter file")
@@ -2,6 +2,7 @@
Digital Twin: Custom BEC widget to support the beamline alignment.
"""
import socket
import sys
from pathlib import Path
from typing import Literal, cast
@@ -20,6 +21,7 @@ from qtpy.QtCore import Qt, QTimer
from qtpy.QtGui import QFont
from qtpy.QtWidgets import (
QApplication,
QComboBox,
QDialog,
QDialogButtonBox,
QHBoxLayout,
@@ -46,12 +48,14 @@ from debye_bec.bec_widgets.widgets.digital_twin.calculations.calc_varia import (
mo1_bragg_angle,
mo1_energy_resolution,
sldi_gap_to_acc,
table_to_smpl_pos,
)
from debye_bec.bec_widgets.widgets.digital_twin.panels.input_panel import InputPanel
from debye_bec.bec_widgets.widgets.digital_twin.panels.mover_panel import MoverPanel
from debye_bec.bec_widgets.widgets.digital_twin.panels.plots import SideviewPlot, SurfacePlots
from debye_bec.bec_widgets.widgets.digital_twin.panels.settings_panel import SettingsPanel
from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict
from debye_bec.bec_widgets.widgets.digital_twin.types import BeamlineId, ConfigDict
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ComboBox, InputNumberField
logger = bec_logger.logger
@@ -70,16 +74,25 @@ class DigitalTwin(BECWidget, QWidget):
super().__init__(parent=parent, theme_update=True, *arg, **kwargs)
self.get_bec_shortcuts()
self.beamline = self.get_beamline_id()
# Debugging, override beamline!
# self.beamline = BeamlineId.X10DA
# Check if devices are all in config
self.check_config()
self.bec_dispatcher.connect_slot(self.check_config, MessageEndpoints.device_config_update())
self.check_bec_config()
self.bec_dispatcher.connect_slot(
self.check_bec_config, MessageEndpoints.device_config_update()
)
logger.info(f"Start Digital Twin with beamline {self.beamline} and all devices available")
central = QWidget()
self.root_layout = QHBoxLayout(central)
self.input_widget = QWidget()
self.input_layout = QVBoxLayout(self.input_widget)
self.input = InputPanel()
self.input = InputPanel(self.beamline)
self.settings = SettingsPanel()
self.input_layout.addWidget(self.input)
self.input_layout.addWidget(self.settings)
@@ -113,7 +126,11 @@ class DigitalTwin(BECWidget, QWidget):
self.input.fm_rotx.value_changed_connect(self.calc_assistant)
self.input.fm_focx.value_changed_connect(self.calc_assistant)
self.input.fm_focy.value_changed_connect(self.calc_assistant)
self.input.smpl.value_changed_connect(self.calc_assistant)
match self.input.smpl:
case InputNumberField():
self.input.smpl.value_changed_connect(self.calc_assistant)
case ComboBox():
self.input.smpl.activated_connect(self.calc_assistant)
self.input.adapt_reality.clicked_connect(self.adapt_reality)
self.settings.load_offsets.clicked_connect(self.load_offsets)
@@ -144,8 +161,30 @@ class DigitalTwin(BECWidget, QWidget):
self.surface_plots.apply_theme(theme)
self.mover.apply_theme(theme)
def get_beamline_id(self) -> BeamlineId:
"""
Based on the bec servers hostname, tries to extract the beamline
identifier (e.g. x01da, x10da, etc).
Raises:
ValueError if beamline cannot be extracted from hostname or beamline not implemented.
"""
bec_hostname = socket.gethostname()
start = bec_hostname.find("x")
if start != -1:
beamline = bec_hostname[start : start + 5]
match beamline:
case "x01da":
return BeamlineId.X01DA
case "x10da":
return BeamlineId.X10DA
case _:
raise ValueError(f"Not implemented beamline {beamline}")
else:
raise ValueError(f"Failed to extract beamline from bec server hostname {bec_hostname}")
@SafeSlot()
def check_config(self, *args):
def check_bec_config(self, *args):
"""
Checks the BEC config and opens a window if not all necessary
devices are loaded in the config. If called from a slot from
@@ -161,7 +200,7 @@ class DigitalTwin(BECWidget, QWidget):
"sldi_gapy",
"cm_trx",
"cm_try",
"cm_bnd_radius",
"cm_bnd",
"cm_rotx",
"mo1_bragg",
"mo1_trx",
@@ -171,18 +210,28 @@ class DigitalTwin(BECWidget, QWidget):
"bm1_try",
"fm_trx",
"fm_try",
"fm_bnd_radius",
"fm_bnd",
"fm_rotx",
"fm_roty",
"fm_rotz",
"sl2_centery",
"sl2_gapy",
"bm2_try",
"ot_try",
"ot_rotx",
"es0wi_try",
"ot_es1_trz",
]
if self.beamline == "x01da": # X01DA specific devices
devices.extend(
[
"cm_bnd_radius",
"fm_bnd_radius",
"sl2_centery",
"sl2_gapy",
"ot_try",
"ot_es1_trz",
]
)
if self.beamline == "x10da": # X10DA specific devices
devices.extend(["mo1_rotx", "es1_try", "es1ic0_try", "es1ic1_try", "es1ic2_try"])
while True:
missing = [d for d in devices if d not in self.dev]
if not missing:
@@ -327,6 +376,13 @@ class DigitalTwin(BECWidget, QWidget):
assert cm_trx is not None, f"No cm_trx found for given stripe {cm_stripe}!"
assert fm_trx is not None, f"No fm_trx found for given stripe {fm_stripe}!"
match self.input.smpl:
case InputNumberField():
smpl = self.input.smpl.value()
case ComboBox():
table = self.input.smpl.currentText()
smpl = table_to_smpl_pos(table)
config: ConfigDict = {
"energy": self.input.energy.value(),
"h_acc": self.input.sldi_hacc.value(),
@@ -342,7 +398,7 @@ class DigitalTwin(BECWidget, QWidget):
"fm_trx": fm_trx,
"fm_qy": fm_qy,
"fm_gain_height": 1,
"smpl": self.input.smpl.value(),
"smpl": smpl,
}
# Apply offsets
@@ -508,9 +564,45 @@ class DigitalTwin(BECWidget, QWidget):
self.input.fm_focus.set_current_text("Manual")
fm_rotx_real = 2 * pos["cm_rotx"] - pos["fm_rotx"]
self.input.fm_rotx.set_number(fm_rotx_real)
self.input.smpl.set_number(pos["ot_es1_trz"])
match self.input.smpl:
case InputNumberField():
self.input.smpl.set_number(pos["ot_es1_trz"])
case ComboBox():
table = self.ask_table_selection()
self.input.smpl.set_current_text(table)
self.calc_assistant(identifier="init")
def ask_table_selection(self) -> str | None:
"""
Opens a dialog asking the user to select a table (ES1 or ES2).
Returns:
The selected table ('ES1' or 'ES2'), or None if the user cancelled.
"""
dialog = QDialog()
dialog.setWindowTitle("Select Table")
layout = QVBoxLayout(dialog)
text = QLabel("Select the current table in use.")
combo = QComboBox()
combo.addItems(["ES1", "ES2"])
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(text)
layout.addWidget(combo)
layout.addWidget(buttons)
if dialog.exec() == QDialog.DialogCode.Accepted:
return combo.currentText()
return None
@SafeSlot()
def load_offsets(self, *_, recalculate: bool = True):
"""
@@ -784,7 +876,12 @@ class DigitalTwin(BECWidget, QWidget):
Literal["Defocused", "Focused", "Manual"], self.input.fm_focus.currentText()
)
fm_stripe = self.input.fm_stripe.currentText()
smpl = self.input.smpl.value()
match self.input.smpl:
case InputNumberField():
smpl = self.input.smpl.value()
case ComboBox():
table = self.input.smpl.currentText()
smpl = table_to_smpl_pos(table)
sldi_hacc = self.input.sldi_hacc.value() * 1e-3
sldi_vacc = self.input.sldi_vacc.value() * 1e-3
fm_focx = self.input.fm_focx.value()
@@ -2,9 +2,12 @@
Panel for user inputs of the digital twin widget
"""
from typing import Union
# pylint: disable=E0611
from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget
from debye_bec.bec_widgets.widgets.digital_twin.types import BeamlineId
from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import (
Button,
ComboBox,
@@ -15,9 +18,14 @@ from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import (
class InputPanel(QWidget):
"""Panel for user inputs of the digital twin widget"""
"""
Panel for user inputs of the digital twin widget
def __init__(self, parent=None):
Args:
beamline (BeamlineId): Beamline id type
"""
def __init__(self, beamline: BeamlineId, parent=None):
super().__init__(parent)
self._layout = QVBoxLayout(self)
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
@@ -91,9 +99,11 @@ class InputPanel(QWidget):
)
# Focusing Mirror
self.fm_stripe = ComboBox(
"fm_stripe", "Stripe", ["Rh (toroid)", "Rh (flat)", "Pt (toroid)", "Pt (flat)"]
)
stripes: dict[BeamlineId, list[str]] = {
BeamlineId.X01DA: ["Rh (toroid)", "Rh (flat)", "Pt (toroid)", "Pt (flat)"],
BeamlineId.X10DA: ["Rh (toroid)", "Pt (toroid)"],
}
self.fm_stripe = ComboBox("fm_stripe", "Stripe", stripes[beamline])
self.fm_focus = ComboBox("fm_focus", "Focus Type", ["Manual", "Focused", "Defocused"])
self.fm_rotx = InputNumberField(
"fm_rotx",
@@ -144,18 +154,9 @@ class InputPanel(QWidget):
# Sample
self.cm_fm_harm_suppr = NumberIndicator("Total Suppression Factor at x eV", "", decimals=0)
self.smpl = InputNumberField(
"smpl",
"Sample Position",
unit="mm",
init=23511,
decimals=0,
single_step=100,
ll=23000,
hl=30000,
)
self.smpl = self._create_smpl(beamline)
# Assemble complete assitant group
# Assemble complete assistant group
self.input_group = Group(
"User Input",
[
@@ -172,3 +173,19 @@ class InputPanel(QWidget):
self._layout.addWidget(self.input_group)
self._layout.addStretch()
def _create_smpl(self, beamline: BeamlineId) -> Union[InputNumberField, ComboBox]:
match beamline:
case BeamlineId.X01DA:
return InputNumberField(
"smpl",
"Sample Position",
unit="mm",
init=23511,
decimals=0,
single_step=100,
ll=23000,
hl=30000,
)
case BeamlineId.X10DA:
return ComboBox("smpl", "Sample Position", ["ES1", "ES2"])
@@ -1,8 +1,18 @@
"""Types used for the beamline config and for plotting data"""
from enum import Enum
from typing import TypedDict
class BeamlineId(str, Enum):
"""
Identifier for supported beamlines.
"""
X01DA = "x01da"
X10DA = "x10da"
class ConfigDict(TypedDict):
"""
Typed dictionary representing the beamline configuration.
@@ -304,6 +304,8 @@ smpl = sample(name="EH-SMPL", center=[0, 23365, sourceHeight])
smpl2 = sample(name="EH-SMPL2", center=[0, 27500, sourceHeight])
tables = {}
# Vacuum pipes
# DN40CF ID = 35 mm oder 37 mm
# DN50CF ID = 47.5 mm