From c57819f31344fce88e9978cc9a579ec1a43648bc Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 6 Oct 2026 13:26:35 +0200 Subject: [PATCH 1/3] feat: consolidate devices and improve undulator handling --- tests/tests_devices/test_cff.py | 57 ++++ xtreme_bec/device_configs/endstation.yaml | 235 +++++++-------- xtreme_bec/device_configs/frontend.yaml | 333 +++++++++++----------- xtreme_bec/device_configs/machine.yaml | 113 +++++--- xtreme_bec/device_configs/main.yaml | 11 +- xtreme_bec/device_configs/optics.yaml | 273 ++++++++++-------- xtreme_bec/devices/__init__.py | 2 + xtreme_bec/devices/adc_signals.py | 32 ++- xtreme_bec/devices/cff.py | 22 ++ xtreme_bec/devices/uind_mode.py | 71 ----- xtreme_bec/devices/undulator.py | 128 +++++++++ xtreme_bec/file_writer/xtreme_nexus.py | 37 +-- 12 files changed, 787 insertions(+), 527 deletions(-) create mode 100644 tests/tests_devices/test_cff.py create mode 100644 xtreme_bec/devices/cff.py delete mode 100644 xtreme_bec/devices/uind_mode.py create mode 100644 xtreme_bec/devices/undulator.py diff --git a/tests/tests_devices/test_cff.py b/tests/tests_devices/test_cff.py new file mode 100644 index 0000000..d09b1b5 --- /dev/null +++ b/tests/tests_devices/test_cff.py @@ -0,0 +1,57 @@ +from unittest.mock import patch + +import pytest +from ophyd.sim import make_fake_device +from ophyd.status import Status + +from xtreme_bec.devices.cff import Cff + + +@pytest.fixture +def cff(): + return make_fake_device(Cff)("X07MA-", name="cff") + + +def test_set_confirms_energy_without_waiting_for_cff_readback(cff): + cff.cff.sim_put(1.8) + + def confirm(value): + cff_put.assert_called_once_with(2.25, wait=True) + assert cff.cff.get() == 1.8 + assert value == 1 + status = Status() + status.set_finished() + return status + + with ( + patch.object(cff.cff, "put") as cff_put, + patch.object(cff.confirm_energy, "set", side_effect=confirm) as confirm_set, + ): + status = cff.set(2.25) + + status.wait(timeout=1) + assert status.success + confirm_set.assert_called_once_with(1) + + +def test_failed_cff_write_does_not_confirm_energy(cff): + with ( + patch.object(cff.cff, "put", side_effect=RuntimeError("CFF write failed")), + patch.object(cff.confirm_energy, "set") as confirm_set, + ): + with pytest.raises(RuntimeError, match="CFF write failed"): + cff.set(2.25) + + confirm_set.assert_not_called() + + +def test_set_returns_confirmation_status(cff): + confirmation = Status() + with patch.object(cff.confirm_energy, "set", return_value=confirmation): + status = cff.set(2.25) + + assert status is confirmation + assert not status.done + confirmation.set_exception(RuntimeError("Energy confirmation failed")) + with pytest.raises(RuntimeError, match="Energy confirmation failed"): + status.wait(timeout=1) diff --git a/xtreme_bec/device_configs/endstation.yaml b/xtreme_bec/device_configs/endstation.yaml index d516346..0449cc9 100644 --- a/xtreme_bec/device_configs/endstation.yaml +++ b/xtreme_bec/device_configs/endstation.yaml @@ -1,146 +1,157 @@ -valve: - readoutPriority: baseline - description: "Endstation valve" - deviceClass: xtreme_bec.devices.VacuumValve - deviceConfig: - prefix: "X07MA-OP-VG13:" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false - deviceTags: - - endstation +# BEC Device Configuration File +# +# The device config consists of a mapping of device names to their configurations. +# +# The following fields are required for each device: +# - deviceClass: The class of the device (e.g., ophyd_devices.SimPositioner). +# - enabled: A boolean indicating if the device is enabled. +# - readoutPriority: Determines how the device is read out during a scan. Possible values are ["monitored", "baseline", "async", "on_request", "continuous"]. +# +# Optional fields (defaults shown): +# - connectionTimeout: Connection timeout in seconds. Default is 5. +# - description: A string description of the device. Default is "" (empty string). +# - deviceConfig: A dictionary of configuration parameters specific to the device class. Default is None. +# - deviceTags: A list/set of tags associated with the device. Default is an empty list/set. +# - needs: A list of other device names that this device depends on. Default is []. +# - onFailure: The action to take on failure. Possible values are ["buffer", "retry", "raise"]. Default is "retry". +# - readOnly: A boolean indicating if the device is read-only. Default is false. +# - softwareTrigger: A boolean indicating if the device uses software triggering. Default is false. +# - userParameter: A dictionary of user-defined parameters. Default is {}. +# +# Default values may be omitted for brevity. +########################################################################################################################## -filter: - readoutPriority: baseline - description: "Filter" - deviceClass: ophyd_devices.EpicsUserMotorVME - deviceConfig: - prefix: "X07MA-OP-DI2:TR1" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false - deviceTags: - - endstation -goldmesh1: - readoutPriority: baseline - description: "Gold mesh 1" - deviceClass: ophyd_devices.EpicsMotor - deviceConfig: - prefix: "X07MA-OP-IO1:TR1" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false - deviceTags: - - endstation +# An example device configuration with all fields: + +# device1: +# deviceClass: ophyd_devices.SimPositioner +# description: Sample positioner device +# deviceConfig: +# delay: 1 +# update_frequency: 10 +# deviceTags: +# - frontend +# - motor +# enabled: true +# connectionTimeout: 20 +# readoutPriority: baseline +# needs: +# - device2 +# - device3 +# onFailure: retry +# readOnly: false +# softwareTrigger: false +# userParameter: +# in: 23.1 +# out: -50 + +########################################################################################################################## -goldmesh2: - readoutPriority: baseline - description: "Gold mesh 2" - deviceClass: ophyd_devices.EpicsMotor - deviceConfig: - prefix: "X07MA-OP-IO2:TR1" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false - deviceTags: - - endstation field_x: - readoutPriority: baseline - description: "Magnetic field x" + description: Magnetic field x deviceClass: xtreme_bec.devices.X07MAMagnetAxis deviceConfig: axis_id: X - ps_prefix: "X07MA-PC-PS2:" - prefix: "X07MA-PC-MAG:" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false + prefix: 'X07MA-PC-MAG:' + ps_prefix: 'X07MA-PC-PS2:' deviceTags: - - endstation - -field_z: + - endstation + enabled: true readoutPriority: baseline - description: "Magnetic field z" +field_z: + description: Magnetic field z deviceClass: xtreme_bec.devices.X07MAMagnetAxis deviceConfig: axis_id: Z - ps_prefix: "X07MA-PC-PS1:" - prefix: "X07MA-PC-MAG:" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false + prefix: 'X07MA-PC-MAG:' + ps_prefix: 'X07MA-PC-PS1:' deviceTags: - - endstation - + - endstation + enabled: true + readoutPriority: baseline +filter: + description: Filter + deviceClass: ophyd_devices.EpicsUserMotorVME + deviceConfig: + prefix: X07MA-OP-DI2:TR1 + deviceTags: + - endstation + enabled: true + readoutPriority: baseline +goldmesh1: + description: Gold mesh 1 + deviceClass: ophyd_devices.EpicsMotor + deviceConfig: + prefix: X07MA-OP-IO1:TR1 + deviceTags: + - endstation + enabled: true + readoutPriority: baseline +goldmesh2: + description: Gold mesh 2 + deviceClass: ophyd_devices.EpicsMotor + deviceConfig: + prefix: X07MA-OP-IO2:TR1 + deviceTags: + - endstation + enabled: true + readoutPriority: baseline keithley_1: - readoutPriority: baseline - description: "Keithley 1 / 428 tey" + description: Keithley 1 / 428 gain deviceClass: ophyd.EpicsSignal deviceConfig: - read_pv: "X07MA-PC-K428:1:setGain" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false + auto_monitor: true + read_pv: X07MA-PC-K428:1:setGain deviceTags: - - endstation - + - endstation + enabled: true + readoutPriority: baseline keithley_2: - readoutPriority: baseline - description: "Keithley 2 / 428 i0" + description: Keithley 2 / 428 i0 deviceClass: ophyd.EpicsSignal deviceConfig: - read_pv: "X07MA-PC-K428:2:setGain" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false + auto_monitor: true + read_pv: X07MA-PC-K428:2:setGain deviceTags: - - endstation - + - endstation + enabled: true + readoutPriority: baseline keithley_3: - readoutPriority: baseline - description: "Keithley 3 / 428 diode" + description: Keithley 3 / 428 diode deviceClass: ophyd.EpicsSignal deviceConfig: - read_pv: "X07MA-PC-K428:3:setGain" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false + auto_monitor: true + read_pv: X07MA-PC-K428:3:setGain deviceTags: - - endstation - + - endstation + enabled: true + readoutPriority: baseline signals: - readoutPriority: monitored - description: 'ADC signals' + description: ADC signals deviceClass: xtreme_bec.devices.adc_signals.ADCSignals deviceConfig: - prefix: 'X07MA-' - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false + prefix: X07MA- deviceTags: - - endstation - + - endstation + enabled: true + readoutPriority: monitored temperature: - readoutPriority: baseline - description: "Temperature controller" + description: Temperature controller deviceClass: xtreme_bec.devices.X07MATemperatureController deviceConfig: - prefix: "X07MA-PC-TC:" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false + prefix: 'X07MA-PC-TC:' deviceTags: - - endstation + - endstation + enabled: true + readoutPriority: baseline +valve: + description: Endstation valve + deviceClass: xtreme_bec.devices.VacuumValve + deviceConfig: + prefix: 'X07MA-OP-VG13:' + deviceTags: + - endstation + enabled: true + readoutPriority: baseline diff --git a/xtreme_bec/device_configs/frontend.yaml b/xtreme_bec/device_configs/frontend.yaml index cd6bc6e..6bb48fe 100644 --- a/xtreme_bec/device_configs/frontend.yaml +++ b/xtreme_bec/device_configs/frontend.yaml @@ -1,209 +1,218 @@ -idgap: - description: "Motor to control the IDGap of X07MA" - deviceClass: ophyd_devices.devices.undulator.UndulatorGap - deviceConfig: - prefix: "X07MA-UIND:" - onFailure: retry - enabled: true - readoutPriority: baseline - readOnly: false - softwareTrigger: false - deviceTags: - - frontend +# BEC Device Configuration File +# +# The device config consists of a mapping of device names to their configurations. +# +# The following fields are required for each device: +# - deviceClass: The class of the device (e.g., ophyd_devices.SimPositioner). +# - enabled: A boolean indicating if the device is enabled. +# - readoutPriority: Determines how the device is read out during a scan. Possible values are ["monitored", "baseline", "async", "on_request", "continuous"]. +# +# Optional fields (defaults shown): +# - connectionTimeout: Connection timeout in seconds. Default is 5. +# - description: A string description of the device. Default is "" (empty string). +# - deviceConfig: A dictionary of configuration parameters specific to the device class. Default is None. +# - deviceTags: A list/set of tags associated with the device. Default is an empty list/set. +# - needs: A list of other device names that this device depends on. Default is []. +# - onFailure: The action to take on failure. Possible values are ["buffer", "retry", "raise"]. Default is "retry". +# - readOnly: A boolean indicating if the device is read-only. Default is false. +# - softwareTrigger: A boolean indicating if the device uses software triggering. Default is false. +# - userParameter: A dictionary of user-defined parameters. Default is {}. +# +# Default values may be omitted for brevity. +########################################################################################################################## -uind_mode: - description: "UIND mode of X07MA" - deviceClass: xtreme_bec.devices.uind_mode.UIndMode - deviceConfig: - onFailure: retry - enabled: true - readoutPriority: baseline - readOnly: false - softwareTrigger: false - deviceTags: - - frontend -uind_harmonic: - readoutPriority: baseline - description: "Undulator harmonic" - deviceClass: ophyd.EpicsSignalRO - deviceConfig: - read_pv: "X07MA-UIND:HARMONIC" - auto_monitor: true - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false - deviceTags: - - frontend +# An example device configuration with all fields: + +# device1: +# deviceClass: ophyd_devices.SimPositioner +# description: Sample positioner device +# deviceConfig: +# delay: 1 +# update_frequency: 10 +# deviceTags: +# - frontend +# - motor +# enabled: true +# connectionTimeout: 20 +# readoutPriority: baseline +# needs: +# - device2 +# - device3 +# onFailure: retry +# readOnly: false +# softwareTrigger: false +# userParameter: +# in: 23.1 +# out: -50 + +########################################################################################################################## -uind_energy: - readoutPriority: baseline - description: "Undulator energy" - deviceClass: ophyd.EpicsSignalRO - deviceConfig: - read_pv: "X07MA-UIND:ENERGY-RBV" - auto_monitor: true - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false - deviceTags: - - frontend energy: - readoutPriority: baseline - description: "Energy in eV" + description: Energy in eV deviceClass: ophyd.EpicsSignal deviceConfig: - read_pv: "X07MA-PHS-E:GO.A" auto_monitor: true - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false + read_pv: X07MA-PHS-E:GO.A deviceTags: - - frontend - -energy_mode: + - frontend + enabled: true readoutPriority: baseline - description: "Energy mode. Either PGM = 0 or PGM+ID = 1" +energy_mode: + description: Energy mode. Either PGM = 0 or PGM+ID = 1 deviceClass: ophyd.EpicsSignal deviceConfig: - read_pv: "X07MA-PHS-E:OPT" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false + auto_monitor: true + read_pv: X07MA-PHS-E:OPT deviceTags: - - frontend - -sl1xi: - description: "slit 1 (frontend) x ring" - deviceClass: ophyd_devices.EpicsMotorEC - deviceConfig: - prefix: "X07MA-FE-SL1:TRXR" - onFailure: raise + - frontend enabled: true readoutPriority: baseline - readOnly: false - softwareTrigger: false - deviceTags: - - frontend - -sl1xo: - description: "slit 1 (frontend) x wall" +fe_slxc: + description: slit 1 (frontend) x center deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: - prefix: "X07MA-FE-SL1:TRXW" - onFailure: raise - enabled: true - readoutPriority: baseline - readOnly: false - softwareTrigger: false + prefix: X07MA-FE-SL1:CENTERX deviceTags: - - frontend - -sl1yb: - description: "slit 1 (frontend) y bottom" + - frontend + enabled: true + onFailure: raise + readoutPriority: baseline +fe_slxi: + description: slit 1 (frontend) x ring deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: - prefix: "X07MA-FE-SL1:TRYB" - onFailure: raise - enabled: true - readoutPriority: baseline - readOnly: false - softwareTrigger: false + prefix: X07MA-FE-SL1:TRXR deviceTags: - - frontend - -sl1yt: - description: "slit 1 (frontend) y top" + - frontend + enabled: true + onFailure: raise + readoutPriority: baseline +fe_slxo: + description: slit 1 (frontend) x wall deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: - prefix: "X07MA-FE-SL1:TRYT" - onFailure: raise - enabled: true - readoutPriority: baseline - readOnly: false - softwareTrigger: false + prefix: X07MA-FE-SL1:TRXW deviceTags: - - frontend - -sl1xc: - description: "slit 1 (frontend) x center" + - frontend + enabled: true + onFailure: raise + readoutPriority: baseline +fe_slxs: + description: slit 1 (frontend) x size deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: - prefix: "X07MA-FE-SL1:CENTERX" - onFailure: raise - enabled: true - readoutPriority: baseline - readOnly: false - softwareTrigger: false + prefix: X07MA-FE-SL1:SIZEX deviceTags: - - frontend - -sl1xs: - description: "slit 1 (frontend) x size" + - frontend + enabled: true + onFailure: raise + readoutPriority: baseline +fe_slyb: + description: slit 1 (frontend) y bottom deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: - prefix: "X07MA-FE-SL1:SIZEX" - onFailure: raise - enabled: true - readoutPriority: baseline - readOnly: false - softwareTrigger: false + prefix: X07MA-FE-SL1:TRYB deviceTags: - - frontend - -sl1yc: - description: "slit 1 (frontend) y center" + - frontend + enabled: true + onFailure: raise + readoutPriority: baseline +fe_slyc: + description: slit 1 (frontend) y center deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: - prefix: "X07MA-FE-SL1:CENTERY" - onFailure: raise - enabled: true - readoutPriority: baseline - readOnly: false - softwareTrigger: false + prefix: X07MA-FE-SL1:CENTERY deviceTags: - - frontend - -sl1ys: - description: "slit 1 (frontend) y size" + - frontend + enabled: true + onFailure: raise + readoutPriority: baseline +fe_slys: + description: slit 1 (frontend) y size deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: - prefix: "X07MA-FE-SL1:SIZEY" + prefix: X07MA-FE-SL1:SIZEY + deviceTags: + - frontend + enabled: true onFailure: raise + readoutPriority: baseline +fe_slyt: + description: slit 1 (frontend) y top + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: X07MA-FE-SL1:TRYT + deviceTags: + - frontend + enabled: true + onFailure: raise + readoutPriority: baseline +id_alpha: + description: Angle of linear polarization relative to the horizontal plane + deviceClass: xtreme_bec.devices.undulator.UndulatorAlpha + deviceConfig: + prefix: 'X07MA-UIND:' + deviceTags: + - frontend enabled: true readoutPriority: baseline - readOnly: false - softwareTrigger: false +id_energy: + description: Undulator energy + deviceClass: ophyd.EpicsSignal + deviceConfig: + auto_monitor: true + read_pv: X07MA-UIND:ENERGY-RBV + write_pv: X07MA-UIND:ENERGY-SP deviceTags: - - frontend - + - frontend + enabled: true + readoutPriority: baseline +id_gap: + description: Motor to control the IDGap of X07MA + deviceClass: xtreme_bec.devices.undulator.UndulatorGap + deviceConfig: + prefix: 'X07MA-UIND:' + deviceTags: + - frontend + enabled: true + readoutPriority: baseline +id_harmonic: + description: Undulator harmonic + deviceClass: xtreme_bec.devices.undulator.UndulatorHarmonic + deviceConfig: + prefix: 'X07MA-UIND:' + deviceTags: + - frontend + enabled: true + readoutPriority: baseline +id_mode: + description: UIND mode of X07MA + deviceClass: xtreme_bec.devices.undulator.UndulatorMode + deviceConfig: + prefix: 'X07MA-UIND:' + deviceTags: + - frontend + enabled: true + readoutPriority: baseline xbpm1x: - description: "X-ray BPM1 in frontend translation x" + description: X-ray BPM1 in frontend translation x deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: - prefix: "X07MA-FE-XBPM1:TRX" - onFailure: raise - enabled: true - readoutPriority: baseline - readOnly: false - softwareTrigger: false + prefix: X07MA-FE-XBPM1:TRX deviceTags: - - frontend - + - frontend + enabled: true + onFailure: raise + readoutPriority: baseline xbpm1y: - description: "X-ray BPM1 in frontend translation y" + description: X-ray BPM1 in frontend translation y deviceClass: ophyd_devices.EpicsMotorEC deviceConfig: - prefix: "X07MA-FE-XBPM1:TRY" - onFailure: raise - enabled: true - readoutPriority: baseline - readOnly: false - softwareTrigger: false + prefix: X07MA-FE-XBPM1:TRY deviceTags: - - frontend + - frontend + enabled: true + onFailure: raise + readoutPriority: baseline diff --git a/xtreme_bec/device_configs/machine.yaml b/xtreme_bec/device_configs/machine.yaml index c6e0a4d..975af45 100644 --- a/xtreme_bec/device_configs/machine.yaml +++ b/xtreme_bec/device_configs/machine.yaml @@ -1,57 +1,96 @@ -############################################## -################## Machine ################### -############################################## +# BEC Device Configuration File +# +# The device config consists of a mapping of device names to their configurations. +# +# The following fields are required for each device: +# - deviceClass: The class of the device (e.g., ophyd_devices.SimPositioner). +# - enabled: A boolean indicating if the device is enabled. +# - readoutPriority: Determines how the device is read out during a scan. Possible values are ["monitored", "baseline", "async", "on_request", "continuous"]. +# +# Optional fields (defaults shown): +# - connectionTimeout: Connection timeout in seconds. Default is 5. +# - description: A string description of the device. Default is "" (empty string). +# - deviceConfig: A dictionary of configuration parameters specific to the device class. Default is None. +# - deviceTags: A list/set of tags associated with the device. Default is an empty list/set. +# - needs: A list of other device names that this device depends on. Default is []. +# - onFailure: The action to take on failure. Possible values are ["buffer", "retry", "raise"]. Default is "retry". +# - readOnly: A boolean indicating if the device is read-only. Default is false. +# - softwareTrigger: A boolean indicating if the device uses software triggering. Default is false. +# - userParameter: A dictionary of user-defined parameters. Default is {}. +# +# Default values may be omitted for brevity. +########################################################################################################################## + + +# An example device configuration with all fields: + +# device1: +# deviceClass: ophyd_devices.SimPositioner +# description: Sample positioner device +# deviceConfig: +# delay: 1 +# update_frequency: 10 +# deviceTags: +# - frontend +# - motor +# enabled: true +# connectionTimeout: 20 +# readoutPriority: baseline +# needs: +# - device2 +# - device3 +# onFailure: retry +# readOnly: false +# softwareTrigger: false +# userParameter: +# in: 23.1 +# out: -50 + +########################################################################################################################## + curr: description: SLS ring current deviceClass: ophyd_devices.EpicsSignalRO deviceConfig: - read_pv: AGEBD-PARAMS:CURRENT auto_monitor: true - onFailure: buffer - enabled: true - readoutPriority: monitored - readOnly: true - softwareTrigger: false + read_pv: AGEBD-PARAMS:CURRENT deviceTags: - - machine - + - machine + enabled: true + onFailure: buffer + readOnly: true + readoutPriority: monitored ofb: description: SLS OFB status deviceClass: ophyd_devices.EpicsSignalRO deviceConfig: - read_pv: ARSGE-CECL-FOFB1:FOFB-OPERATE auto_monitor: true - onFailure: buffer - enabled: true - readoutPriority: baseline - readOnly: true - softwareTrigger: false + read_pv: ARSGE-CECL-FOFB1:FOFB-OPERATE deviceTags: - - machine - + - machine + enabled: true + onFailure: buffer + readOnly: true + readoutPriority: baseline +operator: + description: SLS operator messages + deviceClass: ophyd_devices.devices.sls_devices.SLSOperatorMessages + deviceTags: + - machine + enabled: true + onFailure: buffer + readOnly: true + readoutPriority: on_request shutter: description: X07MA shutter status deviceClass: ophyd_devices.EpicsSignalRO deviceConfig: - read_pv: X07M-OP-PSYS:SH-A-OPEN auto_monitor: true - onFailure: buffer + read_pv: X07M-OP-PSYS:SH-A-OPEN + deviceTags: + - machine enabled: true + onFailure: buffer + readOnly: true readoutPriority: baseline - readOnly: true - softwareTrigger: false - deviceTags: - - machine - -operator: - description: SLS operator messages - deviceClass: ophyd_devices.devices.sls_devices.SLSOperatorMessages - deviceConfig: - onFailure: buffer - enabled: true - readoutPriority: on_request - readOnly: true - softwareTrigger: false - deviceTags: - - machine diff --git a/xtreme_bec/device_configs/main.yaml b/xtreme_bec/device_configs/main.yaml index 5470ce9..aee2b55 100644 --- a/xtreme_bec/device_configs/main.yaml +++ b/xtreme_bec/device_configs/main.yaml @@ -1,12 +1,11 @@ - -machine: - - !include ./machine.yaml +endstation: + - !include ./endstation.yaml frontend: - !include ./frontend.yaml +machine: + - !include ./machine.yaml + optics: - !include ./optics.yaml - -endstation: - - !include ./endstation.yaml \ No newline at end of file diff --git a/xtreme_bec/device_configs/optics.yaml b/xtreme_bec/device_configs/optics.yaml index 20ef80f..b74edba 100644 --- a/xtreme_bec/device_configs/optics.yaml +++ b/xtreme_bec/device_configs/optics.yaml @@ -1,191 +1,230 @@ -cmu_y: - description: CMU y pseudo motor - deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo +# BEC Device Configuration File +# +# The device config consists of a mapping of device names to their configurations. +# +# The following fields are required for each device: +# - deviceClass: The class of the device (e.g., ophyd_devices.SimPositioner). +# - enabled: A boolean indicating if the device is enabled. +# - readoutPriority: Determines how the device is read out during a scan. Possible values are ["monitored", "baseline", "async", "on_request", "continuous"]. +# +# Optional fields (defaults shown): +# - connectionTimeout: Connection timeout in seconds. Default is 5. +# - description: A string description of the device. Default is "" (empty string). +# - deviceConfig: A dictionary of configuration parameters specific to the device class. Default is None. +# - deviceTags: A list/set of tags associated with the device. Default is an empty list/set. +# - needs: A list of other device names that this device depends on. Default is []. +# - onFailure: The action to take on failure. Possible values are ["buffer", "retry", "raise"]. Default is "retry". +# - readOnly: A boolean indicating if the device is read-only. Default is false. +# - softwareTrigger: A boolean indicating if the device uses software triggering. Default is false. +# - userParameter: A dictionary of user-defined parameters. Default is {}. +# +# Default values may be omitted for brevity. +########################################################################################################################## + + +# An example device configuration with all fields: + +# device1: +# deviceClass: ophyd_devices.SimPositioner +# description: Sample positioner device +# deviceConfig: +# delay: 1 +# update_frequency: 10 +# deviceTags: +# - frontend +# - motor +# enabled: true +# connectionTimeout: 20 +# readoutPriority: baseline +# needs: +# - device2 +# - device3 +# onFailure: retry +# readOnly: false +# softwareTrigger: false +# userParameter: +# in: 23.1 +# out: -50 + +########################################################################################################################## + + +cff: + description: PGM CFF + deviceClass: xtreme_bec.devices.Cff deviceConfig: - prefix: X07MA-OP-CMU - override_suffixes: - user_readback: :oy - user_setpoint: :ty.A + prefix: X07MA- + deviceTags: + - optics enabled: true readoutPriority: baseline - deviceTags: - - optics - -cmu_z: - description: CMU z pseudo motor - deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo - deviceConfig: - prefix: X07MA-OP-CMU - override_suffixes: - user_readback: :oz - user_setpoint: :tz.A - enabled: true - readoutPriority: baseline - deviceTags: - - optics - cmu_rx: description: CMU Rx pseudo motor deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo deviceConfig: - prefix: X07MA-OP-CMU override_suffixes: user_readback: :oRx user_setpoint: :tRx.A + prefix: X07MA-OP-CMU + deviceTags: + - optics enabled: true readoutPriority: baseline - deviceTags: - - optics - cmu_ry: description: CMU Ry pseudo motor deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo deviceConfig: - prefix: X07MA-OP-CMU override_suffixes: user_readback: :oRy user_setpoint: :tRy.A + prefix: X07MA-OP-CMU + deviceTags: + - optics enabled: true readoutPriority: baseline - deviceTags: - - optics - cmu_rz: description: CMU Rz pseudo motor deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo deviceConfig: - prefix: X07MA-OP-CMU override_suffixes: user_readback: :oRz user_setpoint: :tRz.A + prefix: X07MA-OP-CMU + deviceTags: + - optics enabled: true readoutPriority: baseline - deviceTags: - - optics - -pgm_id_coupling: - description: "PGM ID coupling" - deviceClass: ophyd_devices.EpicsSignal - deviceConfig: - read_pv: X07MA-PHS-E:OPT - enabled: true - readoutPriority: baseline - deviceTags: - - optics - -mono: - readoutPriority: baseline - description: "PGM Monochromator" - deviceClass: xtreme_bec.devices.PGMMonochromator - deviceConfig: - prefix: "X07MA-" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false - deviceTags: - - optics - -mirror_theta: - readoutPriority: baseline - description: "Mirror theta" - deviceClass: ophyd_devices.EpicsUserMotorVME - deviceConfig: - prefix: "X07MA:m1" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false - deviceTags: - - optics - -grating_beta: - readoutPriority: baseline - description: "Grating beta" - deviceClass: ophyd_devices.EpicsUserMotorVME - deviceConfig: - prefix: "X07MA:m2" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false - deviceTags: - - optics - -fmu_y: - description: FMU y pseudo motor +cmu_y: + description: CMU y pseudo motor deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo deviceConfig: - prefix: X07MA-OP-FMU override_suffixes: user_readback: :oy user_setpoint: :ty.A + prefix: X07MA-OP-CMU + deviceTags: + - optics enabled: true readoutPriority: baseline - deviceTags: - - optics - -fmu_z: - description: FMU z pseudo motor +cmu_z: + description: CMU z pseudo motor deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo deviceConfig: - prefix: X07MA-OP-FMU override_suffixes: user_readback: :oz user_setpoint: :tz.A + prefix: X07MA-OP-CMU + deviceTags: + - optics enabled: true readoutPriority: baseline +exit_slit: + description: Exit slit + deviceClass: ophyd_devices.EpicsSignal + deviceConfig: + auto_monitor: true + read_pv: X07MA-OP-SL1SV1:TR_ISAP + write_pv: X07MA-OP-SL1SV1:TR_AP deviceTags: - - optics - + - optics + enabled: true + readoutPriority: baseline +exit_slit_long: + description: Longitudinal translation exit slit + deviceClass: ophyd_devices.EpicsUserMotorVME + deviceConfig: + limits: + - 180.0 + - 500.0 + prefix: X07MA-OP-SL1:TRX + deviceTags: + - optics + enabled: true + readoutPriority: baseline fmu_rx: description: FMU Rx pseudo motor deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo deviceConfig: - prefix: X07MA-OP-FMU override_suffixes: user_readback: :oRx user_setpoint: :tRx.A + prefix: X07MA-OP-FMU + deviceTags: + - optics enabled: true readoutPriority: baseline - deviceTags: - - optics - fmu_ry: description: FMU Ry pseudo motor deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo deviceConfig: - prefix: X07MA-OP-FMU override_suffixes: user_readback: :oRy user_setpoint: :tRy.A + prefix: X07MA-OP-FMU + deviceTags: + - optics enabled: true readoutPriority: baseline - deviceTags: - - optics - fmu_rz: description: FMU Rz pseudo motor deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo deviceConfig: - prefix: X07MA-OP-FMU override_suffixes: user_readback: :oRz user_setpoint: :tRz.A + prefix: X07MA-OP-FMU + deviceTags: + - optics enabled: true readoutPriority: baseline - deviceTags: - - optics - -exit_slit: - readoutPriority: baseline - description: "Aperture between focusing and refocusing mirror" - deviceClass: ophyd_devices.EpicsSignal +fmu_y: + description: FMU y pseudo motor + deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo deviceConfig: - read_pv: "X07MA-OP-SL1SV1:TR_AP" - onFailure: retry - enabled: true - readOnly: false - softwareTrigger: false + override_suffixes: + user_readback: :oy + user_setpoint: :ty.A + prefix: X07MA-OP-FMU deviceTags: - - optics + - optics + enabled: true + readoutPriority: baseline +fmu_z: + description: FMU z pseudo motor + deviceClass: xtreme_bec.devices.mirror_pseudo.MirrorPseudo + deviceConfig: + override_suffixes: + user_readback: :oz + user_setpoint: :tz.A + prefix: X07MA-OP-FMU + deviceTags: + - optics + enabled: true + readoutPriority: baseline +grating_beta: + description: Grating beta + deviceClass: ophyd_devices.EpicsUserMotorVME + deviceConfig: + prefix: X07MA:m2 + deviceTags: + - optics + enabled: true + readoutPriority: baseline +mirror_theta: + description: Mirror theta + deviceClass: ophyd_devices.EpicsUserMotorVME + deviceConfig: + prefix: X07MA:m1 + deviceTags: + - optics + enabled: true + readoutPriority: baseline +mono: + description: PGM Monochromator + deviceClass: xtreme_bec.devices.PGMMonochromator + deviceConfig: + prefix: X07MA- + deviceTags: + - optics + enabled: true + readoutPriority: baseline diff --git a/xtreme_bec/devices/__init__.py b/xtreme_bec/devices/__init__.py index 478964b..ac5b666 100644 --- a/xtreme_bec/devices/__init__.py +++ b/xtreme_bec/devices/__init__.py @@ -1,2 +1,4 @@ +from .cff import Cff from .pgm import PGMMonochromator +from .undulator import UndulatorAlpha, UndulatorGap, UndulatorHarmonic, UndulatorMode from .x07ma_devices import * diff --git a/xtreme_bec/devices/adc_signals.py b/xtreme_bec/devices/adc_signals.py index 5140d15..1582804 100644 --- a/xtreme_bec/devices/adc_signals.py +++ b/xtreme_bec/devices/adc_signals.py @@ -37,8 +37,20 @@ class NormSignal(Signal): class NormTEYSignals(Device): - signal1 = Cpt(EpicsSignalRO, name="signal1", read_pv="X07MA-ES1-AI:SIGNAL0", kind="omitted") - signal2 = Cpt(EpicsSignalRO, name="signal2", read_pv="X07MA-ES1-AI:SIGNAL1", kind="omitted") + signal1 = Cpt( + EpicsSignalRO, + name="signal1", + read_pv="X07MA-ES1-AI:SIGNAL0", + kind="omitted", + auto_monitor=True, + ) + signal2 = Cpt( + EpicsSignalRO, + name="signal2", + read_pv="X07MA-ES1-AI:SIGNAL1", + kind="omitted", + auto_monitor=True, + ) norm = Cpt(NormSignal, name="norm", kind="hinted") def __init__(self, *args, **kwargs): @@ -47,8 +59,20 @@ class NormTEYSignals(Device): class NormDIODESignals(Device): - signal1 = Cpt(EpicsSignalRO, name="signal1", read_pv="X07MA-ES1-AI:SIGNAL3", kind="omitted") - signal2 = Cpt(EpicsSignalRO, name="signal2", read_pv="X07MA-ES1-AI:SIGNAL2", kind="omitted") + signal1 = Cpt( + EpicsSignalRO, + name="signal1", + read_pv="X07MA-ES1-AI:SIGNAL3", + kind="omitted", + auto_monitor=True, + ) + signal2 = Cpt( + EpicsSignalRO, + name="signal2", + read_pv="X07MA-ES1-AI:SIGNAL2", + kind="omitted", + auto_monitor=True, + ) norm = Cpt(NormSignal, name="norm", kind="hinted") def __init__(self, *args, **kwargs): diff --git a/xtreme_bec/devices/cff.py b/xtreme_bec/devices/cff.py new file mode 100644 index 0000000..48d6e61 --- /dev/null +++ b/xtreme_bec/devices/cff.py @@ -0,0 +1,22 @@ +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, Kind, StatusBase + + +class Cff(Device): + """ + CFF device for X-Treme. Setting the cff will also confirm the energy with PGM.setE. + """ + + cff = Cpt(EpicsSignal, "PGM:rbkcff", write_pv="PGM:cff.A", kind=Kind.hinted, auto_monitor=True) + confirm_energy = Cpt( + EpicsSignal, "PGM:setE.PROC", kind=Kind.config, auto_monitor=True, put_complete=True + ) + + def __init__(self, prefix: str = "", **kwargs): + super().__init__(prefix, **kwargs) + self.cff.name = self.name + + def set(self, value: float) -> StatusBase: + """Write the CFF value, then confirm the energy and return its write status.""" + self.cff.put(value, wait=True) + return self.confirm_energy.set(1) diff --git a/xtreme_bec/devices/uind_mode.py b/xtreme_bec/devices/uind_mode.py deleted file mode 100644 index 5deaf3f..0000000 --- a/xtreme_bec/devices/uind_mode.py +++ /dev/null @@ -1,71 +0,0 @@ -from enum import Enum -from typing import Literal - -from ophyd import Component as Cpt -from ophyd import Device, EpicsSignal, Kind, StatusBase - - -class UIndModeEnum(str, Enum): - OFF = "Off" - LINEAR_H = "Linear H" - LINEAR_V_PLUS = "Linear V+" - LINEAR_V_MINUS = "Linear V-" - CIRCULAR_PLUS = "Circular+" - CIRCULAR_MINUS = "Circular-" - PLUS_45_DEG = "+45 deg" - MINUS_45_DEG = "-45 deg" - - -class UIndMode(Device): - USER_ACCESS = ["set_mode"] - - value = Cpt(EpicsSignal, "X07MA-UIND:MODE", auto_monitor=True, kind=Kind.hinted) - value_string = Cpt(EpicsSignal, "X07MA-UIND:MODE", auto_monitor=True, string=True) - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.value.name = self.name - - def set_mode( - self, - mode: ( - Literal[ - "Off", - "Linear H", - "Linear V+", - "Linear V-", - "Circular+", - "Circular-", - "+45 deg", - "-45 deg", - ] - | int - ), - ) -> StatusBase: - """ - Set the undulator polarization mode. - - Args: - mode (Literal["Off", "Linear H", "Linear V+", "Linear V-", "Circular+", "Circular-", "+45 deg", "-45 deg"] | int): The desired polarization mode, either as a string or an integer index. - """ - if isinstance(mode, str): - try: - mode_enum = UIndModeEnum(mode) - mode_val = mode_enum.value - except ValueError: - # pylint: disable=raise-missing-from - raise ValueError( - f"Invalid mode string: {mode}. Valid options are: {[m.value for m in UIndModeEnum]}" - ) - elif isinstance(mode, int): - try: - mode_enum = list(UIndModeEnum)[mode] - mode_val = mode_enum.value - except IndexError: - # pylint: disable=raise-missing-from - raise ValueError( - f"Invalid mode index: {mode}. Valid indices are: 0 to {len(UIndModeEnum) - 1}, corresponding to: {[m.value for m in UIndModeEnum]}" - ) - else: - raise TypeError("Mode must be either a string or an integer index.") - return self.value.set(mode_val) diff --git a/xtreme_bec/devices/undulator.py b/xtreme_bec/devices/undulator.py new file mode 100644 index 0000000..e4c2a38 --- /dev/null +++ b/xtreme_bec/devices/undulator.py @@ -0,0 +1,128 @@ +from collections.abc import Callable +from enum import Enum +from typing import Literal + +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignalRO +from ophyd_devices.devices.undulator import UndulatorAlpha as _UndulatorAlpha +from ophyd_devices.devices.undulator import UndulatorBase +from ophyd_devices.devices.undulator import UndulatorGap as _UndulatorGap +from ophyd_devices.devices.undulator import UndulatorHarmonic as _UndulatorHarmonic +from ophyd_devices.devices.undulator import UndulatorSetpointSignal, UndulatorStopSignal +from ophyd_devices.utils.psi_device_base_utils import StatusBase + +__all__ = [ + "UndulatorAlpha", + "UndulatorGap", + "UndulatorHarmonic", + "UndulatorMode", + "UndulatorModeEnum", +] + +_CMU_ONLINE_VALIDATIONS = ( + ("cmu_online", 4, "CMU must be On-line (X07MA-OP-CMU:ONLINE = 4) to control the undulator."), +) + + +class _CMUOnlineMixin(Device): + """Expose the CMU state required by X-Treme undulator control validations.""" + + cmu_online = Cpt( + EpicsSignalRO, "X07MA-OP-CMU:ONLINE", add_prefix=(), auto_monitor=True, kind="omitted" + ) + + +class UndulatorGap(_CMUOnlineMixin, _UndulatorGap): + """X-Treme undulator gap control requiring the CMU to be On-line.""" + + control_validations = _UndulatorGap.control_validations + _CMU_ONLINE_VALIDATIONS + + +class UndulatorHarmonic(_CMUOnlineMixin, _UndulatorHarmonic): + """X-Treme undulator harmonic control requiring the CMU to be On-line.""" + + control_validations = _UndulatorHarmonic.control_validations + _CMU_ONLINE_VALIDATIONS + + +class UndulatorAlpha(_CMUOnlineMixin, _UndulatorAlpha): + """X-Treme undulator alpha control requiring the CMU to be On-line.""" + + control_validations = _UndulatorAlpha.control_validations + _CMU_ONLINE_VALIDATIONS + + +class UndulatorModeEnum(str, Enum): + OFF = "Off" + LINEAR_H = "Linear H" + LINEAR_V_PLUS = "Linear V+" + LINEAR_V_MINUS = "Linear V-" + CIRCULAR_PLUS = "Circular+" + CIRCULAR_MINUS = "Circular-" + PLUS_45_DEG = "+45 deg" + MINUS_45_DEG = "-45 deg" + + +class UndulatorMode(_CMUOnlineMixin, UndulatorBase): + """X-Treme undulator polarization positioner, completed by the IOC's DONE signal. + + The CMU must be On-line and control must belong to the beamline. + Use ``set("Circular+")`` or ``set(4)`` to select a mode. As with harmonic + and alpha control, every request writes the target and waits for DONE, + including requests for the currently selected mode. + """ + + control_validations = UndulatorBase.control_validations + _CMU_ONLINE_VALIDATIONS + + setpoint = Cpt(UndulatorSetpointSignal, "MODE", auto_monitor=True, kind="hinted") + value_string = Cpt(EpicsSignalRO, "MODE", auto_monitor=True, string=True) + stop_signal = Cpt(UndulatorStopSignal, "STOP", auto_monitor=True, kind="omitted") + done = Cpt(EpicsSignalRO, "DONE", auto_monitor=True, kind="omitted") + select_control = Cpt(EpicsSignalRO, "SCTRL", auto_monitor=True) + + def __init__(self, prefix: str = "X07MA-UIND:", **kwargs) -> None: + super().__init__(prefix=prefix, **kwargs) + self.setpoint.name = self.name + + def move( + self, + position: ( + Literal[ + "Off", + "Linear H", + "Linear V+", + "Linear V-", + "Circular+", + "Circular-", + "+45 deg", + "-45 deg", + ] + | int + ), + wait: bool = True, + timeout: float | None = None, + moved_cb: Callable | None = None, + ) -> StatusBase: + """Select a mode by label or index and wait for undulator completion. + + ``set()`` delegates here with ``wait=False`` and returns a pending status. + Invalid labels and indices raise ValueError before issuing a command. + """ + modes = list(UndulatorModeEnum) + if isinstance(position, str): + try: + mode = modes.index(UndulatorModeEnum(position)) + except ValueError as exc: + raise ValueError( + f"Invalid mode string: {position}. " + f"Valid options are: {[mode.value for mode in modes]}" + ) from exc + elif isinstance(position, int) and not isinstance(position, bool): + if not 0 <= position < len(modes): + raise ValueError( + f"Invalid mode index: {position}. Valid indices are: 0 to {len(modes) - 1}." + ) + mode = position + else: + raise TypeError("Mode must be either a string or an integer index.") + + self.validate_control() + return super().move(mode, wait=wait, timeout=timeout, moved_cb=moved_cb) diff --git a/xtreme_bec/file_writer/xtreme_nexus.py b/xtreme_bec/file_writer/xtreme_nexus.py index 74bd361..7132749 100644 --- a/xtreme_bec/file_writer/xtreme_nexus.py +++ b/xtreme_bec/file_writer/xtreme_nexus.py @@ -12,7 +12,6 @@ class XtremeNeXusFormat(DefaultFormat): "rmu": ("refocusing_mirror", "Refocusing mirror", 29.8), } _MIRROR_AXES = ("y", "z", "rx", "ry", "rz") - _SLIT_SIGNALS = ("sl1xi", "sl1xo", "sl1yb", "sl1yt", "sl1xc", "sl1xs", "sl1yc", "sl1ys") def format(self) -> None: entry = self.storage.create_group("entry") @@ -67,10 +66,16 @@ class XtremeNeXusFormat(DefaultFormat): insertion_device.attrs["NX_class"] = "NXinsertion_device" insertion_device.create_dataset(name="type", data="undulator") insertion_device.create_dataset(name="description", data="Apple II undulator") - self.safe_dataset(group=insertion_device, name="polarization", device="uind_mode") - self.safe_dataset(group=insertion_device, name="energy", device="uind_energy") - self.safe_dataset(group=insertion_device, name="gap", device="idgap") - self.safe_dataset(group=insertion_device, name="harmonic", device="uind_harmonic") + self.safe_dataset( + group=insertion_device, + name="polarization", + device="id_mode", + signal="id_mode_value_string", + ) + self.safe_dataset(group=insertion_device, name="alpha", device="id_alpha") + self.safe_dataset(group=insertion_device, name="energy", device="id_energy") + self.safe_dataset(group=insertion_device, name="gap", device="id_gap") + self.safe_dataset(group=insertion_device, name="harmonic", device="id_harmonic") xbpm = instrument.create_group("xbpm_1") xbpm.attrs["NX_class"] = "NXpositioner" @@ -96,14 +101,14 @@ class XtremeNeXusFormat(DefaultFormat): front_end_slit = instrument.create_group("front_end_slit") front_end_slit.attrs["NX_class"] = "NXslit" front_end_slit.create_dataset(name="description", data="Front-end slit 1") - self.safe_dataset(group=front_end_slit, name="x_gap", device="sl1xs") - self.safe_dataset(group=front_end_slit, name="y_gap", device="sl1ys") - self.safe_dataset(group=front_end_slit, name="x_translation", device="sl1xc") - self.safe_dataset(group=front_end_slit, name="y_translation", device="sl1yc") - self.safe_dataset(group=front_end_slit, name="x_ring_blade", device="sl1xi") - self.safe_dataset(group=front_end_slit, name="x_wall_blade", device="sl1xo") - self.safe_dataset(group=front_end_slit, name="y_bottom_blade", device="sl1yb") - self.safe_dataset(group=front_end_slit, name="y_top_blade", device="sl1yt") + self.safe_dataset(group=front_end_slit, name="x_gap", device="fe_slxs") + self.safe_dataset(group=front_end_slit, name="y_gap", device="fe_slys") + self.safe_dataset(group=front_end_slit, name="x_translation", device="fe_slxc") + self.safe_dataset(group=front_end_slit, name="y_translation", device="fe_slyc") + self.safe_dataset(group=front_end_slit, name="x_ring_blade", device="fe_slxi") + self.safe_dataset(group=front_end_slit, name="x_wall_blade", device="fe_slxo") + self.safe_dataset(group=front_end_slit, name="y_bottom_blade", device="fe_slyb") + self.safe_dataset(group=front_end_slit, name="y_top_blade", device="fe_slyt") ############################## #### Optics #### @@ -113,7 +118,7 @@ class XtremeNeXusFormat(DefaultFormat): self.safe_dataset(group=monochromator, name="energy", device="mono") self.safe_dataset(group=monochromator, name="beamline_energy", device="energy") self.safe_dataset(group=monochromator, name="mode", device="energy_mode") - self.safe_dataset(group=monochromator, name="cff", device="mono", signal="cff") + self.safe_dataset(group=monochromator, name="cff", device="cff") plane_mirror = monochromator.create_group("plane_mirror") plane_mirror.attrs["NX_class"] = "NXmirror" @@ -198,15 +203,11 @@ class XtremeNeXusFormat(DefaultFormat): "async", [] ) - print(f"signal is async: {signals_is_async}") - if signals_is_async: prefix = "signals_data_" else: prefix = "signals_" - print(self.written_async_signals) - self.safe_dataset(group=group, name="s1", device="signals", signal=f"{prefix}s1") self.safe_dataset(group=group, name="s2", device="signals", signal=f"{prefix}s2") self.safe_dataset(group=group, name="s3", device="signals", signal=f"{prefix}s3") -- 2.54.0 From a4e1ef87ee86d67b825345bed6ba34ca355f6ed6 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 6 Oct 2026 13:49:38 +0200 Subject: [PATCH 2/3] tests: add otf scan tests --- tests/tests_scans/conftest.py | 59 +++++++++++ tests/tests_scans/test_otf_scan.py | 164 +++++++++++++++++++++++++++++ xtreme_bec/scans/otf_scan.py | 2 +- 3 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 tests/tests_scans/conftest.py create mode 100644 tests/tests_scans/test_otf_scan.py diff --git a/tests/tests_scans/conftest.py b/tests/tests_scans/conftest.py new file mode 100644 index 0000000..5d82562 --- /dev/null +++ b/tests/tests_scans/conftest.py @@ -0,0 +1,59 @@ +from unittest import mock + +import pytest +from bec_server.scan_server.tests.scan_fixtures import ( + MockCustomDevice, + device_manager, + nth_done_status_mock, + readout_priority, + v4_scan_assembler, +) + + +@pytest.fixture +def otf_scan_assembler(device_manager, readout_priority, v4_scan_assembler): + """Use BEC's assembler with the beamline-specific signals and RPC responses.""" + device_signals = { + "mono": ["sync_with_undulator"], + "mirror_theta": ["base_velocity", "velocity"], + "grating_beta": ["base_velocity", "velocity"], + "signals": ["store_updates"], + } + for name, signals in device_signals.items(): + device = MockCustomDevice( + name, + device_info={ + "signals": { + signal: { + "obj_name": f"{name}_{signal}", + "kind_str": "config", + "describe": {"precision": 3}, + } + for signal in signals + } + }, + limits=(250, 2000) if name == "mono" else (-10, 10), + ) + for signal in signals: + getattr(device, signal).put = mock.MagicMock() + device_manager.add_device(device) + + mono = device_manager.devices["mono"] + mono.set = mock.MagicMock() + mono.get_max_speeds = mock.MagicMock(return_value=(0.02, 0.03)) + mono.set_progress_range = mock.MagicMock() + readout_priority.monitored_devices = ["mono", "signals"] + + def assemble(e1=500.0, e2=800.0, time=2.0): + scan = v4_scan_assembler("otf_scan", e1=e1, e2=e2, time=time) + # The shared fixture's device dictionary needs aliases for dev. access. + for name in device_signals: + setattr(scan.dev, name, scan.dev[name]) + return scan + + return assemble + + +@pytest.fixture +def otf_scan(otf_scan_assembler): + return otf_scan_assembler() diff --git a/tests/tests_scans/test_otf_scan.py b/tests/tests_scans/test_otf_scan.py new file mode 100644 index 0000000..4bfe9b9 --- /dev/null +++ b/tests/tests_scans/test_otf_scan.py @@ -0,0 +1,164 @@ +from unittest import mock + +import pytest +from bec_lib.bec_errors import ScanInputValidationError +from bec_server.scan_server.scans.scan_base import ScanType +from bec_server.scan_server.tests.scan_hook_tests import DEFAULT_HOOK_TESTS, run_scan_tests + +from xtreme_bec.scans.otf_scan import OTFScan + + +@pytest.mark.parametrize(("hook_name", "hook_tests"), DEFAULT_HOOK_TESTS) +def test_default_hooks(otf_scan, nth_done_status_mock, hook_name, hook_tests): + run_scan_tests(otf_scan, [(hook_name, hook_tests)], nth_done_status_mock) + + +def test_assembly(otf_scan): + assert isinstance(otf_scan, OTFScan) + assert otf_scan.scan_info.scan_type == ScanType.HARDWARE_TRIGGERED + assert (otf_scan.e1, otf_scan.e2, otf_scan.time) == (500.0, 800.0, 2.0) + assert otf_scan.mono is otf_scan.dev["mono"] + assert otf_scan._baseline_readout_status is None + + +@pytest.mark.parametrize( + "parameters", + [{"e1": 249.0}, {"e1": 2001.0}, {"e2": 249.0}, {"e2": 2001.0}, {"time": -1.0}, {"time": 0.0}], +) +def test_assembly_rejects_invalid_scan_parameters(otf_scan_assembler, parameters): + with pytest.raises(ScanInputValidationError): + otf_scan_assembler(**parameters) + + +@pytest.mark.parametrize(("e1", "e2"), [(250.0, 2000.0), (2000.0, 250.0)]) +def test_assembly_accepts_energy_boundaries(otf_scan_assembler, e1, e2): + scan = otf_scan_assembler(e1=e1, e2=e2, time=0.1) + + assert (scan.e1, scan.e2, scan.time) == (e1, e2, 0.1) + + +def test_prepare_routes_adc_to_async_and_reports_mono_progress(otf_scan, nth_done_status_mock): + baseline_status = nth_done_status_mock() + otf_scan.actions.read_baseline_devices = mock.MagicMock(return_value=baseline_status) + + otf_scan.prepare_scan() + + assert otf_scan.scan_info.readout_priority_modification["async"] == ["signals"] + assert otf_scan.scan_info.scan_report_instructions == [{"device_progress": ["mono"]}] + priority = otf_scan.scan_info.readout_priority_modification + assert [device.name for device in otf_scan.dev.monitored_devices(priority)] == ["mono"] + assert [device.name for device in otf_scan.dev.async_devices(priority)] == ["signals"] + assert otf_scan._baseline_readout_status is baseline_status + assert baseline_status.wait_calls == 0 + + +@pytest.mark.parametrize(("e1", "e2"), [(500.0, 800.0), (800.0, 500.0)]) +def test_pre_scan_positions_before_configuring_scan_speed(otf_scan_assembler, e1, e2): + scan = otf_scan_assembler(e1=e1, e2=e2, time=1.5) + calls = mock.MagicMock() + calls.attach_mock(scan.dev.mirror_theta.base_velocity.put, "theta_base") + calls.attach_mock(scan.dev.grating_beta.base_velocity.put, "beta_base") + calls.attach_mock(scan.dev.mirror_theta.velocity.put, "theta_speed") + calls.attach_mock(scan.dev.grating_beta.velocity.put, "beta_speed") + calls.attach_mock(scan.mono.set, "move") + calls.attach_mock(scan.mono.get_max_speeds, "max_speeds") + calls.attach_mock(scan.mono.set_progress_range, "progress") + calls.attach_mock(scan.mono.sync_with_undulator.put, "sync") + scan.actions.pre_scan_all_devices = mock.MagicMock() + calls.attach_mock(scan.actions.pre_scan_all_devices, "pre_scan") + + scan.pre_scan() + + assert calls.mock_calls == [ + mock.call.theta_base(0.008), + mock.call.beta_base(0.008), + mock.call.theta_speed(0.08), + mock.call.beta_speed(0.08), + mock.call.move(e1), + mock.call.move().wait(), + mock.call.max_speeds(e1=e1, e2=e2, travel_time=90.0), + mock.call.progress(e1=e1, e2=e2), + mock.call.theta_base(0.002), + mock.call.beta_base(0.003), + mock.call.theta_speed(0.02), + mock.call.beta_speed(0.03), + mock.call.sync(1), + mock.call.pre_scan(), + ] + + +@pytest.mark.parametrize("resolve_after", [1, 2, 4]) +def test_scan_core_stores_adc_and_reads_while_moving(otf_scan, nth_done_status_mock, resolve_after): + status = nth_done_status_mock(resolve_after=resolve_after) + calls = mock.MagicMock() + calls.attach_mock(otf_scan.dev.signals.store_updates.put, "store") + otf_scan.actions.set = mock.MagicMock(return_value=status) + otf_scan.actions.read_monitored_devices = mock.MagicMock() + calls.attach_mock(otf_scan.actions.set, "move") + calls.attach_mock(otf_scan.actions.read_monitored_devices, "read") + + with mock.patch("xtreme_bec.scans.otf_scan.time.sleep") as sleep: + calls.attach_mock(sleep, "sleep") + otf_scan.scan_core() + + assert calls.mock_calls == [ + mock.call.store(1), + mock.call.move(otf_scan.mono, 800.0, wait=False), + *[ + call + for _ in range(resolve_after - 1) + for call in (mock.call.read(), mock.call.sleep(1)) + ], + ] + + +def test_at_each_point_reads_monitored_devices(otf_scan): + otf_scan.actions.read_monitored_devices = mock.MagicMock() + + otf_scan.at_each_point() + + otf_scan.actions.read_monitored_devices.assert_called_once_with() + + +@pytest.mark.parametrize("hook", ["post_scan", "on_exception"]) +def test_cleanup_disables_acquisition_and_restores_velocity(otf_scan, hook): + calls = mock.MagicMock() + calls.attach_mock(otf_scan.dev.signals.store_updates.put, "store") + otf_scan.actions.complete_all_devices = mock.MagicMock() + calls.attach_mock(otf_scan.actions.complete_all_devices, "complete") + calls.attach_mock(otf_scan.mono.sync_with_undulator.put, "sync") + calls.attach_mock(otf_scan.dev.mirror_theta.base_velocity.put, "theta_base") + calls.attach_mock(otf_scan.dev.grating_beta.base_velocity.put, "beta_base") + calls.attach_mock(otf_scan.dev.mirror_theta.velocity.put, "theta_speed") + calls.attach_mock(otf_scan.dev.grating_beta.velocity.put, "beta_speed") + + if hook == "on_exception": + otf_scan.on_exception(RuntimeError("scan interrupted")) + else: + otf_scan.post_scan() + + assert calls.mock_calls == [ + mock.call.store(0), + *([mock.call.complete()] if hook == "post_scan" else []), + mock.call.sync(0), + mock.call.theta_base(0.008), + mock.call.beta_base(0.008), + mock.call.theta_speed(0.08), + mock.call.beta_speed(0.08), + ] + + +@pytest.mark.parametrize("has_baseline", [False, True]) +def test_close_waits_for_baseline_before_closing_and_checking_statuses(otf_scan, has_baseline): + calls = mock.MagicMock() + otf_scan.actions.close_scan = calls.close + otf_scan.actions.check_for_unchecked_statuses = calls.check + otf_scan._baseline_readout_status = calls.baseline if has_baseline else None + + otf_scan.close_scan() + + assert calls.mock_calls == [ + *([mock.call.baseline.wait()] if has_baseline else []), + mock.call.close(), + mock.call.check(), + ] diff --git a/xtreme_bec/scans/otf_scan.py b/xtreme_bec/scans/otf_scan.py index b26dbc7..79e5970 100644 --- a/xtreme_bec/scans/otf_scan.py +++ b/xtreme_bec/scans/otf_scan.py @@ -49,7 +49,7 @@ class OTFScan(ScanBase): #fmt: off e1: Annotated[float, ScanArgument(display_name="Energy 1", description="Start energy", units=Units.eV, ge=250, le=2000)], e2: Annotated[float, ScanArgument(display_name="Energy 2", description="End energy", units=Units.eV, ge=250, le=2000)], - time: Annotated[float, ScanArgument(display_name="Time", description="Travel time", units=Units.min, ge=0)], + time: Annotated[float, ScanArgument(display_name="Time", description="Travel time", units=Units.min, gt=0)], **kwargs, #fmt: on ): -- 2.54.0 From d82eb881ed5efd206c3efe6292981eb642e16079 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Tue, 6 Oct 2026 13:52:27 +0200 Subject: [PATCH 3/3] fix: mirror pseudo should use put complete --- xtreme_bec/devices/mirror_pseudo.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/xtreme_bec/devices/mirror_pseudo.py b/xtreme_bec/devices/mirror_pseudo.py index 0e9b3a0..fd5b2e3 100644 --- a/xtreme_bec/devices/mirror_pseudo.py +++ b/xtreme_bec/devices/mirror_pseudo.py @@ -1,23 +1,9 @@ from ophyd import Component as Cpt from ophyd import EpicsSignal from ophyd_devices.devices.simple_positioner import PSISimplePositionerBase -from ophyd_devices.utils.psi_device_base_utils import TransitionStatus class MirrorPseudo(PSISimplePositionerBase): user_readback = Cpt(EpicsSignal, "R") user_setpoint = Cpt(EpicsSignal, "S") - motor_done_move = Cpt(EpicsSignal, ":BUSY") - - def _setup_move(self, position): - """Move and do not wait until motion is complete (asynchronous)""" - - def _done_cb(status, *args, **kwargs): - self._done_moving(success=status.success) - self._move_completion_status = None - - self._move_completion_status = TransitionStatus(self.motor_done_move, transitions=[1, 0]) - self.user_setpoint.put(position, wait=False) - - self.cancel_on_stop(self._move_completion_status) - self._move_completion_status.add_callback(_done_cb) + use_put_complete = True -- 2.54.0