From ba82fd1715072a521c48c53d50a716a0040bce44 Mon Sep 17 00:00:00 2001 From: perl_d Date: Wed, 20 May 2026 10:39:21 +0200 Subject: [PATCH 01/23] Updating to template version 1.3.2 --- .copier-answers.yml | 2 +- .gitea/workflows/create_update_pr.yml | 12 ++++--- .../scans/scan_customization/__init__.py | 0 .../scan_customization/scan_components.py | 12 +++++++ .../scans/scan_customization/scan_modifier.py | 33 +++++++++++++++++++ pyproject.toml | 3 ++ 6 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 debye_bec/scans/scan_customization/__init__.py create mode 100644 debye_bec/scans/scan_customization/scan_components.py create mode 100644 debye_bec/scans/scan_customization/scan_modifier.py diff --git a/.copier-answers.yml b/.copier-answers.yml index 2394c31..6da586b 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -2,7 +2,7 @@ # It is needed to track the repo template version, and editing may break things. # This file will be overwritten by copier on template updates. -_commit: v1.2.8 +_commit: v1.3.2 _src_path: https://github.com/bec-project/plugin_copier_template.git make_commit: false project_name: debye_bec diff --git a/.gitea/workflows/create_update_pr.yml b/.gitea/workflows/create_update_pr.yml index 91f791e..52f301d 100644 --- a/.gitea/workflows/create_update_pr.yml +++ b/.gitea/workflows/create_update_pr.yml @@ -17,14 +17,14 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.12' - + - name: Install tools run: | - pip install copier PySide6 + pip install copier PySide6 bec_lib - name: Checkout uses: actions/checkout@v4 - + - name: Perform update run: | git config --global user.email "bec_ci_staging@psi.ch" @@ -35,8 +35,10 @@ jobs: git checkout -b $branch echo "Running copier update..." - output="$(copier update --trust --defaults --conflict inline 2>&1)" - echo "$output" + copier update --trust --defaults --conflict inline 2>&1 | tee copier.log + status=${PIPESTATUS[0]} + output="$(cat copier.log)" + echo $output msg="$(printf '%s\n' "$output" | head -n 1)" if ! grep -q "make_commit: true" .copier-answers.yml ; then diff --git a/debye_bec/scans/scan_customization/__init__.py b/debye_bec/scans/scan_customization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/scans/scan_customization/scan_components.py b/debye_bec/scans/scan_customization/scan_components.py new file mode 100644 index 0000000..809c0f8 --- /dev/null +++ b/debye_bec/scans/scan_customization/scan_components.py @@ -0,0 +1,12 @@ +""" +Scan components for debye_bec. + +The scan components module allows you to define custom components that can be used in your scans. +These components can be used to encapsulate reusable logic, interact with devices, or perform specific actions during the scan lifecycle. +""" + +from bec_server.scan_server.scans.scan_components import ScanComponents + + +class DebyeBecScanComponents(ScanComponents): + """Scan components for debye_bec.""" diff --git a/debye_bec/scans/scan_customization/scan_modifier.py b/debye_bec/scans/scan_customization/scan_modifier.py new file mode 100644 index 0000000..2a6fc28 --- /dev/null +++ b/debye_bec/scans/scan_customization/scan_modifier.py @@ -0,0 +1,33 @@ +""" +Scan modifier plugin for debye_bec. + +The scan modifier allows you to modify the scan lifecycle and run custom actions before or after the scan hook or replace the scan hook entirely. +Note that the scan_modifier module must be registered as a plugin in the pyproject.toml file for it to be recognized by the BEC framework and that +there can only be one scan_modifier plugin registered at a time. If you need to run multiple scan modifiers, you can create a single scan +modifier plugin that runs multiple actions in sequence with conditional logic to determine which actions to run based on the scan context. +""" + +from bec_server.scan_server.scans.scan_modifier import ScanModifier, scan_hook_impl + + +class DebyeBecScanModifier(ScanModifier): + """ + Scan modifier for debye_bec. + + By inheriting from the ScanModifier base class, you get access to currently running scan (self.scan), the devices (self.dev), the scan info (self.scan_info), + the scan components (self.components) and the scan actions (self.actions). + """ + + def __init__(self, **kwargs): + """Initialize the scan modifier.""" + super().__init__(**kwargs) + + # Example of running code before the scan stage for a specific scan + # @scan_hook_impl("stage", "before") + # def before_stage(self): + # """Run before the stage hook.""" + # self.actions.send_client_info("Custom stage logic executed by ScanModifier.") + # if self.scan_info.scan_name == "example_scan": + # self.dev.samx.set(20) + + diff --git a/pyproject.toml b/pyproject.toml index e5873cd..813b6b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,9 @@ plugin_file_writer = "debye_bec.file_writer" [project.entry-points."bec.scans"] plugin_scans = "debye_bec.scans" +[project.entry-points."bec.scans.scan_modifier"] +plugin_scan_modifier = "debye_bec.scans.scan_customization.scan_modifier" + [project.entry-points."bec.scans.metadata_schema"] plugin_metadata_schema = "debye_bec.scans.metadata_schema" -- 2.54.0 From 94aca18a222e655fc2d3516df6cd73d48bed3fc0 Mon Sep 17 00:00:00 2001 From: perl_d Date: Wed, 20 May 2026 11:00:38 +0200 Subject: [PATCH 02/23] Updating to template version 1.4.0 --- .copier-answers.yml | 2 +- .gitea/workflows/create_update_pr.yml | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 6da586b..08016da 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -2,7 +2,7 @@ # It is needed to track the repo template version, and editing may break things. # This file will be overwritten by copier on template updates. -_commit: v1.3.2 +_commit: v1.4.0 _src_path: https://github.com/bec-project/plugin_copier_template.git make_commit: false project_name: debye_bec diff --git a/.gitea/workflows/create_update_pr.yml b/.gitea/workflows/create_update_pr.yml index 52f301d..335858b 100644 --- a/.gitea/workflows/create_update_pr.yml +++ b/.gitea/workflows/create_update_pr.yml @@ -18,15 +18,21 @@ jobs: with: python-version: '3.12' - - name: Install tools - run: | - pip install copier PySide6 bec_lib - - name: Checkout uses: actions/checkout@v4 + - name: Create virtualenv + run: | + python -m virtualenv .venv + + - name: Install tools + run: | + source .venv/bin/activate + pip install copier PySide6 bec_lib + - name: Perform update run: | + source .venv/bin/activate git config --global user.email "bec_ci_staging@psi.ch" git config --global user.name "BEC automated CI" -- 2.54.0 From 70750d6aa1196c71992fbf2518a90e67d3e9fa31 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 5 May 2026 12:45:18 +0200 Subject: [PATCH 03/23] chore(nidaq): improve readbability of nidaq signal definition --- debye_bec/devices/nidaq/nidaq.py | 495 ++++++------------------------- 1 file changed, 95 insertions(+), 400 deletions(-) diff --git a/debye_bec/devices/nidaq/nidaq.py b/debye_bec/devices/nidaq/nidaq.py index c1564ba..99c9caf 100644 --- a/debye_bec/devices/nidaq/nidaq.py +++ b/debye_bec/devices/nidaq/nidaq.py @@ -32,46 +32,23 @@ class NidaqError(Exception): class NidaqControl(Device): """Nidaq control class with all PVs""" + # fmt: off ### Readback PVs for EpicsEmitter ### energy = Cpt(SetableSignal, value=0, kind=Kind.normal) - smpl_abs = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample absorption" - ) - smpl_fluo = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample fluorescence" - ) - ref_abs = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream reference absorption" - ) - cisum = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter sum" - ) + smpl_abs = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample absorption") + smpl_fluo = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample fluorescence") + ref_abs = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream reference absorption") + cisum = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter sum") - ai0_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, MEAN" - ) - ai1_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, MEAN" - ) - ai2_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, MEAN" - ) - ai3_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, MEAN" - ) - ai4_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, MEAN" - ) - ai5_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, MEAN" - ) - ai6_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, MEAN" - ) - ai7_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, MEAN" - ) + ai0_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, MEAN") + ai1_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, MEAN") + ai2_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, MEAN") + ai3_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, MEAN") + ai4_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, MEAN") + ai5_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, MEAN") + ai6_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, MEAN") + ai7_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, MEAN") di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX") di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX") @@ -79,377 +56,91 @@ class NidaqControl(Device): di3_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 3, MAX") di4_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 4, MAX") - ci0_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0, MEAN" - ) - ci1_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1, MEAN" - ) - ci2_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2, MEAN" - ) - ci3_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3, MEAN" - ) - ci4_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4, MEAN" - ) - ci5_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5, MEAN" - ) - ci6_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6, MEAN" - ) - ci7_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7, MEAN" - ) - ci8_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8, MEAN" - ) - ci9_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9, MEAN" - ) - ci10_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10, MEAN" - ) - ci11_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11, MEAN" - ) - ci12_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12, MEAN" - ) - ci13_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13, MEAN" - ) - ci14_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14, MEAN" - ) - ci15_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15, MEAN" - ) - ci16_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16, MEAN" - ) - ci17_mean = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17, MEAN" - ) + ci0_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0, MEAN") + ci1_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1, MEAN") + ci2_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2, MEAN") + ci3_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3, MEAN") + ci4_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4, MEAN") + ci5_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5, MEAN") + ci6_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6, MEAN") + ci7_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7, MEAN") + ci8_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8, MEAN") + ci9_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9, MEAN") + ci10_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10, MEAN") + ci11_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11, MEAN") + ci12_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12, MEAN") + ci13_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13, MEAN") + ci14_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14, MEAN") + ci15_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15, MEAN") + ci16_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16, MEAN") + ci17_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17, MEAN") - ai0 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-AI0", - kind=Kind.normal, - doc="EPICS analog input 0", - auto_monitor=True, - ) - ai1 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-AI1", - kind=Kind.normal, - doc="EPICS analog input 1", - auto_monitor=True, - ) - ai2 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-AI2", - kind=Kind.normal, - doc="EPICS analog input 2", - auto_monitor=True, - ) - ai3 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-AI3", - kind=Kind.normal, - doc="EPICS analog input 3", - auto_monitor=True, - ) - ai4 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-AI4", - kind=Kind.normal, - doc="EPICS analog input 4", - auto_monitor=True, - ) - ai5 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-AI5", - kind=Kind.normal, - doc="EPICS analog input 5", - auto_monitor=True, - ) - ai6 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-AI6", - kind=Kind.normal, - doc="EPICS analog input 6", - auto_monitor=True, - ) - ai7 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-AI7", - kind=Kind.normal, - doc="EPICS analog input 7", - auto_monitor=True, - ) + ai0 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI0", kind=Kind.normal, doc="EPICS analog input 0", auto_monitor=True) + ai1 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI1", kind=Kind.normal, doc="EPICS analog input 1", auto_monitor=True) + ai2 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI2", kind=Kind.normal, doc="EPICS analog input 2", auto_monitor=True) + ai3 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI3", kind=Kind.normal, doc="EPICS analog input 3", auto_monitor=True) + ai4 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI4", kind=Kind.normal, doc="EPICS analog input 4", auto_monitor=True) + ai5 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI5", kind=Kind.normal, doc="EPICS analog input 5", auto_monitor=True) + ai6 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI6", kind=Kind.normal, doc="EPICS analog input 6", auto_monitor=True) + ai7 = Cpt(EpicsSignalRO, suffix="NIDAQ-AI7", kind=Kind.normal, doc="EPICS analog input 7", auto_monitor=True) - ci0 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI0", - kind=Kind.normal, - doc="EPICS counter input 0", - auto_monitor=True, - ) - ci1 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI1", - kind=Kind.normal, - doc="EPICS counter input 1", - auto_monitor=True, - ) - ci2 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI2", - kind=Kind.normal, - doc="EPICS counter input 2", - auto_monitor=True, - ) - ci3 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI3", - kind=Kind.normal, - doc="EPICS counter input 3", - auto_monitor=True, - ) - ci4 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI4", - kind=Kind.normal, - doc="EPICS counter input 4", - auto_monitor=True, - ) - ci5 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI5", - kind=Kind.normal, - doc="EPICS counter input 5", - auto_monitor=True, - ) - ci6 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI6", - kind=Kind.normal, - doc="EPICS counter input 6", - auto_monitor=True, - ) - ci7 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI7", - kind=Kind.normal, - doc="EPICS counter input 7", - auto_monitor=True, - ) - ci8 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI8", - kind=Kind.normal, - doc="EPICS counter input 8", - auto_monitor=True, - ) - ci9 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI9", - kind=Kind.normal, - doc="EPICS counter input 9", - auto_monitor=True, - ) - ci10 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI10", - kind=Kind.normal, - doc="EPICS counter input 0", - auto_monitor=True, - ) - ci11 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI11", - kind=Kind.normal, - doc="EPICS counter input 1", - auto_monitor=True, - ) - ci12 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI12", - kind=Kind.normal, - doc="EPICS counter input 2", - auto_monitor=True, - ) - ci13 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI13", - kind=Kind.normal, - doc="EPICS counter input 3", - auto_monitor=True, - ) - ci14 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI14", - kind=Kind.normal, - doc="EPICS counter input 4", - auto_monitor=True, - ) - ci15 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI15", - kind=Kind.normal, - doc="EPICS counter input 5", - auto_monitor=True, - ) - ci16 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI16", - kind=Kind.normal, - doc="EPICS counter input 6", - auto_monitor=True, - ) - ci17 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-CI17", - kind=Kind.normal, - doc="EPICS counter input 7", - auto_monitor=True, - ) + ci0 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI0", kind=Kind.normal, doc="EPICS counter input 0", auto_monitor=True) + ci1 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI1", kind=Kind.normal, doc="EPICS counter input 1", auto_monitor=True) + ci2 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI2", kind=Kind.normal, doc="EPICS counter input 2", auto_monitor=True) + ci3 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI3", kind=Kind.normal, doc="EPICS counter input 3", auto_monitor=True) + ci4 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI4", kind=Kind.normal, doc="EPICS counter input 4", auto_monitor=True) + ci5 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI5", kind=Kind.normal, doc="EPICS counter input 5", auto_monitor=True) + ci6 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI6", kind=Kind.normal, doc="EPICS counter input 6", auto_monitor=True) + ci7 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI7", kind=Kind.normal, doc="EPICS counter input 7", auto_monitor=True) + ci8 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI8", kind=Kind.normal, doc="EPICS counter input 8", auto_monitor=True) + ci9 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI9", kind=Kind.normal, doc="EPICS counter input 9", auto_monitor=True) + ci10 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI10", kind=Kind.normal, doc="EPICS counter input 0", auto_monitor=True) + ci11 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI11", kind=Kind.normal, doc="EPICS counter input 1", auto_monitor=True) + ci12 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI12", kind=Kind.normal, doc="EPICS counter input 2", auto_monitor=True) + ci13 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI13", kind=Kind.normal, doc="EPICS counter input 3", auto_monitor=True) + ci14 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI14", kind=Kind.normal, doc="EPICS counter input 4", auto_monitor=True) + ci15 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI15", kind=Kind.normal, doc="EPICS counter input 5", auto_monitor=True) + ci16 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI16", kind=Kind.normal, doc="EPICS counter input 6", auto_monitor=True) + ci17 = Cpt(EpicsSignalRO, suffix="NIDAQ-CI17", kind=Kind.normal, doc="EPICS counter input 7", auto_monitor=True) - di0 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-DI0", - kind=Kind.normal, - doc="EPICS digital input 0", - auto_monitor=True, - ) - di1 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-DI1", - kind=Kind.normal, - doc="EPICS digital input 1", - auto_monitor=True, - ) - di2 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-DI2", - kind=Kind.normal, - doc="EPICS digital input 2", - auto_monitor=True, - ) - di3 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-DI3", - kind=Kind.normal, - doc="EPICS digital input 3", - auto_monitor=True, - ) - di4 = Cpt( - EpicsSignalRO, - suffix="NIDAQ-DI4", - kind=Kind.normal, - doc="EPICS digital input 4", - auto_monitor=True, - ) + di0 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI0", kind=Kind.normal, doc="EPICS digital input 0", auto_monitor=True) + di1 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI1", kind=Kind.normal, doc="EPICS digital input 1", auto_monitor=True) + di2 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI2", kind=Kind.normal, doc="EPICS digital input 2", auto_monitor=True) + di3 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI3", kind=Kind.normal, doc="EPICS digital input 3", auto_monitor=True) + di4 = Cpt(EpicsSignalRO, suffix="NIDAQ-DI4", kind=Kind.normal, doc="EPICS digital input 4", auto_monitor=True) - enc_epics = Cpt( - EpicsSignalRO, - suffix="NIDAQ-ENC", - kind=Kind.normal, - doc="EPICS Encoder reading", - auto_monitor=True, - ) + enc_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-ENC", kind=Kind.normal, doc="EPICS Encoder reading", auto_monitor=True) - energy_epics = Cpt( - EpicsSignalRO, - suffix="NIDAQ-ENERGY", - kind=Kind.normal, - doc="EPICS Energy reading", - auto_monitor=True, - ) + energy_epics = Cpt(EpicsSignalRO, suffix="NIDAQ-ENERGY", kind=Kind.normal, doc="EPICS Energy reading", auto_monitor=True) ### Readback for BEC emitter ### - ai0_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, STD" - ) - ai1_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, STD" - ) - ai2_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, STD" - ) - ai3_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, STD" - ) - ai4_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, STD" - ) - ai5_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, STD" - ) - ai6_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, STD" - ) - ai7_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, STD" - ) + ai0_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 0, STD") + ai1_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 1, STD") + ai2_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 2, STD") + ai3_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 3, STD") + ai4_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 4, STD") + ai5_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 5, STD") + ai6_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 6, STD") + ai7_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream analog input 7, STD") - ci0_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0. STD" - ) - ci1_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1. STD" - ) - ci2_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2. STD" - ) - ci3_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3. STD" - ) - ci4_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4. STD" - ) - ci5_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5. STD" - ) - ci6_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6. STD" - ) - ci7_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD" - ) - ci8_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8. STD" - ) - ci9_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9. STD" - ) - ci10_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10. STD" - ) - ci11_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11. STD" - ) - ci12_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12. STD" - ) - ci13_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13. STD" - ) - ci14_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14. STD" - ) - ci15_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15. STD" - ) - ci16_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16. STD" - ) - ci17_std_dev = Cpt( - SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17. STD" - ) + ci0_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0. STD") + ci1_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 1. STD") + ci2_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 2. STD") + ci3_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 3. STD") + ci4_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 4. STD") + ci5_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 5. STD") + ci6_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 6. STD") + ci7_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD") + ci8_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8. STD") + ci9_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9. STD") + ci10_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10. STD") + ci11_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11. STD") + ci12_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12. STD") + ci13_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13. STD") + ci14_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14. STD") + ci15_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15. STD") + ci16_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16. STD") + ci17_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17. STD") xas_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XAS timestamp") xrd_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp") @@ -510,6 +201,8 @@ class NidaqControl(Device): ref_abs_de = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_de", kind=Kind.config, auto_monitor=True) ref_abs_de_string = Cpt(EpicsSignal, suffix="NIDAQ-ref_abs_de", kind=Kind.config, string=True, auto_monitor=True) + # fmt: on + class Nidaq(PSIDeviceBase, NidaqControl): """NIDAQ ophyd wrapper around the NIDAQ backend currently running at x01da-cons-05 @@ -528,7 +221,9 @@ class Nidaq(PSIDeviceBase, NidaqControl): super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs) self.scan_info: ScanInfo self.timeout_wait_for_signal = 5 # put 5s firsts - self._timeout_wait_for_pv = 5 # 5s timeout for pv calls. editted due to timeout issues persisting + self._timeout_wait_for_pv = ( + 5 # 5s timeout for pv calls. editted due to timeout issues persisting + ) self.valid_scan_names = [ "xas_simple_scan", "xas_simple_scan_with_xrd", -- 2.54.0 From f8e5b5e073d6be1a5d07b5a03a30fdeceb36b0b6 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sun, 10 May 2026 14:20:08 +0200 Subject: [PATCH 04/23] feat: migrate XAS simple scan to V4 implementation and remove mono bragg scans --- debye_bec/scans/__init__.py | 12 +- debye_bec/scans/mono_bragg_scans.py | 310 ----------------- debye_bec/scans/xas_simple_scan.py | 326 ++++++++++++++++++ tests/tests_scans/test_mono_bragg_scans_v4.py | 159 +++++++++ 4 files changed, 491 insertions(+), 316 deletions(-) delete mode 100644 debye_bec/scans/mono_bragg_scans.py create mode 100644 debye_bec/scans/xas_simple_scan.py create mode 100644 tests/tests_scans/test_mono_bragg_scans_v4.py diff --git a/debye_bec/scans/__init__.py b/debye_bec/scans/__init__.py index 9a3e710..08e8f07 100644 --- a/debye_bec/scans/__init__.py +++ b/debye_bec/scans/__init__.py @@ -1,7 +1,7 @@ -from .mono_bragg_scans import ( - XASAdvancedScan, - XASAdvancedScanWithXRD, - XASSimpleScan, - XASSimpleScanWithXRD, -) from .nidaq_cont_scan import NIDAQContinuousScan +from .xas_simple_scan import ( + XasAdvancedScan, + XasAdvancedScanWithXrd, + XasSimpleScan, + XasSimpleScanWithXrd, +) diff --git a/debye_bec/scans/mono_bragg_scans.py b/debye_bec/scans/mono_bragg_scans.py deleted file mode 100644 index 03c234b..0000000 --- a/debye_bec/scans/mono_bragg_scans.py +++ /dev/null @@ -1,310 +0,0 @@ -"""This module contains the scan classes for the mono bragg motor of the Debye beamline.""" - -import time -from typing import Literal - -import numpy as np -from bec_lib.device import DeviceBase -from bec_lib.logger import bec_logger -from bec_server.scan_server.scans import AsyncFlyScanBase - -logger = bec_logger.logger - - -class XASSimpleScan(AsyncFlyScanBase): - """Class for the XAS simple scan""" - - scan_name = "xas_simple_scan" - scan_type = "fly" - scan_report_hint = "device_progress" - required_kwargs = [] - use_scan_progress_report = False - pre_move = False - gui_config = { - "Movement Parameters": ["start", "stop"], - "Scan Parameters": ["scan_time", "scan_duration"], - } - - def __init__( - self, - start: float, - stop: float, - scan_time: float, - scan_duration: float, - motor: DeviceBase = "mo1_bragg", - **kwargs, - ): - """The xas_simple_scan is used to start a simple oscillating scan on the mono bragg motor. - Start and Stop define the energy range for the scan, scan_time is the time for one scan - cycle and scan_duration is the duration of the scan. If scan duration is set to 0, the - scan will run infinitely. - - Args: - start (float): Start energy for the scan. - stop (float): Stop energy for the scan. - scan_time (float): Time for one scan cycle. - scan_duration (float): Duration of the scan. - motor (DeviceBase, optional): Motor device to be used for the scan. - Defaults to "mo1_bragg". - Examples: - >>> scans.xas_simple_scan(start=8000, stop=9000, scan_time=1, scan_duration=10) - """ - super().__init__(**kwargs) - self.motor = motor - self.start = start - self.stop = stop - self.scan_time = scan_time - self.scan_duration = scan_duration - self.primary_readout_cycle = 1 - - def update_readout_priority(self): - """Ensure that NIDAQ is not monitored for any quick EXAFS.""" - super().update_readout_priority() - self.readout_priority["async"].append("nidaq") - - def prepare_positions(self): - """Prepare the positions for the scan. - - Use here only start and end energy defining the range for the scan. - """ - self.positions = np.array([self.start, self.stop], dtype=float) - self.num_pos = None - yield None - - def pre_scan(self): - """Pre Scan action.""" - - self._check_limits() - # Ensure parent class pre_scan actions to be called. - yield from super().pre_scan() - - def scan_report_instructions(self): - """ - Return the instructions for the scan report. - """ - yield from self.stubs.scan_report_instruction({"device_progress": [self.motor]}) - - def scan_core(self): - """Run the scan core. - Kickoff the oscillation on the Bragg motor and wait for the completion of the motion. - """ - # Start the oscillation on the Bragg motor. - yield from self.stubs.kickoff(device=self.motor) - complete_status = yield from self.stubs.complete(device=self.motor, wait=False) - - while not complete_status.done: - # Readout monitored devices - yield from self.stubs.read(group="monitored", point_id=self.point_id) - time.sleep(self.primary_readout_cycle) - self.point_id += 1 - - self.num_pos = self.point_id - - -class XASSimpleScanWithXRD(XASSimpleScan): - """Class for the XAS simple scan with XRD""" - - scan_name = "xas_simple_scan_with_xrd" - gui_config = { - "Movement Parameters": ["start", "stop"], - "Scan Parameters": ["scan_time", "scan_duration"], - "Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"], - "High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"], - "XRD Triggers": ["exp_time", "n_of_trigger"], - } - - def __init__( - self, - start: float, - stop: float, - scan_time: float, - scan_duration: float, - break_enable_low: bool, - break_time_low: float, - cycle_low: int, - break_enable_high: bool, - break_time_high: float, - cycle_high: float, - exp_time: float, - n_of_trigger: int, - motor: DeviceBase = "mo1_bragg", - **kwargs, - ): - """The xas_simple_scan_with_xrd is an oscillation motion on the mono motor - with XRD triggering at low and high energy ranges. - If scan duration is set to 0, the scan will run infinitely. - - Args: - start (float): Start energy for the scan. - stop (float): Stop energy for the scan. - scan_time (float): Time for one oscillation . - scan_duration (float): Total duration of the scan. - break_enable_low (bool): Enable breaks for the low energy range. - break_time_low (float): Break time for the low energy range. - cycle_low (int): Specify how often the triggers should be considered, - every nth cycle for low - break_enable_high (bool): Enable breaks for the high energy range. - break_time_high (float): Break time for the high energy range. - cycle_high (int): Specify how often the triggers should be considered, - every nth cycle for high - exp_time (float): Length of 1 trigger period in seconds - n_of_trigger (int): Amount of triggers to be fired during break - motor (DeviceBase, optional): Motor device to be used for the scan. - Defaults to "mo1_bragg". - - Examples: - >>> scans.xas_simple_scan_with_xrd(start=8000, stop=9000, scan_time=1, scan_duration=10, xrd_enable_low=True, num_trigger_low=5, cycle_low=2, exp_time_low=100, xrd_enable_high=False, num_trigger_high=3, cycle_high=1, exp_time_high=1000) - """ - super().__init__( - start=start, - stop=stop, - scan_time=scan_time, - scan_duration=scan_duration, - motor=motor, - **kwargs, - ) - self.break_enable_low = break_enable_low - self.break_time_low = break_time_low - self.cycle_low = cycle_low - self.break_enable_high = break_enable_high - self.break_time_high = break_time_high - self.cycle_high = cycle_high - self.exp_time = exp_time - self.n_of_trigger = n_of_trigger - - -class XASAdvancedScan(XASSimpleScan): - """Class for the XAS advanced scan""" - - scan_name = "xas_advanced_scan" - gui_config = { - "Movement Parameters": ["start", "stop"], - "Scan Parameters": ["scan_time", "scan_duration"], - "Spline Parameters": ["p_kink", "e_kink"], - } - - def __init__( - self, - start: float, - stop: float, - scan_time: float, - scan_duration: float, - p_kink: float, - e_kink: float, - motor: DeviceBase = "mo1_bragg", - **kwargs, - ): - """The xas_advanced_scan is an oscillation motion on the mono motor. - Start and Stop define the energy range for the scan, scan_time is the - time for one scan cycle and scan_duration is the duration of the scan. - If scan duration is set to 0, the scan will run infinitely. - p_kink and e_kink add a kink to the motion profile to slow down in the - exafs region of the scan. - - Args: - start (float): Start angle for the scan. - stop (float): Stop angle for the scan. - scan_time (float): Time for one oscillation . - scan_duration (float): Total duration of the scan. - p_kink (float): Position of the kink. - e_kink (float): Energy of the kink. - motor (DeviceBase, optional): Motor device to be used for the scan. - Defaults to "mo1_bragg". - - Examples: - >>> scans.xas_advanced_scan(start=10000, stop=12000, scan_time=0.5, scan_duration=10, p_kink=50, e_kink=10500) - """ - super().__init__( - start=start, - stop=stop, - scan_time=scan_time, - scan_duration=scan_duration, - motor=motor, - **kwargs, - ) - self.p_kink = p_kink - self.e_kink = e_kink - - -class XASAdvancedScanWithXRD(XASAdvancedScan): - """Class for the XAS advanced scan with XRD""" - - scan_name = "xas_advanced_scan_with_xrd" - gui_config = { - "Movement Parameters": ["start", "stop"], - "Scan Parameters": ["scan_time", "scan_duration"], - "Spline Parameters": ["p_kink", "e_kink"], - "Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"], - "High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"], - "XRD Triggers": ["exp_time", "n_of_trigger"], - } - - def __init__( - self, - start: float, - stop: float, - scan_time: float, - scan_duration: float, - p_kink: float, - e_kink: float, - break_enable_low: bool, - break_time_low: float, - cycle_low: int, - break_enable_high: bool, - break_time_high: float, - cycle_high: float, - exp_time: float, - n_of_trigger: int, - motor: DeviceBase = "mo1_bragg", - **kwargs, - ): - """The xas_advanced_scan is an oscillation motion on the mono motor - with XRD triggering at low and high energy ranges. - Start and Stop define the energy range for the scan, scan_time is the time for - one scan cycle and scan_duration is the duration of the scan. If scan duration - is set to 0, the scan will run infinitely. p_kink and e_kink add a kink to the - motion profile to slow down in the exafs region of the scan. - - Args: - start (float): Start angle for the scan. - stop (float): Stop angle for the scan. - scan_time (float): Time for one oscillation . - scan_duration (float): Total duration of the scan. - p_kink (float): Position of kink. - e_kink (float): Energy of the kink. - break_enable_low (bool): Enable breaks for the low energy range. - break_time_low (float): Break time for the low energy range. - cycle_low (int): Specify how often the triggers should be considered, - every nth cycle for low - break_enable_high (bool): Enable breaks for the high energy range. - break_time_high (float): Break time for the high energy range. - cycle_high (int): Specify how often the triggers should be considered, - every nth cycle for high - exp_time (float): Length of 1 trigger period in seconds - n_of_trigger (int): Amount of triggers to be fired during break - motor (DeviceBase, optional): Motor device to be used for the scan. - Defaults to "mo1_bragg". - - Examples: - >>> scans.xas_advanced_scan_with_xrd(start=10000, stop=12000, scan_time=0.5, scan_duration=10, p_kink=50, e_kink=10500, xrd_enable_low=True, num_trigger_low=5, cycle_low=2, exp_time_low=100, xrd_enable_high=False, num_trigger_high=3, cycle_high=1, exp_time_high=1000) - """ - super().__init__( - start=start, - stop=stop, - scan_time=scan_time, - scan_duration=scan_duration, - p_kink=p_kink, - e_kink=e_kink, - motor=motor, - **kwargs, - ) - self.p_kink = p_kink - self.e_kink = e_kink - self.break_enable_low = break_enable_low - self.break_time_low = break_time_low - self.cycle_low = cycle_low - self.break_enable_high = break_enable_high - self.break_time_high = break_time_high - self.cycle_high = cycle_high - self.exp_time = exp_time - self.n_of_trigger = n_of_trigger diff --git a/debye_bec/scans/xas_simple_scan.py b/debye_bec/scans/xas_simple_scan.py new file mode 100644 index 0000000..581afd2 --- /dev/null +++ b/debye_bec/scans/xas_simple_scan.py @@ -0,0 +1,326 @@ +""" +V4 implementation of the Debye XAS simple scan. + +Scan procedure: + - prepare_scan + - open_scan + - stage + - pre_scan + - scan_core + - at_each_point (optionally called by scan_core) + - post_scan + - unstage + - close_scan + - on_exception (called if any exception is raised during the scan) +""" + +from __future__ import annotations + +import time +from typing import Annotated + +import numpy as np +from bec_lib.device import DeviceBase +from bec_lib.scan_args import ScanArgument, Units +from bec_server.scan_server.scans.scan_base import ScanBase, ScanType +from bec_server.scan_server.scans.scan_modifier import scan_hook + + +class XasSimpleScan(ScanBase): + scan_type = ScanType.HARDWARE_TRIGGERED + scan_name = "xas_simple_scan" + + gui_config = { + "Movement Parameters": ["start", "stop"], + "Scan Parameters": ["scan_time", "scan_duration", "primary_readout_cycle"], + } + + def __init__( + self, + #fmt: off + start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)], + stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)], + scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)], + scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)], + motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None, + daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, + primary_readout_cycle: Annotated[float, ScanArgument(display_name="Primary Readout Cycle", description="Delay between monitored readouts.",units=Units.s, gt=0,)] = 1, + #fmt: on + **kwargs, + ): + """ + Start a simple oscillating scan on the mono bragg motor. + + Args: + start (float): Start energy. + stop (float): Stop energy. + scan_time (float): Time for one scan cycle. + scan_duration (float): Total scan duration. + motor (DeviceBase | None): Bragg motor device. + daq (DeviceBase | None): NIDAQ device. + primary_readout_cycle (float): Delay between monitored readouts. + + Returns: + ScanReport + """ + super().__init__(**kwargs) + self.start = start + self.stop = stop + self.scan_time = scan_time + self.scan_duration = scan_duration + self.motor = motor if motor is not None else self.dev["mo1_bragg"] + self.daq = daq if daq is not None else self.dev["nidaq"] + self.primary_readout_cycle = primary_readout_cycle + self.positions = np.array([self.start, self.stop], dtype=float) + + # We pass on the arguments as "additional_scan_parameters" in the scan info + self.update_scan_info( + positions=self.positions, + scan_time=scan_time, + scan_duration=scan_duration, + primary_readout_cycle=primary_readout_cycle, + ) + self.actions.set_device_readout_priority([self.daq], priority="async") + + @scan_hook + def prepare_scan(self): + """ + Prepare the scan. This can include any steps that need to be executed + before the scan is opened, such as preparing the positions (if not done already) + or setting up the devices. + """ + self.actions.add_scan_report_instruction_device_progress(self.motor) + self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) + + @scan_hook + def open_scan(self): + """ + Open the scan. + This step must call self.actions.open_scan() to ensure that a new scan is + opened. Make sure to prepare the scan metadata before, either in + prepare_scan() or in open_scan() itself and call self.update_scan_info(...) + to update the scan metadata if needed. + """ + self.actions.open_scan() + + @scan_hook + def stage(self): + """ + Stage the devices for the upcoming scan. The stage logic is typically + implemented on the device itself (i.e. by the device's stage method). + However, if there are any additional steps that need to be executed before + staging the devices, they can be implemented here. + """ + self.actions.stage_all_devices() + + @scan_hook + def pre_scan(self): + """ + Pre-scan steps to be executed before the main scan logic. + This is typically the last chance to prepare the devices before the core scan + logic is executed. For example, this is a good place to initialize time-criticial + devices, e.g. devices that have a short timeout. + The pre-scan logic is typically implemented on the device itself. + """ + self.actions.pre_scan_all_devices() + + @scan_hook + def scan_core(self): + """ + Core scan logic to be executed during the scan. + This is where the main scan logic should be implemented. + """ + self.actions.kickoff(self.motor) + completion_status = self.actions.complete(self.motor, wait=False) + + while not completion_status.done: + self.at_each_point() + + @scan_hook + def at_each_point(self): + """ + Logic to be executed at each acquisition point during the scan. + """ + self.actions.read_monitored_devices() + time.sleep(self.primary_readout_cycle) + + @scan_hook + def post_scan(self): + """ + Post-scan steps to be executed after the main scan logic. + """ + self.actions.complete_all_devices() + + @scan_hook + def unstage(self): + """Unstage the scan by executing post-scan steps.""" + self.actions.unstage_all_devices() + + @scan_hook + def close_scan(self): + """Close the scan.""" + if self._baseline_readout_status is not None: + self._baseline_readout_status.wait() + self.actions.close_scan() + self.actions.check_for_unchecked_statuses() + + @scan_hook + def on_exception(self, exception: Exception): + """ + Handle exceptions that occur during the scan. + This is a good place to implement any cleanup logic that needs to be executed in case of an exception, + such as returning the devices to a safe state or moving the motors back to their starting position. + """ + self.actions.complete_all_devices(wait=False) + + +class XasSimpleScanWithXrd(XasSimpleScan): + scan_name = "xas_simple_scan_with_xrd" + gui_config = { + "Movement Parameters": ["start", "stop"], + "Scan Parameters": ["scan_time", "scan_duration", "primary_readout_cycle"], + "Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"], + "High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"], + "XRD Triggers": ["exp_time", "n_of_trigger"], + } + + def __init__( + self, + #fmt: off + start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)], + stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)], + scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)], + scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)], + break_enable_low: Annotated[bool, ScanArgument(display_name="Break Enable Low", description="Enable breaks for the low energy range.")], + break_time_low: Annotated[float, ScanArgument(display_name="Break Time Low", description="Break time for the low energy range.", units=Units.s, ge=0)], + cycle_low: Annotated[int, ScanArgument(display_name="Cycle Low", description="Use triggers every nth low-energy cycle.", ge=0)], + break_enable_high: Annotated[bool, ScanArgument(display_name="Break Enable High", description="Enable breaks for the high energy range.")], + break_time_high: Annotated[float, ScanArgument(display_name="Break Time High", description="Break time for the high energy range.", units=Units.s, ge=0)], + cycle_high: Annotated[int, ScanArgument(display_name="Cycle High", description="Use triggers every nth high-energy cycle.", ge=0)], + exp_time: Annotated[float, ScanArgument(display_name="Exposure Time", description="Length of one trigger period.", units=Units.s, ge=0)], + n_of_trigger: Annotated[int, ScanArgument(display_name="Number Of Trigger", description="Amount of triggers fired during a break.", ge=0)], + motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None, + daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, + primary_readout_cycle: Annotated[float, ScanArgument(display_name="Primary Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, + **kwargs, + #fmt: on + ): + super().__init__( + start=start, + stop=stop, + scan_time=scan_time, + scan_duration=scan_duration, + motor=motor, + daq=daq, + primary_readout_cycle=primary_readout_cycle, + **kwargs, + ) + + # We pass on the arguments as "additional_scan_parameters" in the scan info + self.update_scan_info( + break_enable_low=break_enable_low, + break_time_low=break_time_low, + cycle_low=cycle_low, + break_enable_high=break_enable_high, + break_time_high=break_time_high, + cycle_high=cycle_high, + exp_time=exp_time, + n_of_trigger=n_of_trigger, + ) + + +class XasAdvancedScan(XasSimpleScan): + scan_name = "xas_advanced_scan" + gui_config = { + "Movement Parameters": ["start", "stop"], + "Scan Parameters": ["scan_time", "scan_duration", "primary_readout_cycle"], + "Spline Parameters": ["p_kink", "e_kink"], + } + + def __init__( + self, + #fmt: off + start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)], + stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)], + scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)], + scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)], + p_kink: Annotated[float, ScanArgument(display_name="P Kink", description="Position of the kink.", ge=0)], + e_kink: Annotated[float, ScanArgument(display_name="E Kink", description="Energy of the kink.", units=Units.eV)], + motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None, + daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, + primary_readout_cycle: Annotated[float, ScanArgument(display_name="Primary Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, + **kwargs, + #fmt: on + ): + super().__init__( + start=start, + stop=stop, + scan_time=scan_time, + scan_duration=scan_duration, + motor=motor, + daq=daq, + primary_readout_cycle=primary_readout_cycle, + **kwargs, + ) + # We pass on the arguments as "additional_scan_parameters" in the scan info + self.update_scan_info(p_kink=p_kink, e_kink=e_kink) + + +class XasAdvancedScanWithXrd(XasAdvancedScan): + scan_name = "xas_advanced_scan_with_xrd" + gui_config = { + "Movement Parameters": ["start", "stop"], + "Scan Parameters": ["scan_time", "scan_duration", "primary_readout_cycle"], + "Spline Parameters": ["p_kink", "e_kink"], + "Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"], + "High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"], + "XRD Triggers": ["exp_time", "n_of_trigger"], + } + + def __init__( + self, + #fmt: off + start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)], + stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)], + scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)], + scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)], + p_kink: Annotated[float, ScanArgument(display_name="P Kink", description="Position of the kink.", ge=0)], + e_kink: Annotated[float, ScanArgument(display_name="E Kink", description="Energy of the kink.", units=Units.eV)], + break_enable_low: Annotated[bool, ScanArgument(display_name="Break Enable Low", description="Enable breaks for the low energy range.")], + break_time_low: Annotated[float, ScanArgument(display_name="Break Time Low", description="Break time for the low energy range.", units=Units.s, ge=0)], + cycle_low: Annotated[int, ScanArgument(display_name="Cycle Low", description="Use triggers every nth low-energy cycle.", ge=0)], + break_enable_high: Annotated[bool, ScanArgument(display_name="Break Enable High", description="Enable breaks for the high energy range.")], + break_time_high: Annotated[float, ScanArgument(display_name="Break Time High", description="Break time for the high energy range.", units=Units.s, ge=0)], + cycle_high: Annotated[int, ScanArgument(display_name="Cycle High", description="Use triggers every nth high-energy cycle.", ge=0)], + exp_time: Annotated[float, ScanArgument(display_name="Exposure Time", description="Length of one trigger period.", units=Units.s, ge=0)], + n_of_trigger: Annotated[int, ScanArgument(display_name="Number Of Trigger", description="Amount of triggers fired during a break.", ge=0)], + motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None, + daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, + primary_readout_cycle: Annotated[float, ScanArgument(display_name="Primary Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, + **kwargs, + #fmt: on + ): + super().__init__( + start=start, + stop=stop, + scan_time=scan_time, + scan_duration=scan_duration, + p_kink=p_kink, + e_kink=e_kink, + motor=motor, + daq=daq, + primary_readout_cycle=primary_readout_cycle, + **kwargs, + ) + + # We pass on the arguments as "additional_scan_parameters" in the scan info + self.update_scan_info( + break_enable_low=break_enable_low, + break_time_low=break_time_low, + cycle_low=cycle_low, + break_enable_high=break_enable_high, + break_time_high=break_time_high, + cycle_high=cycle_high, + exp_time=exp_time, + n_of_trigger=n_of_trigger, + ) diff --git a/tests/tests_scans/test_mono_bragg_scans_v4.py b/tests/tests_scans/test_mono_bragg_scans_v4.py new file mode 100644 index 0000000..bf791a1 --- /dev/null +++ b/tests/tests_scans/test_mono_bragg_scans_v4.py @@ -0,0 +1,159 @@ +# pylint: skip-file +from unittest import mock + +import numpy as np +import pytest +from bec_server.scan_server.tests.scan_hook_tests import ( + assert_close_scan_waits_for_baseline_and_closes, + assert_pre_scan_called, + assert_prepare_scan_reads_baseline_devices, + assert_scan_open_called, + assert_stage_all_devices_called, + assert_unstage_all_devices_called, + run_scan_tests, +) + +XAS_SIMPLE_SCAN_DEFAULT_HOOK_TESTS = [ + ("prepare_scan", [assert_prepare_scan_reads_baseline_devices]), + ("open_scan", [assert_scan_open_called]), + ("stage", [assert_stage_all_devices_called]), + ("pre_scan", [assert_pre_scan_called]), + ("unstage", [assert_unstage_all_devices_called]), + ("close_scan", [assert_close_scan_waits_for_baseline_and_closes]), +] + + +def _assemble_xas_simple_scan(v4_scan_assembler, **overrides): + params = { + "start": 8000.0, + "stop": 9000.0, + "scan_time": 1.0, + "scan_duration": 10.0, + "motor": "mo1_bragg", + "daq": "nidaq", + "primary_readout_cycle": 1.0, + } + params.update(overrides) + return v4_scan_assembler("xas_simple_scan", **params) + + +@pytest.mark.parametrize(("hook_name", "hook_tests"), XAS_SIMPLE_SCAN_DEFAULT_HOOK_TESTS) +def test_xas_simple_scan_v4_default_hooks( + v4_scan_assembler, nth_done_status_mock, hook_name, hook_tests +): + scan = _assemble_xas_simple_scan(v4_scan_assembler) + + run_scan_tests(scan, [(hook_name, hook_tests)], nth_done_status_mock=nth_done_status_mock) + + +def test_xas_simple_scan_v4_prepare_scan_updates_metadata(v4_scan_assembler): + scan = _assemble_xas_simple_scan(v4_scan_assembler) + scan.actions.add_scan_report_instruction_device_progress = mock.MagicMock() + baseline_status = mock.MagicMock() + scan.actions.read_baseline_devices = mock.MagicMock(return_value=baseline_status) + + scan.prepare_scan() + + scan.actions._build_scan_status_message("open") + + np.testing.assert_array_equal(scan.scan_info.positions, np.array([8000.0, 9000.0])) + assert scan.scan_info.additional_scan_parameters["scan_time"] == 1.0 + assert scan.scan_info.additional_scan_parameters["scan_duration"] == 10.0 + assert scan.scan_info.readout_priority_modification["async"] == ["nidaq"] + scan.actions.add_scan_report_instruction_device_progress.assert_called_once_with(scan.motor) + scan.actions.read_baseline_devices.assert_called_once_with(wait=False) + assert scan._baseline_readout_status is baseline_status + + +def test_xas_simple_scan_v4_scan_core_reads_until_complete(v4_scan_assembler, nth_done_status_mock): + scan = _assemble_xas_simple_scan(v4_scan_assembler) + completion_status = nth_done_status_mock(resolve_after=3) + scan.actions.kickoff = mock.MagicMock() + scan.actions.complete = mock.MagicMock(return_value=completion_status) + scan.actions.read_monitored_devices = mock.MagicMock() + + with mock.patch("debye_bec.scans.xas_simple_scan.time.sleep"): + scan.scan_core() + + scan.actions.kickoff.assert_called_once_with(scan.motor) + scan.actions.complete.assert_called_once_with(scan.motor, wait=False) + assert scan.actions.read_monitored_devices.call_count == 2 + + +def test_xas_simple_scan_v4_post_scan_completes_all_devices(v4_scan_assembler): + scan = _assemble_xas_simple_scan(v4_scan_assembler) + scan.actions.complete_all_devices = mock.MagicMock() + + scan.post_scan() + + scan.actions.complete_all_devices.assert_called_once_with() + + +def test_xas_simple_scan_with_xrd_v4_updates_xrd_metadata(v4_scan_assembler): + scan = v4_scan_assembler( + "xas_simple_scan_with_xrd", + start=8000.0, + stop=9000.0, + scan_time=1.0, + scan_duration=10.0, + break_enable_low=True, + break_time_low=1.0, + cycle_low=2, + break_enable_high=False, + break_time_high=3.0, + cycle_high=4, + exp_time=0.5, + n_of_trigger=6, + motor="mo1_bragg", + daq="nidaq", + ) + + assert scan.scan_name == "xas_simple_scan_with_xrd" + assert scan.scan_info.additional_scan_parameters["break_enable_low"] is True + assert scan.scan_info.additional_scan_parameters["cycle_high"] == 4 + assert scan.scan_info.additional_scan_parameters["n_of_trigger"] == 6 + + +def test_xas_advanced_scan_v4_updates_spline_metadata(v4_scan_assembler): + scan = v4_scan_assembler( + "xas_advanced_scan", + start=8000.0, + stop=9000.0, + scan_time=1.0, + scan_duration=10.0, + p_kink=50.0, + e_kink=8500.0, + motor="mo1_bragg", + daq="nidaq", + ) + + assert scan.scan_name == "xas_advanced_scan" + assert scan.scan_info.additional_scan_parameters["p_kink"] == 50.0 + assert scan.scan_info.additional_scan_parameters["e_kink"] == 8500.0 + + +def test_xas_advanced_scan_with_xrd_v4_updates_all_metadata(v4_scan_assembler): + scan = v4_scan_assembler( + "xas_advanced_scan_with_xrd", + start=8000.0, + stop=9000.0, + scan_time=1.0, + scan_duration=10.0, + p_kink=55.0, + e_kink=8450.0, + break_enable_low=True, + break_time_low=1.5, + cycle_low=2, + break_enable_high=True, + break_time_high=2.5, + cycle_high=3, + exp_time=0.25, + n_of_trigger=8, + motor="mo1_bragg", + daq="nidaq", + ) + + assert scan.scan_name == "xas_advanced_scan_with_xrd" + assert scan.scan_info.additional_scan_parameters["p_kink"] == 55.0 + assert scan.scan_info.additional_scan_parameters["break_enable_high"] is True + assert scan.scan_info.exp_time == 0.25 -- 2.54.0 From 0e77dd5679fcf12c393317120bbd5ce4515eae14 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sun, 10 May 2026 14:43:27 +0200 Subject: [PATCH 05/23] feat: add NIDAQ continuous scan v4 implementation and update related tests --- debye_bec/scans/__init__.py | 1 + debye_bec/scans/nidaq_continuous_scan.py | 174 +++++++ tests/tests_scans/conftest.py | 8 +- tests/tests_scans/test_mono_bragg_scans.py | 429 ------------------ tests/tests_scans/test_mono_bragg_scans_v4.py | 11 +- .../tests_scans/test_nidaq_continous_scan.py | 183 +++----- 6 files changed, 247 insertions(+), 559 deletions(-) create mode 100644 debye_bec/scans/nidaq_continuous_scan.py delete mode 100644 tests/tests_scans/test_mono_bragg_scans.py diff --git a/debye_bec/scans/__init__.py b/debye_bec/scans/__init__.py index 08e8f07..1339603 100644 --- a/debye_bec/scans/__init__.py +++ b/debye_bec/scans/__init__.py @@ -1,4 +1,5 @@ from .nidaq_cont_scan import NIDAQContinuousScan +from .nidaq_continuous_scan import NidaqContinuousScan from .xas_simple_scan import ( XasAdvancedScan, XasAdvancedScanWithXrd, diff --git a/debye_bec/scans/nidaq_continuous_scan.py b/debye_bec/scans/nidaq_continuous_scan.py new file mode 100644 index 0000000..ca67744 --- /dev/null +++ b/debye_bec/scans/nidaq_continuous_scan.py @@ -0,0 +1,174 @@ +""" +The NIDAQ continuous scan is used to measure with the NIDAQ without moving the monochromator or any other motor. + +Scan procedure: + - prepare_scan + - open_scan + - stage + - pre_scan + - scan_core + - at_each_point (optionally called by scan_core) + - post_scan + - unstage + - close_scan + - on_exception (called if any exception is raised during the scan) +""" + +from __future__ import annotations + +import time +from typing import Annotated + +from bec_lib.device import DeviceBase +from bec_lib.scan_args import ScanArgument, Units +from bec_server.scan_server.scans.scan_base import ScanBase, ScanType +from bec_server.scan_server.scans.scan_modifier import scan_hook + + +class NidaqContinuousScan(ScanBase): + # Scan Type: Hardware triggered or software triggered? + # If the main trigger and readout logic is done within the at_each_point method in scan_core, choose SOFTWARE_TRIGGERED. + # If the main trigger and readout logic is implemented on a device that is simply kicked off in this scan, choose HARDWARE_TRIGGERED. + # This primarily serves as information for devices: The device may need to react differently if a software trigger is expected + # for every point. + scan_type = ScanType.HARDWARE_TRIGGERED + + # Scan name: This is the name of the scan, e.g. "line_scan". This is used for display purposes and to identify the scan type in user interfaces. + # Choose a descriptive name that does not conflict with existing scan names. + # It must be a valid Python identifier, that is, it can only contain letters, numbers, and underscores, and must not start with a number. + scan_name = "nidaq_continuous_scan" + + gui_config = {"Scan Parameters": ["scan_duration", "daq", "compression"]} + + def __init__( + self, + #fmt: off + scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Duration of the scan", units=Units.s)], + daq: Annotated[DeviceBase | None, ScanArgument(display_name="Daq", description="DAQ device to be used for the scan")], + compression: Annotated[bool, ScanArgument(display_name="Compression", description="Whether to compress the data")], + #fmt: on + **kwargs, + ): + """ + The NIDAQ continuous scan is used to measure with the NIDAQ without moving the + monochromator or any other motor. The NIDAQ thus runs in continuous mode, with a + set scan_duration. + + Args: + scan_duration (float): Duration of the scan + daq (DeviceBase): DAQ device to be used for the scan + compression (bool): Whether to compress the data + + Returns: + ScanReport + """ + super().__init__(**kwargs) + self._baseline_readout_status = None + self.scan_duration = scan_duration + self.daq = daq or self.dev["nidaq"] + self.compression = compression + self.primary_readout_cycle = 1.0 # seconds + self.motors = [self.daq] + + self.update_scan_info(scan_duration=scan_duration, compression=compression) + self.actions.set_device_readout_priority([self.daq], priority="async") + + @scan_hook + def prepare_scan(self): + """ + Prepare the scan. This can include any steps that need to be executed + before the scan is opened, such as preparing the positions (if not done already) + or setting up the devices. + """ + + self.actions.add_scan_report_instruction_device_progress(self.daq) + self._baseline_readout_status = self.actions.read_baseline_devices(wait=False) + + @scan_hook + def open_scan(self): + """ + Open the scan. + This step must call self.actions.open_scan() to ensure that a new scan is + opened. Make sure to prepare the scan metadata before, either in + prepare_scan() or in open_scan() itself and call self.update_scan_info(...) + to update the scan metadata if needed. + """ + self.actions.open_scan() + + @scan_hook + def stage(self): + """ + Stage the devices for the upcoming scan. The stage logic is typically + implemented on the device itself (i.e. by the device's stage method). + However, if there are any additional steps that need to be executed before + staging the devices, they can be implemented here. + """ + self.actions.stage_all_devices() + + @scan_hook + def pre_scan(self): + """ + Pre-scan steps to be executed before the main scan logic. + This is typically the last chance to prepare the devices before the core scan + logic is executed. For example, this is a good place to initialize time-criticial + devices, e.g. devices that have a short timeout. + The pre-scan logic is typically implemented on the device itself. + """ + self.actions.pre_scan_all_devices() + + @scan_hook + def scan_core(self): + """ + Core scan logic to be executed during the scan. + This is where the main scan logic should be implemented. + """ + + kickoff_status = self.actions.kickoff(device=self.daq, wait=False) + kickoff_status.wait(timeout=5) # wait for proper kickoff of device + + complete_status = self.actions.complete(device=self.daq, wait=False) + + while not complete_status.done: + self.at_each_point() + time.sleep(self.primary_readout_cycle) + + @scan_hook + def at_each_point(self): + """ + Logic to be executed at each acquisition point during the scan. + """ + self.actions.read_monitored_devices() + + @scan_hook + def post_scan(self): + """ + Post-scan steps to be executed after the main scan logic. + """ + self.actions.complete_all_devices() + + @scan_hook + def unstage(self): + """Unstage the scan by executing post-scan steps.""" + self.actions.unstage_all_devices() + + @scan_hook + def close_scan(self): + """Close the scan.""" + if self._baseline_readout_status is not None: + self._baseline_readout_status.wait() + self.actions.close_scan() + self.actions.check_for_unchecked_statuses() + + @scan_hook + def on_exception(self, exception: Exception): + """ + Handle exceptions that occur during the scan. + This is a good place to implement any cleanup logic that needs to be executed in case of an exception, + such as returning the devices to a safe state or moving the motors back to their starting position. + """ + + ####################################################### + ######### Helper methods for the scan logic ########### + ####################################################### + + # Implement scan-specific helper methods below. diff --git a/tests/tests_scans/conftest.py b/tests/tests_scans/conftest.py index af89857..690abd4 100644 --- a/tests/tests_scans/conftest.py +++ b/tests/tests_scans/conftest.py @@ -3,10 +3,10 @@ from functools import partial import pytest from bec_server.device_server.tests.utils import DeviceMockType, DMMock -from bec_server.scan_server.tests.fixtures import ( - ScanStubStatusMock, - connector_mock, - instruction_handler_mock, +from bec_server.scan_server.tests.scan_fixtures import ( + nth_done_status_mock, + readout_priority, + v4_scan_assembler, ) diff --git a/tests/tests_scans/test_mono_bragg_scans.py b/tests/tests_scans/test_mono_bragg_scans.py deleted file mode 100644 index 789b24b..0000000 --- a/tests/tests_scans/test_mono_bragg_scans.py +++ /dev/null @@ -1,429 +0,0 @@ -# pylint: skip-file -from unittest import mock - -from bec_lib.messages import DeviceInstructionMessage -from bec_server.device_server.tests.utils import DMMock - -from debye_bec.scans import ( - XASAdvancedScan, - XASAdvancedScanWithXRD, - XASSimpleScan, - XASSimpleScanWithXRD, -) - - -def get_instructions(request, ScanStubStatusMock): - request.metadata["RID"] = "my_test_request_id" - - def fake_done(): - """ - Fake done function for ScanStubStatusMock. Upon each call, it returns the next value from the generator. - This is used to simulate the completion of the scan. - """ - yield False - yield False - yield True - - def fake_complete(*args, **kwargs): - yield "fake_complete" - return ScanStubStatusMock(done_func=fake_done) - - with ( - mock.patch.object(request.stubs, "complete", side_effect=fake_complete), - mock.patch.object(request.stubs, "_get_result_from_status", return_value=None), - ): - reference_commands = list(request.run()) - - for cmd in reference_commands: - if not cmd or isinstance(cmd, str): - continue - if "RID" in cmd.metadata: - cmd.metadata["RID"] = "my_test_request_id" - if "rpc_id" in cmd.parameter: - cmd.parameter["rpc_id"] = "my_test_rpc_id" - cmd.metadata.pop("device_instr_id", None) - - return reference_commands - - -def test_xas_simple_scan(scan_assembler, ScanStubStatusMock): - - request = scan_assembler(XASSimpleScan, start=0, stop=5, scan_time=1, scan_duration=10) - request.device_manager.add_device("nidaq") - reference_commands = get_instructions(request, ScanStubStatusMock) - - assert reference_commands == [ - None, - None, - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="scan_report_instruction", - parameter={"device_progress": ["mo1_bragg"]}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="open_scan", - parameter={ - "readout_priority": { - "monitored": [], - "baseline": [], - "on_request": [], - "async": ["nidaq"], - }, - "num_points": None, - "positions": [0.0, 5.0], - "scan_name": "xas_simple_scan", - "scan_type": "fly", - }, - ), - DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}), - DeviceInstructionMessage( - metadata={}, - device=["bpm4i", "eiger", "mo1_bragg", "samx"], - action="stage", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "baseline", "RID": "my_test_request_id"}, - device=["samx"], - action="read", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"], - action="pre_scan", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device="mo1_bragg", - action="kickoff", - parameter={"configure": {}}, - ), - "fake_complete", - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 0}, - device=["bpm4i", "eiger", "mo1_bragg"], - action="read", - parameter={"group": "monitored"}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 1}, - device=["bpm4i", "eiger", "mo1_bragg"], - action="read", - parameter={"group": "monitored"}, - ), - "fake_complete", - DeviceInstructionMessage( - metadata={}, - device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"], - action="unstage", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="close_scan", - parameter={}, - ), - ] - - -def test_xas_simple_scan_with_xrd(scan_assembler, ScanStubStatusMock): - - request = scan_assembler( - XASSimpleScanWithXRD, - start=0, - stop=5, - scan_time=1, - scan_duration=10, - break_enable_low=True, - break_time_low=1, - cycle_low=1, - break_enable_high=True, - break_time_high=2, - exp_time=1, - n_of_trigger=1, - cycle_high=4, - ) - request.device_manager.add_device("nidaq") - reference_commands = get_instructions(request, ScanStubStatusMock) - # TODO #64 based on creating this ScanStatusMessage, we should test the logic of stage/kickoff/complete/unstage in Pilatus and mo1Bragg - - assert reference_commands == [ - None, - None, - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="scan_report_instruction", - parameter={"device_progress": ["mo1_bragg"]}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="open_scan", - parameter={ - "readout_priority": { - "monitored": [], - "baseline": [], - "on_request": [], - "async": ["nidaq"], - }, - "num_points": None, - "positions": [0.0, 5.0], - "scan_name": "xas_simple_scan_with_xrd", - "scan_type": "fly", - }, - ), - DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}), - DeviceInstructionMessage( - metadata={}, - device=["bpm4i", "eiger", "mo1_bragg", "samx"], - action="stage", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "baseline", "RID": "my_test_request_id"}, - device=["samx"], - action="read", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"], - action="pre_scan", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device="mo1_bragg", - action="kickoff", - parameter={"configure": {}}, - ), - "fake_complete", - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 0}, - device=["bpm4i", "eiger", "mo1_bragg"], - action="read", - parameter={"group": "monitored"}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 1}, - device=["bpm4i", "eiger", "mo1_bragg"], - action="read", - parameter={"group": "monitored"}, - ), - "fake_complete", - DeviceInstructionMessage( - metadata={}, - device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"], - action="unstage", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="close_scan", - parameter={}, - ), - ] - - -def test_xas_advanced_scan(scan_assembler, ScanStubStatusMock): - - request = scan_assembler( - XASAdvancedScan, - start=8000, - stop=9000, - scan_time=1, - scan_duration=10, - p_kink=50, - e_kink=8500, - ) - request.device_manager.add_device("nidaq") - reference_commands = get_instructions(request, ScanStubStatusMock) - - assert reference_commands == [ - None, - None, - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="scan_report_instruction", - parameter={"device_progress": ["mo1_bragg"]}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="open_scan", - parameter={ - "readout_priority": { - "monitored": [], - "baseline": [], - "on_request": [], - "async": ["nidaq"], - }, - "num_points": None, - "positions": [8000.0, 9000.0], - "scan_name": "xas_advanced_scan", - "scan_type": "fly", - }, - ), - DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}), - DeviceInstructionMessage( - metadata={}, - device=["bpm4i", "eiger", "mo1_bragg", "samx"], - action="stage", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "baseline", "RID": "my_test_request_id"}, - device=["samx"], - action="read", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"], - action="pre_scan", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device="mo1_bragg", - action="kickoff", - parameter={"configure": {}}, - ), - "fake_complete", - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 0}, - device=["bpm4i", "eiger", "mo1_bragg"], - action="read", - parameter={"group": "monitored"}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 1}, - device=["bpm4i", "eiger", "mo1_bragg"], - action="read", - parameter={"group": "monitored"}, - ), - "fake_complete", - DeviceInstructionMessage( - metadata={}, - device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"], - action="unstage", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="close_scan", - parameter={}, - ), - ] - - -def test_xas_advanced_scan_with_xrd(scan_assembler, ScanStubStatusMock): - - request = scan_assembler( - XASAdvancedScanWithXRD, - start=8000, - stop=9000, - scan_time=1, - scan_duration=10, - p_kink=50, - e_kink=8500, - break_enable_low=True, - break_time_low=1, - cycle_low=1, - break_enable_high=True, - break_time_high=2, - exp_time=1, - n_of_trigger=1, - cycle_high=4, - ) - request.device_manager.add_device("nidaq") - reference_commands = get_instructions(request, ScanStubStatusMock) - - assert reference_commands == [ - None, - None, - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="scan_report_instruction", - parameter={"device_progress": ["mo1_bragg"]}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="open_scan", - parameter={ - "readout_priority": { - "monitored": [], - "baseline": [], - "on_request": [], - "async": ["nidaq"], - }, - "num_points": None, - "positions": [8000.0, 9000.0], - "scan_name": "xas_advanced_scan_with_xrd", - "scan_type": "fly", - }, - ), - DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}), - DeviceInstructionMessage( - metadata={}, - device=["bpm4i", "eiger", "mo1_bragg", "samx"], - action="stage", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "baseline", "RID": "my_test_request_id"}, - device=["samx"], - action="read", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"], - action="pre_scan", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device="mo1_bragg", - action="kickoff", - parameter={"configure": {}}, - ), - "fake_complete", - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 0}, - device=["bpm4i", "eiger", "mo1_bragg"], - action="read", - parameter={"group": "monitored"}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 1}, - device=["bpm4i", "eiger", "mo1_bragg"], - action="read", - parameter={"group": "monitored"}, - ), - "fake_complete", - DeviceInstructionMessage( - metadata={}, - device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"], - action="unstage", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="close_scan", - parameter={}, - ), - ] diff --git a/tests/tests_scans/test_mono_bragg_scans_v4.py b/tests/tests_scans/test_mono_bragg_scans_v4.py index bf791a1..078c53a 100644 --- a/tests/tests_scans/test_mono_bragg_scans_v4.py +++ b/tests/tests_scans/test_mono_bragg_scans_v4.py @@ -3,15 +3,8 @@ from unittest import mock import numpy as np import pytest -from bec_server.scan_server.tests.scan_hook_tests import ( - assert_close_scan_waits_for_baseline_and_closes, - assert_pre_scan_called, - assert_prepare_scan_reads_baseline_devices, - assert_scan_open_called, - assert_stage_all_devices_called, - assert_unstage_all_devices_called, - run_scan_tests, -) +from bec_server.scan_server.tests.scan_fixtures import * +from bec_server.scan_server.tests.scan_hook_tests import * XAS_SIMPLE_SCAN_DEFAULT_HOOK_TESTS = [ ("prepare_scan", [assert_prepare_scan_reads_baseline_devices]), diff --git a/tests/tests_scans/test_nidaq_continous_scan.py b/tests/tests_scans/test_nidaq_continous_scan.py index 4596d84..f2c10ec 100644 --- a/tests/tests_scans/test_nidaq_continous_scan.py +++ b/tests/tests_scans/test_nidaq_continous_scan.py @@ -1,126 +1,75 @@ # pylint: skip-file from unittest import mock -from bec_lib.messages import DeviceInstructionMessage -from bec_server.device_server.tests.utils import DMMock +import pytest +from bec_server.scan_server.tests.scan_fixtures import * +from bec_server.scan_server.tests.scan_hook_tests import * -from debye_bec.scans import NIDAQContinuousScan +NIDAQ_CONTINUOUS_SCAN_DEFAULT_HOOK_TESTS = [ + ("prepare_scan", [assert_prepare_scan_reads_baseline_devices]), + ("open_scan", [assert_scan_open_called]), + ("stage", [assert_stage_all_devices_called]), + ("pre_scan", [assert_pre_scan_called]), + ("unstage", [assert_unstage_all_devices_called]), + ("close_scan", [assert_close_scan_waits_for_baseline_and_closes]), +] -def get_instructions(request, ScanStubStatusMock): - request.metadata["RID"] = "my_test_request_id" - - def fake_done(): - """ - Fake done function for ScanStubStatusMock. Upon each call, it returns the next value from the generator. - This is used to simulate the completion of the scan. - """ - yield False - yield False - yield True - - def fake_complete(*args, **kwargs): - yield "fake_complete" - return ScanStubStatusMock(done_func=fake_done) - - with ( - mock.patch.object(request.stubs, "complete", side_effect=fake_complete), - mock.patch.object(request.stubs, "_get_result_from_status", return_value=None), - ): - reference_commands = list(request.run()) - - for cmd in reference_commands: - if not cmd or isinstance(cmd, str): - continue - if "RID" in cmd.metadata: - cmd.metadata["RID"] = "my_test_request_id" - if "rpc_id" in cmd.parameter: - cmd.parameter["rpc_id"] = "my_test_rpc_id" - cmd.metadata.pop("device_instr_id", None) - - return reference_commands +def _assemble_nidaq_continuous_scan(v4_scan_assembler, **overrides): + params = {"scan_duration": 10.0, "daq": "nidaq", "compression": False} + params.update(overrides) + return v4_scan_assembler("nidaq_continuous_scan", **params) -def test_xas_simple_scan(scan_assembler, ScanStubStatusMock): +@pytest.mark.parametrize(("hook_name", "hook_tests"), NIDAQ_CONTINUOUS_SCAN_DEFAULT_HOOK_TESTS) +def test_nidaq_continuous_scan_v4_default_hooks( + v4_scan_assembler, nth_done_status_mock, hook_name, hook_tests +): + scan = _assemble_nidaq_continuous_scan(v4_scan_assembler) - request = scan_assembler(NIDAQContinuousScan, scan_duration=10) - request.device_manager.add_device("nidaq") - reference_commands = get_instructions(request, ScanStubStatusMock) - assert reference_commands == [ - None, - None, - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="scan_report_instruction", - parameter={"device_progress": ["nidaq"]}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="open_scan", - parameter={ - "readout_priority": { - "monitored": [], - "baseline": [], - "on_request": [], - "async": ["nidaq"], - }, - "num_points": 0, - "positions": [], - "scan_name": "nidaq_continuous_scan", - "scan_type": "fly", - }, - ), - DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}), - DeviceInstructionMessage( - metadata={}, - device=["bpm4i", "eiger", "mo1_bragg", "samx"], - action="stage", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "baseline", "RID": "my_test_request_id"}, - device=["samx"], - action="read", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"], - action="pre_scan", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device="nidaq", - action="kickoff", - parameter={"configure": {}}, - ), - "fake_complete", - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 0}, - device=["bpm4i", "eiger", "mo1_bragg"], - action="read", - parameter={"group": "monitored"}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id", "point_id": 1}, - device=["bpm4i", "eiger", "mo1_bragg"], - action="read", - parameter={"group": "monitored"}, - ), - "fake_complete", - DeviceInstructionMessage( - metadata={}, - device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"], - action="unstage", - parameter={}, - ), - DeviceInstructionMessage( - metadata={"readout_priority": "monitored", "RID": "my_test_request_id"}, - device=None, - action="close_scan", - parameter={}, - ), - ] + run_scan_tests(scan, [(hook_name, hook_tests)], nth_done_status_mock=nth_done_status_mock) + + +def test_nidaq_continuous_scan_v4_prepare_scan_updates_metadata(v4_scan_assembler): + scan = _assemble_nidaq_continuous_scan(v4_scan_assembler) + scan.actions.add_scan_report_instruction_device_progress = mock.MagicMock() + baseline_status = mock.MagicMock() + scan.actions.read_baseline_devices = mock.MagicMock(return_value=baseline_status) + + scan.prepare_scan() + scan.actions._build_scan_status_message("open") + + assert scan.scan_info.additional_scan_parameters["scan_duration"] == 10.0 + assert scan.scan_info.additional_scan_parameters["compression"] is False + assert scan.scan_info.readout_priority_modification["async"] == ["nidaq"] + scan.actions.add_scan_report_instruction_device_progress.assert_called_once_with(scan.daq) + scan.actions.read_baseline_devices.assert_called_once_with(wait=False) + assert scan._baseline_readout_status is baseline_status + + +def test_nidaq_continuous_scan_v4_scan_core_reads_until_complete( + v4_scan_assembler, nth_done_status_mock +): + scan = _assemble_nidaq_continuous_scan(v4_scan_assembler) + kickoff_status = mock.MagicMock() + completion_status = nth_done_status_mock(resolve_after=3) + scan.actions.kickoff = mock.MagicMock(return_value=kickoff_status) + scan.actions.complete = mock.MagicMock(return_value=completion_status) + scan.actions.read_monitored_devices = mock.MagicMock() + + with mock.patch("debye_bec.scans.nidaq_continuous_scan.time.sleep"): + scan.scan_core() + + scan.actions.kickoff.assert_called_once_with(device=scan.daq, wait=False) + kickoff_status.wait.assert_called_once_with(timeout=5) + scan.actions.complete.assert_called_once_with(device=scan.daq, wait=False) + assert scan.actions.read_monitored_devices.call_count == 2 + + +def test_nidaq_continuous_scan_v4_post_scan_completes_all_devices(v4_scan_assembler): + scan = _assemble_nidaq_continuous_scan(v4_scan_assembler) + scan.actions.complete_all_devices = mock.MagicMock() + + scan.post_scan() + + scan.actions.complete_all_devices.assert_called_once_with() -- 2.54.0 From 98d5c2266774cf5a5acaa2b58cc4867a28ff51c8 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 21 May 2026 17:41:49 +0200 Subject: [PATCH 06/23] refactor(nidaq): migrate NIDAQ to v4 scan_info --- debye_bec/devices/nidaq/nidaq.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/debye_bec/devices/nidaq/nidaq.py b/debye_bec/devices/nidaq/nidaq.py index 99c9caf..b403570 100644 --- a/debye_bec/devices/nidaq/nidaq.py +++ b/debye_bec/devices/nidaq/nidaq.py @@ -406,18 +406,18 @@ class Nidaq(PSIDeviceBase, NidaqControl): status.wait(timeout=self.timeout_wait_for_signal) # If scan is not part of the valid_scan_names, - if self.scan_info.msg.scan_name != "nidaq_continuous_scan": + if self.scan_info.msg.scan_name != "nidaq_continuous_scan": # what is the new v4 scan self.scan_type.set(ScanType.TRIGGERED).wait(timeout=self._timeout_wait_for_pv) self.scan_duration.set(0).wait(timeout=self._timeout_wait_for_pv) self.enable_compression.set(1).wait(timeout=self._timeout_wait_for_pv) else: self.scan_type.set(ScanType.CONTINUOUS).wait(timeout=self._timeout_wait_for_pv) - self.scan_duration.set(self.scan_info.msg.scan_parameters["scan_duration"]).wait( - timeout=self._timeout_wait_for_pv - ) - self.enable_compression.set(self.scan_info.msg.scan_parameters["compression"]).wait( - timeout=self._timeout_wait_for_pv - ) + self.scan_duration.set( + self.scan_info.msg.additional_scan_parameters["scan_duration"] + ).wait(timeout=self._timeout_wait_for_pv) + self.enable_compression.set( + self.scan_info.msg.additional_scan_parameters["compression"] + ).wait(timeout=self._timeout_wait_for_pv) # Stage call to IOC status = CompareStatus(self.state, NidaqState.STAGE) @@ -499,7 +499,7 @@ class Nidaq(PSIDeviceBase, NidaqControl): Args: value (int) : current progress value """ - scan_duration = self.scan_info.msg.scan_parameters.get("scan_duration", None) + scan_duration = self.scan_info.msg.additional_scan_parameters.get("scan_duration", None) if not isinstance(scan_duration, (int, float)): return value = scan_duration - value -- 2.54.0 From 262a0b6318bd72f8b0f04176d18dcc87913eab30 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 22 May 2026 10:04:49 +0200 Subject: [PATCH 07/23] refactor(pilatus): migrate to scans v4 interface --- debye_bec/devices/pilatus/pilatus.py | 128 +++++++++++++++------------ debye_bec/devices/utils/__init__.py | 0 debye_bec/devices/utils/utils.py | 9 ++ 3 files changed, 82 insertions(+), 55 deletions(-) create mode 100644 debye_bec/devices/utils/__init__.py create mode 100644 debye_bec/devices/utils/utils.py diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index 2a7668f..6c721fe 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -11,16 +11,26 @@ from typing import TYPE_CHECKING, Tuple import numpy as np from bec_lib.file_utils import get_full_path from bec_lib.logger import bec_logger +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd import Component as Cpt from ophyd import EpicsSignal, EpicsSignalRO, Kind from ophyd.areadetector.cam import ADBase, PilatusDetectorCam from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin from ophyd.status import WaitTimeoutError -from ophyd_devices import AndStatus, CompareStatus, DeviceStatus, FileEventSignal, PreviewSignal +from ophyd_devices import ( + AndStatus, + CompareStatus, + DeviceStatus, + ExceptionStatus, + FileEventSignal, + PreviewSignal, +) from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase from pydantic import BaseModel, Field +from debye_bec.devices.utils.utils import fetch_scan_info + if TYPE_CHECKING: # pragma: no cover from bec_lib.devicemanager import ScanInfo from bec_lib.messages import DevicePreviewMessage, ScanStatusMessage @@ -145,17 +155,17 @@ class Pilatus(PSIDeviceBase, ADBase): # USER_ACCESS = ["start_live_mode", "stop_live_mode"] - cam_gain_menu_string = Cpt(EpicsSignalRO, suffix='cam1:GainMenu', string=True) + cam_gain_menu_string = Cpt(EpicsSignalRO, suffix="cam1:GainMenu", string=True) _default_configuration_attrs = [ - 'cam.threshold_energy', - 'cam.threshold_auto_apply', - 'cam.gain_menu', - 'cam_gain_menu_string', - 'cam.pixel_cut_off', - 'cam.acquire_time', - 'cam.num_exposures', - 'cam.model', + "cam.threshold_energy", + "cam.threshold_auto_apply", + "cam.gain_menu", + "cam_gain_menu_string", + "cam.pixel_cut_off", + "cam.acquire_time", + "cam.num_exposures", + "cam.model", ] cam = Cpt(PilatusDetectorCam, "cam1:") @@ -233,7 +243,6 @@ class Pilatus(PSIDeviceBase, ADBase): super().__init__( name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs ) - self.scan_parameter = ScanParameter() self.device_manager = device_manager self._readout_time = PILATUS_READOUT_TIME self._full_path = "" @@ -251,6 +260,7 @@ class Pilatus(PSIDeviceBase, ADBase): # self._live_mode_run_event = threading.Event() # self._live_mode_stopped_event = threading.Event() # self._live_mode_stopped_event.set() # Initial state is stopped + self.scan_parameters: ScanServerScanInfo | None = None ######################################## # Custom Beamline Methods # @@ -368,19 +378,22 @@ class Pilatus(PSIDeviceBase, ADBase): status = status_acquire & status_writing & status_cam_server return status - def _calculate_trigger(self, scan_msg: ScanStatusMessage) -> Tuple[float, float]: - self._update_scan_parameter() + def _calculate_trigger(self, scan_parameters: ScanServerScanInfo) -> Tuple[float, float]: total_osc = 0 calc_duration = 0 total_trig_lo = 0 total_trig_hi = 0 # Switching high/low is intended as angle is inverse to energy and settings in BEC are always in energy - loc_break_enable_low = self.scan_parameter.break_enable_high - loc_break_time_low = self.scan_parameter.break_time_high - loc_cycle_low = self.scan_parameter.cycle_high - loc_break_enable_high = self.scan_parameter.break_enable_low - loc_break_time_high = self.scan_parameter.break_time_low - loc_cycle_high = self.scan_parameter.cycle_low + loc_break_enable_low = scan_parameters.additional_scan_parameters.get( + "break_enable_high", False + ) + loc_break_time_low = scan_parameters.additional_scan_parameters.get("break_time_high", 0) + loc_cycle_low = scan_parameters.additional_scan_parameters.get("cycle_high", 1) + loc_break_enable_high = scan_parameters.additional_scan_parameters.get( + "break_enable_low", False + ) + loc_break_time_high = scan_parameters.additional_scan_parameters.get("break_time_low", 0) + loc_cycle_high = scan_parameters.additional_scan_parameters.get("cycle_low", 1) if not loc_break_enable_low: loc_break_time_low = 0 @@ -389,28 +402,36 @@ class Pilatus(PSIDeviceBase, ADBase): loc_break_time_high = 0 loc_cycle_high = 1 - total_osc = self.scan_parameter.scan_duration / ( - self.scan_parameter.scan_time + - loc_break_time_low / (2 * loc_cycle_low) + - loc_break_time_high / (2 * loc_cycle_high) + total_osc = scan_parameters.additional_scan_parameters.get("scan_duration", 0) / ( + scan_parameters.additional_scan_parameters.get("scan_time", 0) + + loc_break_time_low / (2 * loc_cycle_low) + + loc_break_time_high / (2 * loc_cycle_high) ) total_osc = np.ceil(total_osc) - total_osc = total_osc + total_osc % 2 # round up to the next even number + total_osc = total_osc + total_osc % 2 # round up to the next even number if loc_break_enable_low: total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low)) if loc_break_enable_high: total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high)) - calc_duration = total_osc * self.scan_parameter.scan_time + total_trig_lo * loc_break_time_low + total_trig_hi * loc_break_time_high - - if calc_duration < self.scan_parameter.scan_duration: + calc_duration = ( + total_osc * scan_parameters.additional_scan_parameters.get("scan_time", 0) + + total_trig_lo * loc_break_time_low + + total_trig_hi * loc_break_time_high + ) + + if calc_duration < scan_parameters.additional_scan_parameters.get("scan_duration", 0): # Due to inaccuracy in formula, this can happen, we then need to manually add two oscillations and recalculate the triggers total_osc = total_osc + 2 if loc_break_enable_low: total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low)) if loc_break_enable_high: total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high)) - calc_duration = total_osc * self.scan_parameter.scan_time + total_trig_lo * loc_break_time_low + total_trig_hi * loc_break_time_high + calc_duration = ( + total_osc * scan_parameters.additional_scan_parameters.get("scan_time", 0) + + total_trig_lo * loc_break_time_low + + total_trig_hi * loc_break_time_high + ) return total_trig_lo, total_trig_hi @@ -464,6 +485,7 @@ class Pilatus(PSIDeviceBase, ADBase): (self.scan_info.msg) object. """ # self.stop_live_mode() # Make sure that live mode is stopped if scan runs + self.scan_parameters = fetch_scan_info(self.scan_info) # If user has activated alignment mode on qt panel, switch back to multitrigger and stop acquisition if self.cam.trigger_mode.get() != TRIGGERMODE.MULT_TRIGGER.value: @@ -475,23 +497,26 @@ class Pilatus(PSIDeviceBase, ADBase): scan_msg: ScanStatusMessage = self.scan_info.msg if scan_msg.scan_name in self.xas_xrd_scan_names: - self._update_scan_parameter() # Compute number of triggers - total_trig_lo, total_trig_hi = self._calculate_trigger(scan_msg) + total_trig_lo, total_trig_hi = self._calculate_trigger(self.scan_parameters) # Set the number of images, we may also set this to a higher values if preferred and stop the acquisition # TODO This logic is prone to errors, as we rely on the scans to nicely resolve to n_images. We should # use here instead a way of settings the n_images independently of the scan parameters to avoid running out of sync # with the complete method. Ideally we comput them in the scan itself.. This is much safer IMO! - self.n_images = (total_trig_lo + total_trig_hi) * self.scan_parameter.n_of_trigger - exp_time = self.scan_parameter.exp_time + self.n_images = ( + total_trig_lo + total_trig_hi + ) * self.scan_parameters.additional_scan_parameters.get("n_of_trigger", 1) + exp_time = self.scan_parameters.exp_time self.trigger_source.set(MONOTRIGGERSOURCE.INPOS).wait(5) - self.trigger_n_of.set(self.scan_parameter.n_of_trigger).wait(5) + self.trigger_n_of.set( + self.scan_parameters.additional_scan_parameters.get("n_of_trigger", 1) + ).wait(5) elif scan_msg.scan_type == "step": - self.n_images = scan_msg.num_points * scan_msg.scan_parameters.get( - "frames_per_trigger", 1 + self.n_images = ( + self.scan_parameters.num_monitored_readouts * scan_msg.frames_per_trigger ) - exp_time = scan_msg.scan_parameters.get("exp_time") + exp_time = self.scan_parameters.exp_time self.trigger_source.set(MONOTRIGGERSOURCE.EPICS).wait(5) self.trigger_n_of.set(1).wait(5) # BEC will trigger each acquisition else: @@ -544,9 +569,9 @@ class Pilatus(PSIDeviceBase, ADBase): def on_pre_scan(self) -> DeviceStatus | None: """Called right before the scan starts on all devices automatically.""" - scan_msg: ScanStatusMessage = self.scan_info.msg if ( - scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == "step" + self.scan_parameters.scan_name in self.xas_xrd_scan_names + or self.scan_parameters.scan_type == "step" ): # TODO how to deal with fly scans? status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value) status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value) @@ -561,8 +586,7 @@ class Pilatus(PSIDeviceBase, ADBase): def on_trigger(self) -> DeviceStatus | None: """Called when the device is triggered.""" - scan_msg: ScanStatusMessage = self.scan_info.msg - if not scan_msg.scan_type == "step": + if not self.scan_parameters.scan_type == "step": return None start_time = time.time() img_counter = self.hdf.num_captured.get() @@ -575,9 +599,9 @@ class Pilatus(PSIDeviceBase, ADBase): def _complete_callback(self, status: DeviceStatus): """Callback for when the device completes a scan.""" - scan_msg: ScanStatusMessage = self.scan_info.msg if ( - scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == "step" + self.scan_parameters.scan_name in self.xas_xrd_scan_names + or self.scan_parameters.scan_type == "step" ): # TODO how to deal with fly scans? if status.success: self.file_event.put( @@ -598,14 +622,15 @@ class Pilatus(PSIDeviceBase, ADBase): def on_complete(self) -> DeviceStatus | None: """Called to inquire if a device has completed a scans.""" - scan_msg: ScanStatusMessage = self.scan_info.msg if ( - scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == "step" + self.scan_parameters.scan_name in self.xas_xrd_scan_names + or self.scan_parameters.scan_type == "step" ): # TODO how to deal with fly scans? status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) - if self.scan_info.msg.scan_name in self.xas_xrd_scan_names: + # status_write_error = ExceptionStatus(self.hdf.write_status, 0, operation="!=") + if self.scan_parameters.scan_name in self.xas_xrd_scan_names: # For long scans, it can be that the mono will execute one cycle more, # meaning a few more XRD triggers will be sent status_img_written = CompareStatus( @@ -614,7 +639,9 @@ class Pilatus(PSIDeviceBase, ADBase): else: status_img_written = CompareStatus(self.hdf.num_captured, self.n_images) status_img_written = CompareStatus(self.hdf.num_captured, self.n_images) - status = status_hdf & status_cam & status_img_written & status_cam_server + status = ( + status_hdf & status_cam & status_img_written & status_cam_server + ) # & status_write_error status.add_callback(self._complete_callback) # Callback that writing was successful self.cancel_on_stop(status) return status @@ -635,15 +662,6 @@ class Pilatus(PSIDeviceBase, ADBase): # TODO do we need to clean the poll thread ourselves? self.on_stop() - def _update_scan_parameter(self): - """Get the scan_info parameters for the scan.""" - for key, value in self.scan_info.msg.request_inputs["inputs"].items(): - if hasattr(self.scan_parameter, key): - setattr(self.scan_parameter, key, value) - for key, value in self.scan_info.msg.request_inputs["kwargs"].items(): - if hasattr(self.scan_parameter, key): - setattr(self.scan_parameter, key, value) - if __name__ == "__main__": try: diff --git a/debye_bec/devices/utils/__init__.py b/debye_bec/devices/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/debye_bec/devices/utils/utils.py b/debye_bec/devices/utils/utils.py new file mode 100644 index 0000000..f747655 --- /dev/null +++ b/debye_bec/devices/utils/utils.py @@ -0,0 +1,9 @@ +"""Utility functions for the devices.""" + +from bec_lib.devicemanager import ScanInfo +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo + + +def fetch_scan_info(scan_info: ScanInfo) -> ScanServerScanInfo: + """Fetch the scan parameters from the scan_info object and return them as a ScanServerScanInfo object.""" + return ScanServerScanInfo.model_validate(scan_info.msg.info) -- 2.54.0 From 359ef0b6d789ab173c36094a4afdaab3c19ab397 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 22 May 2026 10:10:20 +0200 Subject: [PATCH 08/23] refactor(nidaq): migrate nidaq to scans v4 --- debye_bec/devices/nidaq/nidaq.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/debye_bec/devices/nidaq/nidaq.py b/debye_bec/devices/nidaq/nidaq.py index b403570..b7882a1 100644 --- a/debye_bec/devices/nidaq/nidaq.py +++ b/debye_bec/devices/nidaq/nidaq.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Literal from bec_lib.logger import bec_logger +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd import Component as Cpt from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Kind, StatusBase from ophyd.status import WaitTimeoutError @@ -18,6 +19,7 @@ from debye_bec.devices.nidaq.nidaq_enums import ( ScanRates, ScanType, ) +from debye_bec.devices.utils.utils import fetch_scan_info if TYPE_CHECKING: # pragma: no cover from bec_lib.devicemanager import ScanInfo @@ -219,7 +221,7 @@ class Nidaq(PSIDeviceBase, NidaqControl): def __init__(self, prefix: str = "", *, name: str, scan_info: ScanInfo = None, **kwargs): super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs) - self.scan_info: ScanInfo + self.scan_parameters: ScanServerScanInfo = None self.timeout_wait_for_signal = 5 # put 5s firsts self._timeout_wait_for_pv = ( 5 # 5s timeout for pv calls. editted due to timeout issues persisting @@ -236,10 +238,9 @@ class Nidaq(PSIDeviceBase, NidaqControl): # Beamline Methods # ######################################## - def _check_if_scan_name_is_valid(self) -> bool: + def _check_if_scan_name_is_valid(self, scan_parameters: ScanServerScanInfo) -> bool: """Check if the scan is within the list of scans for which the backend is working""" - scan_name = self.scan_info.msg.scan_name - if scan_name in self.valid_scan_names: + if scan_parameters.scan_name in self.valid_scan_names: return True return False @@ -396,7 +397,8 @@ class Nidaq(PSIDeviceBase, NidaqControl): Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object. If the upcoming scan is not in the list of valid scans, return immediately. """ - if not self._check_if_scan_name_is_valid(): + self.scan_parameters = fetch_scan_info(self.scan_info) + if not self._check_if_scan_name_is_valid(self.scan_parameters): return None if self.state.get() != NidaqState.STANDBY: @@ -406,17 +408,17 @@ class Nidaq(PSIDeviceBase, NidaqControl): status.wait(timeout=self.timeout_wait_for_signal) # If scan is not part of the valid_scan_names, - if self.scan_info.msg.scan_name != "nidaq_continuous_scan": # what is the new v4 scan + if self.scan_parameters.scan_name != "nidaq_continuous_scan": # what is the new v4 scan self.scan_type.set(ScanType.TRIGGERED).wait(timeout=self._timeout_wait_for_pv) self.scan_duration.set(0).wait(timeout=self._timeout_wait_for_pv) self.enable_compression.set(1).wait(timeout=self._timeout_wait_for_pv) else: self.scan_type.set(ScanType.CONTINUOUS).wait(timeout=self._timeout_wait_for_pv) self.scan_duration.set( - self.scan_info.msg.additional_scan_parameters["scan_duration"] + self.scan_parameters.additional_scan_parameters["scan_duration"] ).wait(timeout=self._timeout_wait_for_pv) self.enable_compression.set( - self.scan_info.msg.additional_scan_parameters["compression"] + self.scan_parameters.additional_scan_parameters["compression"] ).wait(timeout=self._timeout_wait_for_pv) # Stage call to IOC @@ -428,7 +430,7 @@ class Nidaq(PSIDeviceBase, NidaqControl): # self.stage_call.set(1).wait(timeout=self._timeout_wait_for_pv) self.stage_call.put(1) status.wait(timeout=self.timeout_wait_for_signal) - if self.scan_info.msg.scan_name != "nidaq_continuous_scan": + if self.scan_parameters.scan_name != "nidaq_continuous_scan": status = self.on_kickoff() self.cancel_on_stop(status) status.wait(timeout=self._timeout_wait_for_pv) @@ -459,10 +461,10 @@ class Nidaq(PSIDeviceBase, NidaqControl): before the motor starts its oscillation. This is needed for being properly homed. The NIDAQ should go into Acquiring mode. """ - if not self._check_if_scan_name_is_valid(): + if not self._check_if_scan_name_is_valid(self.scan_parameters): return None - if self.scan_info.msg.scan_name == "nidaq_continuous_scan": + if self.scan_parameters.scan_name == "nidaq_continuous_scan": logger.info(f"Device {self.name} ready to be kicked off for nidaq_continuous_scan") return None @@ -483,12 +485,12 @@ class Nidaq(PSIDeviceBase, NidaqControl): For the NIDAQ we use this method to stop the backend since it would not stop by itself in its current implementation since the number of points are not predefined. """ - if not self._check_if_scan_name_is_valid(): + if not self._check_if_scan_name_is_valid(self.scan_parameters): return None status = CompareStatus(self.state, NidaqState.STANDBY) self.cancel_on_stop(status) - if self.scan_info.msg.scan_name != "nidaq_continuous_scan": + if self.scan_parameters.scan_name != "nidaq_continuous_scan": self.on_stop() return status @@ -499,7 +501,9 @@ class Nidaq(PSIDeviceBase, NidaqControl): Args: value (int) : current progress value """ - scan_duration = self.scan_info.msg.additional_scan_parameters.get("scan_duration", None) + if self.scan_parameters is None: + return + scan_duration = self.scan_parameters.additional_scan_parameters.get("scan_duration", None) if not isinstance(scan_duration, (int, float)): return value = scan_duration - value -- 2.54.0 From 78d58ad26f5c0f123c2712ff5614dde27e4bb5c5 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 22 May 2026 10:16:06 +0200 Subject: [PATCH 09/23] refactor(hutch-cam): migrate hutch cam to scans v4 --- debye_bec/devices/cameras/hutch_cam.py | 31 ++++++++++++++++---------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/debye_bec/devices/cameras/hutch_cam.py b/debye_bec/devices/cameras/hutch_cam.py index 633b8dc..64e68f3 100644 --- a/debye_bec/devices/cameras/hutch_cam.py +++ b/debye_bec/devices/cameras/hutch_cam.py @@ -2,14 +2,17 @@ from __future__ import annotations -import cv2 import threading from typing import TYPE_CHECKING -from bec_lib.logger import bec_logger +import cv2 from bec_lib.file_utils import get_full_path -from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase +from bec_lib.logger import bec_logger +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd_devices import DeviceStatus +from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase + +from debye_bec.devices.utils.utils import fetch_scan_info if TYPE_CHECKING: # pragma: no cover from bec_lib.devicemanager import ScanInfo @@ -21,6 +24,7 @@ CAM_USERNAME = "camera_user" CAM_PASSWORD = "camera_user1" CAM_PORT = 554 + class HutchCam(PSIDeviceBase): """Class for the Hutch Cameras""" @@ -28,7 +32,7 @@ class HutchCam(PSIDeviceBase): def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): super().__init__(name=name, scan_info=scan_info, **kwargs) - + self.scan_parameters: ScanServerScanInfo = None self.hostname = prefix self.status = None @@ -47,33 +51,36 @@ class HutchCam(PSIDeviceBase): def on_stage(self) -> DeviceStatus: """Called while staging the device.""" - - scan_msg: ScanStatusMessage = self.scan_info.msg - file_path = get_full_path(scan_msg, name='hutch_cam_' + self.hostname).removesuffix('h5') + self.scan_parameters = fetch_scan_info(self.scan_info) + file_path = get_full_path(self.scan_info, name="hutch_cam_" + self.hostname).removesuffix( + "h5" + ) self.status = DeviceStatus(self) - thread = threading.Thread(target=self._save_picture, args=(file_path, self.status), daemon=True) + thread = threading.Thread( + target=self._save_picture, args=(file_path, self.status), daemon=True + ) thread.start() return self.status def _save_picture(self, file_path, status): try: - logger.info(f'Capture from camera {self.hostname}') + logger.info(f"Capture from camera {self.hostname}") rtsp_url = f"rtsp://{CAM_USERNAME}:{CAM_PASSWORD}@{self.hostname}.psi.ch:{CAM_PORT}/rtpstream/config1" cap = cv2.VideoCapture(f"{rtsp_url}?tcp") if not cap.isOpened(): logger.error("Connection Failed", "Could not connect to the camera stream.") return - logger.info(f'Connection to camera {self.hostname} established') + logger.info(f"Connection to camera {self.hostname} established") ret, frame = cap.readAsync() cap.release() if not ret: logger.error("Capture Failed", "Failed to capture image from camera.") return - cv2.imwrite(file_path + 'png', frame) + cv2.imwrite(file_path + "png", frame) status.set_finished() - logger.info(f'Capture from camera {self.hostname} done') + logger.info(f"Capture from camera {self.hostname} done") except Exception as e: status.set_exception(e) -- 2.54.0 From 8bc36ed6a2e8ab778a99f034423ede15fc7e4a03 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 22 May 2026 10:28:55 +0200 Subject: [PATCH 10/23] refactor(mo1-bragg): migrate mo1_bragg to scans v4 --- debye_bec/devices/mo1_bragg/mo1_bragg.py | 206 +++++++++++++---------- 1 file changed, 121 insertions(+), 85 deletions(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index 5189816..3e14afd 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -13,6 +13,7 @@ from typing import Literal from bec_lib.devicemanager import ScanInfo from bec_lib.logger import bec_logger +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd import Component as Cpt from ophyd import DeviceStatus, StatusBase from ophyd.status import WaitTimeoutError @@ -33,6 +34,7 @@ from debye_bec.devices.mo1_bragg.mo1_bragg_enums import ( TriggerControlSource, ) from debye_bec.devices.mo1_bragg.mo1_bragg_utils import compute_spline +from debye_bec.devices.utils.utils import fetch_scan_info # Initialise logger logger = bec_logger.logger @@ -44,36 +46,6 @@ class Mo1BraggError(Exception): """Exception for the Mo1 Bragg positioner""" -########## Scan Parameter Model ########## - - -class ScanParameter(BaseModel): - """Dataclass to store the scan parameters for the Mo1 Bragg positioner. - This needs to be in sync with the kwargs of the MO1 Bragg scans from Debye, to - ensure that the scan parameters are correctly set. Any changes in the scan kwargs, - i.e. renaming or adding new parameters, need to be represented here as well.""" - - scan_time: float | None = Field(None, description="Scan time for a half oscillation") - scan_duration: float | None = Field(None, description="Duration of the scan") - break_enable_low: bool | None = Field( - None, description="Break enabled for low, should be PV trig_ena_lo_enum" - ) # trig_enable_low: bool = None - break_enable_high: bool | None = Field( - None, description="Break enabled for high, should be PV trig_ena_hi_enum" - ) # trig_enable_high: bool = None - break_time_low: float | None = Field(None, description="Break time low energy/angle") - break_time_high: float | None = Field(None, description="Break time high energy/angle") - cycle_low: int | None = Field(None, description="Cycle for low energy/angle") - cycle_high: int | None = Field(None, description="Cycle for high energy/angle") - exp_time: float | None = Field(None, description="XRD trigger period") - n_of_trigger: int | None = Field(None, description="Amount of XRD triggers") - start: float | None = Field(None, description="Start value for energy/angle") - stop: float | None = Field(None, description="Stop value for energy/angle") - p_kink: float | None = Field(None, description="P Kink") - e_kink: float | None = Field(None, description="Energy Kink") - model_config: dict = {"validate_assignment": True} - - ########### Mo1 Bragg Motor Class ########### @@ -96,7 +68,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): scan_info (ScanInfo): The scan info to use. """ super().__init__(name=name, scan_info=scan_info, prefix=prefix, **kwargs) - self.scan_parameter = ScanParameter() + self.scan_parameters: ScanServerScanInfo = None self.timeout_for_pvwait = 7.5 ######################################## @@ -124,20 +96,24 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object. """ + self.scan_parameters = fetch_scan_info(self.scan_info) if self.scan_control.scan_msg.get() != ScanControlLoadMessage.PENDING: status = CompareStatus(self.scan_control.scan_msg, ScanControlLoadMessage.PENDING) self.cancel_on_stop(status) self.scan_control.scan_val_reset.put(1) status.wait(timeout=self.timeout_for_pvwait) - scan_name = self.scan_info.msg.scan_name - self._update_scan_parameter() + scan_name = self.scan_parameters.msg.scan_name + start = self.scan_parameters.additional_scan_parameters.get("start", None) + stop = self.scan_parameters.additional_scan_parameters.get("stop", None) + scan_time = self.scan_parameters.additional_scan_parameters.get("scan_time", None) + scan_duration = self.scan_parameters.additional_scan_parameters.get("scan_duration", None) if scan_name == "xas_simple_scan": - self.set_xas_settings( - low=self.scan_parameter.start, - high=self.scan_parameter.stop, - scan_time=self.scan_parameter.scan_time, - ) + if any(param is None for param in [start, stop, scan_time, scan_duration]): + raise Mo1BraggError( + f"Missing scan parameters for xas_simple_scan. Required parameters: start, stop, scan_time, scan_duration in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + ) + self.set_xas_settings(low=start, high=stop, scan_time=scan_time) self.set_trig_settings( enable_low=False, enable_high=False, @@ -148,35 +124,67 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): exp_time=0, n_of_trigger=0, ) - self.set_scan_control_settings( - mode=ScanControlMode.SIMPLE, scan_duration=self.scan_parameter.scan_duration - ) + self.set_scan_control_settings(mode=ScanControlMode.SIMPLE, scan_duration=scan_duration) elif scan_name == "xas_simple_scan_with_xrd": - self.set_xas_settings( - low=self.scan_parameter.start, - high=self.scan_parameter.stop, - scan_time=self.scan_parameter.scan_time, + break_enable_low = self.scan_parameters.additional_scan_parameters.get( + "break_enable_low", None ) + break_enable_high = self.scan_parameters.additional_scan_parameters.get( + "break_enable_high", None + ) + break_time_low = self.scan_parameters.additional_scan_parameters.get( + "break_time_low", None + ) + break_time_high = self.scan_parameters.additional_scan_parameters.get( + "break_time_high", None + ) + cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None) + cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None) + exp_time = self.scan_parameters.additional_scan_parameters.get("exp_time", None) + n_of_trigger = self.scan_parameters.additional_scan_parameters.get("n_of_trigger", None) + if any( + param is None + for param in [ + start, + stop, + scan_time, + scan_duration, + break_enable_low, + break_enable_high, + break_time_low, + break_time_high, + cycle_low, + cycle_high, + exp_time, + n_of_trigger, + ] + ): + raise Mo1BraggError( + f"Missing scan parameters for xas_simple_scan_with_xrd. Required parameters: start, stop, scan_time, scan_duration, break_enable_low, break_enable_high, break_time_low, break_time_high, cycle_low, cycle_high, exp_time, n_of_trigger in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + ) + self.set_xas_settings(low=start, high=stop, scan_time=scan_time) self.set_trig_settings( - enable_low=self.scan_parameter.break_enable_low, - enable_high=self.scan_parameter.break_enable_high, - break_time_low=self.scan_parameter.break_time_low, - break_time_high=self.scan_parameter.break_time_high, - cycle_low=self.scan_parameter.cycle_low, - cycle_high=self.scan_parameter.cycle_high, - exp_time=self.scan_parameter.exp_time, - n_of_trigger=self.scan_parameter.n_of_trigger, - ) - self.set_scan_control_settings( - mode=ScanControlMode.SIMPLE, scan_duration=self.scan_parameter.scan_duration + enable_low=break_enable_low, + enable_high=break_enable_high, + break_time_low=break_time_low, + break_time_high=break_time_high, + cycle_low=cycle_low, + cycle_high=cycle_high, + exp_time=exp_time, + n_of_trigger=n_of_trigger, ) + self.set_scan_control_settings(mode=ScanControlMode.SIMPLE, scan_duration=scan_duration) elif scan_name == "xas_advanced_scan": + p_kink = self.scan_parameters.additional_scan_parameters.get("p_kink", None) + e_kink = self.scan_parameters.additional_scan_parameters.get("e_kink", None) + if any( + param is None for param in [start, stop, scan_time, scan_duration, p_kink, e_kink] + ): + raise Mo1BraggError( + f"Missing scan parameters for xas_advanced_scan. Required parameters: start, stop, scan_time, scan_duration, p_kink, e_kink in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + ) self.set_advanced_xas_settings( - low=self.scan_parameter.start, - high=self.scan_parameter.stop, - scan_time=self.scan_parameter.scan_time, - p_kink=self.scan_parameter.p_kink, - e_kink=self.scan_parameter.e_kink, + low=start, high=stop, scan_time=scan_time, p_kink=p_kink, e_kink=e_kink ) self.set_trig_settings( enable_low=False, @@ -189,28 +197,65 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): n_of_trigger=0, ) self.set_scan_control_settings( - mode=ScanControlMode.ADVANCED, scan_duration=self.scan_parameter.scan_duration + mode=ScanControlMode.ADVANCED, scan_duration=scan_duration ) elif scan_name == "xas_advanced_scan_with_xrd": + p_kink = self.scan_parameters.additional_scan_parameters.get("p_kink", None) + e_kink = self.scan_parameters.additional_scan_parameters.get("e_kink", None) + break_enable_low = self.scan_parameters.additional_scan_parameters.get( + "break_enable_low", None + ) + break_enable_high = self.scan_parameters.additional_scan_parameters.get( + "break_enable_high", None + ) + break_time_low = self.scan_parameters.additional_scan_parameters.get( + "break_time_low", None + ) + break_time_high = self.scan_parameters.additional_scan_parameters.get( + "break_time_high", None + ) + cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None) + cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None) + exp_time = self.scan_parameters.additional_scan_parameters.get("exp_time", None) + n_of_trigger = self.scan_parameters.additional_scan_parameters.get("n_of_trigger", None) + if any( + param is None + for param in [ + start, + stop, + scan_time, + scan_duration, + p_kink, + e_kink, + break_enable_low, + break_enable_high, + break_time_low, + break_time_high, + cycle_low, + cycle_high, + exp_time, + n_of_trigger, + ] + ): + raise Mo1BraggError( + f"Missing scan parameters for xas_advanced_scan_with_xrd. Required parameters: start, stop, scan_time, scan_duration, p_kink, e_kink, break_enable_low, break_enable_high, break_time_low, break_time_high, cycle_low, cycle_high, exp_time, n_of_trigger in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + ) + self.set_advanced_xas_settings( - low=self.scan_parameter.start, - high=self.scan_parameter.stop, - scan_time=self.scan_parameter.scan_time, - p_kink=self.scan_parameter.p_kink, - e_kink=self.scan_parameter.e_kink, + low=start, high=stop, scan_time=scan_time, p_kink=p_kink, e_kink=e_kink ) self.set_trig_settings( - enable_low=self.scan_parameter.break_enable_low, - enable_high=self.scan_parameter.break_enable_high, - break_time_low=self.scan_parameter.break_time_low, - break_time_high=self.scan_parameter.break_time_high, - cycle_low=self.scan_parameter.cycle_low, - cycle_high=self.scan_parameter.cycle_high, - exp_time=self.scan_parameter.exp_time, - n_of_trigger=self.scan_parameter.n_of_trigger, + enable_low=break_enable_low, + enable_high=break_enable_high, + break_time_low=break_time_low, + break_time_high=break_time_high, + cycle_low=cycle_low, + cycle_high=cycle_high, + exp_time=exp_time, + n_of_trigger=n_of_trigger, ) self.set_scan_control_settings( - mode=ScanControlMode.ADVANCED, scan_duration=self.scan_parameter.scan_duration + mode=ScanControlMode.ADVANCED, scan_duration=scan_duration ) else: return @@ -468,12 +513,3 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): for s in status_list: s.wait(timeout=self.timeout_for_pvwait) - - def _update_scan_parameter(self): - """Get the scan_info parameters for the scan.""" - for key, value in self.scan_info.msg.request_inputs["inputs"].items(): - if hasattr(self.scan_parameter, key): - setattr(self.scan_parameter, key, value) - for key, value in self.scan_info.msg.request_inputs["kwargs"].items(): - if hasattr(self.scan_parameter, key): - setattr(self.scan_parameter, key, value) -- 2.54.0 From 87758710d98b48ab29820ee5b287e7b890751102 Mon Sep 17 00:00:00 2001 From: x01da Date: Fri, 22 May 2026 12:36:03 +0200 Subject: [PATCH 11/23] fix: fixes from tests at the beamline --- debye_bec/devices/mo1_bragg/mo1_bragg.py | 9 ++--- debye_bec/devices/utils/utils.py | 6 ++- debye_bec/scans/__init__.py | 2 +- debye_bec/scans/nidaq_cont_scan.py | 4 +- debye_bec/scans/nidaq_continuous_scan.py | 8 ++-- debye_bec/scans/xas_simple_scan.py | 38 +++++++++---------- tests/tests_scans/test_mono_bragg_scans_v4.py | 2 +- 7 files changed, 36 insertions(+), 33 deletions(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index 3e14afd..69df2d2 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -103,9 +103,8 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): self.scan_control.scan_val_reset.put(1) status.wait(timeout=self.timeout_for_pvwait) - scan_name = self.scan_parameters.msg.scan_name - start = self.scan_parameters.additional_scan_parameters.get("start", None) - stop = self.scan_parameters.additional_scan_parameters.get("stop", None) + scan_name = self.scan_parameters.scan_name + start, stop = self.scan_parameters.positions or (None, None) scan_time = self.scan_parameters.additional_scan_parameters.get("scan_time", None) scan_duration = self.scan_parameters.additional_scan_parameters.get("scan_duration", None) if scan_name == "xas_simple_scan": @@ -140,7 +139,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): ) cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None) cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None) - exp_time = self.scan_parameters.additional_scan_parameters.get("exp_time", None) + exp_time = self.scan_parameters.exp_time n_of_trigger = self.scan_parameters.additional_scan_parameters.get("n_of_trigger", None) if any( param is None @@ -216,7 +215,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): ) cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None) cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None) - exp_time = self.scan_parameters.additional_scan_parameters.get("exp_time", None) + exp_time = self.scan_parameters.exp_time n_of_trigger = self.scan_parameters.additional_scan_parameters.get("n_of_trigger", None) if any( param is None diff --git a/debye_bec/devices/utils/utils.py b/debye_bec/devices/utils/utils.py index f747655..8e92287 100644 --- a/debye_bec/devices/utils/utils.py +++ b/debye_bec/devices/utils/utils.py @@ -2,8 +2,12 @@ from bec_lib.devicemanager import ScanInfo from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo +import numpy as np def fetch_scan_info(scan_info: ScanInfo) -> ScanServerScanInfo: """Fetch the scan parameters from the scan_info object and return them as a ScanServerScanInfo object.""" - return ScanServerScanInfo.model_validate(scan_info.msg.info) + info = scan_info.msg.info + if isinstance(info["positions"], list): + info["positions"] = np.array(info["positions"]) + return ScanServerScanInfo.model_validate(info) diff --git a/debye_bec/scans/__init__.py b/debye_bec/scans/__init__.py index 1339603..bdd703c 100644 --- a/debye_bec/scans/__init__.py +++ b/debye_bec/scans/__init__.py @@ -1,4 +1,4 @@ -from .nidaq_cont_scan import NIDAQContinuousScan +# from .nidaq_cont_scan import NIDAQContinuousScan from .nidaq_continuous_scan import NidaqContinuousScan from .xas_simple_scan import ( XasAdvancedScan, diff --git a/debye_bec/scans/nidaq_cont_scan.py b/debye_bec/scans/nidaq_cont_scan.py index 172da13..e36e4b3 100644 --- a/debye_bec/scans/nidaq_cont_scan.py +++ b/debye_bec/scans/nidaq_cont_scan.py @@ -40,7 +40,7 @@ class NIDAQContinuousScan(AsyncFlyScanBase): self.scan_duration = scan_duration self.daq = daq self.start_time = 0 - self.primary_readout_cycle = 1 + self.monitored_readout_cycle = 1 self.scan_parameters["scan_duration"] = scan_duration self.scan_parameters["compression"] = compression @@ -78,7 +78,7 @@ class NIDAQContinuousScan(AsyncFlyScanBase): while not complete_status.done: # Readout monitored devices yield from self.stubs.read(group="monitored", point_id=self.point_id) - time.sleep(self.primary_readout_cycle) + time.sleep(self.monitored_readout_cycle) self.point_id += 1 self.num_pos = self.point_id diff --git a/debye_bec/scans/nidaq_continuous_scan.py b/debye_bec/scans/nidaq_continuous_scan.py index ca67744..6a5da9d 100644 --- a/debye_bec/scans/nidaq_continuous_scan.py +++ b/debye_bec/scans/nidaq_continuous_scan.py @@ -44,8 +44,8 @@ class NidaqContinuousScan(ScanBase): self, #fmt: off scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Duration of the scan", units=Units.s)], - daq: Annotated[DeviceBase | None, ScanArgument(display_name="Daq", description="DAQ device to be used for the scan")], - compression: Annotated[bool, ScanArgument(display_name="Compression", description="Whether to compress the data")], + daq: Annotated[DeviceBase | None, ScanArgument(display_name="Daq", description="DAQ device to be used for the scan")] = None, + compression: Annotated[bool, ScanArgument(display_name="Compression", description="Whether to compress the data")]= False, #fmt: on **kwargs, ): @@ -67,7 +67,7 @@ class NidaqContinuousScan(ScanBase): self.scan_duration = scan_duration self.daq = daq or self.dev["nidaq"] self.compression = compression - self.primary_readout_cycle = 1.0 # seconds + self.monitored_readout_cycle = 1.0 # seconds self.motors = [self.daq] self.update_scan_info(scan_duration=scan_duration, compression=compression) @@ -130,7 +130,7 @@ class NidaqContinuousScan(ScanBase): while not complete_status.done: self.at_each_point() - time.sleep(self.primary_readout_cycle) + time.sleep(self.monitored_readout_cycle) @scan_hook def at_each_point(self): diff --git a/debye_bec/scans/xas_simple_scan.py b/debye_bec/scans/xas_simple_scan.py index 581afd2..871093a 100644 --- a/debye_bec/scans/xas_simple_scan.py +++ b/debye_bec/scans/xas_simple_scan.py @@ -32,19 +32,19 @@ class XasSimpleScan(ScanBase): gui_config = { "Movement Parameters": ["start", "stop"], - "Scan Parameters": ["scan_time", "scan_duration", "primary_readout_cycle"], + "Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"], } def __init__( self, #fmt: off - start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)], - stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)], - scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)], - scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0)], + start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV, ge=4500, le=64000)], + stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV, ge=4500, le=64000)], + scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0.05)], + scan_duration: Annotated[float, ScanArgument(display_name="Scan Duration", description="Total scan duration.", units=Units.s, ge=0.05)], motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None, daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, - primary_readout_cycle: Annotated[float, ScanArgument(display_name="Primary Readout Cycle", description="Delay between monitored readouts.",units=Units.s, gt=0,)] = 1, + monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.",units=Units.s, gt=0)] = 1, #fmt: on **kwargs, ): @@ -58,7 +58,7 @@ class XasSimpleScan(ScanBase): scan_duration (float): Total scan duration. motor (DeviceBase | None): Bragg motor device. daq (DeviceBase | None): NIDAQ device. - primary_readout_cycle (float): Delay between monitored readouts. + monitored_readout_cycle (float): Delay between monitored readouts. Returns: ScanReport @@ -70,7 +70,7 @@ class XasSimpleScan(ScanBase): self.scan_duration = scan_duration self.motor = motor if motor is not None else self.dev["mo1_bragg"] self.daq = daq if daq is not None else self.dev["nidaq"] - self.primary_readout_cycle = primary_readout_cycle + self.monitored_readout_cycle = monitored_readout_cycle self.positions = np.array([self.start, self.stop], dtype=float) # We pass on the arguments as "additional_scan_parameters" in the scan info @@ -78,7 +78,7 @@ class XasSimpleScan(ScanBase): positions=self.positions, scan_time=scan_time, scan_duration=scan_duration, - primary_readout_cycle=primary_readout_cycle, + monitored_readout_cycle=monitored_readout_cycle, ) self.actions.set_device_readout_priority([self.daq], priority="async") @@ -142,7 +142,7 @@ class XasSimpleScan(ScanBase): Logic to be executed at each acquisition point during the scan. """ self.actions.read_monitored_devices() - time.sleep(self.primary_readout_cycle) + time.sleep(self.monitored_readout_cycle) @scan_hook def post_scan(self): @@ -178,7 +178,7 @@ class XasSimpleScanWithXrd(XasSimpleScan): scan_name = "xas_simple_scan_with_xrd" gui_config = { "Movement Parameters": ["start", "stop"], - "Scan Parameters": ["scan_time", "scan_duration", "primary_readout_cycle"], + "Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"], "Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"], "High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"], "XRD Triggers": ["exp_time", "n_of_trigger"], @@ -201,7 +201,7 @@ class XasSimpleScanWithXrd(XasSimpleScan): n_of_trigger: Annotated[int, ScanArgument(display_name="Number Of Trigger", description="Amount of triggers fired during a break.", ge=0)], motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None, daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, - primary_readout_cycle: Annotated[float, ScanArgument(display_name="Primary Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, + monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, **kwargs, #fmt: on ): @@ -212,7 +212,7 @@ class XasSimpleScanWithXrd(XasSimpleScan): scan_duration=scan_duration, motor=motor, daq=daq, - primary_readout_cycle=primary_readout_cycle, + monitored_readout_cycle=monitored_readout_cycle, **kwargs, ) @@ -233,7 +233,7 @@ class XasAdvancedScan(XasSimpleScan): scan_name = "xas_advanced_scan" gui_config = { "Movement Parameters": ["start", "stop"], - "Scan Parameters": ["scan_time", "scan_duration", "primary_readout_cycle"], + "Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"], "Spline Parameters": ["p_kink", "e_kink"], } @@ -248,7 +248,7 @@ class XasAdvancedScan(XasSimpleScan): e_kink: Annotated[float, ScanArgument(display_name="E Kink", description="Energy of the kink.", units=Units.eV)], motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None, daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, - primary_readout_cycle: Annotated[float, ScanArgument(display_name="Primary Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, + monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, **kwargs, #fmt: on ): @@ -259,7 +259,7 @@ class XasAdvancedScan(XasSimpleScan): scan_duration=scan_duration, motor=motor, daq=daq, - primary_readout_cycle=primary_readout_cycle, + monitored_readout_cycle=monitored_readout_cycle, **kwargs, ) # We pass on the arguments as "additional_scan_parameters" in the scan info @@ -270,7 +270,7 @@ class XasAdvancedScanWithXrd(XasAdvancedScan): scan_name = "xas_advanced_scan_with_xrd" gui_config = { "Movement Parameters": ["start", "stop"], - "Scan Parameters": ["scan_time", "scan_duration", "primary_readout_cycle"], + "Scan Parameters": ["scan_time", "scan_duration", "monitored_readout_cycle"], "Spline Parameters": ["p_kink", "e_kink"], "Low Energy Break": ["break_enable_low", "break_time_low", "cycle_low"], "High Energy Break": ["break_enable_high", "break_time_high", "cycle_high"], @@ -296,7 +296,7 @@ class XasAdvancedScanWithXrd(XasAdvancedScan): n_of_trigger: Annotated[int, ScanArgument(display_name="Number Of Trigger", description="Amount of triggers fired during a break.", ge=0)], motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None, daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, - primary_readout_cycle: Annotated[float, ScanArgument(display_name="Primary Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, + monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, **kwargs, #fmt: on ): @@ -309,7 +309,7 @@ class XasAdvancedScanWithXrd(XasAdvancedScan): e_kink=e_kink, motor=motor, daq=daq, - primary_readout_cycle=primary_readout_cycle, + monitored_readout_cycle=monitored_readout_cycle, **kwargs, ) diff --git a/tests/tests_scans/test_mono_bragg_scans_v4.py b/tests/tests_scans/test_mono_bragg_scans_v4.py index 078c53a..e33fe92 100644 --- a/tests/tests_scans/test_mono_bragg_scans_v4.py +++ b/tests/tests_scans/test_mono_bragg_scans_v4.py @@ -24,7 +24,7 @@ def _assemble_xas_simple_scan(v4_scan_assembler, **overrides): "scan_duration": 10.0, "motor": "mo1_bragg", "daq": "nidaq", - "primary_readout_cycle": 1.0, + "monitored_readout_cycle": 1.0, } params.update(overrides) return v4_scan_assembler("xas_simple_scan", **params) -- 2.54.0 From 74ff173f9824c29da33aefd3a80fd88d7c3e355c Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 22 May 2026 14:45:11 +0200 Subject: [PATCH 12/23] refactor: cleanup, fix tests --- debye_bec/devices/mo1_bragg/mo1_bragg.py | 322 ++++++++++++----------- debye_bec/devices/pilatus/pilatus.py | 23 +- debye_bec/devices/utils/utils.py | 17 +- debye_bec/scans/xas_simple_scan.py | 16 +- tests/tests_devices/test_mo1_bragg.py | 54 ---- tests/tests_devices/test_nidaq.py | 38 ++- tests/tests_devices/test_pilatus.py | 70 +++-- 7 files changed, 280 insertions(+), 260 deletions(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index 69df2d2..30419b9 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -70,6 +70,13 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): super().__init__(name=name, scan_info=scan_info, prefix=prefix, **kwargs) self.scan_parameters: ScanServerScanInfo = None self.timeout_for_pvwait = 7.5 + self.valid_scan_names = [ + "xas_simple_scan", + "xas_simple_scan_with_xrd", + "xas_advanced_scan", + "xas_advanced_scan_with_xrd", + "nidaq_continuous_scan", + ] ######################################## # Beamline Specific Implementations # @@ -104,158 +111,172 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): status.wait(timeout=self.timeout_for_pvwait) scan_name = self.scan_parameters.scan_name - start, stop = self.scan_parameters.positions or (None, None) - scan_time = self.scan_parameters.additional_scan_parameters.get("scan_time", None) - scan_duration = self.scan_parameters.additional_scan_parameters.get("scan_duration", None) - if scan_name == "xas_simple_scan": - if any(param is None for param in [start, stop, scan_time, scan_duration]): - raise Mo1BraggError( - f"Missing scan parameters for xas_simple_scan. Required parameters: start, stop, scan_time, scan_duration in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + if self._check_if_scan_name_is_valid(self.scan_parameters): + start, stop = self.scan_parameters.positions or (None, None) + scan_time = self.scan_parameters.additional_scan_parameters.get("scan_time", None) + scan_duration = self.scan_parameters.additional_scan_parameters.get( + "scan_duration", None + ) + if scan_name == "xas_simple_scan": + if any(param is None for param in [start, stop, scan_time, scan_duration]): + raise Mo1BraggError( + f"Missing scan parameters for xas_simple_scan. Required parameters: start, stop, scan_time, scan_duration in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + ) + self.set_xas_settings(low=start, high=stop, scan_time=scan_time) + self.set_trig_settings( + enable_low=False, + enable_high=False, + break_time_low=0, + break_time_high=0, + cycle_low=0, + cycle_high=0, + exp_time=0, + n_of_trigger=0, ) - self.set_xas_settings(low=start, high=stop, scan_time=scan_time) - self.set_trig_settings( - enable_low=False, - enable_high=False, - break_time_low=0, - break_time_high=0, - cycle_low=0, - cycle_high=0, - exp_time=0, - n_of_trigger=0, - ) - self.set_scan_control_settings(mode=ScanControlMode.SIMPLE, scan_duration=scan_duration) - elif scan_name == "xas_simple_scan_with_xrd": - break_enable_low = self.scan_parameters.additional_scan_parameters.get( - "break_enable_low", None - ) - break_enable_high = self.scan_parameters.additional_scan_parameters.get( - "break_enable_high", None - ) - break_time_low = self.scan_parameters.additional_scan_parameters.get( - "break_time_low", None - ) - break_time_high = self.scan_parameters.additional_scan_parameters.get( - "break_time_high", None - ) - cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None) - cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None) - exp_time = self.scan_parameters.exp_time - n_of_trigger = self.scan_parameters.additional_scan_parameters.get("n_of_trigger", None) - if any( - param is None - for param in [ - start, - stop, - scan_time, - scan_duration, - break_enable_low, - break_enable_high, - break_time_low, - break_time_high, - cycle_low, - cycle_high, - exp_time, - n_of_trigger, - ] - ): - raise Mo1BraggError( - f"Missing scan parameters for xas_simple_scan_with_xrd. Required parameters: start, stop, scan_time, scan_duration, break_enable_low, break_enable_high, break_time_low, break_time_high, cycle_low, cycle_high, exp_time, n_of_trigger in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + self.set_scan_control_settings( + mode=ScanControlMode.SIMPLE, scan_duration=scan_duration ) - self.set_xas_settings(low=start, high=stop, scan_time=scan_time) - self.set_trig_settings( - enable_low=break_enable_low, - enable_high=break_enable_high, - break_time_low=break_time_low, - break_time_high=break_time_high, - cycle_low=cycle_low, - cycle_high=cycle_high, - exp_time=exp_time, - n_of_trigger=n_of_trigger, - ) - self.set_scan_control_settings(mode=ScanControlMode.SIMPLE, scan_duration=scan_duration) - elif scan_name == "xas_advanced_scan": - p_kink = self.scan_parameters.additional_scan_parameters.get("p_kink", None) - e_kink = self.scan_parameters.additional_scan_parameters.get("e_kink", None) - if any( - param is None for param in [start, stop, scan_time, scan_duration, p_kink, e_kink] - ): - raise Mo1BraggError( - f"Missing scan parameters for xas_advanced_scan. Required parameters: start, stop, scan_time, scan_duration, p_kink, e_kink in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + elif scan_name == "xas_simple_scan_with_xrd": + break_enable_low = self.scan_parameters.additional_scan_parameters.get( + "break_enable_low", None ) - self.set_advanced_xas_settings( - low=start, high=stop, scan_time=scan_time, p_kink=p_kink, e_kink=e_kink - ) - self.set_trig_settings( - enable_low=False, - enable_high=False, - break_time_low=0, - break_time_high=0, - cycle_low=0, - cycle_high=0, - exp_time=0, - n_of_trigger=0, - ) - self.set_scan_control_settings( - mode=ScanControlMode.ADVANCED, scan_duration=scan_duration - ) - elif scan_name == "xas_advanced_scan_with_xrd": - p_kink = self.scan_parameters.additional_scan_parameters.get("p_kink", None) - e_kink = self.scan_parameters.additional_scan_parameters.get("e_kink", None) - break_enable_low = self.scan_parameters.additional_scan_parameters.get( - "break_enable_low", None - ) - break_enable_high = self.scan_parameters.additional_scan_parameters.get( - "break_enable_high", None - ) - break_time_low = self.scan_parameters.additional_scan_parameters.get( - "break_time_low", None - ) - break_time_high = self.scan_parameters.additional_scan_parameters.get( - "break_time_high", None - ) - cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None) - cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None) - exp_time = self.scan_parameters.exp_time - n_of_trigger = self.scan_parameters.additional_scan_parameters.get("n_of_trigger", None) - if any( - param is None - for param in [ - start, - stop, - scan_time, - scan_duration, - p_kink, - e_kink, - break_enable_low, - break_enable_high, - break_time_low, - break_time_high, - cycle_low, - cycle_high, - exp_time, - n_of_trigger, - ] - ): - raise Mo1BraggError( - f"Missing scan parameters for xas_advanced_scan_with_xrd. Required parameters: start, stop, scan_time, scan_duration, p_kink, e_kink, break_enable_low, break_enable_high, break_time_low, break_time_high, cycle_low, cycle_high, exp_time, n_of_trigger in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + break_enable_high = self.scan_parameters.additional_scan_parameters.get( + "break_enable_high", None ) + break_time_low = self.scan_parameters.additional_scan_parameters.get( + "break_time_low", None + ) + break_time_high = self.scan_parameters.additional_scan_parameters.get( + "break_time_high", None + ) + cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None) + cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None) + exp_time = self.scan_parameters.exp_time + n_of_trigger = self.scan_parameters.additional_scan_parameters.get( + "n_of_trigger", None + ) + if any( + param is None + for param in [ + start, + stop, + scan_time, + scan_duration, + break_enable_low, + break_enable_high, + break_time_low, + break_time_high, + cycle_low, + cycle_high, + exp_time, + n_of_trigger, + ] + ): + raise Mo1BraggError( + f"Missing scan parameters for xas_simple_scan_with_xrd. Required parameters: start, stop, scan_time, scan_duration, break_enable_low, break_enable_high, break_time_low, break_time_high, cycle_low, cycle_high, exp_time, n_of_trigger in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + ) + self.set_xas_settings(low=start, high=stop, scan_time=scan_time) + self.set_trig_settings( + enable_low=break_enable_low, + enable_high=break_enable_high, + break_time_low=break_time_low, + break_time_high=break_time_high, + cycle_low=cycle_low, + cycle_high=cycle_high, + exp_time=exp_time, + n_of_trigger=n_of_trigger, + ) + self.set_scan_control_settings( + mode=ScanControlMode.SIMPLE, scan_duration=scan_duration + ) + elif scan_name == "xas_advanced_scan": + p_kink = self.scan_parameters.additional_scan_parameters.get("p_kink", None) + e_kink = self.scan_parameters.additional_scan_parameters.get("e_kink", None) + if any( + param is None + for param in [start, stop, scan_time, scan_duration, p_kink, e_kink] + ): + raise Mo1BraggError( + f"Missing scan parameters for xas_advanced_scan. Required parameters: start, stop, scan_time, scan_duration, p_kink, e_kink in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + ) + self.set_advanced_xas_settings( + low=start, high=stop, scan_time=scan_time, p_kink=p_kink, e_kink=e_kink + ) + self.set_trig_settings( + enable_low=False, + enable_high=False, + break_time_low=0, + break_time_high=0, + cycle_low=0, + cycle_high=0, + exp_time=0, + n_of_trigger=0, + ) + self.set_scan_control_settings( + mode=ScanControlMode.ADVANCED, scan_duration=scan_duration + ) + elif scan_name == "xas_advanced_scan_with_xrd": + p_kink = self.scan_parameters.additional_scan_parameters.get("p_kink", None) + e_kink = self.scan_parameters.additional_scan_parameters.get("e_kink", None) + break_enable_low = self.scan_parameters.additional_scan_parameters.get( + "break_enable_low", None + ) + break_enable_high = self.scan_parameters.additional_scan_parameters.get( + "break_enable_high", None + ) + break_time_low = self.scan_parameters.additional_scan_parameters.get( + "break_time_low", None + ) + break_time_high = self.scan_parameters.additional_scan_parameters.get( + "break_time_high", None + ) + cycle_low = self.scan_parameters.additional_scan_parameters.get("cycle_low", None) + cycle_high = self.scan_parameters.additional_scan_parameters.get("cycle_high", None) + exp_time = self.scan_parameters.exp_time + n_of_trigger = self.scan_parameters.additional_scan_parameters.get( + "n_of_trigger", None + ) + if any( + param is None + for param in [ + start, + stop, + scan_time, + scan_duration, + p_kink, + e_kink, + break_enable_low, + break_enable_high, + break_time_low, + break_time_high, + cycle_low, + cycle_high, + exp_time, + n_of_trigger, + ] + ): + raise Mo1BraggError( + f"Missing scan parameters for xas_advanced_scan_with_xrd. Required parameters: start, stop, scan_time, scan_duration, p_kink, e_kink, break_enable_low, break_enable_high, break_time_low, break_time_high, cycle_low, cycle_high, exp_time, n_of_trigger in additional_scan_parameters dict {self.scan_parameters.additional_scan_parameters}" + ) - self.set_advanced_xas_settings( - low=start, high=stop, scan_time=scan_time, p_kink=p_kink, e_kink=e_kink - ) - self.set_trig_settings( - enable_low=break_enable_low, - enable_high=break_enable_high, - break_time_low=break_time_low, - break_time_high=break_time_high, - cycle_low=cycle_low, - cycle_high=cycle_high, - exp_time=exp_time, - n_of_trigger=n_of_trigger, - ) - self.set_scan_control_settings( - mode=ScanControlMode.ADVANCED, scan_duration=scan_duration - ) + self.set_advanced_xas_settings( + low=start, high=stop, scan_time=scan_time, p_kink=p_kink, e_kink=e_kink + ) + self.set_trig_settings( + enable_low=break_enable_low, + enable_high=break_enable_high, + break_time_low=break_time_low, + break_time_high=break_time_high, + cycle_low=cycle_low, + cycle_high=cycle_high, + exp_time=exp_time, + n_of_trigger=n_of_trigger, + ) + self.set_scan_control_settings( + mode=ScanControlMode.ADVANCED, scan_duration=scan_duration + ) + else: + return # Should never happen. else: return # Setting scan duration seems to lag behind slightly in the backend, include small sleep @@ -335,6 +356,13 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): self.stopped = True # Needs to be set to stop motion ######### Utility Methods ######### + + def _check_if_scan_name_is_valid(self, scan_parameters: ScanServerScanInfo) -> bool: + """Check if the scan is within the list of scans for which the backend is working""" + if scan_parameters.scan_name in self.valid_scan_names: + return True + return False + def _progress_update(self, value, **kwargs) -> None: """Callback method to update the scan progress, runs a callback to SUB_PROGRESS subscribers, i.e. BEC. diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index 6c721fe..91a422e 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -260,7 +260,7 @@ class Pilatus(PSIDeviceBase, ADBase): # self._live_mode_run_event = threading.Event() # self._live_mode_stopped_event = threading.Event() # self._live_mode_stopped_event.set() # Initial state is stopped - self.scan_parameters: ScanServerScanInfo | None = None + self.scan_parameters: ScanServerScanInfo = None ######################################## # Custom Beamline Methods # @@ -495,8 +495,7 @@ class Pilatus(PSIDeviceBase, ADBase): status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) status_cam.wait(timeout=5) - scan_msg: ScanStatusMessage = self.scan_info.msg - if scan_msg.scan_name in self.xas_xrd_scan_names: + if self.scan_parameters.scan_name in self.xas_xrd_scan_names: # Compute number of triggers total_trig_lo, total_trig_hi = self._calculate_trigger(self.scan_parameters) # Set the number of images, we may also set this to a higher values if preferred and stop the acquisition @@ -511,10 +510,12 @@ class Pilatus(PSIDeviceBase, ADBase): self.trigger_n_of.set( self.scan_parameters.additional_scan_parameters.get("n_of_trigger", 1) ).wait(5) - - elif scan_msg.scan_type == "step": + # TODO migrate logic to v4 once old scans are deprecated, + # TODO if num_points=None and no logic from scan_name applies, can't measure with this detector.. + elif self.scan_parameters.scan_type == "software_triggered": self.n_images = ( - self.scan_parameters.num_monitored_readouts * scan_msg.frames_per_trigger + self.scan_parameters.num_monitored_readouts + * self.scan_parameters.frames_per_trigger ) exp_time = self.scan_parameters.exp_time self.trigger_source.set(MONOTRIGGERSOURCE.EPICS).wait(5) @@ -537,7 +538,7 @@ class Pilatus(PSIDeviceBase, ADBase): ) ) detector_exp_time = exp_time - self._readout_time - self._full_path = get_full_path(scan_msg, name="pilatus") + self._full_path = get_full_path(self.scan_info.msg, name="pilatus") file_path = "/".join(self._full_path.split("/")[:-1]) file_name = self._full_path.split("/")[-1] # Prepare detector and backend @@ -571,7 +572,7 @@ class Pilatus(PSIDeviceBase, ADBase): """Called right before the scan starts on all devices automatically.""" if ( self.scan_parameters.scan_name in self.xas_xrd_scan_names - or self.scan_parameters.scan_type == "step" + or self.scan_parameters.scan_type == "software_triggered" ): # TODO how to deal with fly scans? status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value) status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value) @@ -586,7 +587,7 @@ class Pilatus(PSIDeviceBase, ADBase): def on_trigger(self) -> DeviceStatus | None: """Called when the device is triggered.""" - if not self.scan_parameters.scan_type == "step": + if not self.scan_parameters.scan_type == "software_triggered": return None start_time = time.time() img_counter = self.hdf.num_captured.get() @@ -601,7 +602,7 @@ class Pilatus(PSIDeviceBase, ADBase): """Callback for when the device completes a scan.""" if ( self.scan_parameters.scan_name in self.xas_xrd_scan_names - or self.scan_parameters.scan_type == "step" + or self.scan_parameters.scan_type == "software_triggered" ): # TODO how to deal with fly scans? if status.success: self.file_event.put( @@ -624,7 +625,7 @@ class Pilatus(PSIDeviceBase, ADBase): """Called to inquire if a device has completed a scans.""" if ( self.scan_parameters.scan_name in self.xas_xrd_scan_names - or self.scan_parameters.scan_type == "step" + or self.scan_parameters.scan_type == "software_triggered" ): # TODO how to deal with fly scans? status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) diff --git a/debye_bec/devices/utils/utils.py b/debye_bec/devices/utils/utils.py index 8e92287..1475961 100644 --- a/debye_bec/devices/utils/utils.py +++ b/debye_bec/devices/utils/utils.py @@ -1,8 +1,11 @@ """Utility functions for the devices.""" +from copy import deepcopy + +import numpy as np from bec_lib.devicemanager import ScanInfo from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo -import numpy as np +from pydantic import ValidationError def fetch_scan_info(scan_info: ScanInfo) -> ScanServerScanInfo: @@ -10,4 +13,14 @@ def fetch_scan_info(scan_info: ScanInfo) -> ScanServerScanInfo: info = scan_info.msg.info if isinstance(info["positions"], list): info["positions"] = np.array(info["positions"]) - return ScanServerScanInfo.model_validate(info) + try: + msg = ScanServerScanInfo.model_validate(info) + except ValidationError: # This means we have an old scan_info object. + info = deepcopy(info) + # We need to convert a few parameters manually. + info["scan_type"] = ( + "hardware_triggered" if info["scan_type"] == "fly" else "software_triggered" + ) + msg = ScanServerScanInfo.model_validate(info) + + return msg diff --git a/debye_bec/scans/xas_simple_scan.py b/debye_bec/scans/xas_simple_scan.py index 871093a..d6a313f 100644 --- a/debye_bec/scans/xas_simple_scan.py +++ b/debye_bec/scans/xas_simple_scan.py @@ -37,7 +37,7 @@ class XasSimpleScan(ScanBase): def __init__( self, - #fmt: off + # fmt: off start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV, ge=4500, le=64000)], stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV, ge=4500, le=64000)], scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0.05)], @@ -45,7 +45,7 @@ class XasSimpleScan(ScanBase): motor: Annotated[DeviceBase | None, ScanArgument(display_name="Motor", description="Bragg motor device.")] = None, daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.",units=Units.s, gt=0)] = 1, - #fmt: on + # fmt: on **kwargs, ): """ @@ -186,7 +186,7 @@ class XasSimpleScanWithXrd(XasSimpleScan): def __init__( self, - #fmt: off + # fmt: off start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)], stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)], scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)], @@ -203,7 +203,7 @@ class XasSimpleScanWithXrd(XasSimpleScan): daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, **kwargs, - #fmt: on + # fmt: on ): super().__init__( start=start, @@ -239,7 +239,7 @@ class XasAdvancedScan(XasSimpleScan): def __init__( self, - #fmt: off + # fmt: off start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)], stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)], scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)], @@ -250,7 +250,7 @@ class XasAdvancedScan(XasSimpleScan): daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, **kwargs, - #fmt: on + # fmt: on ): super().__init__( start=start, @@ -279,7 +279,7 @@ class XasAdvancedScanWithXrd(XasAdvancedScan): def __init__( self, - #fmt: off + # fmt: off start: Annotated[float, ScanArgument(display_name="Start Energy", description="Start energy.", units=Units.eV)], stop: Annotated[float, ScanArgument(display_name="Stop Energy", description="Stop energy.", units=Units.eV)], scan_time: Annotated[float, ScanArgument(display_name="Scan Time", description="Time for one scan cycle.", units=Units.s, ge=0)], @@ -298,7 +298,7 @@ class XasAdvancedScanWithXrd(XasAdvancedScan): daq: Annotated[DeviceBase | None, ScanArgument(display_name="DAQ", description="NIDAQ device.")] = None, monitored_readout_cycle: Annotated[float, ScanArgument(display_name="Monitored Readout Cycle", description="Delay between monitored readouts.", units=Units.s, gt=0)] = 1, **kwargs, - #fmt: on + # fmt: on ): super().__init__( start=start, diff --git a/tests/tests_devices/test_mo1_bragg.py b/tests/tests_devices/test_mo1_bragg.py index e1fd819..89fd6e2 100644 --- a/tests/tests_devices/test_mo1_bragg.py +++ b/tests/tests_devices/test_mo1_bragg.py @@ -159,60 +159,6 @@ def test_set_control_settings(mock_bragg): assert dev.scan_control.scan_duration.get() == 5 -def test_update_scan_parameters(mock_bragg): - dev = mock_bragg - msg = ScanStatusMessage( - scan_id="my_scan_id", - status="closed", - request_inputs={ - "inputs": {}, - "kwargs": { - "start": 0, - "stop": 5, - "scan_time": 1, - "scan_duration": 10, - "xrd_enable_low": True, - "xrd_enable_high": False, - "num_trigger_low": 1, - "num_trigger_high": 7, - "exp_time_low": 1, - "exp_time_high": 3, - "cycle_low": 1, - "cycle_high": 5, - "p_kink": 50, - "e_kink": 8000, - }, - }, - info={ - "kwargs": { - "start": 0, - "stop": 5, - "scan_time": 1, - "scan_duration": 10, - "xrd_enable_low": True, - "xrd_enable_high": False, - "num_trigger_low": 1, - "num_trigger_high": 7, - "exp_time_low": 1, - "exp_time_high": 3, - "cycle_low": 1, - "cycle_high": 5, - "p_kink": 50, - "e_kink": 8000, - } - }, - metadata={}, - ) - mock_bragg.scan_info.msg = msg - scan_param = dev.scan_parameter.model_dump() - for _, v in scan_param.items(): - assert v == None - dev._update_scan_parameter() - scan_param = dev.scan_parameter.model_dump() - for k, v in scan_param.items(): - assert v == msg.content["request_inputs"]["kwargs"].get(k, None) - - def test_kickoff_scan(mock_bragg): dev = mock_bragg dev.scan_control.scan_status._read_pv.mock_data = ScanControlScanStatus.READY diff --git a/tests/tests_devices/test_nidaq.py b/tests/tests_devices/test_nidaq.py index 972eb4a..85057a2 100644 --- a/tests/tests_devices/test_nidaq.py +++ b/tests/tests_devices/test_nidaq.py @@ -6,6 +6,7 @@ from unittest import mock import ophyd import pytest from bec_server.scan_server.scan_worker import ScanWorker +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo from ophyd.status import WaitTimeoutError from ophyd_devices.interfaces.base_classes.psi_device_base import DeviceStoppedError from ophyd_devices.tests.utils import MockPV @@ -15,6 +16,13 @@ from debye_bec.devices.nidaq.nidaq import Nidaq, NidaqError # TODO move this function to ophyd_devices, it is duplicated in csaxs_bec and needed for other pluging repositories from debye_bec.devices.test_utils.utils import patch_dual_pvs +from debye_bec.devices.utils.utils import fetch_scan_info + + +@pytest.fixture(scope="function") +def scan_info_mock(): + """Fixture for the ScanInfo object.""" + return ScanServerScanInfo(scan_name="xas_simple_scan", scan_id="test") @pytest.fixture(scope="function") @@ -52,13 +60,17 @@ def test_init(mock_nidaq): ] -def test_check_if_scan_name_is_valid(mock_nidaq): +def test_check_if_scan_name_is_valid(mock_nidaq, scan_info_mock): """Test the check_if_scan_name_is_valid method.""" dev = mock_nidaq - dev.scan_info.msg.scan_name = "xas_simple_scan" - assert dev._check_if_scan_name_is_valid() - dev.scan_info.msg.scan_name = "invalid_scan_name" - assert not dev._check_if_scan_name_is_valid() + scan_info_mock.scan_name = "xas_simple_scan" + dev.scan_info.msg.info.update(scan_info_mock.model_dump()) + scan_parameters = fetch_scan_info(dev.scan_info) + assert dev._check_if_scan_name_is_valid(scan_parameters) + scan_info_mock.scan_name = "invalid_scan_name" + dev.scan_info.msg.info.update(scan_info_mock.model_dump()) + scan_parameters = fetch_scan_info(dev.scan_info) + assert not dev._check_if_scan_name_is_valid(scan_parameters) def test_set_config(mock_nidaq): @@ -120,11 +132,13 @@ def test_on_unstage(mock_nidaq): ("nidaq_continuous_scan", False, 0), ], ) -def test_on_pre_scan(mock_nidaq, scan_name, raise_error, nidaq_state): +def test_on_pre_scan(mock_nidaq, scan_name, raise_error, nidaq_state, scan_info_mock): """Test the on_pre_scan method of the Nidaq device.""" dev = mock_nidaq dev.state.put(nidaq_state) - dev.scan_info.msg.scan_name = scan_name + scan_info_mock.scan_name = scan_name + dev.scan_info.msg.info.update(scan_info_mock.model_dump()) + dev.scan_parameters = fetch_scan_info(dev.scan_info) dev._timeout_wait_for_pv = 0.1 # Set a short timeout for testing if not raise_error: dev.pre_scan() @@ -133,11 +147,13 @@ def test_on_pre_scan(mock_nidaq, scan_name, raise_error, nidaq_state): dev.pre_scan() -def test_on_complete(mock_nidaq): +def test_on_complete(mock_nidaq, scan_info_mock): """Test the on_complete method of the Nidaq device.""" dev = mock_nidaq + scan_info_mock.scan_name = "nidaq_continuous_scan" + dev.scan_info.msg.info.update(scan_info_mock.model_dump()) + dev.scan_parameters = fetch_scan_info(dev.scan_info) # Check for nidaq_continuous_scan - dev.scan_info.msg.scan_name = "nidaq_continuous_scan" dev.state.put(0) # Set state to DISABLED status = dev.complete() assert status.done is False @@ -147,7 +163,9 @@ def test_on_complete(mock_nidaq): assert status.done is True # Check for XAS simple scan - dev.scan_info.msg.scan_name = "xas_simple_scan" + scan_info_mock.scan_name = "xas_simple_scan" + dev.scan_info.msg.info.update(scan_info_mock.model_dump()) + dev.scan_parameters = fetch_scan_info(dev.scan_info) dev.state.put(0) # Set state to ACQUIRE dev.stop_call.put(0) dev._timeout_wait_for_pv = 5 diff --git a/tests/tests_devices/test_pilatus.py b/tests/tests_devices/test_pilatus.py index 6403e94..3c0bac2 100644 --- a/tests/tests_devices/test_pilatus.py +++ b/tests/tests_devices/test_pilatus.py @@ -7,6 +7,9 @@ import ophyd import pytest from bec_lib.messages import ScanStatusMessage from bec_server.scan_server.scan_worker import ScanWorker +from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo +from bec_server.scan_server.tests.scan_fixtures import * +from bec_server.scan_server.tests.scan_fixtures import _MockDevice from ophyd_devices import CompareStatus, DeviceStatus from ophyd_devices.interfaces.base_classes.psi_device_base import DeviceStoppedError from ophyd_devices.tests.utils import MockPV, patch_dual_pvs @@ -20,6 +23,7 @@ from debye_bec.devices.pilatus.pilatus import ( TRIGGERMODE, Pilatus, ) +from debye_bec.devices.utils.utils import fetch_scan_info if TYPE_CHECKING: # pragma no cover from bec_lib.messages import FileMessage @@ -34,32 +38,38 @@ if TYPE_CHECKING: # pragma no cover @pytest.fixture( scope="function", params=[ - (0.1, 1, 1, "line_scan", "step"), - (0.2, 2, 2, "time_scan", "step"), - (0.5, 5, 5, "xas_advanced_scan", "fly"), + (("samx", 0.1, 1, 5, "samy", 0, 1, 5), {"relative": True}, "_v4_hexagonal_scan"), + ((1, 0.2), {}, "_v4_time_scan"), + ((9000, 10000, 1, 20, 0.1, 9500), {}, "xas_advanced_scan"), ], ) -def mock_scan_info(request, tmpdir): - exp_time, frames_per_trigger, num_points, scan_name, scan_type = request.param - scan_info = ScanStatusMessage( - scan_id="test_id", - status="open", - scan_type=scan_type, - scan_number=1, - scan_parameters={ - "exp_time": exp_time, - "frames_per_trigger": frames_per_trigger, - "system_config": {}, - }, - info={"file_components": (f"{tmpdir}/data/S00000/S000001", "h5")}, - num_points=num_points, - scan_name=scan_name, - ) - yield scan_info +def mock_scan_info(request, tmpdir, v4_scan_assembler, device_manager): + args, kwargs, scan_name = request.param + mo1_bragg = _MockDevice(name="mo1_bragg") + nidaq = _MockDevice(name="nidaq") + device_manager.add_device(mo1_bragg) + device_manager.add_device(nidaq) + scan = v4_scan_assembler(scan_name, *args, **kwargs) + yield scan.scan_info @pytest.fixture(scope="function") -def pilatus(mock_scan_info) -> Generator[Pilatus, None, None]: +def mock_scan_status_message(mock_scan_info, tmpdir) -> ScanStatusMessage: + info = mock_scan_info.model_dump() + info.update({"file_components": (f"{tmpdir}/data/S00000/S000001", "h5")}) + return ScanStatusMessage( + scan_id=mock_scan_info.scan_id, + status="open", + scan_number=1, + scan_name=mock_scan_info.scan_name, + scan_type="fly" if mock_scan_info.scan_type == "hardware_triggered" else "step", + num_points=mock_scan_info.num_points, + info=info, + ) + + +@pytest.fixture(scope="function") +def pilatus(mock_scan_status_message) -> Generator[Pilatus, None, None]: name = "pilatus" prefix = "X01DA-OP-MO1:PILATUS:" with mock.patch.object(ophyd, "cl") as mock_cl: @@ -70,8 +80,9 @@ def pilatus(mock_scan_info) -> Generator[Pilatus, None, None]: # dev.image1 = mock.MagicMock() # with mock.patch.object(dev, "image1"): with mock.patch.object(dev, "task_handler"): - dev.scan_info.msg = mock_scan_info + dev.scan_info.msg = mock_scan_status_message try: + dev.scan_parameters = fetch_scan_info(dev.scan_info) yield dev finally: try: @@ -177,7 +188,6 @@ def test_pilatus_on_trigger_cancel_on_stop(pilatus): def test_pilatus_on_complete(pilatus: Pilatus): """Test the on_complete logic of the Pilatus detector.""" - if pilatus.scan_info.msg.scan_name.startswith("xas"): # TODO add test cases for xas scans # status = pilatus.complete() @@ -196,8 +206,9 @@ def test_pilatus_on_complete(pilatus: Pilatus): pilatus.cam.acquire._read_pv.mock_data = ACQUIREMODE.ACQUIRING.value pilatus.hdf.capture._read_pv.mock_data = ACQUIREMODE.ACQUIRING.value pilatus.cam.armed._read_pv.mock_data = DETECTORSTATE.ARMED.value - num_images = pilatus.scan_info.msg.num_points * pilatus.scan_info.msg.scan_parameters.get( - "frames_per_trigger", 1 + num_images = ( + pilatus.scan_parameters.num_points + * pilatus.scan_parameters.additional_scan_parameters.get("frames_per_trigger", 1) ) pilatus.hdf.num_captured._read_pv.mock_data = num_images - 1 # Call on complete @@ -275,9 +286,12 @@ def test_pilatus_on_complete(pilatus: Pilatus): def test_pilatus_on_stage_raises_low_exp_time(pilatus): """Test that on_stage raises a ValueError if the exposure time is too low.""" - pilatus.scan_info.msg.scan_parameters["exp_time"] = 0.09 - scan_msg = pilatus.scan_info.msg - if scan_msg.scan_type != "step" and scan_msg.scan_name not in pilatus.xas_xrd_scan_names: + pilatus.scan_info.msg.info["exp_time"] = 0.09 + pilatus.scan_parameters = fetch_scan_info(pilatus.scan_info) + if ( + pilatus.scan_parameters.scan_type != "software_triggered" + and pilatus.scan_parameters.scan_name not in pilatus.xas_xrd_scan_names + ): return with pytest.raises(ValueError): pilatus.on_stage() -- 2.54.0 From 7e20d468813e48168c75ed67be3ac7ba99c3d9ca Mon Sep 17 00:00:00 2001 From: x01da Date: Wed, 27 May 2026 09:09:45 +0200 Subject: [PATCH 13/23] fix: fix bug in mo1_bragg stage --- debye_bec/devices/mo1_bragg/mo1_bragg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index 30419b9..682e1c9 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -112,7 +112,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): scan_name = self.scan_parameters.scan_name if self._check_if_scan_name_is_valid(self.scan_parameters): - start, stop = self.scan_parameters.positions or (None, None) + start, stop = self.scan_parameters.positions if len(self.scan_parameters.positions) == 2 else (None, None) scan_time = self.scan_parameters.additional_scan_parameters.get("scan_time", None) scan_duration = self.scan_parameters.additional_scan_parameters.get( "scan_duration", None -- 2.54.0 From 617cca71a521b710d183d058fa894ea3889d03d0 Mon Sep 17 00:00:00 2001 From: x01da Date: Wed, 27 May 2026 10:06:21 +0200 Subject: [PATCH 14/23] fix: nexus structure safe guards. --- .../file_writer/debye_nexus_structure.py | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/debye_bec/file_writer/debye_nexus_structure.py b/debye_bec/file_writer/debye_nexus_structure.py index 8364845..b472e94 100644 --- a/debye_bec/file_writer/debye_nexus_structure.py +++ b/debye_bec/file_writer/debye_nexus_structure.py @@ -239,49 +239,52 @@ class DebyeNexusStructure(DefaultFormat): measurement_mode = entry.create_group(name="mode") measurement_mode.attrs["NX_class"] = "NX_CHAR" - if (int(ci_chans_bits) & 0x7F) != 0: - # Create a dataset - rayspec_sdd_active = measurement_mode.create_group( - name="Multi_Element_Partial_Fluorescence_Yield" - ) - me_sdd = rayspec_sdd_active.create_dataset( - name="Detector", data="Rayspec 7 element Silicon Drift Detector" - ) - me_sdd.attrs["NX_class"] = "NX_CHAR" + if ci_chans_bits is not None: + if (int(ci_chans_bits) & 0x7F) != 0: + # Create a dataset + rayspec_sdd_active = measurement_mode.create_group( + name="Multi_Element_Partial_Fluorescence_Yield" + ) + me_sdd = rayspec_sdd_active.create_dataset( + name="Detector", data="Rayspec 7 element Silicon Drift Detector" + ) + me_sdd.attrs["NX_class"] = "NX_CHAR" - if (int(ci_chans_bits) & (1 << 8)) != 0: - # Create a dataset - ketek_sdd_active = measurement_mode.create_group( - name="Single_Element_Partial_Fluorescence_Yield" - ) - se_sdd = ketek_sdd_active.create_dataset( - name="Detector", data="Ketex mini single element Silicon Drift Detector" - ) - se_sdd.attrs["NX_class"] = "NX_CHAR" + if (int(ci_chans_bits) & (1 << 8)) != 0: + # Create a dataset + ketek_sdd_active = measurement_mode.create_group( + name="Single_Element_Partial_Fluorescence_Yield" + ) + se_sdd = ketek_sdd_active.create_dataset( + name="Detector", data="Ketex mini single element Silicon Drift Detector" + ) + se_sdd.attrs["NX_class"] = "NX_CHAR" - if (int(ai_chans_bits) & (1 << 6)) != 0: - # Create a dataset - pips_active = measurement_mode.create_group(name="Total_Flourescence_Yield") - tfy = pips_active.create_dataset( - name="Detector", data="Mirion Technologies Partially Depeleted PIPS Detector" - ) - tfy.attrs["NX_class"] = "NX_CHAR" + if ai_chans_bits is not None: + if (int(ai_chans_bits) & (1 << 6)) != 0: + # Create a dataset + pips_active = measurement_mode.create_group(name="Total_Flourescence_Yield") + tfy = pips_active.create_dataset( + name="Detector", + data="Mirion Technologies Partially Depeleted PIPS Detector", + ) + tfy.attrs["NX_class"] = "NX_CHAR" - if ((int(ai_chans_bits) & (1 << 0)) != 0) & ((int(ai_chans_bits) & (1 << 2)) != 0): - # Create a dataset - ai0ai2_active = measurement_mode.create_group(name="Sample_Transmission") - sam_trans = ai0ai2_active.create_dataset( - name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers" - ) - sam_trans.attrs["NX_class"] = "NX_CHAR" + if ((int(ai_chans_bits) & (1 << 0)) != 0) & ((int(ai_chans_bits) & (1 << 2)) != 0): + # Create a dataset + ai0ai2_active = measurement_mode.create_group(name="Sample_Transmission") + sam_trans = ai0ai2_active.create_dataset( + name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers" + ) + sam_trans.attrs["NX_class"] = "NX_CHAR" - if ((int(ai_chans_bits) & (1 << 2)) != 0) & ((int(ai_chans_bits) & (1 << 4)) != 0): - # Create a dataset - ai2ai4_active = measurement_mode.create_group(name="Reference_Transmission") - ref_trans = ai2ai4_active.create_dataset( - name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers" - ) - ref_trans.attrs["NX_class"] = "NX_CHAR" + if ((int(ai_chans_bits) & (1 << 2)) != 0) & ((int(ai_chans_bits) & (1 << 4)) != 0): + # Create a dataset + ai2ai4_active = measurement_mode.create_group(name="Reference_Transmission") + ref_trans = ai2ai4_active.create_dataset( + name="Detector", data="Ionitec 15 cm gas filled Ionisation Chambers" + ) + ref_trans.attrs["NX_class"] = "NX_CHAR" main_data = entry.create_group(name="data") main_data.attrs["NX_class"] = "NXdata" -- 2.54.0 From 8ddf67e8175ca053f6442472b95cb8bf12705f14 Mon Sep 17 00:00:00 2001 From: x01da Date: Wed, 27 May 2026 10:14:49 +0200 Subject: [PATCH 15/23] fix: catch positions is None --- debye_bec/devices/mo1_bragg/mo1_bragg.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index 682e1c9..0779dd0 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -112,7 +112,10 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): scan_name = self.scan_parameters.scan_name if self._check_if_scan_name_is_valid(self.scan_parameters): - start, stop = self.scan_parameters.positions if len(self.scan_parameters.positions) == 2 else (None, None) + if self.scan_parameters.positions: + start, stop = self.scan_parameters.positions if len(self.scan_parameters.positions) == 2 else (None, None) + else: + start, stop = (None, None) scan_time = self.scan_parameters.additional_scan_parameters.get("scan_time", None) scan_duration = self.scan_parameters.additional_scan_parameters.get( "scan_duration", None -- 2.54.0 From 2d21eb90fe19fbdbde7f72ba0aace8cf89bf34ff Mon Sep 17 00:00:00 2001 From: x01da Date: Wed, 27 May 2026 11:09:30 +0200 Subject: [PATCH 16/23] fix: fix scan_done PV with RBV --- debye_bec/devices/mo1_bragg/mo1_bragg.py | 10 +++++++--- debye_bec/devices/mo1_bragg/mo1_bragg_devices.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index 0779dd0..2c673f7 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -57,7 +57,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): progress_signal = Cpt(ProgressSignal, name="progress_signal") - USER_ACCESS = ["set_advanced_xas_settings", "set_xtal"] + USER_ACCESS = ["set_advanced_xas_settings", "set_xtal", "convert_angle_energy"] def __init__(self, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): # type: ignore """ @@ -112,8 +112,12 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): scan_name = self.scan_parameters.scan_name if self._check_if_scan_name_is_valid(self.scan_parameters): - if self.scan_parameters.positions: - start, stop = self.scan_parameters.positions if len(self.scan_parameters.positions) == 2 else (None, None) + if self.scan_parameters.positions is not None: + start, stop = ( + self.scan_parameters.positions + if len(self.scan_parameters.positions) == 2 + else (None, None) + ) else: start, stop = (None, None) scan_time = self.scan_parameters.additional_scan_parameters.get("scan_time", None) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py index fe1d5e5..3b6aca7 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg_devices.py @@ -182,7 +182,7 @@ class Mo1TriggerSettings(Device): class Mo1BraggCalculator(Device): """Mo1 Bragg PVs to convert angle to energy or vice-versa.""" - calc_reset = Cpt(EpicsSignal, suffix="calc_reset", kind="config", put_complete=True) + calc_reset = Cpt(EpicsSignalWithRBV, suffix="calc_reset", kind="config", put_complete=True) calc_done = Cpt(EpicsSignalRO, suffix="calc_done_RBV", kind="config") calc_energy = Cpt(EpicsSignalWithRBV, suffix="calc_energy", kind="config") calc_angle = Cpt(EpicsSignalWithRBV, suffix="calc_angle", kind="config") -- 2.54.0 From 2f0265fff75c77c14c9b366b936309fdaa25f7dd Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 27 May 2026 13:23:41 +0200 Subject: [PATCH 17/23] refactor: deprecate old nidaq_cont_scan implementation --- debye_bec/scans/__init__.py | 1 - debye_bec/scans/nidaq_cont_scan.py | 84 ------------------------------ 2 files changed, 85 deletions(-) delete mode 100644 debye_bec/scans/nidaq_cont_scan.py diff --git a/debye_bec/scans/__init__.py b/debye_bec/scans/__init__.py index bdd703c..d59f41e 100644 --- a/debye_bec/scans/__init__.py +++ b/debye_bec/scans/__init__.py @@ -1,4 +1,3 @@ -# from .nidaq_cont_scan import NIDAQContinuousScan from .nidaq_continuous_scan import NidaqContinuousScan from .xas_simple_scan import ( XasAdvancedScan, diff --git a/debye_bec/scans/nidaq_cont_scan.py b/debye_bec/scans/nidaq_cont_scan.py deleted file mode 100644 index e36e4b3..0000000 --- a/debye_bec/scans/nidaq_cont_scan.py +++ /dev/null @@ -1,84 +0,0 @@ -"""This module contains the scan class for the nidaq of the Debye beamline for use in continuous mode.""" - -import time -from typing import Literal - -import numpy as np -from bec_lib.device import DeviceBase -from bec_lib.logger import bec_logger -from bec_server.scan_server.scans import AsyncFlyScanBase - -logger = bec_logger.logger - - -class NIDAQContinuousScan(AsyncFlyScanBase): - """Class for the nidaq continuous scan (without mono)""" - - scan_name = "nidaq_continuous_scan" - scan_type = "fly" - scan_report_hint = "device_progress" - required_kwargs = [] - use_scan_progress_report = False - pre_move = False - gui_config = {"Scan Parameters": ["scan_duration"], "Data Compression": ["compression"]} - - def __init__( - self, scan_duration: float, daq: DeviceBase = "nidaq", compression: bool = False, **kwargs - ): - """The NIDAQ continuous scan is used to measure with the NIDAQ without moving the - monochromator or any other motor. The NIDAQ thus runs in continuous mode, with a - set scan_duration. - - Args: - scan_duration (float): Duration of the scan. - daq (DeviceBase, optional): DAQ device to be used for the scan. - Defaults to "nidaq". - Examples: - >>> scans.nidaq_continuous_scan(scan_duration=10) - """ - super().__init__(**kwargs) - self.scan_duration = scan_duration - self.daq = daq - self.start_time = 0 - self.monitored_readout_cycle = 1 - self.scan_parameters["scan_duration"] = scan_duration - self.scan_parameters["compression"] = compression - - def update_readout_priority(self): - """Ensure that NIDAQ is not monitored for any quick EXAFS.""" - super().update_readout_priority() - self.readout_priority["async"].append("nidaq") - - def prepare_positions(self): - """Prepare the positions for the scan.""" - yield None - - def pre_scan(self): - """Pre Scan action.""" - - self.start_time = time.time() - # Ensure parent class pre_scan actions to be called. - yield from super().pre_scan() - - def scan_report_instructions(self): - """ - Return the instructions for the scan report. - """ - yield from self.stubs.scan_report_instruction({"device_progress": [self.daq]}) - - def scan_core(self): - """Run the scan core. - Kickoff the acquisition of the NIDAQ wait for the completion of the scan. - """ - kickoff_status = yield from self.stubs.kickoff(device=self.daq) - kickoff_status.wait(timeout=5) # wait for proper kickoff of device - - complete_status = yield from self.stubs.complete(device=self.daq, wait=False) - - while not complete_status.done: - # Readout monitored devices - yield from self.stubs.read(group="monitored", point_id=self.point_id) - time.sleep(self.monitored_readout_cycle) - self.point_id += 1 - - self.num_pos = self.point_id -- 2.54.0 From 40309491b06b4582136601c073ed485224cc687d Mon Sep 17 00:00:00 2001 From: x01da Date: Wed, 27 May 2026 14:23:21 +0200 Subject: [PATCH 18/23] fix: nexus file structure for nidaq continuous scan --- .../file_writer/debye_nexus_structure.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/debye_bec/file_writer/debye_nexus_structure.py b/debye_bec/file_writer/debye_nexus_structure.py index b472e94..b7c7e6c 100644 --- a/debye_bec/file_writer/debye_nexus_structure.py +++ b/debye_bec/file_writer/debye_nexus_structure.py @@ -236,6 +236,8 @@ class DebyeNexusStructure(DefaultFormat): self.configuration.get("nidaq", {}).get("nidaq_add_chans", {}).get("value") ) + rle = self.configuration.get("nidaq", {}).get("nidaq_rle", {}).get("value") + measurement_mode = entry.create_group(name="mode") measurement_mode.attrs["NX_class"] = "NX_CHAR" @@ -311,10 +313,11 @@ class DebyeNexusStructure(DefaultFormat): i0.attrs["NX_class"] = "NXdata" i0.attrs["units"] = "V" - main_data.create_soft_link( - name="i0", - target="/entry/collection/readout_groups/async/nidaq/nidaq_ai0_mean/value", - ) + if rle: + target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai0_mean/value" + else: + target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai0/value" + main_data.create_soft_link(name="i0", target=target) ################## ## i1 @@ -325,10 +328,11 @@ class DebyeNexusStructure(DefaultFormat): i1.attrs["NX_class"] = "NXdata" i1.attrs["units"] = "V" - main_data.create_soft_link( - name="i1", - target="/entry/collection/readout_groups/async/nidaq/nidaq_ai2_mean/value", - ) + if rle: + target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai2_mean/value" + else: + target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai2/value" + main_data.create_soft_link(name="i1", target=target) ################## ## i2 @@ -339,10 +343,11 @@ class DebyeNexusStructure(DefaultFormat): i2.attrs["NX_class"] = "NXdata" i2.attrs["units"] = "V" - main_data.create_soft_link( - name="i2", - target="/entry/collection/readout_groups/async/nidaq/nidaq_ai4_mean/value", - ) + if rle: + target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai4_mean/value" + else: + target = "/entry/collection/readout_groups/async/nidaq/nidaq_ai4/value" + main_data.create_soft_link(name="i2", target=target) ################## ## ci sum -- 2.54.0 From 52d97b2d292cb57e7b8e20d8fb0dc7c755491754 Mon Sep 17 00:00:00 2001 From: x01da Date: Tue, 2 Jun 2026 13:24:14 +0200 Subject: [PATCH 19/23] fix: mo1_bragg calculator --- debye_bec/devices/mo1_bragg/mo1_bragg.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/debye_bec/devices/mo1_bragg/mo1_bragg.py b/debye_bec/devices/mo1_bragg/mo1_bragg.py index 2c673f7..ac5b85a 100644 --- a/debye_bec/devices/mo1_bragg/mo1_bragg.py +++ b/debye_bec/devices/mo1_bragg/mo1_bragg.py @@ -416,25 +416,31 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner): Returns: output (float): Converted angle or energy """ + self.calculator.calc_reset.put(0) self.calculator.calc_reset.put(1) status = CompareStatus(self.calculator.calc_done, 0) self.cancel_on_stop(status) status.wait(self.timeout_for_pvwait) + self.calculator.calc_reset.put(0) if mode == "AngleToEnergy": - self.calculator.calc_angle.put(inp) + in_signal = self.calculator.calc_angle + out_signal = self.calculator.calc_energy elif mode == "EnergyToAngle": - self.calculator.calc_energy.put(inp) + in_signal = self.calculator.calc_energy + out_signal = self.calculator.calc_angle + else: + raise Mo1BraggError(f'Unknown mode {mode}') + in_signal.put(inp) status = CompareStatus(self.calculator.calc_done, 1) self.cancel_on_stop(status) status.wait(self.timeout_for_pvwait) - time.sleep(0.25) # TODO needed still? Needed due to update frequency of softIOC - if mode == "AngleToEnergy": - return self.calculator.calc_energy.get() - elif mode == "EnergyToAngle": - return self.calculator.calc_angle.get() + status = CompareStatus(out_signal, 0, operation_success='>') + self.cancel_on_stop(status) + status.wait(self.timeout_for_pvwait) + return out_signal.get() def set_advanced_xas_settings( self, low: float, high: float, scan_time: float, p_kink: float, e_kink: float -- 2.54.0 From 1a6eb5ab902bc176b43142a90c750ea315ebeacb Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Wed, 3 Jun 2026 13:10:38 +0200 Subject: [PATCH 20/23] fix: update info storage with num_monitored_readouts --- debye_bec/devices/utils/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/debye_bec/devices/utils/utils.py b/debye_bec/devices/utils/utils.py index 1475961..3e8794c 100644 --- a/debye_bec/devices/utils/utils.py +++ b/debye_bec/devices/utils/utils.py @@ -13,6 +13,7 @@ def fetch_scan_info(scan_info: ScanInfo) -> ScanServerScanInfo: info = scan_info.msg.info if isinstance(info["positions"], list): info["positions"] = np.array(info["positions"]) + info["num_monitored_readouts"] = scan_info.msg.num_monitored_readouts try: msg = ScanServerScanInfo.model_validate(info) except ValidationError: # This means we have an old scan_info object. -- 2.54.0 From 6bce6f890785e48c966362d2eff389b8e72c4be5 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 9 Jun 2026 15:41:10 +0200 Subject: [PATCH 21/23] 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 0cc43f0..e5911d8 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 7b7ee59..140efad 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -309,10 +309,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 @@ -600,13 +600,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) @@ -659,7 +659,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 @@ -745,11 +745,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"] @@ -767,7 +767,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 dc6966ee3109e7df07708f6475c26ddde8fce28c Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 9 Jun 2026 15:42:26 +0200 Subject: [PATCH 22/23] 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 | 9 +- .../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, 102 insertions(+), 150 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 140efad..63af101 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -22,10 +22,13 @@ from qtpy.QtWidgets import ( QApplication, QDialog, QDialogButtonBox, + QFrame, QHBoxLayout, QLabel, QPlainTextEdit, QPushButton, + QScrollArea, + QSizePolicy, QStyle, QVBoxLayout, QWidget, @@ -74,32 +77,47 @@ class DigitalTwin(BECWidget, QWidget): self.check_config() self.bec_dispatcher.connect_slot(self.check_config, MessageEndpoints.device_config_update()) - 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.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) @@ -127,12 +145,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 @@ -152,8 +184,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", @@ -234,7 +266,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 093dae9..be65480 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 @@ -3,7 +3,7 @@ Panel for user inputs of 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, @@ -20,7 +20,8 @@ class InputPanel(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) # 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 3f26f06..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 = [] @@ -189,7 +190,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 +198,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/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 d820d5adb5cb2f255b1b4374b3d3087ff2c17ec1 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 9 Jun 2026 15:42:39 +0200 Subject: [PATCH 23/23] 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 | 9 ++++----- 1 file changed, 4 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 63af101..6b466a4 100644 --- a/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py +++ b/debye_bec/bec_widgets/widgets/digital_twin/digital_twin.py @@ -58,7 +58,7 @@ from debye_bec.bec_widgets.widgets.digital_twin.types import ConfigDict logger = bec_logger.logger -OFFSET_FILE = "debye_bec/debye_bec/bec_widgets/widgets/digital_twin/x01da_offsets.yaml" +OFFSET_FILE = Path(__file__).with_name("x01da_offsets.yaml") class DigitalTwin(BECWidget, QWidget): @@ -555,11 +555,10 @@ class DigitalTwin(BECWidget, QWidget): if self.offsets == {}: # Load offsets - file = Path(OFFSET_FILE) - if not file.exists(): + 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): @@ -600,7 +599,7 @@ class DigitalTwin(BECWidget, QWidget): intro_label.setWordWrap(True) layout.addWidget(intro_label) - file = QLabel(OFFSET_FILE) + file = QLabel(str(OFFSET_FILE)) file.setWordWrap(True) font = QFont() font.setItalic(True) -- 2.54.0