From 8be85f7a9a2154f2e699d86f043d3cb6924e6a53 Mon Sep 17 00:00:00 2001 From: x01da Date: Fri, 22 May 2026 10:50:55 +0200 Subject: [PATCH 01/10] 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 -- 2.54.0 From a6814eff2bcb668d9e6cd232457558a48156ce61 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 26 May 2026 08:57:36 +0200 Subject: [PATCH 02/10] wip --- .../widgets/digital_twin/digital_twin.py | 38 ++- .../digital_twin/panels/mover_panel.py | 4 +- .../widgets/digital_twin/x10da_offsets.yaml | 0 .../widgets/digital_twin/x10da_parameters.py | 269 ++++++++++++++++++ 4 files changed, 299 insertions(+), 12 deletions(-) create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py 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 00d1959..dcb083b 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -59,8 +59,6 @@ from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ComboB logger = bec_logger.logger -OFFSET_FILE = "debye_bec/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml" - class DigitalTwin(BECWidget, QWidget): """ @@ -79,6 +77,9 @@ class DigitalTwin(BECWidget, QWidget): # Debugging, override beamline! # self.beamline = BeamlineId.X10DA + self.offset_fie = None + self.set_offset_file() + # Check if devices are all in config self.check_bec_config() self.bec_dispatcher.connect_slot( @@ -183,6 +184,16 @@ class DigitalTwin(BECWidget, QWidget): else: raise ValueError(f"Failed to extract beamline from bec server hostname {bec_hostname}") + def set_offset_file(self): + """ + Depending on the beamline, set the offset file path accordingly. + """ + files: dict[BeamlineId, str] = { + BeamlineId.X01DA: "debye_bec/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml", + BeamlineId.X10DA: "superxas_bec/superxas_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml", + } + self.offset_file = files[self.beamline] + @SafeSlot() def check_bec_config(self, *args): """ @@ -236,7 +247,7 @@ class DigitalTwin(BECWidget, QWidget): missing = [d for d in devices if d not in self.dev] if not missing: break - dialog = QDialog() + dialog = QDialog(self) dialog.setWindowTitle("Digital Twin - Config Check") dialog.setFixedWidth(400) layout = QVBoxLayout() @@ -568,26 +579,33 @@ class DigitalTwin(BECWidget, QWidget): case InputNumberField(): self.input.smpl.set_number(pos["ot_es1_trz"]) case ComboBox(): - table = self.ask_table_selection() + table = self.ask_table_selection(self.input.smpl.currentText()) self.input.smpl.set_current_text(table) self.calc_assistant(identifier="init") - def ask_table_selection(self) -> str | None: + def ask_table_selection(self, preset=None) -> str | None: """ Opens a dialog asking the user to select a table (ES1 or ES2). + Args: + preset (str): Preset text for the table, either 'ES1' or 'ES2'. + Returns: The selected table ('ES1' or 'ES2'), or None if the user cancelled. """ - dialog = QDialog() + dialog = QDialog(self) dialog.setWindowTitle("Select Table") layout = QVBoxLayout(dialog) text = QLabel("Select the current table in use.") combo = QComboBox() - combo.addItems(["ES1", "ES2"]) + choice = ["ES1", "ES2"] + combo.addItems(choice) + if preset is not None: + if preset in choice: + combo.setCurrentText(preset) buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel @@ -615,9 +633,9 @@ class DigitalTwin(BECWidget, QWidget): if self.offsets == {}: # Load offsets - file = Path(OFFSET_FILE) + file = Path(self.offset_file) if not file.exists(): - raise FileNotFoundError(f"Offset file not found: {OFFSET_FILE}") + raise FileNotFoundError(f"Offset file not found: {self.offset_file}") with file.open("r", encoding="utf-8") as f: data = yaml.safe_load(f) @@ -660,7 +678,7 @@ class DigitalTwin(BECWidget, QWidget): intro_label.setWordWrap(True) layout.addWidget(intro_label) - file = QLabel(OFFSET_FILE) + file = QLabel(self.offset_file) file.setWordWrap(True) font = QFont() font.setItalic(True) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py index 3f26f06..ec82186 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py @@ -189,7 +189,7 @@ class MoverPanel(QWidget): ) self.mover_widgets.append(self.es0wi_try) - self.es0_mov_group = Group("Expperimental Station 0", [self.es0wi_try]) + self.es0_mov_group = Group("Experimental Station 0", [self.es0wi_try]) # Experimental Station 1 self.ot_es1_trz = MoveWidget( @@ -197,7 +197,7 @@ class MoverPanel(QWidget): ) self.mover_widgets.append(self.ot_es1_trz) - self.es1_mov_group = Group("Expperimental Station 1", [self.ot_es1_trz]) + self.es1_mov_group = Group("Experimental Station 1", [self.ot_es1_trz]) # Assemble complete mover group self.mover_group = Group( diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml b/debye_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py b/debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py new file mode 100644 index 0000000..5bd76c3 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/digital_twin/x10da_parameters.py @@ -0,0 +1,269 @@ +""" +X10DA / SuperXAS Beamline Parameters. +This file describes the parameter of each component of the SuperXAS beamline +to be used for raytracing and geometrical calculations. +""" + +from collections import namedtuple + +import numpy as np +import xrt.backends.raycing.materials as rm + +# XRT definitions +filterBeryl = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType] +filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType] +filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType] + +stripeSi = rm.Material("Si", rho=2.33) # pyright: ignore[reportArgumentType] +stripePt = rm.Material("Pt", rho=21.45) # pyright: ignore[reportArgumentType] +stripeRh = rm.Material("Rh", rho=12.41) # pyright: ignore[reportArgumentType] +stripeCr = rm.Material("Cr", rho=7.14) # pyright: ignore[reportArgumentType] +stripePyrex = rm.Material( + "Si", rho=2.20 +) # Use Si as bare element and the density of SiO2 # pyright: ignore[reportArgumentType] + +si111_1 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # first xtal surface +si311_1 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # first xtal surface +si333_1 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # first xtal surface +si511_1 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # first xtal surface +si111_2 = rm.CrystalSi(hkl=(1, 1, 1), tK=77) # second xtal surface +si311_2 = rm.CrystalSi(hkl=(3, 1, 1), tK=77) # second xtal surface +si333_2 = rm.CrystalSi(hkl=(3, 3, 3), tK=77) # second xtal surface +si511_2 = rm.CrystalSi(hkl=(5, 1, 1), tK=77) # second xtal surface + +filterDiamond = rm.Material("C", rho=3.52, kind="plate") # pyright: ignore[reportArgumentType] +filterBe = rm.Material("Be", rho=1.85, kind="plate") # pyright: ignore[reportArgumentType] +filterSi3N4 = rm.Material( + ["Si", "N"], quantities=[3, 4], rho=3.44, kind="plate" +) # pyright: ignore[reportArgumentType] +filterAl = rm.Material("Al", rho=2.69, kind="plate") # pyright: ignore[reportArgumentType] +filterGraphite = rm.Material("C", rho=2.266, kind="plate") # pyright: ignore[reportArgumentType] + +# General parameters +sourceHeight = 0 + +# Synchrotron +synchrotron = namedtuple( + "synchrotron", ["eE", "eI", "eEspread", "eEpsilonX", "eEpsilonZ", "betaX", "betaZ"] +) + +sls1 = synchrotron( + eE=2.4, eI=0.4, eEspread=0.878e-3, eEpsilonX=5.63, eEpsilonZ=0.007, betaX=0.45, betaZ=14.4 +) + +sls2 = synchrotron( + eE=2.7, eI=0.4, eEspread=1.147e-3, eEpsilonX=0.156, eEpsilonZ=0.01, betaX=0.18, betaZ=4.6 +) + +# Source +bendingMagnet = namedtuple("bendingMagnet", ["name", "center", "sync", "B0"]) + +sls1_14t = bendingMagnet(name="FE-BM-SLS1-1.4T", center=(0, 0, 0), sync=sls1, B0=1.4) + +sls2_21t = bendingMagnet(name="FE-BM-SLS2-2.1T", center=(0, 0, 0), sync=sls2, B0=2.1) + +sls2_35t = bendingMagnet(name="FE-BM-SLS2-3.5T", center=(0, 0, 0), sync=sls2, B0=3.5) + +sls2_50t = bendingMagnet(name="FE-BM-SLS2-5.0T", center=(0, 0, 0), sync=sls2, B0=5.0) + +# FE slits +slits = namedtuple("slits", ["name", "center", "maxDivH", "maxDivV"]) + +feSlits = slits(name="FE-SLITS", center=(0, 5290, sourceHeight), maxDivH=1.8e-3, maxDivV=0.8e-3) + +# Filters +filt = namedtuple( + "filt", ["name", "center", "pitch", "limPhysX", "limPhysY", "surface", "material", "thickness"] +) + +feWindow = filt( + name="FE-WINDOW", + center=(0.0, 6158, sourceHeight), + pitch=np.pi / 2, + limPhysX=(-6, 6), + limPhysY=(-3.0, 3.0), + surface="None", + material=filterDiamond, + thickness=0.1, +) +feWindow = feWindow._replace( + surface="CVD Diamond window {0:0.0f} $\mu$m".format(feWindow.thickness * 1e3) +) + +feFilt = filt( + name="FE-FI", + center=(0.0, 6590, sourceHeight), + pitch=np.pi / 2, + limPhysX=(-15, 15), + limPhysY=(-10, 10), + surface="None", + material=filterGraphite, + thickness=0.25, +) +feFilt = feFilt._replace(surface="Graphite filter {0:0.0f} $\mu$m".format(feFilt.thickness * 1e3)) + +# Collimating mirror +collimatingMirror = namedtuple( + "collimatingMirror", + [ + "name", + "center", + "surface", + "material", + "limPhysX", + "limPhysY", + "limOptX", + "limOptY", + "R", + "pitch", + "jack1", + "jack2", + "jack3", + "tx1", + "tx2", + ], +) + +cm = collimatingMirror( + name="FE-CM", + center=[0, 7618, sourceHeight], + surface=("Si", "Pt", "Rh"), + material=(stripeSi, stripePt, stripeRh), + limPhysX=(-30, 30), + limPhysY=(-600, 600), + limOptX=((-21, -8, 5), (-11, 2, 21)), + limOptY=((-500, -500, -500), (500, 500, 500)), + R=[3e6, 15e6], + pitch=[1.4e-3, 4.5e-3], + jack1=[0.0, 7210.0, 0.0], # Tripod X, Y, Z (global) + jack2=[-210.0, 8310.0, 0.0], + jack3=[210.0, 8310.0, 0.0], + tx1=[0.0, -575.5], # X-Stage 1 [x, y] (local) + tx2=[0.0, 575], +) # X-Stage 2 + +apertures = namedtuple("apertures", ["name", "center", "opening"]) + +fePS = apertures( + name="FE-PS", center=[0, 8760, sourceHeight], opening=[-39 / 2, 39 / 2, -10, 29] +) # left, right, bottom, top + +opWbBsBlock = apertures( + name="OP-WB-BS-BLOCK", center=[0.0, 13606 - 135, sourceHeight], opening=[-18.0, 18.0, 42, 76] +) # left, right, bottom, top + +opSlits = apertures( + name="OP-SLITS", center=[0, 14145 - 135, sourceHeight], opening=[-35 / 2, 35 / 2, 47.5, 82.5] +) + +# Monochromator +monochromator = namedtuple( + "monochromator", + [ + "name", + "center", + "xtal", + "material1", + "material2", + "xtalWidth", + "xtalOffsetX", + "xtalLength1", + "xtalLength2", + "xtalGap", + "rotOffset", + "heightOffset", + "braggLim", + "jack1", + "jack2", + "jack3", + "tx", + ], +) + +mo1 = monochromator( + name="OP-CCM1", + center=[0.0, 11670 - 135, sourceHeight], + xtal=("Si311", "Si111"), + material1=(si311_1, si111_1), + material2=(si311_2, si111_2), + xtalWidth=(20, 20), + xtalOffsetX=(-19.2, 19.2), + xtalLength1=(60, 60), + xtalLength2=(60, 60), + xtalGap=(8, 8), + rotOffset=6, # not sure what it is + heightOffset=8.5, # not sure what it is + braggLim=[4, 35], + jack1=[0.0, 11350.0, 0.0], # Tripod not available! + jack2=[-400.0, 12350.0, 0.0], + jack3=[400.0, 12350.0, 0.0], + tx=0.0, +) # X-Stage [x] + +# Focusing mirror +focusingMirror = namedtuple( + "focusingMirror", + [ + "name", + "center", + "surfaceToroid", + "materialToroid", + "limPhysXToroid", + "limPhysYToroid", + "limOptXToroid", + "limOptYToroid", + "R", + "pitch", + "r", + "xToroid", + "hToroid", + "jack1", + "jack2", + "jack3", + "tx1", + "tx2", + ], +) + +fm = focusingMirror( + name="OP-FM", + center=[0.0, 15580 - 135, sourceHeight], + surfaceToroid=("Rh (toroid)", "Pt (toroid)"), + materialToroid=(stripeRh, stripePt), + limPhysXToroid=(-54.0, 54.0), + limPhysYToroid=(-565.0, 565.0), + limOptXToroid=((4.865, -40.882), (43.388, -4.865)), + limOptYToroid=((-500.0, -500.0), (500.0, 500.0)), + R=[3e6, 15e6], + pitch=[1.4e-3, 4.5e-3], + r=[30, 20], + xToroid=[24.126, -22, 874], # offset in local x + hToroid=[7.0, 11.3], # depth of the cylinder at x = xCylinder1 and x = xCylinder2. + jack1=[0.0, 14980.0, 0.0], + jack2=[-75.0, 16180.0, 0.0], + jack3=[75.0, 16180.0, 0.0], + tx1=[0.0, -575.0], # X-Stage 1 [x, y] + tx2=[0.0, 575.0], +) # X-Stage 2 [x, y] + +ehWindow = filt( + name="EH-WINDOW", + center=(0.0, 22225 - 135, sourceHeight), + pitch=np.pi / 2, + limPhysX=(-10.0, 10.0), + limPhysY=(17.5, 92.5), + surface="None", + material=filterBe, + thickness=0.25, +) +ehWindow = ehWindow._replace( + surface="Beryllium window {0:0.0f} $\mu$m".format(ehWindow.thickness * 1e3) +) + +# Sample +sample = namedtuple("sample", ["name", "center"]) + +smpl = sample(name="OP-SMPL", center=[0, 24000 - 135, sourceHeight]) + +ES1 = sample(name="EH-ES1", center=[0, 24000, sourceHeight]) +ES2 = sample(name="EH-ES2", center=[0, 25000, sourceHeight]) -- 2.54.0 From 5c0bd34641866b36b62503c46246fa3b1a26b44e Mon Sep 17 00:00:00 2001 From: x01da Date: Wed, 27 May 2026 08:21:16 +0200 Subject: [PATCH 03/10] wip-from-the-beamline --- .../bec_widgets/widgets/digital_twin/calculations/__init__.py | 0 debye_bec/bec_widgets/widgets/digital_twin/panels/__init__.py | 0 debye_bec/bec_widgets/widgets/digital_twin/widgets/__init__.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/calculations/__init__.py create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/panels/__init__.py create mode 100644 debye_bec/bec_widgets/widgets/digital_twin/widgets/__init__.py diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/__init__.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/__init__.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/bec_widgets/widgets/digital_twin/widgets/__init__.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/__init__.py new file mode 100644 index 0000000..e69de29 -- 2.54.0 From fbadb44b338d0c0d87708a67352c642e85d23d32 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 9 Jun 2026 15:41:10 +0200 Subject: [PATCH 04/10] fix(digital-twin): use exact string comparisons Replace substring membership checks with equality for modes, stripes, scene names, and plot identifiers so partial strings cannot select the wrong branch. --- .../digital_twin/calculations/calc_positions.py | 6 +++--- .../digital_twin/calculations/calc_sideview.py | 2 +- .../digital_twin/calculations/calc_surfaces.py | 2 +- .../digital_twin/calculations/calc_varia.py | 14 +++++++------- .../widgets/digital_twin/digital_twin.py | 16 ++++++++-------- .../widgets/digital_twin/panels/plots.py | 16 ++++++++-------- .../widgets/digital_twin/widgets/move_widget.py | 2 +- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py index 79cb809..885dcb3 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_positions.py @@ -183,7 +183,7 @@ def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]: if cfg["fm_stripe"] in ("Rh (toroid)", "Pt (toroid)"): # TRY - if cfg["fm_stripe"] in "Rh (toroid)": + if cfg["fm_stripe"] == "Rh (toroid)": r = bl.fm.r[0] h_cyl = bl.fm.hToroid[0] else: # PT toroid @@ -199,7 +199,7 @@ def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]: pos["fm_try"] = {"value": fm_height} # TRX - if cfg["fm_stripe"] in "Rh (toroid)": + if cfg["fm_stripe"] == "Rh (toroid)": x_cyl = -bl.fm.xToroid[0] else: x_cyl = -bl.fm.xToroid[1] @@ -213,7 +213,7 @@ def calc_positions(cfg: ConfigDict) -> dict[str, dict[str, float]]: pos["fm_try"] = {"value": fm_height} # TRX - if cfg["fm_stripe"] in "Rh (flat)": + if cfg["fm_stripe"] == "Rh (flat)": x_flat = -bl.fm.xFlat[0] else: x_flat = -bl.fm.xFlat[1] diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py index afa0b29..7ec677d 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_sideview.py @@ -27,7 +27,7 @@ def calc_sideview(cfg: ConfigDict) -> DataDict: beam["y"].append(bl.sourceHeight) beam["x"].append(bl.cm.center[1]) # CM beam["y"].append(bl.sourceHeight) - if cfg["mo1_mode"] in "Monochromatic": + if cfg["mo1_mode"] == "Monochromatic": diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono dy = diag * np.sin(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) dz = diag * np.cos(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"])) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py index dcac3dd..0b90b93 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/calculations/calc_surfaces.py @@ -65,7 +65,7 @@ def calc_surfaces(cfg: ConfigDict) -> SurfaceDict: height_beam = 2 * bl.cm.center[1] * np.tan(cfg["v_acc"]) w = height_beam / np.sin(cfg["mo1_bragg"]) - if cfg["mo1_mode"] in "Monochromatic": + if cfg["mo1_mode"] == "Monochromatic": out["mo1_1"]["x"] = [ xtal_pos - width_beam / 2, xtal_pos + width_beam / 2, 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 e660369..b70e751 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 @@ -258,7 +258,7 @@ def fm_ideal_pitch( """ p = bl.fm.center[1] # posFM q = smpl - bl.fm.center[1] # dist posFM to posEX - if fm_focus in "Defocused": + if fm_focus == "Defocused": assert sldi_hacc is not None, "sldi_hacc must be provided for Defocused mode" assert sldi_vacc is not None, "sldi_vacc must be provided for Defocused mode" assert fm_focx is not None, "fm_focx must be provided for Defocused mode" @@ -294,9 +294,9 @@ def cm_critical_angle(cm_stripe: Literal["Si", "Pt", "Rh"], energy) -> float: Returns: float: Critical angle in rad """ - if cm_stripe in "Si": + if cm_stripe == "Si": stripe = bl.stripeSi - elif cm_stripe in "Pt": + elif cm_stripe == "Pt": stripe = bl.stripePt else: stripe = bl.stripeRh @@ -320,15 +320,15 @@ def mirror_surface_geometries( dict[str, tuple[float, float, float, float]]: Dictionary mapping surface names to tuples of (x, y, width, height). """ - if mirror in "cm": + if mirror == "cm": surface = bl.cm.surface lim_opt_x = bl.cm.limOptX lim_opt_y = bl.cm.limOptY - elif mirror in "fm_toroid": + elif mirror == "fm_toroid": surface = bl.fm.surfaceToroid lim_opt_x = bl.fm.limOptXToroid lim_opt_y = bl.fm.limOptYToroid - elif mirror in "fm_flat": + elif mirror == "fm_flat": surface = bl.fm.surfaceFlat lim_opt_x = bl.fm.limOptXFlat lim_opt_y = bl.fm.limOptYFlat @@ -354,7 +354,7 @@ def mo_surface_geometries( dict[str, tuple[float, float, float, float]]: Dictionary mapping surface names to tuples of (x, y, width, height). """ - if mo in "mo1": + if mo == "mo1": xtal = bl.mo1.xtal xtal_width = bl.mo1.xtalWidth xtal_offset_x = bl.mo1.xtalOffsetX 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 dcb083b..9ae7ace 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -369,10 +369,10 @@ class DigitalTwin(BECWidget, QWidget): ConfigDict: config of the assistant """ fm_focus = self.input.fm_focus.currentText() - if fm_focus in "Manual": + if fm_focus == "Manual": fm_rotx = self.input.fm_rotx.value() fm_qy = None - elif fm_focus in "Focused": + elif fm_focus == "Focused": fm_rotx = self.input.fm_rotx_ideal.value() fm_qy = None else: # Focused @@ -710,13 +710,13 @@ class DigitalTwin(BECWidget, QWidget): selection of the focus strategy. """ fm_focus = self.input.fm_focus.currentText() - if fm_focus in "Manual": + if fm_focus == "Manual": self.input.fm_rotx.setVisible(True) self.input.fm_rotx_ideal.setVisible(True) self.input.fm_focx.setVisible(False) self.input.fm_focy.setVisible(False) self.input.fm_rotx_ideal.setLabel("Incidence Angle for focused beam") - elif fm_focus in "Focused": + elif fm_focus == "Focused": self.input.fm_rotx.setVisible(False) self.input.fm_rotx_ideal.setVisible(True) self.input.fm_focx.setVisible(False) @@ -769,7 +769,7 @@ class DigitalTwin(BECWidget, QWidget): """ fm_stripe = self.input.fm_stripe.currentText() fm_focus = self.input.fm_focus.currentText() - if fm_focus in "Manual": + if fm_focus == "Manual": fm_rotx = -self.input.fm_rotx.value() * 1e-3 else: fm_rotx = -self.input.fm_rotx_ideal.value() * 1e-3 @@ -855,11 +855,11 @@ class DigitalTwin(BECWidget, QWidget): Calculates bragg angle in rad """ xtal = self.input.mo1_xtal.currentText() - if xtal in "Si(111)": + if xtal == "Si(111)": d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si111.read(cached=True)[ "mo1_bragg_crystal_d_spacing_si111" ]["value"] - elif xtal in "Si(311)": + elif xtal == "Si(311)": d_spacing = self.dev.mo1_bragg.crystal.d_spacing_si311.read(cached=True)[ "mo1_bragg_crystal_d_spacing_si311" ]["value"] @@ -877,7 +877,7 @@ class DigitalTwin(BECWidget, QWidget): Updates the monochromator input group based on the selection of the mode. """ - if self.input.mo1_mode.currentText() in "Monochromatic": + if self.input.mo1_mode.currentText() == "Monochromatic": self.input.mo1_xtal.setVisible(True) self.input.mo1_bragg_angle.setVisible(True) self.input.mo1_eres.setVisible(True) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py index 5e17c85..2eb0cf9 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py @@ -74,7 +74,7 @@ class SurfacePlots(QWidget): # Create surfaces for idx, scene in enumerate(self.surfaces): for name, _ in self.surfaces[scene].items(): - if scene in "assistant": + if scene == "assistant": brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern) pen = pg.mkPen( QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine @@ -130,7 +130,7 @@ class SurfacePlots(QWidget): for idx, scene in enumerate(self.surfaces): for name, _ in self.surfaces[scene].items(): - if scene in "assistant": + if scene == "assistant": brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern) pen = pg.mkPen( QColor(*self.colors[idx], 255), width=1, style=Qt.PenStyle.DashLine @@ -165,13 +165,13 @@ class SurfacePlots(QWidget): self.texts.append(text) for name, plot in self.plots.items(): - if name in "cm": + if name == "cm": plot_surface(plot["widget"], mirror_surface_geometries("cm")) - elif name in "mo1_1": + elif name == "mo1_1": plot_surface(plot["widget"], mo_surface_geometries("mo1", 0)) - elif name in "mo1_2": + elif name == "mo1_2": plot_surface(plot["widget"], mo_surface_geometries("mo1", 1)) - elif name in "fm": + elif name == "fm": plot_surface(plot["widget"], mirror_surface_geometries("fm_flat")) plot_surface(plot["widget"], mirror_surface_geometries("fm_toroid")) else: @@ -223,7 +223,7 @@ class SideviewPlot(QWidget): self.walls = [] for idx, scene in enumerate(self.data.keys()): - if scene in "assistant": + if scene == "assistant": pen = pg.mkPen(color=self.colors[idx], width=2, style=Qt.PenStyle.DotLine) z_value = 2 else: @@ -281,7 +281,7 @@ class SideviewPlot(QWidget): self.text_color = (0, 0, 0) for idx, scene in enumerate(self.data): - if scene in "assistant": + if scene == "assistant": brush = QBrush(QColor(*self.colors[idx], 255), Qt.BrushStyle.DiagCrossPattern) pen = pg.mkPen(QColor(*self.colors[idx], 255), width=3, style=Qt.PenStyle.DashLine) else: diff --git a/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py index 8266b58..08e87d5 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py @@ -485,7 +485,7 @@ class MoveWidget(QWidget): def _on_motion_finished(self): """Finished a movement""" target = self.target - if self.status not in Status.ERROR: + if self.status != Status.ERROR: if abs(self.fb - target) <= self.deadband: self._set_status(Status.IN_POSITION) else: -- 2.54.0 From 0bfa9c10b80da82801fe5fb9a167a1df3cb322fa Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 9 Jun 2026 15:42:26 +0200 Subject: [PATCH 05/10] refactor(digital-twin): compact widget layout Wrap side panels in vertical scroll areas, tighten row and group spacing, use a balanced 1:1 plot area, and avoid assigning a second top-level layout. Also aligns MotionWorker signal signatures with the slots connected to them as part of the widget cleanup. --- .../widgets/digital_twin/digital_twin.py | 60 ++++++-- .../digital_twin/panels/input_panel.py | 5 +- .../digital_twin/panels/mover_panel.py | 5 +- .../widgets/digital_twin/panels/plots.py | 6 +- .../digital_twin/panels/settings_panel.py | 5 +- .../digital_twin/widgets/move_widget.py | 38 +++--- .../digital_twin/widgets/qt_widgets.py | 129 ++++-------------- 7 files changed, 100 insertions(+), 148 deletions(-) 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 9ae7ace..f9c3fb8 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -24,10 +24,13 @@ from qtpy.QtWidgets import ( QComboBox, QDialog, QDialogButtonBox, + QFrame, QHBoxLayout, QLabel, QPlainTextEdit, QPushButton, + QScrollArea, + QSizePolicy, QStyle, QVBoxLayout, QWidget, @@ -88,32 +91,47 @@ class DigitalTwin(BECWidget, QWidget): logger.info(f"Start Digital Twin with beamline {self.beamline} and all devices available") - central = QWidget() - self.root_layout = QHBoxLayout(central) + self.content_widget = QWidget(self) + self.root_layout = QHBoxLayout(self.content_widget) + self.root_layout.setContentsMargins(6, 6, 6, 6) + self.root_layout.setSpacing(6) self.input_widget = QWidget() self.input_layout = QVBoxLayout(self.input_widget) + self.input_layout.setContentsMargins(4, 4, 4, 4) + self.input_layout.setSpacing(6) self.input = InputPanel(self.beamline) self.settings = SettingsPanel() self.input_layout.addWidget(self.input) self.input_layout.addWidget(self.settings) + self.input_layout.addStretch() self.plot_widget = QWidget() self.plot_layout = QVBoxLayout(self.plot_widget) + self.plot_layout.setContentsMargins(4, 4, 4, 4) + self.plot_layout.setSpacing(6) self.sideview_plot = SideviewPlot() self.surface_plots = SurfacePlots() - self.plot_layout.addWidget(self.sideview_plot) - self.plot_layout.addWidget(self.surface_plots) + self.plot_layout.addWidget(self.sideview_plot, stretch=1) + self.plot_layout.addWidget(self.surface_plots, stretch=1) + self.plot_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.mover = MoverPanel(self.dev) - self.root_layout.addWidget(self.input_widget, alignment=Qt.AlignmentFlag.AlignTop) - self.root_layout.addWidget(self.plot_widget, alignment=Qt.AlignmentFlag.AlignTop) - self.root_layout.addWidget(self.mover, alignment=Qt.AlignmentFlag.AlignTop) + self.input_scroll = self._scroll_area(self.input_widget, min_width=320, max_width=360) + self.mover_scroll = self._scroll_area(self.mover, min_width=380, max_width=460) - self.setLayout(self.root_layout) + self.root_layout.addWidget(self.input_scroll) + self.root_layout.addWidget(self.plot_widget, stretch=1) + self.root_layout.addWidget(self.mover_scroll) + widget_layout = self.layout() + if widget_layout is None: + widget_layout = QVBoxLayout(self) + widget_layout.setContentsMargins(0, 0, 0, 0) + widget_layout.setSpacing(0) + widget_layout.addWidget(self.content_widget) self.setWindowTitle("Digital Twin") - self.resize(1800, 800) + self.resize(1450, 760) self.input.energy.value_changed_connect(self.calc_assistant) self.input.sldi_hacc.value_changed_connect(self.calc_assistant) @@ -145,12 +163,26 @@ class DigitalTwin(BECWidget, QWidget): self.load_offsets(recalculate=False) self.calc_assistant(identifier="init") - # Timer: update plots every 1 second + # Timer: update reality plots every 1 second self._timer = QTimer(self) - self._timer.setInterval(100) + self._timer.setInterval(1000) self._timer.timeout.connect(self.calc_reality) self._timer.start() + @staticmethod + def _scroll_area(widget: QWidget, min_width: int, max_width: int) -> QScrollArea: + """Wrap a side panel in a compact vertical scroll area.""" + scroll = QScrollArea() + scroll.setWidgetResizable(True) + widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding) + scroll.setWidget(widget) + scroll.setFrameShape(QFrame.Shape.NoFrame) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + scroll.setMinimumWidth(min_width) + scroll.setMaximumWidth(max_width) + scroll.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) + return scroll + def apply_theme(self, theme: Literal["dark", "light"]): """ Apply the theme @@ -202,8 +234,8 @@ class DigitalTwin(BECWidget, QWidget): BEC dispatcher whenever there is a config update, stop the timer that updates the plot in the background. """ - reload = (args[0] if args else {}).get("action") == "reload" - if reload: + reload_config = (args[0] if args else {}).get("action") == "reload" + if reload_config: self._timer.stop() devices = [ "abs", @@ -294,7 +326,7 @@ class DigitalTwin(BECWidget, QWidget): running_app = QApplication.instance() if running_app is not None: running_app.exit(0) - if reload: + if reload_config: self._timer.start() @SafeSlot() 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 9773fb8..2620b9b 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 @@ -5,7 +5,7 @@ Panel for user inputs of the digital twin widget from typing import Union # pylint: disable=E0611 -from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget +from qtpy.QtWidgets import 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 ( @@ -28,7 +28,8 @@ class InputPanel(QWidget): def __init__(self, beamline: BeamlineId, parent=None): super().__init__(parent) self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + self._layout.setContentsMargins(4, 4, 4, 4) + self._layout.setSpacing(4) # Adapt to reality self.adapt_reality = Button(label_button="Adapt to reality", enabled=True) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py index ec82186..fcf3703 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/mover_panel.py @@ -5,7 +5,7 @@ Panel to move an axis to a certain position from typing import Literal # pylint: disable=E0611 -from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget +from qtpy.QtWidgets import QVBoxLayout, QWidget from debye_bec.bec_widgets.widgets.digital_twin.widgets.move_widget import ( AbsorberWidget, @@ -20,7 +20,8 @@ class MoverPanel(QWidget): def __init__(self, dev, parent=None): super().__init__(parent) self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + self._layout.setContentsMargins(4, 4, 4, 4) + self._layout.setSpacing(4) self.mover_widgets = [] diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py index 2eb0cf9..8cbafce 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/plots.py @@ -33,6 +33,8 @@ class SurfacePlots(QWidget): def __init__(self, parent=None): super().__init__(parent=parent) self._layout = QHBoxLayout(self) + self._layout.setContentsMargins(4, 4, 4, 4) + self._layout.setSpacing(6) self.surfaces: dict[str, SurfaceDict] = { "assistant": { @@ -202,7 +204,8 @@ class SideviewPlot(QWidget): def __init__(self, parent=None): super().__init__(parent=parent) self._layout = QVBoxLayout(self) - # self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + self._layout.setContentsMargins(4, 4, 4, 4) + self._layout.setSpacing(0) self.plot_widget = pg.PlotWidget() self.plot_widget.getAxis("bottom").enableAutoSIPrefix(False) @@ -243,7 +246,6 @@ class SideviewPlot(QWidget): self.plot_widget.hideButtons() self._layout.addWidget(self.plot_group) - self._layout.addStretch() self.plot_vacuum_pipes() self.plot_walls() diff --git a/debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py b/debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py index 8947ea3..88ce3e1 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/panels/settings_panel.py @@ -3,7 +3,7 @@ Settings panel for the digital twin widget """ # pylint: disable=E0611 -from qtpy.QtWidgets import QLayout, QVBoxLayout, QWidget +from qtpy.QtWidgets import QVBoxLayout, QWidget from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ( Button, @@ -18,7 +18,8 @@ class SettingsPanel(QWidget): def __init__(self, parent=None): super().__init__(parent) self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + self._layout.setContentsMargins(4, 4, 4, 4) + self._layout.setSpacing(4) # Reload offsets self.load_offsets = Button(label="Load Offsets", label_button="Load", enabled=True) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py index 08e87d5..65efd74 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/widgets/move_widget.py @@ -128,8 +128,8 @@ class MotionWorker(QObject): """ position_changed = Signal(float) - error = Signal(bool) # True = error - finished = Signal(bool) # True = reached target, False = stopped + error = Signal() + finished = Signal() def __init__(self, dev, motor, target_pos: float): super().__init__() @@ -284,7 +284,7 @@ class MotionWorker(QObject): fb = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"] if abs(fb - surv_ax["old_value"]) > surv_ax["abs_tol"]: self.dev[self.motor].stop() - self.error.emit(1) + self.error.emit() break self.finished.emit() @@ -315,35 +315,33 @@ class MoveWidget(QWidget): self.decimals = decimals layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(4, 0, 4, 0) + layout.setSpacing(4) # Name self.label = QLabel(label) - self.label.setFixedWidth(100) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(76) self.label.setWordWrap(True) layout.addWidget(self.label) # Target self.target_label = QLabel("-") - self.target_label.setFixedWidth(100) + self.target_label.setFixedWidth(84) layout.addWidget(self.target_label) # Feedback self.fb_label = QLabel("-") - self.fb_label.setFixedWidth(100) + self.fb_label.setFixedWidth(84) layout.addWidget(self.fb_label) # Status icon self.status_icon = StatusIcon() - self.status_icon.setFixedWidth(30) - self.status_icon.setContentsMargins(0, 0, 10, 0) + self.status_icon.setFixedWidth(24) layout.addWidget(self.status_icon) # Start / Stop button self.btn_action = QPushButton("Move") - self.btn_action.setFixedWidth(90) + self.btn_action.setFixedWidth(64) self.btn_action.setFixedHeight(20) self.btn_action.clicked.connect(self._on_button_clicked) layout.addWidget(self.btn_action) @@ -522,35 +520,33 @@ class AbsorberWidget(QWidget): self.text_color = (0, 0, 0) layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(4, 0, 4, 0) + layout.setSpacing(4) # Name self.label = QLabel(label) - self.label.setFixedWidth(100) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(76) self.label.setWordWrap(True) layout.addWidget(self.label) # Blank self.blank_label = QLabel("") - self.blank_label.setFixedWidth(100) + self.blank_label.setFixedWidth(84) layout.addWidget(self.blank_label) # Feedback self.fb_label = QLabel("-") - self.fb_label.setFixedWidth(100) + self.fb_label.setFixedWidth(84) layout.addWidget(self.fb_label) # Blank icon self.blank_icon = QLabel("") - self.blank_icon.setFixedWidth(30) - self.blank_icon.setContentsMargins(0, 0, 10, 0) + self.blank_icon.setFixedWidth(24) layout.addWidget(self.blank_icon) # Open self.btn_action = QPushButton("Open") - self.btn_action.setFixedWidth(90) + self.btn_action.setFixedWidth(64) self.btn_action.setFixedHeight(20) self.btn_action.clicked.connect(self._on_button_clicked) layout.addWidget(self.btn_action) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py index 5a0f59b..311c63b 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py @@ -21,11 +21,17 @@ from qtpy.QtWidgets import ( QWidget, ) +LABEL_WIDTH = 118 +ROW_MARGINS = (4, 0, 4, 0) +ROW_SPACING = 6 + class Group(QGroupBox): def __init__(self, label, widgets): super().__init__(label) self.layout = QVBoxLayout(self) # type: ignore + self.layout.setContentsMargins(6, 6, 6, 6) + self.layout.setSpacing(4) for widget in widgets: self.layout.addWidget(widget) # type: ignore @@ -34,16 +40,14 @@ class NumberIndicator(QWidget): def __init__(self, label="", unit=None, highlight=False, decimals=3): super().__init__() layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) self.label = QLabel(label) - self.label.setFixedWidth(140) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(LABEL_WIDTH) self.label.setWordWrap(True) layout.addWidget(self.label) self.val = QLabel("-") self.val.setAlignment(Qt.AlignTop) # type: ignore - # self.val.setFixedWidth(140) layout.addWidget(self.val) self.unit = unit self.highlight = highlight @@ -85,12 +89,11 @@ class InputNumberField(QWidget): ): super().__init__() layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) self.identifier = identifier self.label = QLabel(label) - self.label.setFixedWidth(140) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(LABEL_WIDTH) self.label.setWordWrap(True) layout.addWidget(self.label) self.val = QDoubleSpinBox() @@ -102,7 +105,6 @@ class InputNumberField(QWidget): self.val.setSuffix(" " + unit) if prefix is not None: self.val.setPrefix(prefix + " ") - # self.val.setFixedWidth(140) layout.addWidget(self.val) def set_number(self, number): @@ -124,19 +126,18 @@ class InputNumberField(QWidget): class ComboBox(QWidget): - def __init__(self, identifier="", label="", enums=[]): + def __init__(self, identifier="", label="", enums=None): super().__init__() layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) self.identifier = identifier self.label = QLabel(label) - self.label.setFixedWidth(140) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(LABEL_WIDTH) self.label.setWordWrap(True) layout.addWidget(self.label) self.value = QComboBox() - for entry in enums: + for entry in enums or []: self.value.addItem(entry) layout.addWidget(self.value) @@ -168,15 +169,15 @@ class Button(QWidget): def __init__(self, label=None, label_button: str = "", enabled=False): super().__init__() layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) if label is not None: self.label = QLabel(label) - self.label.setFixedWidth(140) + self.label.setFixedWidth(LABEL_WIDTH) layout.addWidget(self.label) self.button = QPushButton(label_button) if label is not None: - self.button.setFixedWidth(160) + self.button.setFixedWidth(130) self.enable_button(enabled) layout.addWidget(self.button) @@ -204,11 +205,10 @@ class TextIndicator(QWidget): def __init__(self, label): super().__init__() layout = QHBoxLayout(self) - layout.setContentsMargins(10, 0, 0, 0) - layout.setSpacing(0) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) self.label = QLabel(label) - self.label.setFixedWidth(140) - self.label.setContentsMargins(0, 0, 10, 0) + self.label.setFixedWidth(LABEL_WIDTH) self.label.setWordWrap(True) layout.addWidget(self.label) self.text = QLabel("-") @@ -223,84 +223,3 @@ class TextIndicator(QWidget): def setColor(self, color: str): self.text.setStyleSheet(f"QLabel {{color:{color}}}") - - -# class Button(QWidget): -# def __init__(self, label, label_button): -# super().__init__() -# layout = QHBoxLayout(self) -# layout.setContentsMargins(10, 0, 0, 0) -# layout.setSpacing(0) -# self.label = QLabel(label) -# self.label.setFixedWidth(150) -# layout.addWidget(self.label) -# self.button = QPushButton(label_button) -# self.button.setStyleSheet("color: black; background-color: dodgerblue;") -# self.button.setFixedWidth(160) -# layout.addWidget(self.button) - -# def set_on_press(self, func): -# """Connect a function to the button press.""" -# self.button.clicked.connect(func) - -# def enable_button(self): -# self.button.setEnabled(True) -# self.button.setStyleSheet("color: black; background-color: dodgerblue;") - -# def disable_button(self): -# self.button.setEnabled(False) -# self.button.setStyleSheet("color: black; background-color: grey;") - -# def set_button_text(self, text): -# self.button.setText(text) - -# class LED(QWidget): -# def __init__(self, states, colors, label): -# super().__init__() -# self.states = states -# self.colors = colors -# layout = QHBoxLayout(self) -# layout.setContentsMargins(10, 0, 0, 0) -# layout.setSpacing(0) -# self.label = QLabel(label) -# self.label.setFixedWidth(150) -# layout.addWidget(self.label) -# self.led = QLabel() -# self.led.setFixedWidth(160) -# layout.addWidget(self.led) - -# def apply_color(self, val): -# color = self.colors[self.states.index(val)] -# self.led.setStyleSheet(f"background-color: {color}; border: 1px solid black;") - -# class InputTextField(QWidget): -# def __init__(self, topic, label): -# super().__init__() -# self.topic = topic -# layout = QHBoxLayout(self) -# layout.setContentsMargins(10, 0, 0, 0) -# layout.setSpacing(0) -# self.label = QLabel(label) -# self.label.setFixedWidth(140) -# self.label.setContentsMargins(0, 0, 10, 0) -# self.label.setWordWrap(True) -# layout.addWidget(self.label) -# self.val = QLineEdit() -# self.val.setPlaceholderText('0') -# # self.val.setFixedWidth(140) -# layout.addWidget(self.val) - -# def set_text(self, text): -# self.val.setText(text) - -# def has_focus(self) -> bool: -# return self.val.hasFocus() - -# def text(self) -> str: -# return self.val.text() - -# def set_on_return(self, func): -# """Connect a function to the Enter/Return key press.""" -# self.val.returnPressed.connect( -# partial(func, self.val, self.topic, lambda: self.val.text()) -# ) -- 2.54.0 From 95d920e6615880fcee2158e3e1ce31746180ded2 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 9 Jun 2026 15:42:39 +0200 Subject: [PATCH 06/10] fix(digital-twin): resolve offsets file from package Load x01da_offsets.yaml relative to digital_twin.py instead of the process working directory so the widget starts correctly from any launch location. --- .../bec_widgets/widgets/digital_twin/digital_twin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 f9c3fb8..d11b0de 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -62,6 +62,8 @@ from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ComboB logger = bec_logger.logger +OFFSET_FILE = Path(__file__).with_name("x01da_offsets.yaml") + class DigitalTwin(BECWidget, QWidget): """ @@ -665,11 +667,10 @@ class DigitalTwin(BECWidget, QWidget): if self.offsets == {}: # Load offsets - file = Path(self.offset_file) - if not file.exists(): - raise FileNotFoundError(f"Offset file not found: {self.offset_file}") + if not OFFSET_FILE.exists(): + raise FileNotFoundError(f"Offset file not found: {OFFSET_FILE}") - with file.open("r", encoding="utf-8") as f: + with OFFSET_FILE.open("r", encoding="utf-8") as f: data = yaml.safe_load(f) if not isinstance(data, dict): @@ -710,7 +711,7 @@ class DigitalTwin(BECWidget, QWidget): intro_label.setWordWrap(True) layout.addWidget(intro_label) - file = QLabel(self.offset_file) + file = QLabel(str(OFFSET_FILE)) file.setWordWrap(True) font = QFont() font.setItalic(True) -- 2.54.0 From 112b359e9e915c5ead4a32abcdc3d41bd34da78a Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 10 Jun 2026 15:48:16 +0200 Subject: [PATCH 07/10] wip --- .../widgets/digital_twin/digital_twin.py | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) 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 d11b0de..41b8833 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -62,8 +62,8 @@ from debye_bec.bec_widgets.widgets.digital_twin.widgets.qt_widgets import ComboB logger = bec_logger.logger -OFFSET_FILE = Path(__file__).with_name("x01da_offsets.yaml") - +OFFSET_FILE_X01DA = Path(__file__).with_name("x01da_offsets.yaml") +OFFSET_FILE_X10DA = Path(__file__).with_name("x10da_offsets.yaml") class DigitalTwin(BECWidget, QWidget): """ @@ -78,12 +78,15 @@ class DigitalTwin(BECWidget, QWidget): self.get_bec_shortcuts() self.beamline = self.get_beamline_id() - # Debugging, override beamline! # self.beamline = BeamlineId.X10DA - self.offset_fie = None - self.set_offset_file() + self.offset_file = Path() + match self.beamline: + case "x01da": + self.offset_file = OFFSET_FILE_X01DA + case "x10da": + self.offset_file = OFFSET_FILE_X10DA # Check if devices are all in config self.check_bec_config() @@ -218,16 +221,6 @@ class DigitalTwin(BECWidget, QWidget): else: raise ValueError(f"Failed to extract beamline from bec server hostname {bec_hostname}") - def set_offset_file(self): - """ - Depending on the beamline, set the offset file path accordingly. - """ - files: dict[BeamlineId, str] = { - BeamlineId.X01DA: "debye_bec/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml", - BeamlineId.X10DA: "superxas_bec/superxas_bec/bec_widgets/widgets/digital_twin/x10da_offsets.yaml", - } - self.offset_file = files[self.beamline] - @SafeSlot() def check_bec_config(self, *args): """ @@ -667,10 +660,10 @@ class DigitalTwin(BECWidget, QWidget): if self.offsets == {}: # Load offsets - if not OFFSET_FILE.exists(): - raise FileNotFoundError(f"Offset file not found: {OFFSET_FILE}") - - with OFFSET_FILE.open("r", encoding="utf-8") as f: + if not self.offset_file.exists(): + raise FileNotFoundError(f"Offset file not found: {self.offset_file}") + + with self.offset_file.open("r", encoding="utf-8") as f: data = yaml.safe_load(f) if not isinstance(data, dict): @@ -711,7 +704,7 @@ class DigitalTwin(BECWidget, QWidget): intro_label.setWordWrap(True) layout.addWidget(intro_label) - file = QLabel(str(OFFSET_FILE)) + file = QLabel(str(self.offset_file)) file.setWordWrap(True) font = QFont() font.setItalic(True) -- 2.54.0 From 1d0b6aa1b56200d29722d5f7acf1156b5861492c Mon Sep 17 00:00:00 2001 From: hitz_s Date: Thu, 11 Jun 2026 10:33:53 +0200 Subject: [PATCH 08/10] Adding simulated devices for development --- debye_bec/device_configs/x01da_sim.yaml | 302 ++++++++++++++++++++++++ debye_bec/devices/sim/absorber.py | 74 ++++++ debye_bec/devices/sim/mo1_bragg.py | 94 ++++++++ debye_bec/devices/sim/positioner.py | 27 +++ 4 files changed, 497 insertions(+) create mode 100644 debye_bec/device_configs/x01da_sim.yaml create mode 100644 debye_bec/devices/sim/absorber.py create mode 100644 debye_bec/devices/sim/mo1_bragg.py create mode 100644 debye_bec/devices/sim/positioner.py diff --git a/debye_bec/device_configs/x01da_sim.yaml b/debye_bec/device_configs/x01da_sim.yaml new file mode 100644 index 0000000..ff1467e --- /dev/null +++ b/debye_bec/device_configs/x01da_sim.yaml @@ -0,0 +1,302 @@ + +################################### +## Frontend Absorber ## +################################### + +abs: + readoutPriority: baseline + description: Frontend Absorber + deviceClass: debye_bec.devices.sim.absorber.SimAbsorber + deviceConfig: {} + enabled: true + +################################### +## Frontend Slits ## +################################### + +sldi_gapx: + readoutPriority: baseline + description: Front-end slit diaphragm X-gap + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 4.75 + enabled: true + +sldi_gapy: + readoutPriority: baseline + description: Front-end slit diaphragm Y-gap + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 1.5 + enabled: true + +################################### +## Collimating Mirror ## +################################### + +cm_bnd: + readoutPriority: baseline + description: Collimating Mirror bender + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -2573 + enabled: true + +cm_bnd_radius: + readoutPriority: baseline + description: Collimating Mirror Bending Radius (simulated) + deviceClass: ophyd.Signal + deviceConfig: + value: 5.51 + readOnly: true + enabled: true + +cm_rotx: + readoutPriority: baseline + description: Collimating Mirror Pitch + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -2.498 + enabled: true + +cm_rotz: + readoutPriority: baseline + description: Collimating Mirror Roll + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 0 + enabled: true + +cm_roty: + readoutPriority: baseline + description: Collimating Mirror Yaw + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 0 + enabled: true + +cm_trx: + readoutPriority: baseline + description: Collimating Mirror Center Point X + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -1.5 + enabled: true + +cm_try: + readoutPriority: baseline + description: Collimating Mirror Center Point Y + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 0.2 + enabled: true + + +################################### +## Monochromator ## +################################### + +mo1_bragg: + readoutPriority: baseline + deviceClass: debye_bec.devices.sim.mo1_bragg.SimMo1BraggPositioner + deviceConfig: {} + enabled: true + +mo1_try: + readoutPriority: baseline + description: Monochromator Y Translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 30.4 + enabled: true + +mo1_trx: + readoutPriority: baseline + description: Monochromator X Translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 21.2 + enabled: true + +################################### +## Optics Slits + Beam Monitor 1 ## +################################### + +bm1_try: + readoutPriority: baseline + description: Beam Monitor 1 Y-translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 105 + enabled: true + +sl1_centery: + readoutPriority: baseline + description: Optics slits 1 Y-center + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 52 + enabled: true + +sl1_gapy: + readoutPriority: baseline + description: Optics slits 1 Y-gap + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 5 + enabled: true + +################################### +## Focusing Mirror ## +################################### + +fm_bnd: + readoutPriority: baseline + description: Focusing Mirror bender + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -30500 + enabled: true + +fm_bnd_radius: + readoutPriority: baseline + description: Focusing Mirror Bending Radius (simulated) + deviceClass: ophyd.Signal + deviceConfig: + value: 6.55 + readOnly: true + enabled: true + +fm_rotx: + readoutPriority: baseline + description: Focusing Morror Pitch + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -2.578 + enabled: true + +fm_roty: + readoutPriority: baseline + description: Focusing Morror Yaw + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -0.038 + enabled: true + +fm_rotz: + readoutPriority: baseline + description: Focusing Morror Roll + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -0.004 + enabled: true + +fm_trx: + readoutPriority: baseline + description: Focusing Morror Center Point X + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -49.5 + enabled: true + +fm_try: + readoutPriority: baseline + description: Focusing Morror Center Point Y + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 51.75 + enabled: true + +################################### +## Optics Slits + Beam Monitor 2 ## +################################### + +bm2_try: + readoutPriority: baseline + description: Beam Monitor 2 Y-translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 115 + enabled: true + +sl2_centery: + readoutPriority: baseline + description: Optics slits 2 Y-center + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 58.9 + enabled: true + +sl2_gapy: + readoutPriority: baseline + description: Optics slits 2 Y-gap + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 5 + enabled: true + +################################### +## Optical Table ## +################################### + +ot_es1_trz: + readoutPriority: baseline + description: Optical Table ES1 Z-Translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 23508 + enabled: true + +ot_try: + readoutPriority: baseline + description: Optical Table Y-Translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 60.185 + enabled: true + +ot_rotx: + readoutPriority: baseline + description: Optical Table Pitch + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: -0.2 + enabled: true + +################################### +## Exit Window ## +################################### + +es0wi_try: + readoutPriority: baseline + description: End Station 0 Exit Window Y-translation + deviceClass: debye_bec.devices.sim.positioner.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 5 + enabled: true diff --git a/debye_bec/devices/sim/absorber.py b/debye_bec/devices/sim/absorber.py new file mode 100644 index 0000000..fb3b492 --- /dev/null +++ b/debye_bec/devices/sim/absorber.py @@ -0,0 +1,74 @@ +"""Frontend Absorber""" + +from __future__ import annotations + +import enum +from typing import TYPE_CHECKING + +from ophyd import Component as Cpt +from ophyd.sim import SynSignal +from ophyd_devices import CompareStatus, DeviceStatus +from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase + + +if TYPE_CHECKING: + from bec_lib.devicemanager import ScanInfo + +class AbsorberError(Exception): + """Absorber specific exception""" + +class STATUS(int, enum.Enum): + """Absorber States""" + + MOVING_CLOSE = 0 + OPEN = 1 + MOVING_OPEN = 2 + CLOSED = 3 + NOT_ENABLED = 4 + TIMEOUT_CLOSE = 5 + TIMEOUT_OPEN = 6 + CLOSE_LS_LOST = 7 + OPEN_LS_LOST = 8 + CLOSE_LS_NOT_FREE = 9 + OPEN_LS_NOT_FREE = 10 + ERROR_LS = 11 + TO_CONNECT = 12 + MAN_OPEN = 13 + UNDEFINED = 14 + +class SimAbsorber(PSIDeviceBase): + """Simulated class for the Frontend Absorber""" + + USER_ACCESS = ["open", "close"] + + request = Cpt(SynSignal, name="request", kind="config", doc="Open/Close Absorber") + status = Cpt(SynSignal, name="status", kind="normal", doc="Absorber Status") + status_string = Cpt(SynSignal, name="status_string", kind="normal", doc="Absorber Status") + + def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): + super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs) + + self.timeout_for_move = 10 + # Set initial simulated state + self.status.put(STATUS.CLOSED) + self.status_string.put("CLOSED") + + def open(self) -> DeviceStatus | None: + """Open the Absorber""" + if self.status.get() == STATUS.CLOSED: + self.status.put(STATUS.OPEN) + self.status_string.put("OPEN") + status_open = CompareStatus(self.status, STATUS.OPEN, timeout=self.timeout_for_move) + return status_open + else: + return None + + def close(self) -> DeviceStatus | None: + """Close the Absorber""" + if self.status.get() == STATUS.OPEN: + self.status.put(STATUS.CLOSED) + self.status_string.put("CLOSED") + status_close = CompareStatus(self.status, STATUS.CLOSED, timeout=self.timeout_for_move) + return status_close + else: + return None diff --git a/debye_bec/devices/sim/mo1_bragg.py b/debye_bec/devices/sim/mo1_bragg.py new file mode 100644 index 0000000..7378d93 --- /dev/null +++ b/debye_bec/devices/sim/mo1_bragg.py @@ -0,0 +1,94 @@ +"""Module for the Mo1 Bragg positioner""" + +from ophyd import Component as Cpt +from ophyd import ( + Device, + DeviceStatus, +) +from ophyd.sim import SynSignal, Signal +from ophyd_devices.sim.sim_positioner import ReadOnlySignal +from ophyd.ophydobj import Kind + +from debye_bec.devices.sim.positioner import SimPositionerWithInitVal + + +class SimMo1BraggCrystalCurrent(Device): + + d_spacing = Cpt(Signal, value=3.1344024, kind=Kind.normal) + bragg_off = Cpt(Signal, value=1.01063, kind=Kind.normal) + phi_off = Cpt(Signal, value=0.07078, kind=Kind.normal) + azm_off = Cpt(Signal, value=0.0425, kind=Kind.normal) + miscut = Cpt(Signal, value=0.05, kind=Kind.normal) + xtal = Cpt(Signal, value=0, kind=Kind.normal) + xtal_string = Cpt(Signal, value="Si(111)", kind=Kind.normal) + + +class SimMo1BraggCrystal(Device): + + current = Cpt(SimMo1BraggCrystalCurrent, "") + d_spacing_si111 = Cpt(Signal, value=3.1344024, kind=Kind.normal) + d_spacing_si311 = Cpt(Signal, value=1.6375, kind=Kind.normal) + + +class SimMo1BraggPositioner(SimPositionerWithInitVal): + + crystal = Cpt(SimMo1BraggCrystal, "") + angle = Cpt(Signal, value=6.310127056633, kind=Kind.normal) + + ####### Energy PVs ####### + + readback = Cpt(SynSignal, name="readback", kind="hinted") + setpoint = Cpt(SynSignal, name="setpoint", kind="normal") + motor_is_moving = Cpt(SynSignal, name="motor_is_moving", kind="normal") + low_lim = Cpt(SynSignal, name="low_lim", kind="config") + high_lim = Cpt(SynSignal, name="high_lim", kind="config") + velocity = Cpt(SynSignal, name="velocity", kind="config") + + ####### Move Command PVs ####### + + move_abs = Cpt(SynSignal, name="move_abs", kind="config") + move_stop = Cpt(SynSignal, name="move_stop", kind="config") + + SUB_READBACK = "readback" + _default_sub = SUB_READBACK + SUB_PROGRESS = "progress" + + def __init__(self, *, name: str, prefix: str = "", **kwargs): + super().__init__(name=name, prefix=prefix, **kwargs) + + self.tolerance.put(0.001) + + self._set_status = None + self.readback.put(17995) + self.setpoint.put(17995) + self.motor_is_moving.put(0) + self.low_lim.put(-100.0) + self.high_lim.put(100.0) + self.velocity.put(1.0) + + @property + def position(self): + return self.readback.get() + + @property + def egu(self) -> str: + """Return the engineering units of the positioner""" + return "eV" + + def set(self, new_pos: float, timeout: float = 10.0) -> DeviceStatus: + """Move to new_pos immediately (no real motion).""" + self.motor_is_moving.put(1) + self.readback.put(new_pos) + self.setpoint.put(new_pos) + self._position = new_pos + self.motor_is_moving.put(0) + self._run_subs(sub_type=self.SUB_READBACK, value=new_pos, obj=self) + + status = DeviceStatus(self) + status._finished() + return status + + def stop(self, success: bool = False): + self.motor_is_moving.put(0) + if self._set_status is not None and not self._set_status.done: + self._set_status._finished(success=success) \ No newline at end of file diff --git a/debye_bec/devices/sim/positioner.py b/debye_bec/devices/sim/positioner.py new file mode 100644 index 0000000..119cb36 --- /dev/null +++ b/debye_bec/devices/sim/positioner.py @@ -0,0 +1,27 @@ +from ophyd_devices.sim.sim_positioner import SimPositioner + +class SimPositionerWithInitVal(SimPositioner): + """SimPositioner with support for an initial readback value via sim_init. + + Example YAML config: + samx: + deviceClass: my_plugin.devices.SimPositionerWithInitVal + deviceConfig: + sim_init: + init_val: 1234.0 + enabled: true + """ + + def __init__(self, *args, sim_init: dict = None, **kwargs): + # Extract init_val before passing sim_init to the parent, + # since the parent's set_init doesn't know about it. + self._init_val = None + if sim_init: + self._init_val = sim_init.pop("init_val", None) + + super().__init__(*args, sim_init=sim_init if sim_init else None, update_frequency=10, **kwargs) + + self.tolerance.put(0.001) + + if self._init_val is not None: + self._update_readback(self._init_val) \ No newline at end of file -- 2.54.0 From ab1553b8f227bd73ff643b5e99086785820d5544 Mon Sep 17 00:00:00 2001 From: hitz_s Date: Thu, 11 Jun 2026 10:34:06 +0200 Subject: [PATCH 09/10] style fix buttons --- .../bec_widgets/widgets/digital_twin/widgets/qt_widgets.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py b/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py index 311c63b..b127418 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/widgets/qt_widgets.py @@ -176,8 +176,6 @@ class Button(QWidget): self.label.setFixedWidth(LABEL_WIDTH) layout.addWidget(self.label) self.button = QPushButton(label_button) - if label is not None: - self.button.setFixedWidth(130) self.enable_button(enabled) layout.addWidget(self.button) -- 2.54.0 From b2db41088725f6dbaadbaeccde47811e3313d7a4 Mon Sep 17 00:00:00 2001 From: hitz_s Date: Thu, 11 Jun 2026 10:34:34 +0200 Subject: [PATCH 10/10] Add manual beamline selection (for development) --- .../widgets/digital_twin/digital_twin.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 41b8833..03d1ba9 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -136,7 +136,7 @@ class DigitalTwin(BECWidget, QWidget): widget_layout.setSpacing(0) widget_layout.addWidget(self.content_widget) self.setWindowTitle("Digital Twin") - self.resize(1450, 760) + self.resize(1450, 950) self.input.energy.value_changed_connect(self.calc_assistant) self.input.sldi_hacc.value_changed_connect(self.calc_assistant) @@ -219,7 +219,17 @@ class DigitalTwin(BECWidget, QWidget): case _: raise ValueError(f"Not implemented beamline {beamline}") else: - raise ValueError(f"Failed to extract beamline from bec server hostname {bec_hostname}") + logger.warning(f"Failed to extract beamline from bec server hostname {bec_hostname}") + choice = input("Do you want to manually select a beamline? (yes/no): ").strip().lower() + if choice in ["yes", "y"]: + bl = input(f"Choose from: {[bl.value for bl in BeamlineId]}") + if bl in BeamlineId: + logger.info(f'Manually selected beamline {bl}') + return BeamlineId(bl) + else: + raise ValueError(f'Wrong selection {bl}') + else: + raise ValueError('Cannot open digital twin without a beamline') @SafeSlot() def check_bec_config(self, *args): -- 2.54.0