From 8be85f7a9a2154f2e699d86f043d3cb6924e6a53 Mon Sep 17 00:00:00 2001 From: x01da Date: Fri, 22 May 2026 10:50:55 +0200 Subject: [PATCH] wip from beamline, 22.05.2026 --- .../digital_twin/calculations/calc_varia.py | 13 ++ .../widgets/digital_twin/digital_twin.py | 127 +++++++++++++++--- .../digital_twin/panels/input_panel.py | 49 ++++--- .../bec_widgets/widgets/digital_twin/types.py | 10 ++ .../widgets/digital_twin/x01da_parameters.py | 2 + 5 files changed, 170 insertions(+), 31 deletions(-) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py index 0cc43f0..e660369 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_varia.py @@ -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") diff --git a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py index 7b7ee59..00d1959 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -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() diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py index 093dae9..9773fb8 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/input_panel.py @@ -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"]) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/types.py b/debye_bec/bec_widgets/widgets/digital_twin/types.py index 674b59f..d5c0393 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/types.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/types.py @@ -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. diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py b/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py index 630d7fc..214cb7d 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/x01da_parameters.py @@ -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