Merge feature branch of Debye back to main #51

Merged
appel_c merged 31 commits from resolve_mr_conflict into main 2025-05-07 13:42:11 +02:00
32 changed files with 3272 additions and 2508 deletions

View File

@@ -1,6 +1,6 @@
"""
Pre-startup script for BEC client. This script is executed before the BEC client
is started. It can be used to add additional command line arguments.
is started. It can be used to add additional command line arguments.
"""
from bec_lib.service_config import ServiceConfig

View File

@@ -210,7 +210,7 @@ cm_xstripe:
mo1_bragg:
readoutPriority: baseline
description: Positioner for the Monochromator
deviceClass: debye_bec.devices.mo1_bragg.Mo1Bragg
deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg.Mo1Bragg
deviceConfig:
prefix: "X01DA-OP-MO1:BRAGG:"
onFailure: retry

View File

@@ -1,8 +1,210 @@
## Slit Diaphragm -- Physical positioners
sldi_trxr:
readoutPriority: baseline
description: Front-end slit diaphragm X-translation Ring-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:TRXR
onFailure: retry
enabled: true
softwareTrigger: false
sldi_trxw:
readoutPriority: baseline
description: Front-end slit diaphragm X-translation Wall-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:TRXW
onFailure: retry
enabled: true
softwareTrigger: false
sldi_tryb:
readoutPriority: baseline
description: Front-end slit diaphragm Y-translation Bottom-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:TRYB
onFailure: retry
enabled: true
softwareTrigger: false
sldi_tryt:
readoutPriority: baseline
description: Front-end slit diaphragm X-translation Top-edge
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:TRYT
onFailure: retry
enabled: true
softwareTrigger: false
## Slit Diaphragm -- Virtual positioners
sldi_centerx:
readoutPriority: baseline
description: Front-end slit diaphragm X-center
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:CENTERX
onFailure: retry
enabled: true
softwareTrigger: false
sldi_gapx:
readoutPriority: baseline
description: Front-end slit diaphragm X-gap
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:GAPX
onFailure: retry
enabled: true
softwareTrigger: false
sldi_centery:
readoutPriority: baseline
description: Front-end slit diaphragm Y-center
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:CENTERY
onFailure: retry
enabled: true
softwareTrigger: false
sldi_gapy:
readoutPriority: baseline
description: Front-end slit diaphragm Y-gap
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-SLDI:GAPY
onFailure: retry
enabled: true
softwareTrigger: false
## Collimating Mirror -- Physical Positioners
cm_trxu:
readoutPriority: baseline
description: Collimating Mirror X-translation upstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:TRXU
onFailure: retry
enabled: true
softwareTrigger: false
cm_trxd:
readoutPriority: baseline
description: Collimating Mirror X-translation downstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:TRXD
onFailure: retry
enabled: true
softwareTrigger: false
cm_tryu:
readoutPriority: baseline
description: Collimating Mirror Y-translation upstream
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:TRYU
onFailure: retry
enabled: true
softwareTrigger: false
cm_trydr:
readoutPriority: baseline
description: Collimating Mirror Y-translation downstream ring
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:TRYDR
onFailure: retry
enabled: true
softwareTrigger: false
cm_trydw:
readoutPriority: baseline
description: Collimating Mirror Y-translation downstream wall
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:TRYDW
onFailure: retry
enabled: true
softwareTrigger: false
cm_bnd:
readoutPriority: baseline
description: Collimating Mirror bender
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:BND
onFailure: retry
enabled: true
softwareTrigger: false
## Collimating Mirror -- Virtual Positioners
cm_rotx:
readoutPriority: baseline
description: Collimating Morror Pitch
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:ROTX
onFailure: retry
enabled: true
softwareTrigger: false
cm_roty:
readoutPriority: baseline
description: Collimating Morror Yaw
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
cm_rotz:
readoutPriority: baseline
description: Collimating Morror Roll
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:ROTZ
onFailure: retry
enabled: true
softwareTrigger: false
cm_trx:
readoutPriority: baseline
description: Collimating Morror Center Point X
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:XTCP
onFailure: retry
enabled: true
softwareTrigger: false
cm_try:
readoutPriority: baseline
description: Collimating Morror Center Point Y
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:YTCP
onFailure: retry
enabled: true
softwareTrigger: false
cm_ztcp:
readoutPriority: baseline
description: Collimating Morror Center Point Z
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:ZTCP
onFailure: retry
enabled: true
softwareTrigger: false
cm_xstripe:
readoutPriority: baseline
description: Collimating Morror X Stripe
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-FE-CM:XSTRIPE
onFailure: retry
enabled: true
softwareTrigger: false
## Bragg Monochromator
mo1_bragg:
readoutPriority: baseline
description: Positioner for the Monochromator
deviceClass: debye_bec.devices.mo1_bragg.Mo1Bragg
deviceClass: debye_bec.devices.mo1_bragg.mo1_bragg.Mo1Bragg
deviceConfig:
prefix: "X01DA-OP-MO1:BRAGG:"
onFailure: retry
@@ -18,64 +220,136 @@ dummy_pv:
enabled: true
softwareTrigger: false
## NIDAQ
# NIDAQ
nidaq:
readoutPriority: async
readoutPriority: monitored
description: NIDAQ backend for data reading for debye scans
deviceClass: debye_bec.devices.nidaq.NIDAQ
deviceClass: debye_bec.devices.nidaq.nidaq.Nidaq
deviceConfig:
prefix: "X01DA-PC-SCANSERVER:"
onFailure: retry
enabled: true
softwareTrigger: false
## Monochromator -- Physical Positioners
mo_try:
readoutPriority: baseline
description: Monochromator Y Translation
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-MO1:TRY
onFailure: retry
enabled: true
softwareTrigger: false
mo_trx:
readoutPriority: baseline
description: Monochromator X Translation
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-MO1:TRY
onFailure: retry
enabled: true
softwareTrigger: false
mo_roty:
readoutPriority: baseline
description: Monochromator Yaw
deviceClass: ophyd.EpicsMotor
deviceConfig:
prefix: X01DA-OP-MO1:ROTY
onFailure: retry
enabled: true
softwareTrigger: false
# Ionization Chambers
ic0:
readoutPriority: baseline
description: Ionization chamber 0
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
ic1:
readoutPriority: baseline
description: Ionization chamber 1
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
ic2:
readoutPriority: baseline
description: Ionization chamber 2
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
# ES0 Filter
# es0filter:
es0filter:
readoutPriority: baseline
description: ES0 filter station
deviceClass: debye_bec.devices.es0filter.ES0Filter
deviceConfig:
prefix: "X01DA-ES0-FI:"
onFailure: retry
enabled: true
softwareTrigger: false
# Reference foil changer
reffoilchanger:
readoutPriority: baseline
description: ES2 reference foil changer
deviceClass: debye_bec.devices.reffoilchanger.Reffoilchanger
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
# Beam Monitors
# beam_monitor_1:
# readoutPriority: async
# description: ES0 filter station
# deviceClass: debye_bec.devices.es0filter.ES0Filter
# description: Beam monitor 1
# deviceClass: debye_bec.devices.cameras.prosilica_cam.ProsilicaCam
# deviceConfig:
# prefix: "X01DA-ES0-FI:"
# prefix: "X01DA-OP-GIGE01:"
# onFailure: retry
# enabled: true
# softwareTrigger: false
# Current amplifiers
# amplifiers:
# beam_monitor_2:
# readoutPriority: async
# description: ES current amplifiers
# deviceClass: debye_bec.devices.amplifiers.Amplifiers
# description: Beam monitor 2
# deviceClass: debye_bec.devices.cameras.prosilica_cam.ProsilicaCam
# deviceConfig:
# prefix: "X01DA-ES:AMP5004"
# prefix: "X01DA-OP-GIGE02:"
# onFailure: retry
# enabled: true
# softwareTrigger: false
# HV power supplies
# hv_supplies:
# readoutPriority: async
# description: HV power supplies
# deviceClass: debye_bec.devices.hv_supplies.HVSupplies
# deviceConfig:
# prefix: "X01DA-"
# onFailure: retry
# enabled: true
# softwareTrigger: false
# Gas Mix Setup
# gas_mix_setup:
# readoutPriority: async
# description: Gas Mix Setup for Ionization Chambers
# deviceClass: debye_bec.devices.gas_mix_setup.GasMixSetup
# deviceConfig:
# prefix: "X01DA-ES-GMES:"
# onFailure: retry
# enabled: true
# softwareTrigger: false
# xray_eye:
# readoutPriority: async
# description: X-ray eye
# deviceClass: debye_bec.devices.cameras.basler_cam.BaslerCam
# deviceConfig:
# prefix: "X01DA-ES-XRAYEYE:"
# onFailure: retry
# enabled: true
# softwareTrigger: false
# Pilatus Curtain
# pilatus_curtain:
# readoutPriority: async
# readoutPriority: baseline
# description: Pilatus Curtain
# deviceClass: debye_bec.devices.pilatus_curtain.PilatusCurtain
# deviceConfig:
@@ -90,7 +364,7 @@ nidaq:
################################
es_temperature1:
readoutPriority: monitored
readoutPriority: baseline
description: ES temperature sensor 1
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
@@ -100,7 +374,7 @@ es_temperature1:
softwareTrigger: false
es_humidity1:
readoutPriority: monitored
readoutPriority: baseline
description: ES humidity sensor 1
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
@@ -110,7 +384,7 @@ es_humidity1:
softwareTrigger: false
es_pressure1:
readoutPriority: monitored
readoutPriority: baseline
description: ES ambient pressure sensor 1
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
@@ -120,7 +394,7 @@ es_pressure1:
softwareTrigger: false
es_temperature2:
readoutPriority: monitored
readoutPriority: baseline
description: ES temperature sensor 2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
@@ -130,7 +404,7 @@ es_temperature2:
softwareTrigger: false
es_humidity2:
readoutPriority: monitored
readoutPriority: baseline
description: ES humidity sensor 2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
@@ -140,7 +414,7 @@ es_humidity2:
softwareTrigger: false
es_pressure2:
readoutPriority: monitored
readoutPriority: baseline
description: ES ambient pressure sensor 2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
@@ -150,7 +424,7 @@ es_pressure2:
softwareTrigger: false
es_light_toggle:
readoutPriority: monitored
readoutPriority: baseline
description: ES light toggle
deviceClass: ophyd.EpicsSignal
deviceConfig:
@@ -164,7 +438,7 @@ es_light_toggle:
#################
sdd1_temperature:
readoutPriority: monitored
readoutPriority: baseline
description: SDD1 temperature sensor
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
@@ -174,11 +448,12 @@ sdd1_temperature:
softwareTrigger: false
sdd1_humidity:
readoutPriority: monitored
readoutPriority: baseline
description: SDD1 humidity sensor
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-ES1-DET1:Humidity"
kind: "config"
onFailure: retry
enabled: true
softwareTrigger: false
@@ -188,7 +463,7 @@ sdd1_humidity:
#####################
es1_alignment_laser:
readoutPriority: monitored
readoutPriority: baseline
description: ES1 alignment laser
deviceClass: ophyd.EpicsSignal
deviceConfig:

View File

@@ -1,186 +0,0 @@
""" ES Current amplifiers"""
import enum
from typing import Literal
from ophyd import Component as Cpt
from ophyd import Device, Kind, EpicsSignalWithRBV
from ophyd_devices.utils import bec_utils
class Enable(int, enum.Enum):
"""Enum class for the enable signal of the channel"""
OFF = 0
STARTUP = 1
ON = 2
class Gain(int, enum.Enum):
"""Enum class for the gain of the channel"""
G1E6 = 0
G1E7 = 1
G5E7 = 2
G1E8 = 3
G1E9 = 4
class Filter(int, enum.Enum):
"""Enum class for the filter of the channel"""
F1US = 0
F3US = 1
F10US = 2
F30US = 3
F100US = 4
F300US = 5
F1MS = 6
F3MS = 7
class Amplifiers(Device):
"""Class for the ES current amplifiers"""
USER_ACCESS = ['set_channel']
ic0_enable = Cpt(
EpicsSignalWithRBV, suffix=".cOnOff1", kind="config", doc='Enable ch1 -> IC0'
)
ic0_gain = Cpt(
EpicsSignalWithRBV, suffix=":cGain1_ENUM", kind="config", doc='Gain of ch1 -> IC0'
)
ic0_filter = Cpt(
EpicsSignalWithRBV, suffix=":cFilter1_ENUM", kind="config", doc='Filter of ch1 -> IC0'
)
ic1_enable = Cpt(
EpicsSignalWithRBV, suffix=".cOnOff2", kind="config", doc='Enable ch2 -> IC1'
)
ic1_gain = Cpt(
EpicsSignalWithRBV, suffix=":cGain2_ENUM", kind="config", doc='Gain of ch2 -> IC1'
)
ic1_filter = Cpt(
EpicsSignalWithRBV, suffix=":cFilter2_ENUM", kind="config", doc='Filter of ch2 -> IC1'
)
ic2_enable = Cpt(
EpicsSignalWithRBV, suffix=".cOnOff3", kind="config", doc='Enable ch3 -> IC2'
)
ic2_gain = Cpt(
EpicsSignalWithRBV, suffix=":cGain3_ENUM", kind="config", doc='Gain of ch3 -> IC2'
)
ic2_filter = Cpt(
EpicsSignalWithRBV, suffix=":cFilter3_ENUM", kind="config", doc='Filter of ch3 -> IC2'
)
pips_enable = Cpt(
EpicsSignalWithRBV, suffix=".cOnOff4", kind="config", doc='Enable ch4 -> PIPS'
)
pips_gain = Cpt(
EpicsSignalWithRBV, suffix=":cGain4_ENUM", kind="config", doc='Gain of ch4 -> PIPS'
)
pips_filter = Cpt(
EpicsSignalWithRBV, suffix=":cFilter4_ENUM", kind="config", doc='Filter of ch4 -> PIPS'
)
def __init__(
self, prefix="", *, name: str, kind: Kind = None, device_manager=None, parent=None, **kwargs
):
"""Initialize the Current Amplifiers.
Args:
prefix (str): EPICS prefix for the device
name (str): Name of the device
kind (Kind): Kind of the device
device_manager (DeviceManager): Device manager instance
parent (Device): Parent device
kwargs: Additional keyword arguments
"""
super().__init__(prefix, name=name, kind=kind, parent=parent, **kwargs)
self.device_manager = device_manager
self.service_cfg = None
self.timeout_for_pvwait = 2.5
self.readback.name = self.name
# Wait for connection on all components, ensure IOC is connected
self.wait_for_connection(all_signals=True, timeout=5)
if device_manager:
self.device_manager = device_manager
else:
self.device_manager = bec_utils.DMMock()
self.connector = self.device_manager.connector
def set_channel(
self,
detector: Literal['ic0', 'ic1', 'ic2', 'pips'],
gain: Literal['1e6', '1e7', '5e7', '1e8', '1e9'],
filter: Literal['1us', '3us', '10us', '30us', '100us', '300us', '1ms', '3ms']
) -> None:
"""Configure the gain setting of the specified channel
Args:
detector (Literal['ic0', 'ic1', 'ic2', 'pips']) : Detector
gain (Literal['1e6', '1e7', '5e7', '1e8', '1e9']) : Desired gain
filter (Literal['1us', '3us', '10us', '30us', '100us', '300us', '1ms', '3ms']) : Desired filter
"""
ch_enable = None
ch_gain = None
ch_filter = None
match detector:
case 'ic0':
ch_enable = self.ic0_enable
ch_gain = self.ic0_gain
ch_filter = self.ic0_filter
case 'ic1':
ch_enable = self.ic1_enable
ch_gain = self.ic1_gain
ch_filter = self.ic1_filter
case 'ic2':
ch_enable = self.ic2_enable
ch_gain = self.ic2_gain
ch_filter = self.ic2_filter
case 'pips':
ch_enable = self.pips_enable
ch_gain = self.pips_gain
ch_filter = self.pips_filter
ch_enable.put(Enable.ON)
# Wait until channel is switched on
if not self.wait_for_signals(
signal_conditions=[(ch_enable.get, Enable.ON)],
timeout=self.timeout_for_pvwait,
check_stopped=True,
):
raise TimeoutError(
f"Enabling channel run into timeout after {self.timeout_for_pvwait} seconds"
)
match gain:
case '1e6':
ch_gain.put(Gain.G1E6)
case '1e7':
ch_gain.put(Gain.G1E7)
case '5e7':
ch_gain.put(Gain.G5E7)
case '1e8':
ch_gain.put(Gain.G1E8)
case '1e9':
ch_gain.put(Gain.G1E9)
match filter:
case '1us':
ch_filter.put(Filter.F1US)
case '3us':
ch_filter.put(Filter.F3US)
case '10us':
ch_filter.put(Filter.F10US)
case '30us':
ch_filter.put(Filter.F30US)
case '100us':
ch_filter.put(Filter.F100US)
case '300us':
ch_filter.put(Filter.F300US)
case '1ms':
ch_filter.put(Filter.F1MS)
case '3ms':
ch_filter.put(Filter.F3MS)

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
import time
from typing import TYPE_CHECKING
import numpy as np
from ophyd import ADBase
from ophyd import ADComponent as ADCpt
from ophyd_devices.devices.areadetector.cam import AravisDetectorCam
from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
if TYPE_CHECKING:
from bec_lib.devicemanager import ScanInfo
class BaslerCamBase(ADBase):
cam1 = ADCpt(AravisDetectorCam, "cam1:")
image1 = ADCpt(ImagePlugin_V35, "image1:")
class BaslerCam(PSIDeviceBase, BaslerCamBase):
# preview_2d = PSIComponent(SetableSignal, signal_type=SignalType.PREVIEW, ndim=2, kind=Kind.omitted)
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.last_emit = time.time()
self.update_frequency = 5 # Hz
def emit_to_bec(self, *args, obj=None, old_value=None, value=None, **kwargs):
if (time.time() - self.last_emit) < (1 / self.update_frequency):
return # Check logic
width = self.image1.array_size.width.get()
height = self.image1.array_size.height.get()
data = np.rot90(np.reshape(value, (height, width)), k=-1, axes=(0, 1))
# self.preview_2d.put(data)
self._run_subs(sub_type=self.SUB_DEVICE_MONITOR_2D, value=data)
self.last_emit = time.time()
def on_connected(self):
self.image1.array_data.subscribe(self.emit_to_bec, run=False)

View File

@@ -0,0 +1,41 @@
from __future__ import annotations
import time
from typing import TYPE_CHECKING
import numpy as np
from ophyd import ADBase
from ophyd import ADComponent as ADCpt
from ophyd import Component as Cpt
from ophyd import Device
from ophyd_devices.devices.areadetector.cam import ProsilicaDetectorCam
from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
class ProsilicaCamBase(ADBase):
cam1 = ADCpt(ProsilicaDetectorCam, "cam1:")
image1 = ADCpt(ImagePlugin_V35, "image1:")
class ProsilicaCam(PSIDeviceBase, ProsilicaCamBase):
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.last_emit = time.time()
self.update_frequency = 5 # Hz
def emit_to_bec(self, *args, obj=None, old_value=None, value=None, **kwargs):
if (time.time() - self.last_emit) < (1 / self.update_frequency):
return # Check logic
width = self.image1.array_size.width.get()
height = self.image1.array_size.height.get()
data = np.rot90(np.reshape(value, (height, width)), k=-1, axes=(0, 1))
self._run_subs(sub_type=self.SUB_DEVICE_MONITOR_2D, value=data)
self.last_emit = time.time()
def on_connected(self):
self.image1.array_data.subscribe(self.emit_to_bec, run=False)

View File

@@ -1,89 +1,54 @@
""" ES0 Filter Station"""
"""ES0 Filter Station"""
from typing import Literal
from ophyd import Component as Cpt
from ophyd import Device, Kind, EpicsSignalWithRBV
from ophyd import Device, EpicsSignal, Kind
from ophyd_devices.utils import bec_utils
from typeguard import typechecked
class EpicsSignalWithRBVBit(EpicsSignal):
def __init__(self, prefix, *, bit: int, **kwargs):
super().__init__(prefix, **kwargs)
self.bit = bit
@typechecked
def put(self, value: Literal[0, 1], **kwargs):
bit_value = super().get()
# convert to int
bit_value = int(bit_value)
if value == 1:
new_value = bit_value | (1 << self.bit)
else:
new_value = bit_value & ~(1 << self.bit)
super().put(new_value, **kwargs)
def get(self, **kwargs) -> Literal[0, 1]:
bit_value = super().get()
# convert to int
bit_value = int(bit_value)
if (bit_value & (1 << self.bit)) == 0:
return 0
return 1
class ES0Filter(Device):
"""Class for the ES0 filter station"""
"""Class for the ES0 filter station X01DA-ES0-FI:"""
USER_ACCESS = ['set_filters']
filter_output = Cpt(
EpicsSignalWithRBV,
suffix="BIO",
kind="config",
doc='Packed value of filter positions'
)
def __init__(
self, prefix="", *, name: str, kind: Kind = None, device_manager=None, parent=None, **kwargs
):
"""Initialize the ES0 Filter Station.
Args:
prefix (str): EPICS prefix for the device
name (str): Name of the device
kind (Kind): Kind of the device
device_manager (DeviceManager): Device manager instance
parent (Device): Parent device
kwargs: Additional keyword arguments
"""
super().__init__(prefix, name=name, kind=kind, parent=parent, **kwargs)
self.device_manager = device_manager
self.service_cfg = None
self.timeout_for_pvwait = 2.5
self.readback.name = self.name
# Wait for connection on all components, ensure IOC is connected
self.wait_for_connection(all_signals=True, timeout=5)
if device_manager:
self.device_manager = device_manager
else:
self.device_manager = bec_utils.DMMock()
self.connector = self.device_manager.connector
def set_filters(self, filters: list) -> None:
"""Configure the filters according to the list
Args:
filters (list) : List of strings representing the filters, e.g. ['Mo400', 'Al20']
"""
output = 0
for filter in filters:
match filter:
case 'Mo400':
output = output & (1 << 1)
case 'Mo300':
output = output & (1 << 2)
case 'Mo200':
output = output & (1 << 3)
case 'Zn500':
output = output & (1 << 4)
case 'Zn250':
output = output & (1 << 5)
case 'Zn125':
output = output & (1 << 6)
case 'Zn50':
output = output & (1 << 7)
case 'Zn25':
output = output & (1 << 8)
case 'Al500':
output = output & (1 << 9)
case 'Al320':
output = output & (1 << 10)
case 'Al200':
output = output & (1 << 11)
case 'Al100':
output = output & (1 << 12)
case 'Al50':
output = output & (1 << 13)
case 'Al20':
output = output & (1 << 14)
case 'Al10':
output = output & (1 << 15)
self.filter_output.put(output)
Mo400 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=1, kind="config", doc="Mo400 filter")
Mo300 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=2, kind="config", doc="Mo300 filter")
Mo200 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=3, kind="config", doc="Mo200 filter")
Zn500 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=4, kind="config", doc="Zn500 filter")
Zn250 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=5, kind="config", doc="Zn250 filter")
Zn125 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=6, kind="config", doc="Zn125 filter")
Zn50 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=7, kind="config", doc="Zn50 filter")
Zn25 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=8, kind="config", doc="Zn25 filter")
Al500 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=9, kind="config", doc="Al500 filter")
Al320 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=10, kind="config", doc="Al320 filter")
Al200 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=11, kind="config", doc="Al200 filter")
Al100 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=12, kind="config", doc="Al100 filter")
Al50 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=13, kind="config", doc="Al50 filter")
Al20 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=14, kind="config", doc="Al20 filter")
Al10 = Cpt(EpicsSignalWithRBVBit, suffix="BIO", bit=15, kind="config", doc="Al10 filter")

View File

@@ -1,236 +0,0 @@
""" ES Gas Mix Setup"""
import time
from typing import Literal
from ophyd import Component as Cpt
from ophyd import Device, Kind, EpicsSignalWithRBV, EpicsSignal, EpicsSignalRO
from ophyd_devices.utils import bec_utils
class GasMixSetup(Device):
"""Class for the ES HGas Mix Setup"""
USER_ACCESS = ['fill_ic']
# IC0
ic0_gas1_req = Cpt(
EpicsSignalWithRBV, suffix="IC0Gas1Req", kind="config", doc='IC0 Gas 1 requirement'
)
ic0_conc1_req = Cpt(
EpicsSignalWithRBV, suffix="IC0Conc1Req", kind="config", doc='IC0 Concentration 1 requirement'
)
ic0_gas2_req = Cpt(
EpicsSignalWithRBV, suffix="IC0Gas2Req", kind="config", doc='IC0 Gas 2 requirement'
)
ic0_conc2_req = Cpt(
EpicsSignalWithRBV, suffix="IC0Conc2Req", kind="config", doc='IC0 Concentration 2 requirement'
)
ic0_press_req = Cpt(
EpicsSignalWithRBV, suffix="IC0PressReq", kind="config", doc='IC0 Pressure requirement'
)
ic0_fill = Cpt(
EpicsSignal, suffix="IC0Fill", kind="config", doc='Fill IC0'
)
ic0_status = Cpt(
EpicsSignalRO, suffix="IC0Status", kind="config", doc='Status of IC0'
)
ic0_gas1 = Cpt(
EpicsSignalRO, suffix="IC0Gas1", kind="config", doc='IC0 Gas 1'
)
ic0_conc1 = Cpt(
EpicsSignalRO, suffix="IC0Conc1", kind="config", doc='IC0 Concentration 1'
)
ic0_gas2 = Cpt(
EpicsSignalRO, suffix="IC0Gas2", kind="config", doc='IC0 Gas 2'
)
ic0_conc2 = Cpt(
EpicsSignalRO, suffix="IC0Conc2", kind="config", doc='IC0 Concentration 2'
)
ic0_press = Cpt(
EpicsSignalRO, suffix="IC0PressTransm", kind="config", doc='Current IC0 Pressure'
)
# IC1
ic1_gas1_req = Cpt(
EpicsSignalWithRBV, suffix="IC1Gas1Req", kind="config", doc='IC1 Gas 1 requirement'
)
ic1_conc1_req = Cpt(
EpicsSignalWithRBV, suffix="IC1Conc1Req", kind="config", doc='IC1 Concentration 1 requirement'
)
ic1_gas2_req = Cpt(
EpicsSignalWithRBV, suffix="IC1Gas2Req", kind="config", doc='IC1 Gas 2 requirement'
)
ic1_conc2_req = Cpt(
EpicsSignalWithRBV, suffix="IC1Conc2Req", kind="config", doc='IC1 Concentration 2 requirement'
)
ic1_press_req = Cpt(
EpicsSignalWithRBV, suffix="IC1PressReq", kind="config", doc='IC1 Pressure requirement'
)
ic1_fill = Cpt(
EpicsSignal, suffix="IC1Fill", kind="config", doc='Fill IC1'
)
ic1_status = Cpt(
EpicsSignalRO, suffix="IC1Status", kind="config", doc='Status of IC1'
)
ic1_gas1 = Cpt(
EpicsSignalRO, suffix="IC1Gas1", kind="config", doc='IC1 Gas 1'
)
ic1_conc1 = Cpt(
EpicsSignalRO, suffix="IC1Conc1", kind="config", doc='IC1 Concentration 1'
)
ic1_gas2 = Cpt(
EpicsSignalRO, suffix="IC1Gas2", kind="config", doc='IC1 Gas 2'
)
ic1_conc2 = Cpt(
EpicsSignalRO, suffix="IC1Conc2", kind="config", doc='IC1 Concentration 2'
)
ic1_press = Cpt(
EpicsSignalRO, suffix="IC1PressTransm", kind="config", doc='Current IC1 Pressure'
)
# IC2
ic2_gas1_req = Cpt(
EpicsSignalWithRBV, suffix="IC2Gas1Req", kind="config", doc='IC2 Gas 1 requirement'
)
ic2_conc1_req = Cpt(
EpicsSignalWithRBV, suffix="IC2Conc1Req", kind="config", doc='IC2 Concentration 1 requirement'
)
ic2_gas2_req = Cpt(
EpicsSignalWithRBV, suffix="IC2Gas2Req", kind="config", doc='IC2 Gas 2 requirement'
)
ic2_conc2_req = Cpt(
EpicsSignalWithRBV, suffix="IC2Conc2Req", kind="config", doc='IC2 Concentration 2 requirement'
)
ic2_press_req = Cpt(
EpicsSignalWithRBV, suffix="IC2PressReq", kind="config", doc='IC2 Pressure requirement'
)
ic2_fill = Cpt(
EpicsSignal, suffix="IC2Fill", kind="config", doc='Fill IC2'
)
ic2_status = Cpt(
EpicsSignalRO, suffix="IC2Status", kind="config", doc='Status of IC2'
)
ic2_gas1 = Cpt(
EpicsSignalRO, suffix="IC2Gas1", kind="config", doc='IC2 Gas 1'
)
ic2_conc1 = Cpt(
EpicsSignalRO, suffix="IC2Conc1", kind="config", doc='IC2 Concentration 1'
)
ic2_gas2 = Cpt(
EpicsSignalRO, suffix="IC2Gas2", kind="config", doc='IC2 Gas 2'
)
ic2_conc2 = Cpt(
EpicsSignalRO, suffix="IC2Conc2", kind="config", doc='IC2 Concentration 2'
)
ic2_press = Cpt(
EpicsSignalRO, suffix="IC2PressTransm", kind="config", doc='Current IC2 Pressure'
)
def __init__(
self, prefix="", *, name: str, kind: Kind = None, device_manager=None, parent=None, **kwargs
):
"""Initialize the Gas Mix Setup.
Args:
prefix (str): EPICS prefix for the device
name (str): Name of the device
kind (Kind): Kind of the device
device_manager (DeviceManager): Device manager instance
parent (Device): Parent device
kwargs: Additional keyword arguments
"""
super().__init__(prefix, name=name, kind=kind, parent=parent, **kwargs)
self.device_manager = device_manager
self.service_cfg = None
self.timeout_for_pvwait = 360
self.readback.name = self.name
# Wait for connection on all components, ensure IOC is connected
self.wait_for_connection(all_signals=True, timeout=5)
if device_manager:
self.device_manager = device_manager
else:
self.device_manager = bec_utils.DMMock()
self.connector = self.device_manager.connector
def fill_ic(
self,
detector: Literal['ic0', 'ic1', 'ic2'],
gas1: Literal['He', 'LN2', 'Ar', 'Kr'],
conc1: float,
gas2: Literal['He', 'LN2', 'Ar', 'Kr'],
conc2: float,
pressure: float,
) -> None:
"""Fill an ionization chamber with the specified gas mixture.
Args:
detector (Literal['ic0', 'ic1', 'ic2']) : Detector
gas1 (Literal['He', 'LN2', 'Ar', 'Kr']) : Gas 1 requirement,
conc1 (float) : Concentration 1 requirement in %,
gas2 (Literal['He', 'LN2', 'Ar', 'Kr']) : Gas 2 requirement,
conc2 (float) : Concentration 2 requirement in %,
pressure (float) : Required pressure in bar abs,
"""
if 100 < conc1 < 0:
raise ValueError('Concentration 1 out of range [0 .. 100 %]')
if 100 < conc2 < 0:
raise ValueError('Concentration 2 out of range [0 .. 100 %]')
if 3 < pressure < 0:
raise ValueError('Pressure out of range [0 .. 3 bar abs]')
ch_gas1_req = None
ch_conc1_req = None
ch_gas2_req = None
ch_conc2_req = None
ch_press_req = None
ch_fill = None
ch_status = None
match detector:
case 'ic0':
ch_gas1_req = self.ic0_gas1_req
ch_conc1_req = self.ic0_conc1_req
ch_gas2_req = self.ic0_gas2_req
ch_conc2_req = self.ic0_conc2_req
ch_press_req = self.ic0_press_req
ch_fill = self.ic0_fill
ch_status = self.ic0_status
case 'ic1':
ch_gas1_req = self.ic1_gas1_req
ch_conc1_req = self.ic1_conc1_req
ch_gas2_req = self.ic1_gas2_req
ch_conc2_req = self.ic1_conc2_req
ch_press_req = self.ic1_press_req
ch_fill = self.ic1_fill
ch_status = self.ic1_status
case 'ic2':
ch_gas1_req = self.ic2_gas1_req
ch_conc1_req = self.ic2_conc1_req
ch_gas2_req = self.ic2_gas2_req
ch_conc2_req = self.ic2_conc2_req
ch_press_req = self.ic2_press_req
ch_fill = self.ic2_fill
ch_status = self.ic2_status
ch_gas1_req.put(gas1)
ch_conc1_req.put(conc1)
ch_gas2_req.put(gas2)
ch_conc2_req.put(conc2)
ch_press_req.put(pressure)
time.sleep(0.5)
ch_fill.put(1)
time.sleep(1)
# Wait until ionization chamber is filled successfully
if not self.wait_for_signals(
signal_conditions=[(ch_status.get, 1)],
timeout=self.timeout_for_pvwait,
check_stopped=True,
):
raise TimeoutError(
f"Ionization chamber still not filled after {self.timeout_for_pvwait} seconds, check caqtdm panel"
)

View File

@@ -1,165 +0,0 @@
""" ES HV power supplies"""
from typing import Literal
from ophyd import Component as Cpt
from ophyd import Device, Kind, EpicsSignalWithRBV, EpicsSignal, EpicsSignalRO
from ophyd_devices.utils import bec_utils
class Amplifiers(Device):
"""Class for the ES HV power supplies"""
USER_ACCESS = ['set_ic']
# IC0
ic0_ext_ena = Cpt(
EpicsSignalRO, suffix="ES1-IC0:HV-Ext-Ena", kind="config", doc='External enable signal of HV IC0'
)
ic0_ena = Cpt(
EpicsSignalWithRBV, suffix="ES1-IC0:HV-Ena", kind="config", doc='Enable signal of HV IC0'
)
ic0_hv_v = Cpt(
EpicsSignal, suffix="ES1-IC0:HV1-VSet", kind="config", doc='HV voltage of IC0'
)
ic0_hv_i = Cpt(
EpicsSignal, suffix="ES1-IC0:HV1-V-RB", kind="config", doc='HV current of IC0'
)
ic0_grid_v = Cpt(
EpicsSignal, suffix="ES1-IC0:HV2-VSet", kind="config", doc='Grid voltage of IC0'
)
ic0_grid_i = Cpt(
EpicsSignal, suffix="ES1-IC0:HV2-V-RB", kind="config", doc='Grid current of IC0'
)
# IC1
ic1_ext_ena = Cpt(
EpicsSignalRO, suffix="ES2-IC12:HV-Ext-Ena", kind="config", doc='External enable signal of HV IC1/IC2'
)
ic1_ena = Cpt(
EpicsSignalWithRBV, suffix="ES2-IC12:HV-Ena", kind="config", doc='Enable signal of HV IC1/IC2'
)
ic1_hv_v = Cpt(
EpicsSignal, suffix="ES2-IC1:HV1-VSet", kind="config", doc='HV voltage of IC1'
)
ic1_hv_i = Cpt(
EpicsSignal, suffix="ES2-IC1:HV1-V-RB", kind="config", doc='HV current of IC1'
)
ic1_grid_v = Cpt(
EpicsSignal, suffix="ES2-IC1:HV2-VSet", kind="config", doc='Grid voltage of IC1'
)
ic1_grid_i = Cpt(
EpicsSignal, suffix="ES2-IC1:HV2-V-RB", kind="config", doc='Grid current of IC1'
)
# IC2
ic2_ext_ena = Cpt(
EpicsSignalRO, suffix="ES2-IC12:HV-Ext-Ena", kind="config", doc='External enable signal of HV IC1/IC2'
)
ic2_ena = Cpt(
EpicsSignalWithRBV, suffix="ES2-IC12:HV-Ena", kind="config", doc='Enable signal of HV IC1/IC2'
)
ic2_hv_v = Cpt(
EpicsSignal, suffix="ES2-IC2:HV1-VSet", kind="config", doc='HV voltage of IC2'
)
ic2_hv_i = Cpt(
EpicsSignal, suffix="ES2-IC2:HV1-V-RB", kind="config", doc='HV current of IC2'
)
ic2_grid_v = Cpt(
EpicsSignal, suffix="ES2-IC2:HV2-VSet", kind="config", doc='Grid voltage of IC2'
)
ic2_grid_i = Cpt(
EpicsSignal, suffix="ES2-IC2:HV2-V-RB", kind="config", doc='Grid current of IC2'
)
def __init__(
self, prefix="", *, name: str, kind: Kind = None, device_manager=None, parent=None, **kwargs
):
"""Initialize the Current Amplifiers.
Args:
prefix (str): EPICS prefix for the device
name (str): Name of the device
kind (Kind): Kind of the device
device_manager (DeviceManager): Device manager instance
parent (Device): Parent device
kwargs: Additional keyword arguments
"""
super().__init__(prefix, name=name, kind=kind, parent=parent, **kwargs)
self.device_manager = device_manager
self.service_cfg = None
self.timeout_for_pvwait = 2.5
self.readback.name = self.name
# Wait for connection on all components, ensure IOC is connected
self.wait_for_connection(all_signals=True, timeout=5)
if device_manager:
self.device_manager = device_manager
else:
self.device_manager = bec_utils.DMMock()
self.connector = self.device_manager.connector
def set_voltage(
self,
detector: Literal['ic0', 'ic1', 'ic2'],
hv: float,
grid: float
) -> None:
"""Configure the voltage settings of the specified detector, this will
enable the high voltage (if external enable is active)!
Args:
detector (Literal['ic0', 'ic1', 'ic2']) : Detector
hv (float) : Desired voltage for the 'HV' terminal
grid (float) : Desired voltage for the 'Grid' terminal
"""
if 3000 < hv < 0:
raise ValueError('specified HV not within range [0 .. 3000]')
if 3000 < grid < 0:
raise ValueError('specified Grid not within range [0 .. 3000]')
if grid > hv:
raise ValueError('Grid must not be higher than HV!')
ch_ena = None
ch_hv_v = None
ch_hv_i = None
ch_grid_v = None
ch_grid_i = None
match detector:
case 'ic0':
ch_ena = self.ic0_ena
ch_hv_v = self.ic0_hv_v
ch_hv_i = self.ic0_hv_i
ch_grid_v = self.ic0_grid_v
ch_grid_i = self.ic0_grid_i
case 'ic1':
ch_ena = self.ic1_ena
ch_hv_v = self.ic1_hv_v
ch_hv_i = self.ic1_hv_i
ch_grid_v = self.ic1_grid_v
ch_grid_i = self.ic1_grid_i
case 'ic2':
ch_ena = self.ic2_ena
ch_hv_v = self.ic2_hv_v
ch_hv_i = self.ic2_hv_i
ch_grid_v = self.ic2_grid_v
ch_grid_i = self.ic2_grid_i
ch_ena.put(1)
# Wait until channel is switched on
if not self.wait_for_signals(
signal_conditions=[(ch_ena.get, 1)],
timeout=self.timeout_for_pvwait,
check_stopped=True,
):
raise TimeoutError(
f"Enabling channel run into timeout after {self.timeout_for_pvwait} seconds"
)
# Set current fixed to 3 mA (max)
ch_hv_i.put(3)
ch_hv_v.put(hv)
ch_grid_i.put(3)
ch_grid_v.put(grid)

View File

@@ -0,0 +1,365 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Literal
import numpy as np
from ophyd import Component as Cpt
from ophyd import Device
from ophyd import DynamicDeviceComponent as Dcpt
from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Kind
from ophyd.status import DeviceStatus, SubscriptionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from typeguard import typechecked
from debye_bec.devices.ionization_chambers.ionization_chamber_enums import (
AmplifierEnable,
AmplifierFilter,
AmplifierGain,
)
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
class EpicsSignalSplit(EpicsSignal):
"""Wrapper around EpicsSignal with different read and write pv"""
def __init__(self, prefix, **kwargs):
super().__init__(prefix + "-RB", write_pv=prefix + "Set", **kwargs)
class GasMixSetupControl(Device):
"""GasMixSetup Control for Inonization Chamber 0"""
gas1_req = Cpt(EpicsSignalWithRBV, suffix="Gas1Req", kind="config", doc="Gas 1 requirement")
conc1_req = Cpt(
EpicsSignalWithRBV, suffix="Conc1Req", kind="config", doc="Concentration 1 requirement"
)
gas2_req = Cpt(EpicsSignalWithRBV, suffix="Gas2Req", kind="config", doc="Gas 2 requirement")
conc2_req = Cpt(
EpicsSignalWithRBV, suffix="Conc2Req", kind="config", doc="Concentration 2 requirement"
)
press_req = Cpt(
EpicsSignalWithRBV, suffix="PressReq", kind="config", doc="Pressure requirement"
)
fill = Cpt(EpicsSignal, suffix="Fill", kind="config", doc="Fill the chamber")
status = Cpt(EpicsSignalRO, suffix="Status", kind="config", doc="Status")
gas1 = Cpt(EpicsSignalRO, suffix="Gas1", kind="config", doc="Gas 1")
conc1 = Cpt(EpicsSignalRO, suffix="Conc1", kind="config", doc="Concentration 1")
gas2 = Cpt(EpicsSignalRO, suffix="Gas2", kind="config", doc="Gas 2")
conc2 = Cpt(EpicsSignalRO, suffix="Conc2", kind="config", doc="Concentration 2")
press = Cpt(EpicsSignalRO, suffix="PressTransm", kind="config", doc="Current Pressure")
class HighVoltageSuppliesControl(Device):
"""HighVoltage Supplies Control for Ionization Chamber 0"""
hv_v = Cpt(EpicsSignalSplit, suffix="HV2-V", kind="config", doc="HV voltage")
hv_i = Cpt(EpicsSignalSplit, suffix="HV2-I", kind="config", doc="HV current")
grid_v = Cpt(EpicsSignalSplit, suffix="HV1-V", kind="config", doc="Grid voltage")
grid_i = Cpt(EpicsSignalSplit, suffix="HV1-I", kind="config", doc="Grid current")
class IonizationChamber0(PSIDeviceBase):
"""Ionization Chamber 0, prefix should be 'X01DA-'."""
USER_ACCESS = ["set_gain", "set_filter", "set_hv", "set_grid", "fill"]
num = 1
amp_signals = {
"cOnOff": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}"},
),
"cGain_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}"},
),
"cFilter_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
}
amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES1-IC{num-1}:")
hv_en_signals = {
"ext_ena": (
EpicsSignalRO,
"ES1-IC0:HV-Ext-Ena",
{"kind": "config", "doc": "External enable signal of HV"},
),
"ena": (EpicsSignal, "ES1-IC0:HV-Ena", {"kind": "config", "doc": "Enable signal of HV"}),
}
hv_en = Dcpt(hv_en_signals)
def __init__(self, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
self.timeout_for_pvwait = 2.5
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
@typechecked
def set_gain(self, gain: Literal["1e6", "1e7", "5e7", "1e8", "1e9"] | AmplifierGain) -> None:
"""Configure the gain setting of the specified channel
Args:
gain (Literal['1e6', '1e7', '5e7', '1e8', '1e9']) : Desired gain
"""
if self.amp.cOnOff.get() == AmplifierEnable.OFF:
self.amp.cOnOff.put(AmplifierEnable.ON)
# Wait until channel is switched on
def _wait_enabled():
return self.amp.cOnOff.get() == AmplifierEnable.ON
if not self.wait_for_condition(
_wait_enabled, check_stopped=True, timeout=self.timeout_for_pvwait
):
raise TimeoutError(
f"Enabling channel run into timeout after {self.timeout_for_pvwait} seconds"
)
match gain:
case "1e6":
self.amp.cGain_ENUM.put(AmplifierGain.G1E6)
case "1e7":
self.amp.cGain_ENUM.put(AmplifierGain.G1E7)
case "5e7":
self.amp.cGain_ENUM.put(AmplifierGain.G5E7)
case "1e8":
self.amp.cGain_ENUM.put(AmplifierGain.G1E8)
case "1e9":
self.amp.cGain_ENUM.put(AmplifierGain.G1E9)
def set_filter(
self,
value: (
Literal["1us", "3us", "10us", "30us", "100us", "300us", "1ms", "3ms"] | AmplifierFilter
),
) -> None:
"""Configure the filter setting of the specified channel
Args:
value (Literal['1us', '3us', '10us', '30us', '100us', '300us', '1ms', '3ms']) : Desired filter
"""
if self.amp.cOnOff.get() == AmplifierEnable.OFF:
self.amp.cOnOff.put(AmplifierEnable.ON)
# Wait until channel is switched on
def _wait_enabled():
return self.amp.cOnOff.get() == AmplifierEnable.ON
if not self.wait_for_condition(
_wait_enabled, check_stopped=True, timeout=self.timeout_for_pvwait
):
raise TimeoutError(
f"Enabling channel run into timeout after {self.timeout_for_pvwait} seconds"
)
match value:
case "1us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F1US)
case "3us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F3US)
case "10us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F10US)
case "30us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F30US)
case "100us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F100US)
case "300us":
self.amp.cFilter_ENUM.put(AmplifierFilter.F300US)
case "1ms":
self.amp.cFilter_ENUM.put(AmplifierFilter.F1MS)
case "3ms":
self.amp.cFilter_ENUM.put(AmplifierFilter.F3MS)
@typechecked
def set_hv(self, hv: float) -> None:
"""Configure the high voltage settings , this will
enable the high voltage (if external enable is active)!
Args:
hv (float) : Desired voltage for the 'HV' terminal. Voltage has to be between 0...3000
"""
if not (0 <= hv <= 3000):
raise ValueError(f"specified HV {hv} not within range [0 .. 3000]")
if not np.isclose(np.abs(hv - self.hv.grid_v.get()), 0, atol=3):
raise ValueError(f"Grid {self.hv.grid_v.get()} must not be higher than HV {hv}!")
if not self.hv_en.ena.get() == 1:
def check_ch_ena(*, old_value, value, **kwargs):
return value == 1
status = SubscriptionStatus(device=self.hv_en.ena, callback=check_ch_ena)
self.hv_en.ena.put(1)
# Wait after setting ena to 1
status.wait(timeout=2)
# Set current fixed to 3 mA (max)
self.hv.hv_i.put(3)
self.hv.hv_v.put(hv)
@typechecked
def set_grid(self, grid: float) -> None:
"""Configure the high voltage settings , this will
enable the high voltage (if external enable is active)!
Args:
grid (float) : Desired voltage for the 'Grid' terminal, Grid Voltage has to be between 0...3000
"""
if not (0 <= grid <= 3000):
raise ValueError(f"specified Grid {grid} not within range [0 .. 3000]")
if not np.isclose(np.abs(grid - self.hv.hv_v.get()), 0, atol=3):
raise ValueError(f"Grid {grid} must not be higher than HV {self.hv.hv_v.get()}!")
if not self.hv_en.ena.get() == 1:
def check_ch_ena(*, old_value, value, **kwargs):
return value == 1
status = SubscriptionStatus(device=self.hv_en.ena, callback=check_ch_ena)
self.hv_en.ena.put(1)
# Wait after setting ena to 1
status.wait(timeout=2)
# Set current fixed to 3 mA (max)
self.hv.grid_i.put(3)
self.hv.grid_v.put(grid)
@typechecked
def fill(
self,
gas1: Literal["He", "N2", "Ar", "Kr"],
conc1: float,
gas2: Literal["He", "N2", "Ar", "Kr"],
conc2: float,
pressure: float,
*,
wait: bool = False,
) -> DeviceStatus:
"""Fill an ionization chamber with the specified gas mixture.
Args:
gas1 (Literal['He', 'N2', 'Ar', 'Kr']) : Gas 1 requirement,
conc1 (float) : Concentration 1 requirement in %,
gas2 (Literal['He', 'N2', 'Ar', 'Kr']) : Gas 2 requirement,
conc2 (float) : Concentration 2 requirement in %,
pressure (float) : Required pressure in bar abs,
wait (bool): If you like to wait for the filling to finish.
"""
if not (0 <= conc1 <= 100):
raise ValueError(f"Concentration 1 {conc1} out of range [0 .. 100 %]")
if not (0 <= conc2 <= 100):
raise ValueError(f"Concentration 2 {conc2} out of range [0 .. 100 %]")
if not np.isclose((conc1 + conc2), 100, atol=0.1):
raise ValueError(f"Conc1 {conc1} and conc2 {conc2} must sum to 100 +- 0.1")
if not (0 <= pressure <= 3):
raise ValueError(f"Pressure {pressure} out of range [0 .. 3 bar abs]")
self.gmes.gas1_req.set(gas1).wait(timeout=3)
self.gmes.conc1_req.set(conc1).wait(timeout=3)
self.gmes.gas2_req.set(gas2).wait(timeout=3)
self.gmes.conc2_req.set(conc2).wait(timeout=3)
self.gmes.fill.put(1)
def wait_for_status():
return self.gmes.status.get() == 0
timeout = 3
if not self.wait_for_condition(wait_for_status, timeout=timeout, check_stopped=True):
raise TimeoutError(
f"Ionization chamber filling process did not start after {timeout}s. Last log message {self.gmes_status.get()}"
)
def wait_for_filling_finished():
return self.gmes.status.get() == 1
# Wait until ionization chamber is filled successfully
status = self.task_handler.submit_task(
task=self.wait_for_condition, task_args=(wait_for_filling_finished, 360, True)
)
if wait:
status.wait()
return status
class IonizationChamber1(IonizationChamber0):
"""Ionization Chamber 1, prefix should be 'X01DA-'."""
num = 2
amp_signals = {
"cOnOff": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}"},
),
"cGain_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}"},
),
"cFilter_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
}
amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:")
hv_en_signals = {
"ext_ena": (
EpicsSignalRO,
"ES2-IC12:HV-Ext-Ena",
{"kind": "config", "doc": "External enable signal of HV"},
),
"ena": (EpicsSignal, "ES2-IC12:HV-Ena", {"kind": "config", "doc": "Enable signal of HV"}),
}
hv_en = Dcpt(hv_en_signals)
class IonizationChamber2(IonizationChamber0):
"""Ionization Chamber 2, prefix should be 'X01DA-'."""
num = 3
amp_signals = {
"cOnOff": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}"},
),
"cGain_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}"},
),
"cFilter_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
}
amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:")
hv_en_signals = {
"ext_ena": (
EpicsSignalRO,
"ES2-IC12:HV-Ext-Ena",
{"kind": "config", "doc": "External enable signal of HV"},
),
"ena": (EpicsSignal, "ES2-IC12:HV-Ena", {"kind": "config", "doc": "Enable signal of HV"}),
}
hv_en = Dcpt(hv_en_signals)

View File

@@ -0,0 +1,32 @@
import enum
class AmplifierEnable(int, enum.Enum):
"""Enum class for the enable signal of the channel"""
OFF = 0
STARTUP = 1
ON = 2
class AmplifierGain(int, enum.Enum):
"""Enum class for the gain of the channel"""
G1E6 = 0
G1E7 = 1
G5E7 = 2
G1E8 = 3
G1E9 = 4
class AmplifierFilter(int, enum.Enum):
"""Enum class for the filter of the channel"""
F1US = 0
F3US = 1
F10US = 2
F30US = 3
F100US = 4
F300US = 5
F1MS = 6
F3MS = 7

File diff suppressed because it is too large Load Diff

View File

View File

@@ -0,0 +1,491 @@
"""Module for the Mo1 Bragg positioner of the Debye beamline.
The softIOC is reachable via the EPICS prefix X01DA-OP-MO1:BRAGG: and connected
to a motor controller via web sockets. The Mo1 Bragg positioner is not only a
positioner, but also a scan controller to setup XAS and XRD scans. A few scan modes
are programmed in the controller, e.g. simple and advanced XAS scans + XRD triggering mode.
Note: For some of the Epics PVs, in particular action buttons, the put_complete=True is
used to ensure that the action is executed completely. This is believed
to allow for a more stable execution of the action."""
import time
from typing import Any, Literal
from bec_lib.devicemanager import ScanInfo
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import DeviceStatus, Signal, StatusBase
from ophyd.status import SubscriptionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from ophyd_devices.utils.errors import DeviceStopError
from pydantic import BaseModel, Field
from typeguard import typechecked
from debye_bec.devices.mo1_bragg.mo1_bragg_devices import Mo1BraggPositioner
# pylint: disable=unused-import
from debye_bec.devices.mo1_bragg.mo1_bragg_enums import (
MoveType,
ScanControlLoadMessage,
ScanControlMode,
ScanControlScanStatus,
TriggerControlMode,
TriggerControlSource,
)
from debye_bec.devices.mo1_bragg.mo1_bragg_utils import compute_spline
# Initialise logger
logger = bec_logger.logger
########### Exceptions ###########
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")
xrd_enable_low: bool | None = Field(
None, description="XRD enabled for low, should be PV trig_ena_lo_enum"
) # trig_enable_low: bool = None
xrd_enable_high: bool | None = Field(
None, description="XRD enabled for high, should be PV trig_ena_hi_enum"
) # trig_enable_high: bool = None
exp_time_low: float | None = Field(None, description="Exposure time low energy/angle")
exp_time_high: float | None = Field(None, description="Exposure 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")
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 ###########
class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner):
"""Mo1 Bragg motor for the Debye beamline.
The prefix to connect to the soft IOC is X01DA-OP-MO1:BRAGG:
"""
USER_ACCESS = ["set_advanced_xas_settings"]
def __init__(self, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs): # type: ignore
"""
Initialize the PSI Device Base class.
Args:
name (str) : Name of the device
scan_info (ScanInfo): The scan info to use.
"""
super().__init__(name=name, scan_info=scan_info, prefix=prefix, **kwargs)
self.scan_parameter = ScanParameter()
self.timeout_for_pvwait = 2.5
########################################
# Beamline Specific Implementations #
########################################
def on_init(self) -> None:
"""
Called when the device is initialized.
No signals are connected at this point. If you like to
set default values on signals, please use on_connected instead.
"""
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
self.scan_control.scan_progress.subscribe(self._progress_update, run=False)
def on_stage(self) -> DeviceStatus | StatusBase | None:
"""
Called while staging the device.
Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object.
"""
self._check_scan_msg(ScanControlLoadMessage.PENDING)
scan_name = self.scan_info.msg.scan_name
self._update_scan_parameter()
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,
)
self.set_trig_settings(
enable_low=False,
enable_high=False,
exp_time_low=0,
exp_time_high=0,
cycle_low=0,
cycle_high=0,
)
self.set_scan_control_settings(
mode=ScanControlMode.SIMPLE, scan_duration=self.scan_parameter.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,
)
self.set_trig_settings(
enable_low=self.scan_parameter.xrd_enable_low, # enable_low=self.scan_parameter.trig_enable_low,
enable_high=self.scan_parameter.xrd_enable_high, # enable_high=self.scan_parameter.trig_enable_high,
exp_time_low=self.scan_parameter.exp_time_low,
exp_time_high=self.scan_parameter.exp_time_high,
cycle_low=self.scan_parameter.cycle_low,
cycle_high=self.scan_parameter.cycle_high,
)
self.set_scan_control_settings(
mode=ScanControlMode.SIMPLE, scan_duration=self.scan_parameter.scan_duration
)
elif scan_name == "xas_advanced_scan":
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,
)
self.set_trig_settings(
enable_low=False,
enable_high=False,
exp_time_low=0,
exp_time_high=0,
cycle_low=0,
cycle_high=0,
)
self.set_scan_control_settings(
mode=ScanControlMode.ADVANCED, scan_duration=self.scan_parameter.scan_duration
)
elif scan_name == "xas_advanced_scan_with_xrd":
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,
)
self.set_trig_settings(
enable_low=self.scan_parameter.xrd_enable_low, # enable_low=self.scan_parameter.trig_enable_low,
enable_high=self.scan_parameter.xrd_enable_high, # enable_high=self.scan_parameter.trig_enable_high,
exp_time_low=self.scan_parameter.exp_time_low,
exp_time_high=self.scan_parameter.exp_time_high,
cycle_low=self.scan_parameter.cycle_low,
cycle_high=self.scan_parameter.cycle_high,
)
self.set_scan_control_settings(
mode=ScanControlMode.ADVANCED, scan_duration=self.scan_parameter.scan_duration
)
else:
return
# Load the scan parameters to the controller
self.scan_control.scan_load.put(1)
# Wait for params to be checked from controller
self.wait_for_signal(
self.scan_control.scan_msg,
ScanControlLoadMessage.SUCCESS,
timeout=self.timeout_for_pvwait,
)
return None
def on_unstage(self) -> DeviceStatus | StatusBase | None:
"""Called while unstaging the device."""
if self.stopped is True:
logger.warning(f"Resetting stopped in unstage for device {self.name}.")
self._stopped = False
current_state = self.scan_control.scan_msg.get()
# Case 1, message is already ScanControlLoadMessage.PENDING
if current_state == ScanControlLoadMessage.PENDING:
return None
# Case 2, probably called after scan, backend should resolve on its own. Timeout to wait
if current_state in [ScanControlLoadMessage.STARTED, ScanControlLoadMessage.SUCCESS]:
try:
self.wait_for_signal(
self.scan_control.scan_msg,
ScanControlLoadMessage.PENDING,
timeout=self.timeout_for_pvwait,
)
return
except TimeoutError:
logger.warning(
f"Timeout in on_unstage of {self.name} after {self.timeout_for_pvwait}s, current scan_control_message : {self.scan_control.scan_msg.get()}"
)
def callback(*, old_value, value, **kwargs):
if value == ScanControlLoadMessage.PENDING:
return True
return False
status = SubscriptionStatus(self.scan_control.scan_msg, callback=callback)
self.scan_control.scan_val_reset.put(1)
status.wait(timeout=self.timeout_for_pvwait)
return None
def on_pre_scan(self) -> DeviceStatus | StatusBase | None:
"""Called right before the scan starts on all devices automatically."""
def on_trigger(self) -> DeviceStatus | StatusBase | None:
"""Called when the device is triggered."""
def on_complete(self) -> DeviceStatus | StatusBase | None:
"""Called to inquire if a device has completed a scans."""
def wait_for_complete():
"""Wait for the scan to complete. No timeout is set."""
start_time = time.time()
while True:
if self.stopped is True:
raise DeviceStopError(
f"Device {self.name} was stopped while waiting for scan to complete"
)
if self.scan_control.scan_done.get() == 1:
return
time.sleep(0.1)
status = self.task_handler.submit_task(wait_for_complete)
return status
def on_kickoff(self) -> DeviceStatus | StatusBase | None:
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
scan_duration = self.scan_control.scan_duration.get()
# TODO implement better logic for infinite scans, at least bring it up with Debye
start_func = (
self.scan_control.scan_start_infinite.put
if scan_duration < 0.1
else self.scan_control.scan_start_timer.put
)
def callback(*, old_value, value, **kwargs):
if old_value == ScanControlScanStatus.READY and value == ScanControlScanStatus.RUNNING:
return True
return False
status = SubscriptionStatus(self.scan_control.scan_status, callback=callback)
start_func(1)
return status
def on_stop(self) -> None:
"""Called when the device is stopped."""
self.stopped = True # Needs to be set to stop motion
######### Utility Methods #########
# FIXME this should become the ProgressSignal
# pylint: disable=unused-argument
def _progress_update(self, value, **kwargs) -> None:
"""Callback method to update the scan progress, runs a callback
to SUB_PROGRESS subscribers, i.e. BEC.
Args:
value (int) : current progress value
"""
max_value = 100
self._run_subs(
sub_type=self.SUB_PROGRESS,
value=value,
max_value=max_value,
done=bool(max_value == value),
)
def set_xas_settings(self, low: float, high: float, scan_time: float) -> None:
"""Set XAS parameters for upcoming scan.
Args:
low (float): Low energy/angle value of the scan
high (float): High energy/angle value of the scan
scan_time (float): Time for a half oscillation
"""
move_type = self.move_type.get()
if move_type == MoveType.ENERGY:
self.scan_settings.s_scan_energy_lo.put(low)
self.scan_settings.s_scan_energy_hi.put(high)
else:
self.scan_settings.s_scan_angle_lo.put(low)
self.scan_settings.s_scan_angle_hi.put(high)
self.scan_settings.s_scan_scantime.put(scan_time)
def wait_for_signal(self, signal: Cpt, value: Any, timeout: float | None = None) -> None:
"""Wait for a signal to reach a certain value."""
if timeout is None:
timeout = self.timeout_for_pvwait
start_time = time.time()
while time.time() - start_time < timeout:
if signal.get() == value:
return None
if self.stopped is True: # Should this check be optional or configurable?!
raise DeviceStopError(f"Device {self.name} was stopped while waiting for signal")
time.sleep(0.1)
# If we end up here, the status did not resolve
raise TimeoutError(
f"Device {self.name} run into timeout after {timeout}s for signal {signal.name} with value {signal.get()}, expected {value}"
)
@typechecked
def convert_angle_energy(
self, mode: Literal["AngleToEnergy", "EnergyToAngle"], inp: float
) -> float:
"""Calculate energy to angle or vice versa
Args:
mode (Literal["AngleToEnergy", "EnergyToAngle"]): Mode of calculation
input (float): Either angle or energy
Returns:
output (float): Converted angle or energy
"""
self.calculator.calc_reset.put(0)
self.calculator.calc_reset.put(1)
self.wait_for_signal(self.calculator.calc_done, 0)
if mode == "AngleToEnergy":
self.calculator.calc_angle.put(inp)
elif mode == "EnergyToAngle":
self.calculator.calc_energy.put(inp)
self.wait_for_signal(self.calculator.calc_done, 1)
time.sleep(0.25) # 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()
def set_advanced_xas_settings(
self, low: float, high: float, scan_time: float, p_kink: float, e_kink: float
) -> None:
"""Set Advanced XAS parameters for upcoming scan.
Args:
low (float): Low angle value of the scan in eV
high (float): High angle value of the scan in eV
scan_time (float): Time for a half oscillation in s
p_kink (float): Position of kink in %
e_kink (float): Energy of kink in eV
"""
# TODO Add fallback solution for automatic testing, otherwise test will fail
# because no monochromator will calculate the angle
# Unsure how to implement this
move_type = self.move_type.get()
if move_type == MoveType.ENERGY:
e_kink_deg = self.convert_angle_energy(mode="EnergyToAngle", inp=e_kink)
# Angle and Energy are inverse proportional!
high_deg = self.convert_angle_energy(mode="EnergyToAngle", inp=low)
low_deg = self.convert_angle_energy(mode="EnergyToAngle", inp=high)
else:
raise Mo1BraggError("MoveType Angle not implemented for advanced scans, use Energy")
pos, vel, dt = compute_spline(
low_deg=low_deg,
high_deg=high_deg,
p_kink=p_kink,
e_kink_deg=e_kink_deg,
scan_time=scan_time,
)
self.scan_settings.a_scan_pos.set(pos)
self.scan_settings.a_scan_vel.set(vel)
self.scan_settings.a_scan_time.set(dt)
def set_trig_settings(
self,
enable_low: bool,
enable_high: bool,
exp_time_low: int,
exp_time_high: int,
cycle_low: int,
cycle_high: int,
) -> None:
"""Set TRIG settings for the upcoming scan.
Args:
enable_low (bool): Enable TRIG for low energy/angle
enable_high (bool): Enable TRIG for high energy/angle
num_trigger_low (int): Number of triggers for low energy/angle
num_trigger_high (int): Number of triggers for high energy/angle
exp_time_low (int): Exposure time for low energy/angle
exp_time_high (int): Exposure time for high energy/angle
cycle_low (int): Cycle for low energy/angle
cycle_high (int): Cycle for high energy/angle
"""
self.scan_settings.trig_ena_hi_enum.put(int(enable_high))
self.scan_settings.trig_ena_lo_enum.put(int(enable_low))
self.scan_settings.trig_time_hi.put(exp_time_high)
self.scan_settings.trig_time_lo.put(exp_time_low)
self.scan_settings.trig_every_n_hi.put(cycle_high)
self.scan_settings.trig_every_n_lo.put(cycle_low)
def set_scan_control_settings(self, mode: ScanControlMode, scan_duration: float) -> None:
"""Set the scan control settings for the upcoming scan.
Args:
mode (ScanControlMode): Mode for the scan, either simple or advanced
scan_duration (float): Duration of the scan
"""
val = ScanControlMode(mode).value
self.scan_control.scan_mode_enum.put(val)
self.scan_control.scan_duration.put(scan_duration)
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)
def _check_scan_msg(self, target_state: ScanControlLoadMessage) -> None:
"""Check if the scan message is gettting available
Args:
target_state (ScanControlLoadMessage): Target state to check for
Raises:
TimeoutError: If the scan message is not available after the timeout
"""
try:
self.wait_for_signal(self.scan_control.scan_msg, target_state, timeout=1)
except TimeoutError as exc:
logger.warning(
f"Resetting scan validation in stage for state: {ScanControlLoadMessage(self.scan_control.scan_msg.get())}, "
f"retry .get() on scan_control: {ScanControlLoadMessage(self.scan_control.scan_msg.get())} and sleeping 1s"
)
current_scan_msg = self.scan_control.scan_msg.get()
def callback(*, old_value, value, **kwargs):
if old_value == current_scan_msg and value == target_state:
return True
return False
status = SubscriptionStatus(self.scan_control.scan_msg, callback=callback)
self.scan_control.scan_val_reset.put(1)
status.wait(timeout=self.timeout_for_pvwait)
# try:
# self.wait_for_signal(self.scan_control.scan_msg, target_state, timeout=4)
# except TimeoutError as exc:
# raise TimeoutError(
# f"Timeout after {self.timeout_for_pvwait} while waiting for scan status,"
# f" current state: {ScanControlScanStatus(self.scan_control.scan_msg.get())}"
# ) from exc

View File

@@ -0,0 +1,436 @@
"""Module for the Mo1 Bragg positioner"""
import threading
import time
import traceback
from typing import Literal
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import (
Device,
DeviceStatus,
EpicsSignal,
EpicsSignalRO,
EpicsSignalWithRBV,
PositionerBase,
Signal,
)
from ophyd.utils import LimitError
from debye_bec.devices.mo1_bragg.mo1_bragg_enums import MoveType
# Initialise logger
logger = bec_logger.logger
############# Exceptions #############
class Mo1BraggStoppedError(Exception):
"""Exception to raise when the Bragg positioner is stopped."""
############# Signal classes #############
class MoveTypeSignal(Signal):
"""Custom Signal to set the move type of the Bragg positioner"""
# pylint: disable=arguments-differ
def set(self, value: str | MoveType) -> None:
"""Returns currently active move method
Args:
value (str | MoveType) : Can be either 'energy' or 'angle'
"""
value = MoveType(value.lower())
self._readback = value.value
############# Utility devices to separate the namespace #############
class Mo1BraggStatus(Device):
"""Mo1 Bragg PVs for status monitoring"""
error_status = Cpt(EpicsSignalRO, suffix="error_status_RBV", kind="config", auto_monitor=True)
brake_enabled = Cpt(EpicsSignalRO, suffix="brake_enabled_RBV", kind="config", auto_monitor=True)
mot_commutated = Cpt(
EpicsSignalRO, suffix="mot_commutated_RBV", kind="config", auto_monitor=True
)
axis_enabled = Cpt(EpicsSignalRO, suffix="axis_enabled_RBV", kind="config", auto_monitor=True)
enc_initialized = Cpt(
EpicsSignalRO, suffix="enc_initialized_RBV", kind="config", auto_monitor=True
)
heartbeat = Cpt(EpicsSignalRO, suffix="heartbeat_RBV", kind="config", auto_monitor=True)
class Mo1BraggEncoder(Device):
"""Mo1 Bragg PVs to communicate with the encoder"""
enc_reinit = Cpt(EpicsSignal, suffix="enc_reinit", kind="config")
enc_reinit_done = Cpt(EpicsSignalRO, suffix="enc_reinit_done_RBV", kind="config")
class Mo1BraggCrystal(Device):
"""Mo1 Bragg PVs to set the crystal parameters"""
offset_si111 = Cpt(EpicsSignalWithRBV, suffix="offset_si111", kind="config")
offset_si311 = Cpt(EpicsSignalWithRBV, suffix="offset_si311", kind="config")
xtal_enum = Cpt(EpicsSignalWithRBV, suffix="xtal_ENUM", kind="config")
d_spacing_si111 = Cpt(EpicsSignalWithRBV, suffix="d_spacing_si111", kind="config")
d_spacing_si311 = Cpt(EpicsSignalWithRBV, suffix="d_spacing_si311", kind="config")
set_offset = Cpt(EpicsSignal, suffix="set_offset", kind="config", put_complete=True)
current_xtal = Cpt(
EpicsSignalRO, suffix="current_xtal_ENUM_RBV", kind="normal", auto_monitor=True
)
class Mo1BraggScanSettings(Device):
"""Mo1 Bragg PVs to set the scan setttings"""
# TRIG settings
trig_select_ref_enum = Cpt(EpicsSignalWithRBV, suffix="trig_select_ref_ENUM", kind="config")
trig_ena_hi_enum = Cpt(EpicsSignalWithRBV, suffix="trig_ena_hi_ENUM", kind="config")
trig_time_hi = Cpt(EpicsSignalWithRBV, suffix="trig_time_hi", kind="config")
trig_every_n_hi = Cpt(EpicsSignalWithRBV, suffix="trig_every_n_hi", kind="config")
trig_ena_lo_enum = Cpt(EpicsSignalWithRBV, suffix="trig_ena_lo_ENUM", kind="config")
trig_time_lo = Cpt(EpicsSignalWithRBV, suffix="trig_time_lo", kind="config")
trig_every_n_lo = Cpt(EpicsSignalWithRBV, suffix="trig_every_n_lo", kind="config")
# XAS simple scan settings
s_scan_angle_hi = Cpt(EpicsSignalWithRBV, suffix="s_scan_angle_hi", kind="config")
s_scan_angle_lo = Cpt(EpicsSignalWithRBV, suffix="s_scan_angle_lo", kind="config")
s_scan_energy_lo = Cpt(
EpicsSignalWithRBV, suffix="s_scan_energy_lo", kind="config", auto_monitor=True
)
s_scan_energy_hi = Cpt(
EpicsSignalWithRBV, suffix="s_scan_energy_hi", kind="config", auto_monitor=True
)
s_scan_scantime = Cpt(
EpicsSignalWithRBV, suffix="s_scan_scantime", kind="config", auto_monitor=True
)
# XAS advanced scan settings
a_scan_pos = Cpt(EpicsSignalWithRBV, suffix="a_scan_pos", kind="config", auto_monitor=True)
a_scan_vel = Cpt(EpicsSignalWithRBV, suffix="a_scan_vel", kind="config", auto_monitor=True)
a_scan_time = Cpt(EpicsSignalWithRBV, suffix="a_scan_time", kind="config", auto_monitor=True)
class Mo1TriggerSettings(Device):
"""Mo1 Trigger settings"""
settle_time = Cpt(EpicsSignalWithRBV, suffix="settle_time", kind="config")
max_dev = Cpt(EpicsSignalWithRBV, suffix="max_dev", kind="config")
xrd_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_src_ENUM", kind="config")
xrd_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_mode_ENUM", kind="config")
xrd_trig_len = Cpt(EpicsSignalWithRBV, suffix="xrd_trig_len", kind="config")
xrd_trig_req = Cpt(EpicsSignal, suffix="xrd_trig_req", kind="config")
falcon_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_src_ENUM", kind="config")
falcon_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_mode_ENUM", kind="config")
falcon_trig_len = Cpt(EpicsSignalWithRBV, suffix="falcon_trig_len", kind="config")
falcon_trig_req = Cpt(EpicsSignal, suffix="falcon_trig_req", kind="config")
univ1_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_src_ENUM", kind="config")
univ1_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_mode_ENUM", kind="config")
univ1_trig_len = Cpt(EpicsSignalWithRBV, suffix="univ1_trig_len", kind="config")
univ1_trig_req = Cpt(EpicsSignal, suffix="univ1_trig_req", kind="config")
univ2_trig_src_enum = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_src_ENUM", kind="config")
univ2_trig_mode_enum = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_mode_ENUM", kind="config")
univ2_trig_len = Cpt(EpicsSignalWithRBV, suffix="univ2_trig_len", kind="config")
univ2_trig_req = Cpt(EpicsSignal, suffix="univ2_trig_req", kind="config")
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_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")
class Mo1BraggScanControl(Device):
"""Mo1 Bragg PVs to control the scan after setting the parameters."""
scan_mode_enum = Cpt(EpicsSignalWithRBV, suffix="scan_mode_ENUM", kind="config")
scan_duration = Cpt(
EpicsSignalWithRBV, suffix="scan_duration", kind="config", auto_monitor=True
)
scan_load = Cpt(EpicsSignal, suffix="scan_load", kind="config", put_complete=True)
scan_msg = Cpt(EpicsSignalRO, suffix="scan_msg_ENUM_RBV", kind="config", auto_monitor=True)
scan_start_infinite = Cpt(
EpicsSignal, suffix="scan_start_infinite", kind="config", put_complete=True
)
scan_start_timer = Cpt(EpicsSignal, suffix="scan_start_timer", kind="config", put_complete=True)
scan_stop = Cpt(EpicsSignal, suffix="scan_stop", kind="config", put_complete=True)
scan_status = Cpt(
EpicsSignalRO, suffix="scan_status_ENUM_RBV", kind="config", auto_monitor=True
)
scan_time_left = Cpt(
EpicsSignalRO, suffix="scan_time_left_RBV", kind="config", auto_monitor=True
)
scan_done = Cpt(EpicsSignalRO, suffix="scan_done_RBV", kind="config", auto_monitor=True)
scan_val_reset = Cpt(EpicsSignal, suffix="scan_val_reset", kind="config", put_complete=True)
scan_progress = Cpt(EpicsSignalRO, suffix="scan_progress_RBV", kind="config", auto_monitor=True)
scan_spectra_done = Cpt(
EpicsSignalRO, suffix="scan_n_osc_RBV", kind="config", auto_monitor=True
)
scan_spectra_left = Cpt(
EpicsSignalRO, suffix="scan_n_osc_left_RBV", kind="config", auto_monitor=True
)
class Mo1BraggPositioner(Device, PositionerBase):
"""
Positioner implementation of the MO1 Bragg positioner.
The prefix to connect to the soft IOC is X01DA-OP-MO1:BRAGG:
This soft IOC connects to the NI motor and its control loop.
"""
USER_ACCESS = ["set_advanced_xas_settings"]
####### Sub-components ########
# Namespace is cleaner and easier to maintain
crystal = Cpt(Mo1BraggCrystal, "")
encoder = Cpt(Mo1BraggEncoder, "")
scan_settings = Cpt(Mo1BraggScanSettings, "")
trigger_settings = Cpt(Mo1TriggerSettings, "")
calculator = Cpt(Mo1BraggCalculator, "")
scan_control = Cpt(Mo1BraggScanControl, "")
status = Cpt(Mo1BraggStatus, "")
############# switch between energy and angle #############
# TODO should be removed/replaced once decision about pseudo motor is made
move_type = Cpt(MoveTypeSignal, value=MoveType.ENERGY, kind="config")
############# Energy PVs #############
readback = Cpt(
EpicsSignalRO, suffix="feedback_pos_energy_RBV", kind="hinted", auto_monitor=True
)
setpoint = Cpt(
EpicsSignalWithRBV, suffix="set_abs_pos_energy", kind="normal", auto_monitor=True
)
motor_is_moving = Cpt(
EpicsSignalRO, suffix="move_abs_done_RBV", kind="normal", auto_monitor=True
)
low_lim = Cpt(EpicsSignalRO, suffix="lo_lim_pos_energy_RBV", kind="config", auto_monitor=True)
high_lim = Cpt(EpicsSignalRO, suffix="hi_lim_pos_energy_RBV", kind="config", auto_monitor=True)
velocity = Cpt(EpicsSignalWithRBV, suffix="move_velocity", kind="config", auto_monitor=True)
########### Angle PVs #############
# TODO Pseudo motor for angle?
feedback_pos_angle = Cpt(
EpicsSignalRO, suffix="feedback_pos_angle_RBV", kind="normal", auto_monitor=True
)
setpoint_abs_angle = Cpt(
EpicsSignalWithRBV, suffix="set_abs_pos_angle", kind="normal", auto_monitor=True
)
low_limit_angle = Cpt(
EpicsSignalRO, suffix="lo_lim_pos_angle_RBV", kind="config", auto_monitor=True
)
high_limit_angle = Cpt(
EpicsSignalRO, suffix="hi_lim_pos_angle_RBV", kind="config", auto_monitor=True
)
########## Move Command PVs ##########
move_abs = Cpt(EpicsSignal, suffix="move_abs", kind="config", put_complete=True)
move_stop = Cpt(EpicsSignal, suffix="move_stop", kind="config", put_complete=True)
SUB_READBACK = "readback"
_default_sub = SUB_READBACK
SUB_PROGRESS = "progress"
def __init__(self, prefix="", *, name: str, **kwargs):
"""Initialize the Mo1 Bragg positioner.
Args:
prefix (str): EPICS prefix for the device
name (str): Name of the device
kwargs: Additional keyword arguments
"""
super().__init__(prefix, name=name, **kwargs)
self._move_thread = None
self._stopped = False
self.readback.name = self.name
def stop(self, *, success=False) -> None:
"""Stop any motion on the positioner
Args:
success (bool) : Flag to indicate if the motion was successful
"""
self.move_stop.put(1)
if self._move_thread is not None:
self._move_thread.join()
self._move_thread = None
super().stop(success=success)
def stop_scan(self) -> None:
"""Stop the currently running scan gracefully, this finishes the running oscillation."""
self.scan_control.scan_stop.put(1)
@property
def stopped(self) -> bool:
"""Return the status of the positioner"""
return self._stopped
######### Positioner specific methods #########
@property
def limits(self) -> tuple:
"""Return limits of the Bragg positioner"""
if self.move_type.get() == MoveType.ENERGY:
return (self.low_lim.get(), self.high_lim.get())
return (self.low_limit_angle.get(), self.high_limit_angle.get())
@property
def low_limit(self) -> float:
"""Return low limit of axis"""
return self.limits[0]
@property
def high_limit(self) -> float:
"""Return high limit of axis"""
return self.limits[1]
@property
def egu(self) -> str:
"""Return the engineering units of the positioner"""
if self.move_type.get() == MoveType.ENERGY:
return "eV"
return "deg"
@property
def position(self) -> float:
"""Return the current position of Mo1Bragg, considering the move type"""
move_type = self.move_type.get()
move_cpt = self.readback if move_type == MoveType.ENERGY else self.feedback_pos_angle
return move_cpt.get()
# pylint: disable=arguments-differ
def check_value(self, value: float) -> None:
"""Method to check if a value is within limits of the positioner.
Called by PositionerBase.move()
Args:
value (float) : value to move axis to.
"""
low_limit, high_limit = self.limits
if low_limit < high_limit and not low_limit <= value <= high_limit:
raise LimitError(f"position={value} not within limits {self.limits}")
def _move_and_finish(
self, target_pos: float, move_cpt: Cpt, status: DeviceStatus, update_frequency: float = 0.1
) -> None:
"""
Method to be called in the move thread to move the Bragg positioner
to the target position.
Args:
target_pos (float) : target position for the motion
move_cpt (Cpt) : component to set the target position on the IOC,
either setpoint or setpoint_abs_angle depending
on the move type
read_cpt (Cpt) : component to read the current position of the motion,
readback or feedback_pos_angle
status (DeviceStatus) : status object to set the status of the motion
update_frequency (float): Optional, frequency to update the current position of
the motion, defaults to 0.1s
"""
try:
# Set the target position on IOC
move_cpt.put(target_pos)
self.move_abs.put(1)
# Currently sleep is needed due to delay in updates on PVs, maybe time can be reduced
time.sleep(0.5)
while self.motor_is_moving.get() == 0:
if self.stopped:
raise Mo1BraggStoppedError(f"Device {self.name} was stopped")
time.sleep(update_frequency)
# pylint: disable=protected-access
status.set_finished()
# pylint: disable=broad-except
except Exception as exc:
content = traceback.format_exc()
logger.error(f"Error in move thread of device {self.name}: {content}")
status.set_exception(exc=exc)
def move(self, value: float, move_type: str | MoveType = None, **kwargs) -> DeviceStatus:
"""
Move the Bragg positioner to the specified value, allows to
switch between move types angle and energy.
Args:
value (float) : target value for the motion
move_type (str | MoveType) : Optional, specify the type of move,
either 'energy' or 'angle'
Returns:
DeviceStatus : status object to track the motion
"""
self._stopped = False
if move_type is not None:
self.move_type.put(move_type)
move_type = self.move_type.get()
move_cpt = self.setpoint if move_type == MoveType.ENERGY else self.setpoint_abs_angle
self.check_value(value)
status = DeviceStatus(device=self)
self._move_thread = threading.Thread(
target=self._move_and_finish, args=(value, move_cpt, status, 0.1)
)
self._move_thread.start()
return status
# -------------- End of Positioner specific methods -----------------#
# -------------- MO1 Bragg specific methods -----------------#
def set_xtal(
self,
xtal_enum: Literal["111", "311"],
offset_si111: float = None,
offset_si311: float = None,
d_spacing_si111: float = None,
d_spacing_si311: float = None,
) -> None:
"""Method to set the crystal parameters of the Bragg positioner
Args:
xtal_enum (Literal["111", "311"]) : Enum to set the crystal orientation
offset_si111 (float) : Offset for the 111 crystal
offset_si311 (float) : Offset for the 311 crystal
d_spacing_si111 (float) : d-spacing for the 111 crystal
d_spacing_si311 (float) : d-spacing for the 311 crystal
"""
if offset_si111 is not None:
self.crystal.offset_si111.put(offset_si111)
if offset_si311 is not None:
self.crystal.offset_si311.put(offset_si311)
if d_spacing_si111 is not None:
self.crystal.d_spacing_si111.put(d_spacing_si111)
if d_spacing_si311 is not None:
self.crystal.d_spacing_si311.put(d_spacing_si311)
if xtal_enum == "111":
crystal_set = 0
elif xtal_enum == "311":
crystal_set = 1
else:
raise ValueError(
f"Invalid argument for xtal_enum : {xtal_enum}, choose from '111' or '311'"
)
self.crystal.xtal_enum.put(crystal_set)
self.crystal.set_offset.put(1)

View File

@@ -0,0 +1,61 @@
"""Enums for the Bragg positioner and trigger generator"""
import enum
class TriggerControlSource(int, enum.Enum):
"""Enum class for the trigger control source of the trigger generator"""
EPICS = 0
INPOS = 1
class TriggerControlMode(int, enum.Enum):
"""Enum class for the trigger control mode of the trigger generator"""
PULSE = 0
CONDITION = 1
class ScanControlScanStatus(int, enum.Enum):
"""Enum class for the scan status of the Bragg positioner"""
PARAMETER_WRONG = 0
VALIDATION_PENDING = 1
READY = 2
RUNNING = 3
class ScanControlLoadMessage(int, enum.Enum):
"""Enum for validating messages for load message of the Bragg positioner"""
PENDING = 0
STARTED = 1
SUCCESS = 2
ERR_TRIG_MEAS_LEN_LOW = 3
ERR_TRIG_N_TRIGGERS_LOW = 4
ERR_TRIG_TRIGS_EVERY_N_LOW = 5
ERR_TRIG_MEAS_LEN_HI = 6
ERR_TRIG_N_TRIGGERS_HI = 7
ERR_TRIG_TRIGS_EVERY_N_HI = 8
ERR_SCAN_HI_ANGLE_LIMIT = 9
ERR_SCAN_LOW_ANGLE_LIMITS = 10
ERR_SCAN_TIME = 11
ERR_SCAN_VEL_TOO_HI = 12
ERR_SCAN_ANGLE_OUT_OF_LIM = 13
ERR_SCAN_HIGH_VEL_LAR_42 = 14
ERR_SCAN_MODE_INVALID = 15
class MoveType(str, enum.Enum):
"""Enum class to switch between move types energy and angle for the Bragg positioner"""
ENERGY = "energy"
ANGLE = "angle"
class ScanControlMode(int, enum.Enum):
"""Enum class for the scan control mode of the Bragg positioner"""
SIMPLE = 0
ADVANCED = 1

View File

@@ -0,0 +1,93 @@
"""Module for additional utils of the Mo1 Bragg Positioner"""
import numpy as np
from scipy.interpolate import BSpline
################ Define Constants ############
SAFETY_FACTOR = 0.025 # safety factor to limit acceleration -> NEVER SET TO ZERO !
N_SAMPLES = 41 # number of samples to generate -> Always choose uneven number,
# otherwise peak value will not be included
DEGREE_SPLINE = 3 # DEGREE_SPLINE of spline, 3 works good
TIME_COMPENSATE_SPLINE = 0.0062 # time to be compensated each spline in s
POSITION_COMPONSATION = 0.02 # angle to add at both limits, must be same values
# as used on ACS controller for simple scans
class Mo1UtilsSplineError(Exception):
"""Exception for spline computation"""
def compute_spline(
low_deg: float, high_deg: float, p_kink: float, e_kink_deg: float, scan_time: float
) -> tuple[float, float, float]:
"""Spline computation for the advanced scan mode
Args:
low_deg (float): Low angle value of the scan in deg
high_deg (float): High angle value of the scan in deg
scan_time (float): Time for a half oscillation in s
p_kink (float): Position of kink in %
e_kink_deg (float): Position of kink in degree
Returns:
tuple[float,float,float] : Position, Velocity and delta T arrays for the spline
"""
# increase motion range slightly so that xas trigger signals will occur at defined energy limits
low_deg = low_deg - POSITION_COMPONSATION
high_deg = high_deg + POSITION_COMPONSATION
if not (0 <= p_kink <= 100):
raise Mo1UtilsSplineError(
"Kink position not within range of [0..100%]" + f"for p_kink: {p_kink}"
)
if not (low_deg < e_kink_deg < high_deg):
raise Mo1UtilsSplineError(
"Kink energy not within selected energy range of scan,"
+ f"for e_kink_deg {e_kink_deg}, low_deg {low_deg} and"
+ f"high_deg {high_deg}."
)
tc1 = SAFETY_FACTOR / scan_time * TIME_COMPENSATE_SPLINE
t_kink = (scan_time - TIME_COMPENSATE_SPLINE - 2 * (SAFETY_FACTOR - tc1)) * p_kink / 100 + (
SAFETY_FACTOR - tc1
)
t_input = [
0,
SAFETY_FACTOR - tc1,
t_kink,
scan_time - TIME_COMPENSATE_SPLINE - SAFETY_FACTOR + tc1,
scan_time - TIME_COMPENSATE_SPLINE,
]
p_input = [0, 0, e_kink_deg - low_deg, high_deg - low_deg, high_deg - low_deg]
cv = np.stack((t_input, p_input)).T # spline coefficients
max_param = len(cv) - DEGREE_SPLINE
kv = np.clip(np.arange(len(cv) + DEGREE_SPLINE + 1) - DEGREE_SPLINE, 0, max_param) # knots
spl = BSpline(kv, cv, DEGREE_SPLINE) # get spline function
p = spl(np.linspace(0, max_param, N_SAMPLES))
v = spl(np.linspace(0, max_param, N_SAMPLES), 1)
a = spl(np.linspace(0, max_param, N_SAMPLES), 2)
j = spl(np.linspace(0, max_param, N_SAMPLES), 3)
tim, pos = p.T
pos = pos + low_deg
vel = v[:, 1] / v[:, 0]
acc = []
for item in a:
acc.append(0) if item[1] == 0 else acc.append(item[1] / item[0])
jerk = []
for item in j:
jerk.append(0) if item[1] == 0 else jerk.append(item[1] / item[0])
dt = np.zeros(len(tim))
for i in np.arange(len(tim)):
if i == 0:
dt[i] = 0
else:
dt[i] = 1000 * (tim[i] - tim[i - 1])
return pos, vel, dt

View File

@@ -1,304 +0,0 @@
import enum
from typing import Literal
from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin
from ophyd import Device, Kind, DeviceStatus, Component as Cpt
from ophyd import EpicsSignal, EpicsSignalRO
from ophyd_devices.sim.sim_signals import SetableSignal
from bec_lib.logger import bec_logger
logger = bec_logger.logger
class NidaqError(Exception):
""" Nidaq specific error"""
class NIDAQCompression(str, enum.Enum):
""" Options for Compression"""
OFF = 0
ON = 1
class ScanType(int, enum.Enum):
""" Triggering options of the backend"""
TRIGGERED = 0
CONTINUOUS = 1
class NidaqState(int, enum.Enum):
""" Possible States of the NIDAQ backend"""
DISABLED = 0
STANDBY = 1
STAGE = 2
KICKOFF = 3
ACQUIRE = 4
UNSTAGE = 5
class ScanRates(int, enum.Enum):
""" Sampling Rate options for the backend, in kHZ and MHz"""
HUNDRED_KHZ = 0
FIVE_HUNDRED_KHZ = 1
ONE_MHZ = 2
TWO_MHZ = 3
FOUR_MHZ = 4
FIVE_MHZ = 5
TEN_MHZ = 6
FOURTEEN_THREE_MHZ = 7
class ReadoutRange(int, enum.Enum):
"""ReadoutRange in +-V"""
ONE_V = 0
TWO_V = 1
FIVE_V = 2
TEN_V = 3
class EncoderTypes(int, enum.Enum):
""" Encoder Types"""
X_1 = 0
X_2 = 1
X_4 = 2
class NIDAQCustomMixin(CustomDetectorMixin):
""" NIDAQ Custom Mixin class to implement the device and beamline-specific actions
to the psidetectorbase class via custom_prepare methods"""
def __init__(self, *_args, parent: Device = None, **_kwargs) -> None:
super().__init__(args=_args, parent=parent, kwargs=_kwargs)
self.timeout_wait_for_signal = 5 # put 5s firsts
self.valid_scan_names = ["xas_simple_scan",
"xas_simple_scan_with_xrd",
"xas_advanced_scan",
"xas_advanced_scan_with_xrd"]
def _check_if_scan_name_is_valid(self) -> bool:
""" Check if the scan is within the list of scans for which the backend is working"""
scan_name = self.parent.scaninfo.scan_msg.content["info"].get("scan_name", "")
if scan_name in self.valid_scan_names:
return True
return False
def on_connection_established(self) -> None:
"""Method called once wait_for_connection is called on the parent class.
This should be used to implement checks that require the device to be connected, i.e. setting standard pvs.
"""
if not self.wait_for_signals(signal_conditions=[(self.parent.state.get, NidaqState.STANDBY)],
timeout = self.timeout_wait_for_signal,
check_stopped=True):
raise NidaqError(f"Device {self.parent.name} has not been reached in state STANDBY, current state {NidaqState(self.parent.state.get())}")
self.parent.scan_duration.set(0).wait()
def on_stop(self):
""" Stop the NIDAQ backend"""
self.parent.stop_call.set(1).wait()
def on_complete(self) -> None | DeviceStatus:
""" Complete actions. 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():
return None
self.on_stop()
#TODO check if this wait can be removed. We are waiting in unstage anyways which will always be called afterwards
# Wait for device to be stopped
status = self.wait_with_status(signal_conditions=[(self.parent.state.get, NidaqState.STANDBY)],
check_stopped= True,
timeout=self.timeout_wait_for_signal,
)
return status
def on_stage(self):
""" Prepare the device for the upcoming acquisition. If the upcoming scan is not in the list
of valid scans, return immediately. """
if not self._check_if_scan_name_is_valid():
return None
if not self.wait_for_signals(signal_conditions=[(self.parent.state.get, NidaqState.STANDBY)],
timeout = self.timeout_wait_for_signal,
check_stopped=True):
raise NidaqError(f"Device {self.parent.name} has not been reached in state STANDBY, current state {NidaqState(self.parent.state.get())}")
self.parent.scan_type.set(ScanType.TRIGGERED).wait()
self.parent.scan_duration.set(0).wait()
self.parent.stage_call.set(1).wait()
if not self.wait_for_signals(signal_conditions=[(self.parent.state.get, NidaqState.STAGE)],
timeout = self.timeout_wait_for_signal,
check_stopped=True,
):
raise NidaqError(f"Device {self.parent.name} has not been reached in state STAGE, current state {NidaqState(self.parent.state.get())}")
self.parent.kickoff_call.set(1).wait()
logger.info(f"Device {self.parent.name} was staged: {NidaqState(self.parent.state.get())}")
def on_pre_scan(self) -> None:
""" Execute time critical actions. Here we ensure that the NIDAQ master task is running
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():
return None
if not self.wait_for_signals(signal_conditions=[(self.parent.state.get, NidaqState.KICKOFF)],
timeout = self.timeout_wait_for_signal,
check_stopped=True,
):
raise NidaqError(f"Device {self.parent.name} failed to reach state KICKOFF during pre scan, current state {NidaqState(self.parent.state.get())}")
logger.info(f"Device {self.parent.name} ready to take data after pre_scan: {NidaqState(self.parent.state.get())}")
def on_unstage(self) -> None:
""" Unstage actions, the NIDAQ has to be in STANDBY state."""
if not self.wait_for_signals(signal_conditions=[(self.parent.state.get, NidaqState.STANDBY)],
timeout = self.timeout_wait_for_signal,
check_stopped=False):
raise NidaqError(f"Device {self.parent.name} has not been reached in state STANDBY, current state {NidaqState(self.parent.state.get())}")
logger.info(f"Device {self.parent.name} was unstaged: {NidaqState(self.parent.state.get())}")
class NIDAQ(PSIDetectorBase):
""" NIDAQ ophyd wrapper around the NIDAQ backend currently running at x01da-cons-05
Args:
prefix (str) : Prefix to the NIDAQ soft ioc, currently X01DA-PC-SCANSERVER:
name (str) : Name of the device
kind (Kind) : Ophyd Kind of the device
parent (Device) : Parent clas
device_manager : device manager as forwarded by BEC
"""
USER_ACCESS = ['set_config']
encoder_angle = Cpt(SetableSignal,value=0, kind=Kind.normal)
signal_1 = Cpt(SetableSignal,value=0, kind=Kind.normal)
signal_2 = Cpt(SetableSignal,value=0, kind=Kind.normal)
signal_3 = Cpt(SetableSignal,value=0, kind=Kind.normal)
signal_4 = Cpt(SetableSignal,value=0, kind=Kind.normal)
signal_5 = Cpt(SetableSignal,value=0, kind=Kind.normal)
signal_6 = Cpt(SetableSignal,value=0, kind=Kind.normal)
signal_7 = Cpt(SetableSignal,value=0, kind=Kind.normal)
signal_8 = Cpt(SetableSignal,value=0, kind=Kind.normal)
enable_compression = Cpt(EpicsSignal, suffix="NIDAQ-EnableRLE", kind=Kind.config)
kickoff_call = Cpt(EpicsSignal, suffix="NIDAQ-Kickoff", kind=Kind.config)
stage_call = Cpt(EpicsSignal, suffix="NIDAQ-Stage", kind = Kind.config)
state = Cpt(EpicsSignal, suffix="NIDAQ-FSMState", kind= Kind.config)
server_status = Cpt(EpicsSignalRO, suffix="NIDAQ-ServerStatus", kind=Kind.config)
compression_ratio = Cpt(EpicsSignalRO, suffix="NIDAQ-CompressionRatio", kind=Kind.config)
scan_type = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config)
sampling_rate = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config)
scan_duration = Cpt(EpicsSignal, suffix="NIDAQ-SamplingDuration", kind=Kind.config)
readout_range = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config)
encoder_type = Cpt(EpicsSignal, suffix="NIDAQ-EncoderType", kind=Kind.config)
stop_call = Cpt(EpicsSignal, suffix="NIDAQ-Stop", kind=Kind.config)
ai_chans = Cpt(EpicsSignal, suffix="NIDAQ-AIChans", kind=Kind.config)
ci_chans = Cpt(EpicsSignal, suffix="NIDAQ-CIChans6614", kind=Kind.config)
di_chans = Cpt(EpicsSignal, suffix="NIDAQ-DIChans", kind=Kind.config)
custom_prepare_cls = NIDAQCustomMixin
def __init__(self, prefix="", *, name, kind=None, parent=None, device_manager=None, **kwargs):
super().__init__(name=name, prefix=prefix, kind=kind, parent=parent, device_manager=device_manager, **kwargs)
def set_config(
self,
sampling_rate: Literal[100000,
500000,
1000000,
2000000,
4000000,
5000000,
10000000,
14286000,
],
ai: list,
ci: list,
di: list,
scan_type: Literal['continuous', 'triggered'] = 'triggered',
scan_duration: float = 0,
readout_range: Literal[1, 2, 5, 10] = 10,
encoder_type: Literal['X_1', 'X_2', 'X_4'] = 'X_4',
enable_compression: bool = True,
) -> None:
"""Method to configure the NIDAQ
Args:
sampling_rate(Literal[100000, 500000, 1000000, 2000000, 4000000, 5000000,
10000000, 14286000]): Sampling rate in Hz
ai(list): List of analog input channel numbers to add, i.e. [0, 1, 2] for
input 0, 1 and 2
ci(list): List of counter input channel numbers to add, i.e. [0, 1, 2] for
input 0, 1 and 2
di(list): List of digital input channel numbers to add, i.e. [0, 1, 2] for
input 0, 1 and 2
scan_type(Literal['continuous', 'triggered']): Triggered to use with monochromator,
otherwise continuous, default 'triggered'
scan_duration(float): Scan duration in seconds, use 0 for infinite scan, default 0
readout_range(Literal[1, 2, 5, 10]): Readout range in +- Volts, default +-10V
encoder_type(Literal['X_1', 'X_2', 'X_4']): Encoder readout type, default 'X_4'
enable_compression(bool): Enable or disable compression of data, default True
"""
if sampling_rate == 100000:
self.sampling_rate.put(ScanRates.HUNDRED_KHZ)
elif sampling_rate == 500000:
self.sampling_rate.put(ScanRates.FIVE_HUNDRED_KHZ)
elif sampling_rate == 1000000:
self.sampling_rate.put(ScanRates.ONE_MHZ)
elif sampling_rate == 2000000:
self.sampling_rate.put(ScanRates.TWO_MHZ)
elif sampling_rate == 4000000:
self.sampling_rate.put(ScanRates.FOUR_MHZ)
elif sampling_rate == 5000000:
self.sampling_rate.put(ScanRates.FIVE_MHZ)
elif sampling_rate == 10000000:
self.sampling_rate.put(ScanRates.TEN_MHZ)
elif sampling_rate == 14286000:
self.sampling_rate.put(ScanRates.FOURTEEN_THREE_MHZ)
ai_chans = 0
if isinstance(ai, list):
for ch in ai:
if isinstance(ch, int):
if ch >= 0 and ch <= 7:
ai_chans = ai_chans | (1 << ch)
self.ai_chans.put(ai_chans)
ci_chans = 0
if isinstance(ci, list):
for ch in ci:
if isinstance(ch, int):
if ch >= 0 and ch <= 7:
ci_chans = ci_chans | (1 << ch)
self.ci_chans.put(ci_chans)
di_chans = 0
if isinstance(di, list):
for ch in di:
if isinstance(ch, int):
if ch >= 0 and ch <= 4:
di_chans = di_chans | (1 << ch)
self.di_chans.put(di_chans)
if scan_type in 'continuous':
self.scan_type.put(ScanType.CONTINUOUS)
elif scan_type in 'triggered':
self.scan_type.put(ScanType.TRIGGERED)
if scan_duration >= 0:
self.scan_duration.put(scan_duration)
if readout_range == 1:
self.readout_range.put(ReadoutRange.ONE_V)
elif readout_range == 2:
self.readout_range.put(ReadoutRange.TWO_V)
elif readout_range == 5:
self.readout_range.put(ReadoutRange.FIVE_V)
elif readout_range == 10:
self.readout_range.put(ReadoutRange.TEN_V)
if encoder_type in 'X_1':
self.encoder_type.put(EncoderTypes.X_1)
elif encoder_type in 'X_2':
self.encoder_type.put(EncoderTypes.X_2)
elif encoder_type in 'X_4':
self.encoder_type.put(EncoderTypes.X_4)
if enable_compression is True:
self.enable_compression.put(NIDAQCompression.ON)
elif enable_compression is False:
self.enable_compression.put(NIDAQCompression.OFF)

View File

View File

@@ -0,0 +1,583 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Literal, cast
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Kind, StatusBase
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from ophyd_devices.sim.sim_signals import SetableSignal
from debye_bec.devices.nidaq.nidaq_enums import (
EncoderTypes,
NIDAQCompression,
NidaqState,
ReadoutRange,
ScanRates,
ScanType,
)
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
logger = bec_logger.logger
class NidaqError(Exception):
"""Nidaq specific error"""
class NidaqControl(Device):
"""Nidaq control class with all PVs"""
### Readback PVs for EpicsEmitter ###
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,
)
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,
)
### Readback for BEC emitter ###
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_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_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"
)
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"
)
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")
di2_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 2, MAX")
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")
enc = Cpt(SetableSignal, value=0, kind=Kind.normal)
### Control PVs ###
enable_compression = Cpt(EpicsSignal, suffix="NIDAQ-EnableRLE", kind=Kind.config)
kickoff_call = Cpt(EpicsSignal, suffix="NIDAQ-Kickoff", kind=Kind.config)
stage_call = Cpt(EpicsSignal, suffix="NIDAQ-Stage", kind=Kind.config)
state = Cpt(EpicsSignal, suffix="NIDAQ-FSMState", kind=Kind.config, auto_monitor=True)
server_status = Cpt(EpicsSignalRO, suffix="NIDAQ-ServerStatus", kind=Kind.config)
compression_ratio = Cpt(EpicsSignalRO, suffix="NIDAQ-CompressionRatio", kind=Kind.config)
scan_type = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config)
sampling_rate = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config)
scan_duration = Cpt(EpicsSignal, suffix="NIDAQ-SamplingDuration", kind=Kind.config)
readout_range = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config)
encoder_type = Cpt(EpicsSignal, suffix="NIDAQ-EncoderType", kind=Kind.config)
stop_call = Cpt(EpicsSignal, suffix="NIDAQ-Stop", kind=Kind.config)
ai_chans = Cpt(EpicsSignal, suffix="NIDAQ-AIChans", kind=Kind.config)
ci_chans = Cpt(EpicsSignal, suffix="NIDAQ-CIChans6614", kind=Kind.config)
di_chans = Cpt(EpicsSignal, suffix="NIDAQ-DIChans", kind=Kind.config)
class Nidaq(PSIDeviceBase, NidaqControl):
"""NIDAQ ophyd wrapper around the NIDAQ backend currently running at x01da-cons-05
Args:
prefix (str) : Prefix to the NIDAQ soft ioc, currently X01DA-PC-SCANSERVER:
name (str) : Name of the device
scan_info (ScanInfo) : ScanInfo object passed by BEC's devicemanager.
"""
USER_ACCESS = ["set_config"]
def __init__(self, prefix: str = "", *, name: str, scan_info: ScanInfo = None, **kwargs):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.timeout_wait_for_signal = 5 # put 5s firsts
self._timeout_wait_for_pv = 3 # 3s timeout for pv calls
self.valid_scan_names = [
"xas_simple_scan",
"xas_simple_scan_with_xrd",
"xas_advanced_scan",
"xas_advanced_scan_with_xrd",
]
########################################
# Beamline Methods #
########################################
def _check_if_scan_name_is_valid(self) -> 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:
return True
return False
def set_config(
self,
sampling_rate: Literal[
100000, 500000, 1000000, 2000000, 4000000, 5000000, 10000000, 14286000
],
ai: list,
ci: list,
di: list,
scan_type: Literal["continuous", "triggered"] = "triggered",
scan_duration: float = 0,
readout_range: Literal[1, 2, 5, 10] = 10,
encoder_type: Literal["X_1", "X_2", "X_4"] = "X_4",
enable_compression: bool = True,
) -> None:
"""Method to configure the NIDAQ
Args:
sampling_rate(Literal[100000, 500000, 1000000, 2000000, 4000000, 5000000,
10000000, 14286000]): Sampling rate in Hz
ai(list): List of analog input channel numbers to add, i.e. [0, 1, 2] for
input 0, 1 and 2
ci(list): List of counter input channel numbers to add, i.e. [0, 1, 2] for
input 0, 1 and 2
di(list): List of digital input channel numbers to add, i.e. [0, 1, 2] for
input 0, 1 and 2
scan_type(Literal['continuous', 'triggered']): Triggered to use with monochromator,
otherwise continuous, default 'triggered'
scan_duration(float): Scan duration in seconds, use 0 for infinite scan, default 0
readout_range(Literal[1, 2, 5, 10]): Readout range in +- Volts, default +-10V
encoder_type(Literal['X_1', 'X_2', 'X_4']): Encoder readout type, default 'X_4'
enable_compression(bool): Enable or disable compression of data, default True
"""
if sampling_rate == 100000:
self.sampling_rate.put(ScanRates.HUNDRED_KHZ)
elif sampling_rate == 500000:
self.sampling_rate.put(ScanRates.FIVE_HUNDRED_KHZ)
elif sampling_rate == 1000000:
self.sampling_rate.put(ScanRates.ONE_MHZ)
elif sampling_rate == 2000000:
self.sampling_rate.put(ScanRates.TWO_MHZ)
elif sampling_rate == 4000000:
self.sampling_rate.put(ScanRates.FOUR_MHZ)
elif sampling_rate == 5000000:
self.sampling_rate.put(ScanRates.FIVE_MHZ)
elif sampling_rate == 10000000:
self.sampling_rate.put(ScanRates.TEN_MHZ)
elif sampling_rate == 14286000:
self.sampling_rate.put(ScanRates.FOURTEEN_THREE_MHZ)
ai_chans = 0
if isinstance(ai, list):
for ch in ai:
if isinstance(ch, int):
if ch >= 0 and ch <= 7:
ai_chans = ai_chans | (1 << ch)
self.ai_chans.put(ai_chans)
ci_chans = 0
if isinstance(ci, list):
for ch in ci:
if isinstance(ch, int):
if ch >= 0 and ch <= 7:
ci_chans = ci_chans | (1 << ch)
self.ci_chans.put(ci_chans)
di_chans = 0
if isinstance(di, list):
for ch in di:
if isinstance(ch, int):
if ch >= 0 and ch <= 4:
di_chans = di_chans | (1 << ch)
self.di_chans.put(di_chans)
if scan_type in "continuous":
self.scan_type.put(ScanType.CONTINUOUS)
elif scan_type in "triggered":
self.scan_type.put(ScanType.TRIGGERED)
if scan_duration >= 0:
self.scan_duration.put(scan_duration)
if readout_range == 1:
self.readout_range.put(ReadoutRange.ONE_V)
elif readout_range == 2:
self.readout_range.put(ReadoutRange.TWO_V)
elif readout_range == 5:
self.readout_range.put(ReadoutRange.FIVE_V)
elif readout_range == 10:
self.readout_range.put(ReadoutRange.TEN_V)
if encoder_type in "X_1":
self.encoder_type.put(EncoderTypes.X_1)
elif encoder_type in "X_2":
self.encoder_type.put(EncoderTypes.X_2)
elif encoder_type in "X_4":
self.encoder_type.put(EncoderTypes.X_4)
if enable_compression is True:
self.enable_compression.put(NIDAQCompression.ON)
elif enable_compression is False:
self.enable_compression.put(NIDAQCompression.OFF)
########################################
# Beamline Specific Implementations #
########################################
def on_init(self) -> None:
"""
Called when the device is initialized.
No signals are connected at this point. If you like to
set default values on signals, please use on_connected instead.
"""
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
if not self.wait_for_condition(
condition=lambda: self.state.get() == NidaqState.STANDBY,
timeout=self.timeout_wait_for_signal,
check_stopped=True,
):
raise NidaqError(
f"Device {self.name} has not been reached in state STANDBY, current state {NidaqState(self.state.get())}"
)
self.scan_duration.set(0).wait(timeout=self._timeout_wait_for_pv)
def on_stage(self) -> DeviceStatus | StatusBase | None:
"""
Called while staging the device.
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():
return None
if not self.wait_for_condition(
condition=lambda: self.state.get() == NidaqState.STANDBY,
timeout=self.timeout_wait_for_signal,
check_stopped=True,
):
raise NidaqError(
f"Device {self.name} has not been reached in state STANDBY, current state {NidaqState(self.state.get())}"
)
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.stage_call.set(1).wait(timeout=self._timeout_wait_for_pv)
if not self.wait_for_condition(
condition=lambda: self.state.get() == NidaqState.STAGE,
timeout=self.timeout_wait_for_signal,
check_stopped=True,
):
raise NidaqError(
f"Device {self.name} has not been reached in state STAGE, current state {NidaqState(self.state.get())}"
)
self.kickoff_call.set(1).wait(timeout=self._timeout_wait_for_pv)
logger.info(f"Device {self.name} was staged: {NidaqState(self.state.get())}")
def on_unstage(self) -> DeviceStatus | StatusBase | None:
"""Called while unstaging the device. Check that the Nidaq goes into Standby"""
def _get_state():
return self.state.get() == NidaqState.STANDBY
if not self.wait_for_condition(
condition=_get_state, timeout=self.timeout_wait_for_signal, check_stopped=False
):
raise NidaqError(
f"Device {self.name} has not been reached in state STANDBY, current state {NidaqState(self.state.get())}"
)
logger.info(f"Device {self.name} was unstaged: {NidaqState(self.state.get())}")
def on_pre_scan(self) -> DeviceStatus | StatusBase | None:
"""
Called right before the scan starts on all devices automatically.
Here we ensure that the NIDAQ master task is running
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():
return None
def _wait_for_state():
return self.state.get() == NidaqState.KICKOFF
if not self.wait_for_condition(
_wait_for_state, timeout=self.timeout_wait_for_signal, check_stopped=True
):
raise NidaqError(
f"Device {self.name} failed to reach state KICKOFF during pre scan, current state {NidaqState(self.state.get())}"
)
logger.info(
f"Device {self.name} ready to take data after pre_scan: {NidaqState(self.state.get())}"
)
def on_trigger(self) -> DeviceStatus | StatusBase | None:
"""Called when the device is triggered."""
def on_complete(self) -> DeviceStatus | StatusBase | None:
"""
Called to inquire if a device has completed a scans.
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():
return None
self.on_stop()
# TODO check if this wait can be removed. We are waiting in unstage anyways which will always be called afterwards
# Wait for device to be stopped
status = self.wait_for_condition(
condition=lambda: self.state.get() == NidaqState.STANDBY,
check_stopped=True,
timeout=self.timeout_wait_for_signal,
)
return status
def on_kickoff(self) -> DeviceStatus | StatusBase | None:
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
def on_stop(self) -> None:
"""Called when the device is stopped."""
self.stop_call.put(1)

View File

@@ -0,0 +1,56 @@
import enum
class NIDAQCompression(str, enum.Enum):
"""Options for Compression"""
OFF = 0
ON = 1
class ScanType(int, enum.Enum):
"""Triggering options of the backend"""
TRIGGERED = 0
CONTINUOUS = 1
class NidaqState(int, enum.Enum):
"""Possible States of the NIDAQ backend"""
DISABLED = 0
STANDBY = 1
STAGE = 2
KICKOFF = 3
ACQUIRE = 4
UNSTAGE = 5
class ScanRates(int, enum.Enum):
"""Sampling Rate options for the backend, in kHZ and MHz"""
HUNDRED_KHZ = 0
FIVE_HUNDRED_KHZ = 1
ONE_MHZ = 2
TWO_MHZ = 3
FOUR_MHZ = 4
FIVE_MHZ = 5
TEN_MHZ = 6
FOURTEEN_THREE_MHZ = 7
class ReadoutRange(int, enum.Enum):
"""ReadoutRange in +-V"""
ONE_V = 0
TWO_V = 1
FIVE_V = 2
TEN_V = 3
class EncoderTypes(int, enum.Enum):
"""Encoder Types"""
X_1 = 0
X_2 = 1
X_4 = 2

View File

@@ -1,35 +1,27 @@
""" ES2 Pilatus Curtain"""
"""ES2 Pilatus Curtain"""
import time
from ophyd import Component as Cpt
from ophyd import Device, Kind, EpicsSignal, EpicsSignalRO
from ophyd import Device, EpicsSignal, EpicsSignalRO, Kind
from ophyd_devices.utils import bec_utils
class GasMixSetup(Device):
"""Class for the ES2 Pilatus Curtain"""
USER_ACCESS = ['open', 'close']
USER_ACCESS = ["open", "close"]
open_cover = Cpt(
EpicsSignal, suffix="OpenCover", kind="config", doc='Open Cover'
)
close_cover = Cpt(
EpicsSignal, suffix="CloseCover", kind="config", doc='Close Cover'
)
open_cover = Cpt(EpicsSignal, suffix="OpenCover", kind="config", doc="Open Cover")
close_cover = Cpt(EpicsSignal, suffix="CloseCover", kind="config", doc="Close Cover")
cover_is_closed = Cpt(
EpicsSignalRO, suffix="CoverIsClosed", kind="config", doc='Cover is closed'
)
cover_is_open = Cpt(
EpicsSignalRO, suffix="CoverIsOpen", kind="config", doc='Cover is open'
EpicsSignalRO, suffix="CoverIsClosed", kind="config", doc="Cover is closed"
)
cover_is_open = Cpt(EpicsSignalRO, suffix="CoverIsOpen", kind="config", doc="Cover is open")
cover_is_moving = Cpt(
EpicsSignalRO, suffix="CoverIsMoving", kind="config", doc='Cover is moving'
EpicsSignalRO, suffix="CoverIsMoving", kind="config", doc="Cover is moving"
)
cover_error = Cpt(
EpicsSignalRO, suffix="CoverError", kind="config", doc='Cover error'
)
cover_error = Cpt(EpicsSignalRO, suffix="CoverError", kind="config", doc="Cover error")
def __init__(
self, prefix="", *, name: str, kind: Kind = None, device_manager=None, parent=None, **kwargs
@@ -68,7 +60,7 @@ class GasMixSetup(Device):
while not self.cover_is_open.get():
time.sleep(0.1)
if self.cover_error.get():
raise TimeoutError('Curtain did not open successfully and is now in an error state')
raise TimeoutError("Curtain did not open successfully and is now in an error state")
def close(self) -> None:
"""Close the cover"""
@@ -78,4 +70,6 @@ class GasMixSetup(Device):
while not self.cover_is_closed.get():
time.sleep(0.1)
if self.cover_error.get():
raise TimeoutError('Curtain did not close successfully and is now in an error state')
raise TimeoutError(
"Curtain did not close successfully and is now in an error state"
)

View File

@@ -0,0 +1,194 @@
"""ES2 Reference Foil Changer"""
from __future__ import annotations
import enum
from typing import TYPE_CHECKING
from ophyd import Component as Cpt
from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
from ophyd.status import DeviceStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from ophyd_devices.utils.errors import DeviceStopError
if TYPE_CHECKING:
from bec_lib.devicemanager import ScanInfo
class Status(int, enum.Enum):
"""Enum class for the status field"""
BOOT = 0
RETRACTED = 1
INSERTED = 2
MOVING = 3
ERROR = 4
class OpMode(int, enum.Enum):
"""Enum class for the Operating Mode field"""
USERMODE = 0
MAINTENANCEMODE = 1
DIAGNOSTICMODE = 2
ERRORMODE = 3
class Reffoilchanger(PSIDeviceBase):
"""Class for the ES2 Reference Foil Changer"""
USER_ACCESS = ["insert"]
inserted = Cpt(
EpicsSignalRO, suffix="ES2-REF:TRY-FilterInserted", kind="config", doc="Inserted indicator"
)
retracted = Cpt(
EpicsSignalRO,
suffix="ES2-REF:TRY-FilterRetracted",
kind="config",
doc="Retracted indicator",
)
moving = Cpt(EpicsSignalRO, suffix="ES2-REF:ROTY.MOVN", kind="config", doc="Moving indicator")
status = Cpt(
EpicsSignal, suffix="ES2-REF:SELN-FilterState-ENUM_RBV", kind="config", doc="Status"
)
op_mode = Cpt(
EpicsSignalWithRBV, suffix="ES2-REF:SELN-OpMode-ENUM", kind="config", doc="Status"
)
ref_set = Cpt(EpicsSignal, suffix="ES2-REF:SELN-SET", kind="config", doc="Requested reference")
ref_rb = Cpt(
EpicsSignalRO, suffix="ES2-REF:SELN-RB", kind="config", doc="Currently set reference"
)
foil01 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL01.DESC", kind="config", doc="Foil 01")
foil02 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL02.DESC", kind="config", doc="Foil 02")
foil03 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL03.DESC", kind="config", doc="Foil 03")
foil04 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL04.DESC", kind="config", doc="Foil 04")
foil05 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL05.DESC", kind="config", doc="Foil 05")
foil06 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL06.DESC", kind="config", doc="Foil 06")
foil07 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL07.DESC", kind="config", doc="Foil 07")
foil08 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL08.DESC", kind="config", doc="Foil 08")
foil09 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL09.DESC", kind="config", doc="Foil 09")
foil10 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL10.DESC", kind="config", doc="Foil 10")
foil11 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL11.DESC", kind="config", doc="Foil 11")
foil12 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL12.DESC", kind="config", doc="Foil 12")
foil13 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL13.DESC", kind="config", doc="Foil 13")
foil14 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL14.DESC", kind="config", doc="Foil 14")
foil15 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL15.DESC", kind="config", doc="Foil 15")
foil16 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL16.DESC", kind="config", doc="Foil 16")
foil17 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL17.DESC", kind="config", doc="Foil 17")
foil18 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL18.DESC", kind="config", doc="Foil 18")
foil19 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL19.DESC", kind="config", doc="Foil 19")
foil20 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL20.DESC", kind="config", doc="Foil 20")
foil21 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL21.DESC", kind="config", doc="Foil 21")
foil22 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL22.DESC", kind="config", doc="Foil 22")
foil23 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL23.DESC", kind="config", doc="Foil 23")
foil24 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL24.DESC", kind="config", doc="Foil 24")
foil25 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL25.DESC", kind="config", doc="Foil 25")
foil26 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL26.DESC", kind="config", doc="Foil 26")
foil27 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL27.DESC", kind="config", doc="Foil 27")
foil28 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL28.DESC", kind="config", doc="Foil 28")
foil29 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL29.DESC", kind="config", doc="Foil 29")
foil30 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL30.DESC", kind="config", doc="Foil 30")
foil31 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL31.DESC", kind="config", doc="Foil 31")
foil32 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL32.DESC", kind="config", doc="Foil 32")
foil33 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL33.DESC", kind="config", doc="Foil 33")
foil34 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL34.DESC", kind="config", doc="Foil 34")
foil35 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL35.DESC", kind="config", doc="Foil 35")
foil36 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL36.DESC", kind="config", doc="Foil 36")
foil37 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL37.DESC", kind="config", doc="Foil 37")
foil38 = Cpt(EpicsSignalRO, suffix="ES-REFFOIL:FOIL38.DESC", kind="config", doc="Foil 38")
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.foils = [
self.foil01,
self.foil02,
self.foil03,
self.foil04,
self.foil05,
self.foil06,
self.foil07,
self.foil08,
self.foil09,
self.foil10,
self.foil11,
self.foil12,
self.foil13,
self.foil14,
self.foil15,
self.foil16,
self.foil17,
self.foil18,
self.foil19,
self.foil20,
self.foil21,
self.foil22,
self.foil23,
self.foil24,
self.foil25,
self.foil26,
self.foil27,
self.foil28,
self.foil29,
self.foil30,
self.foil31,
self.foil32,
self.foil33,
self.foil34,
self.foil35,
self.foil36,
self.foil37,
self.foil38,
]
def insert(self, ref: str, wait: bool = False) -> DeviceStatus:
"""Insert a reference
Args:
ref (str) : Desired reference foil name, e.g. Fe or Pt
wait (bool): If you like to wait for the filling to finish. Default False.
"""
filter_number = -1
for i, foil in enumerate(self.foils):
if foil.get() == ref:
filter_number = i + 1
break
if filter_number == -1:
raise ValueError(f"Requested foil ({ref}) is not in list of available foils")
if self.op_mode.get() == OpMode.USERMODE:
self.ref_set.put(filter_number)
def wait_for_status():
return (
(self.status.get() == Status.RETRACTED)
or (self.status.get() == Status.MOVING)
or (
self.ref_rb.get() < (filter_number + 0.2)
and self.ref_rb.get() > (filter_number - 0.2)
)
)
timeout = 3
if not self.wait_for_condition(wait_for_status, timeout=timeout, check_stopped=True):
raise TimeoutError(
f"Reference foil changer did not retract the current foil within {timeout}s"
)
def wait_for_change_finished():
return self.status.get() == Status.INSERTED and self.op_mode == OpMode.USERMODE
# Wait until new reference foil is inserted
status = self.task_handler.submit_task(
task=self.wait_for_condition, task_args=(wait_for_change_finished, 5, True)
)
if wait:
status.wait()
return status
else:
raise DeviceStopError(
f"Reference foil changer must be in User Mode but is in {self.op_mode.get(as_string=True)}"
)

View File

@@ -1,94 +0,0 @@
""" Module for additional utils of the Mo1 Bragg Positioner"""
import numpy as np
from scipy.interpolate import BSpline
################ Define Constants ############
SAFETY_FACTOR = 0.025 # safety factor to limit acceleration -> NEVER SET TO ZERO !
N_SAMPLES = 41 # number of samples to generate -> Always choose uneven number,
# otherwise peak value will not be included
DEGREE_SPLINE = 3 # DEGREE_SPLINE of spline, 3 works good
TIME_COMPENSATE_SPLINE = 0.0062 # time to be compensated each spline in s
POSITION_COMPONSATION = 0.02 # angle to add at both limits, must be same values
# as used on ACS controller for simple scans
class Mo1UtilsSplineError(Exception):
""" Exception for spline computation"""
def compute_spline(
low_deg:float,
high_deg:float,
p_kink:float,
e_kink_deg:float,
scan_time:float,
) -> tuple[float, float, float]:
""" Spline computation for the advanced scan mode
Args:
low_deg (float): Low angle value of the scan in deg
high_deg (float): High angle value of the scan in deg
scan_time (float): Time for a half oscillation in s
p_kink (float): Position of kink in %
e_kink_deg (float): Position of kink in degree
Returns:
tuple[float,float,float] : Position, Velocity and delta T arrays for the spline
"""
# increase motion range slightly so that xas trigger signals will occur at defined energy limits
low_deg = low_deg - POSITION_COMPONSATION
high_deg = high_deg + POSITION_COMPONSATION
if p_kink < 0 or p_kink > 100:
raise Mo1UtilsSplineError("Kink position not within range of [0..100%]"+
f"for p_kink: {p_kink}")
if e_kink_deg < low_deg or e_kink_deg > high_deg:
raise Mo1UtilsSplineError("Kink energy not within selected energy range of scan,"+
f"for e_kink_deg {e_kink_deg}, low_deg {low_deg} and"+
f"high_deg {high_deg}.")
tc1 = SAFETY_FACTOR / scan_time * TIME_COMPENSATE_SPLINE
t_kink = (scan_time - TIME_COMPENSATE_SPLINE - 2*(SAFETY_FACTOR - tc1)) * p_kink/100 + (SAFETY_FACTOR - tc1)
t_input = [0,
SAFETY_FACTOR - tc1,
t_kink,
scan_time - TIME_COMPENSATE_SPLINE - SAFETY_FACTOR + tc1,
scan_time - TIME_COMPENSATE_SPLINE]
p_input = [0,
0,
e_kink_deg - low_deg,
high_deg - low_deg,
high_deg - low_deg]
cv = np.stack((t_input, p_input)).T # spline coefficients
max_param = len(cv) - DEGREE_SPLINE
kv = np.clip(np.arange(len(cv)+DEGREE_SPLINE+1)-DEGREE_SPLINE,0,max_param) # knots
spl = BSpline(kv, cv, DEGREE_SPLINE) # get spline function
p = spl(np.linspace(0,max_param,N_SAMPLES))
v = spl(np.linspace(0,max_param,N_SAMPLES), 1)
a = spl(np.linspace(0,max_param,N_SAMPLES), 2)
j = spl(np.linspace(0,max_param,N_SAMPLES), 3)
tim, pos = p.T
pos = pos + low_deg
vel = v[:,1]/v[:,0]
acc = []
for item in a:
acc.append(0) if item[1] == 0 else acc.append(item[1]/item[0])
jerk = []
for item in j:
jerk.append(0) if item[1] == 0 else jerk.append(item[1]/item[0])
dt = np.zeros(len(tim))
for i in np.arange(len(tim)):
if i == 0:
dt[i] = 0
else:
dt[i] = 1000*(tim[i]-tim[i-1])
return pos, vel, dt

View File

@@ -1 +1,6 @@
from .mono_bragg_scans import XASSimpleScan, XASSimpleScanWithXRD, XASAdvancedScan, XASAdvancedScanWithXRD
from .mono_bragg_scans import (
XASAdvancedScan,
XASAdvancedScanWithXRD,
XASSimpleScan,
XASSimpleScanWithXRD,
)

View File

@@ -1,6 +1,7 @@
"""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
@@ -56,6 +57,11 @@ class XASSimpleScan(AsyncFlyScanBase):
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.
@@ -96,7 +102,7 @@ class XASSimpleScan(AsyncFlyScanBase):
time.sleep(self.primary_readout_cycle)
self.point_id += 1
self.num_pos = self.point_id + 1
self.num_pos = self.point_id
class XASSimpleScanWithXRD(XASSimpleScan):

View File

@@ -17,7 +17,7 @@ dependencies = ["numpy", "scipy", "bec_lib", "h5py", "ophyd_devices"]
[project.optional-dependencies]
dev = [
"bec_server",
"black ~= 24.0",
"black ~= 25.0",
"isort",
"coverage",
"pylint",

View File

@@ -16,14 +16,14 @@ from ophyd.utils import LimitError
from ophyd_devices.tests.utils import MockPV
# from bec_server.device_server.tests.utils import DMMock
from debye_bec.devices.mo1_bragg import (
from debye_bec.devices.mo1_bragg.mo1_bragg import (
Mo1Bragg,
Mo1BraggError,
MoveType,
ScanControlLoadMessage,
ScanControlMode,
ScanControlScanStatus,
)
from debye_bec.devices.mo1_bragg.mo1_bragg_devices import MoveType
# 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
@@ -40,10 +40,7 @@ def scan_worker_mock(scan_server_mock):
def mock_bragg():
name = "bragg"
prefix = "X01DA-OP-MO1:BRAGG:"
with (
mock.patch.object(ophyd, "cl") as mock_cl,
mock.patch("debye_bec.devices.mo1_bragg.Mo1Bragg", "_on_init"),
):
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
dev = Mo1Bragg(name=name, prefix=prefix)
@@ -183,6 +180,25 @@ def test_update_scan_parameters(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,
@@ -203,14 +219,14 @@ def test_update_scan_parameters(mock_bragg):
},
metadata={},
)
mock_bragg.scaninfo.scan_msg = msg
for field in fields(dev.scan_parameter):
assert getattr(dev.scan_parameter, field.name) == None
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()
for field in fields(dev.scan_parameter):
assert getattr(dev.scan_parameter, field.name) == msg.content["info"]["kwargs"].get(
field.name, None
)
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):
@@ -221,9 +237,14 @@ def test_kickoff_scan(mock_bragg):
dev.scan_control.scan_start_infinite._read_pv.mock_data = 0
status = dev.kickoff()
assert status.done is False
dev.scan_control.scan_status._read_pv.mock_data = ScanControlScanStatus.RUNNING
time.sleep(0.2)
assert status.done is True
# TODO MockPV does not support callbacks yet, so we need to improve here #16
# dev.scan_control.scan_status._read_pv.mock_data = ScanControlScanStatus.RUNNING
# dev.scan_control.scan_status._read_pv.
# status.wait(timeout=3) # Callback should resolve now
# assert status.done is True
# # dev.scan_control.scan_status._read_pv.mock_data = ScanControlScanStatus.RUNNING
# time.sleep(0.2)
# assert status.done is True
assert dev.scan_control.scan_start_timer.get() == 1
dev.scan_control.scan_duration._read_pv.mock_data = 0
@@ -243,7 +264,8 @@ def test_complete(mock_bragg):
assert status.done is False
assert status.success is False
dev.scan_control.scan_done._read_pv.mock_data = 1
time.sleep(0.2)
status.wait()
# time.sleep(0.2)
assert status.done is True
assert status.success is True
@@ -264,7 +286,7 @@ def test_unstage(mock_bragg):
mock_bragg.scan_control.scan_msg._read_pv.mock_data = ScanControlLoadMessage.PENDING
with mock.patch.object(mock_bragg.scan_control.scan_val_reset, "put") as mock_put:
mock_bragg.unstage()
status = mock_bragg.unstage()
assert mock_put.call_count == 0
mock_bragg.scan_control.scan_msg._read_pv.mock_data = ScanControlLoadMessage.SUCCESS
with pytest.raises(TimeoutError):
@@ -272,269 +294,284 @@ def test_unstage(mock_bragg):
assert mock_put.call_count == 1
@pytest.mark.parametrize(
"msg",
[
ScanQueueMessage(
scan_type="monitor_scan",
parameter={
"args": {},
"kwargs": {
"device": "mo1_bragg",
"start": 0,
"stop": 10,
"relative": True,
"system_config": {"file_suffix": None, "file_directory": None},
},
"num_points": 100,
},
queue="primary",
metadata={"RID": "test1234"},
),
ScanQueueMessage(
scan_type="xas_simple_scan",
parameter={
"args": {},
"kwargs": {
"motor": "mo1_bragg",
"start": 0,
"stop": 10,
"scan_time": 1,
"scan_duration": 10,
"system_config": {"file_suffix": None, "file_directory": None},
},
"num_points": 100,
},
queue="primary",
metadata={"RID": "test1234"},
),
ScanQueueMessage(
scan_type="xas_simple_scan_with_xrd",
parameter={
"args": {},
"kwargs": {
"motor": "mo1_bragg",
"start": 0,
"stop": 10,
"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,
"system_config": {"file_suffix": None, "file_directory": None},
},
"num_points": 10,
},
queue="primary",
metadata={"RID": "test1234"},
),
ScanQueueMessage(
scan_type="xas_advanced_scan",
parameter={
"args": {},
"kwargs": {
"motor": "mo1_bragg",
"start": 8000,
"stop": 9000,
"scan_time": 1,
"scan_duration": 10,
"p_kink": 50,
"e_kink": 8500,
"system_config": {"file_suffix": None, "file_directory": None},
},
"num_points": 100,
},
queue="primary",
metadata={"RID": "test1234"},
),
ScanQueueMessage(
scan_type="xas_advanced_scan_with_xrd",
parameter={
"args": {},
"kwargs": {
"motor": "mo1_bragg",
"start": 8000,
"stop": 9000,
"scan_time": 1,
"scan_duration": 10,
"p_kink": 50,
"e_kink": 8500,
"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,
"system_config": {"file_suffix": None, "file_directory": None},
},
"num_points": 10,
},
queue="primary",
metadata={"RID": "test1234"},
),
],
)
def test_stage(mock_bragg, scan_worker_mock, msg):
"""This test is important to check that the stage method of the device is working correctly.
Changing the kwargs names in the scans is tightly linked to the logic on the device, thus
it is important to check that the stage method is working correctly for the current implementation.
# TODO reimplement the test for stage method
# @pytest.mark.parametrize(
# "msg",
# [
# ScanQueueMessage(
# scan_type="monitor_scan",
# parameter={
# "args": {},
# "kwargs": {
# "device": "mo1_bragg",
# "start": 0,
# "stop": 10,
# "relative": True,
# "system_config": {"file_suffix": None, "file_directory": None},
# },
# "num_points": 100,
# },
# queue="primary",
# metadata={"RID": "test1234"},
# ),
# ScanQueueMessage(
# scan_type="xas_simple_scan",
# parameter={
# "args": {},
# "kwargs": {
# "motor": "mo1_bragg",
# "start": 0,
# "stop": 10,
# "scan_time": 1,
# "scan_duration": 10,
# "system_config": {"file_suffix": None, "file_directory": None},
# },
# "num_points": 100,
# },
# queue="primary",
# metadata={"RID": "test1234"},
# ),
# ScanQueueMessage(
# scan_type="xas_simple_scan_with_xrd",
# parameter={
# "args": {},
# "kwargs": {
# "motor": "mo1_bragg",
# "start": 0,
# "stop": 10,
# "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,
# "system_config": {"file_suffix": None, "file_directory": None},
# },
# "num_points": 10,
# },
# queue="primary",
# metadata={"RID": "test1234"},
# ),
# ScanQueueMessage(
# scan_type="xas_advanced_scan",
# parameter={
# "args": {},
# "kwargs": {
# "motor": "mo1_bragg",
# "start": 8000,
# "stop": 9000,
# "scan_time": 1,
# "scan_duration": 10,
# "p_kink": 50,
# "e_kink": 8500,
# "system_config": {"file_suffix": None, "file_directory": None},
# },
# "num_points": 100,
# },
# queue="primary",
# metadata={"RID": "test1234"},
# ),
# ScanQueueMessage(
# scan_type="xas_advanced_scan_with_xrd",
# parameter={
# "args": {},
# "kwargs": {
# "motor": "mo1_bragg",
# "start": 8000,
# "stop": 9000,
# "scan_time": 1,
# "scan_duration": 10,
# "p_kink": 50,
# "e_kink": 8500,
# "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,
# "system_config": {"file_suffix": None, "file_directory": None},
# },
# "num_points": 10,
# },
# queue="primary",
# metadata={"RID": "test1234"},
# ),
# ],
# )
# def test_stage(mock_bragg, scan_worker_mock, msg):
# """This test is important to check that the stage method of the device is working correctly.
# Changing the kwargs names in the scans is tightly linked to the logic on the device, thus
# it is important to check that the stage method is working correctly for the current implementation.
Therefor, this test creates a scaninfo message using the scan.open_scan() method to always check
agains the currently implemented scans vs. the logic on the device"""
# Create a scaninfo message using scans the ScanQueueMessages above, 3 cases of fly scan; for the general case the procedure is not defined yet
worker = scan_worker_mock
scan_server = worker.parent
rb = RequestBlock(msg, assembler=ScanAssembler(parent=scan_server))
with mock.patch.object(worker, "current_instruction_queue_item"):
worker.scan_motors = []
worker.readout_priority = {
"monitored": [],
"baseline": [],
"async": [],
"continuous": [],
"on_request": [],
}
open_scan_msg = list(rb.scan.open_scan())[0]
worker._initialize_scan_info(rb, open_scan_msg, msg.content["parameter"].get("num_points"))
scan_status_msg = ScanStatusMessage(
scan_id="test1234", status="closed", info=worker.current_scan_info, metadata={}
)
mock_bragg.scaninfo.scan_msg = scan_status_msg
# Therefor, this test creates a scaninfo message using the scan.open_scan() method to always check
# agains the currently implemented scans vs. the logic on the device"""
# # Create a scaninfo message using scans the ScanQueueMessages above, 3 cases of fly scan; for the general case the procedure is not defined yet
# worker = scan_worker_mock
# scan_server = worker.parent
# rb = RequestBlock(msg, assembler=ScanAssembler(parent=scan_server))
# with mock.patch.object(worker, "current_instruction_queue_item"):
# worker.scan_motors = []
# worker.readout_priority = {
# "monitored": [],
# "baseline": [],
# "async": [],
# "continuous": [],
# "on_request": [],
# }
# open_scan_msg = list(rb.scan.open_scan())[0]
# worker._initialize_scan_info(
# rb, open_scan_msg, msg.content["parameter"].get("num_points", 1)
# )
# # TODO find a better solution to this...
# scan_status_msg = ScanStatusMessage(
# scan_id=worker.current_scan_id,
# status="open",
# scan_name=worker.current_scan_info.get("scan_name"),
# scan_number=worker.current_scan_info.get("scan_number"),
# session_id=worker.current_scan_info.get("session_id"),
# dataset_number=worker.current_scan_info.get("dataset_number"),
# num_points=worker.current_scan_info.get("num_points"),
# scan_type=worker.current_scan_info.get("scan_type"),
# scan_report_devices=worker.current_scan_info.get("scan_report_devices"),
# user_metadata=worker.current_scan_info.get("user_metadata"),
# readout_priority=worker.current_scan_info.get("readout_priority"),
# scan_parameters=worker.current_scan_info.get("scan_parameters"),
# request_inputs=worker.current_scan_info.get("request_inputs"),
# info=worker.current_scan_info,
# )
# mock_bragg.scan_info.msg = scan_status_msg
# Ensure that ScanControlLoadMessage is set to SUCCESS
mock_bragg.scan_control.scan_msg._read_pv.mock_data = ScanControlLoadMessage.SUCCESS
with (
mock.patch.object(mock_bragg.scaninfo, "load_scan_metadata") as mock_load_scan_metadata,
mock.patch.object(mock_bragg, "_check_scan_msg") as mock_check_scan_msg,
mock.patch.object(mock_bragg, "on_unstage"),
):
scan_name = scan_status_msg.content["info"].get("scan_name", "")
# Chek the not implemented fly scan first, should raise Mo1BraggError
if scan_name not in [
"xas_simple_scan",
"xas_simple_scan_with_xrd",
"xas_advanced_scan",
"xas_advanced_scan_with_xrd",
]:
with pytest.raises(Mo1BraggError):
mock_bragg.stage()
assert mock_check_scan_msg.call_count == 1
assert mock_load_scan_metadata.call_count == 1
else:
with (
mock.patch.object(mock_bragg, "set_xas_settings") as mock_xas_settings,
mock.patch.object(
mock_bragg, "set_advanced_xas_settings"
) as mock_advanced_xas_settings,
mock.patch.object(mock_bragg, "set_trig_settings") as mock_trig_settings,
mock.patch.object(
mock_bragg, "set_scan_control_settings"
) as mock_set_scan_control_settings,
):
# Check xas_simple_scan
if scan_name == "xas_simple_scan":
mock_bragg.stage()
assert mock_xas_settings.call_args == mock.call(
low=scan_status_msg.content["info"]["kwargs"]["start"],
high=scan_status_msg.content["info"]["kwargs"]["stop"],
scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
)
assert mock_trig_settings.call_args == mock.call(
enable_low=False,
enable_high=False,
exp_time_low=0,
exp_time_high=0,
cycle_low=0,
cycle_high=0,
)
assert mock_set_scan_control_settings.call_args == mock.call(
mode=ScanControlMode.SIMPLE,
scan_duration=scan_status_msg.content["info"]["kwargs"][
"scan_duration"
],
)
# Check xas_simple_scan_with_xrd
elif scan_name == "xas_simple_scan_with_xrd":
mock_bragg.stage()
assert mock_xas_settings.call_args == mock.call(
low=scan_status_msg.content["info"]["kwargs"]["start"],
high=scan_status_msg.content["info"]["kwargs"]["stop"],
scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
)
assert mock_trig_settings.call_args == mock.call(
enable_low=scan_status_msg.content["info"]["kwargs"]["xrd_enable_low"],
enable_high=scan_status_msg.content["info"]["kwargs"][
"xrd_enable_high"
],
exp_time_low=scan_status_msg.content["info"]["kwargs"]["exp_time_low"],
exp_time_high=scan_status_msg.content["info"]["kwargs"][
"exp_time_high"
],
cycle_low=scan_status_msg.content["info"]["kwargs"]["cycle_low"],
cycle_high=scan_status_msg.content["info"]["kwargs"]["cycle_high"],
)
assert mock_set_scan_control_settings.call_args == mock.call(
mode=ScanControlMode.SIMPLE,
scan_duration=scan_status_msg.content["info"]["kwargs"][
"scan_duration"
],
)
# Check xas_advanced_scan
elif scan_name == "xas_advanced_scan":
mock_bragg.stage()
assert mock_advanced_xas_settings.call_args == mock.call(
low=scan_status_msg.content["info"]["kwargs"]["start"],
high=scan_status_msg.content["info"]["kwargs"]["stop"],
scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
p_kink=scan_status_msg.content["info"]["kwargs"]["p_kink"],
e_kink=scan_status_msg.content["info"]["kwargs"]["e_kink"],
)
assert mock_trig_settings.call_args == mock.call(
enable_low=False,
enable_high=False,
exp_time_low=0,
exp_time_high=0,
cycle_low=0,
cycle_high=0,
)
assert mock_set_scan_control_settings.call_args == mock.call(
mode=ScanControlMode.ADVANCED,
scan_duration=scan_status_msg.content["info"]["kwargs"][
"scan_duration"
],
)
# Check xas_advanced_scan_with_xrd
elif scan_name == "xas_advanced_scan_with_xrd":
mock_bragg.stage()
assert mock_advanced_xas_settings.call_args == mock.call(
low=scan_status_msg.content["info"]["kwargs"]["start"],
high=scan_status_msg.content["info"]["kwargs"]["stop"],
scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
p_kink=scan_status_msg.content["info"]["kwargs"]["p_kink"],
e_kink=scan_status_msg.content["info"]["kwargs"]["e_kink"],
)
assert mock_trig_settings.call_args == mock.call(
enable_low=scan_status_msg.content["info"]["kwargs"]["xrd_enable_low"],
enable_high=scan_status_msg.content["info"]["kwargs"][
"xrd_enable_high"
],
exp_time_low=scan_status_msg.content["info"]["kwargs"]["exp_time_low"],
exp_time_high=scan_status_msg.content["info"]["kwargs"][
"exp_time_high"
],
cycle_low=scan_status_msg.content["info"]["kwargs"]["cycle_low"],
cycle_high=scan_status_msg.content["info"]["kwargs"]["cycle_high"],
)
assert mock_set_scan_control_settings.call_args == mock.call(
mode=ScanControlMode.ADVANCED,
scan_duration=scan_status_msg.content["info"]["kwargs"][
"scan_duration"
],
)
# # Ensure that ScanControlLoadMessage is set to SUCCESS
# mock_bragg.scan_control.scan_msg._read_pv.mock_data = ScanControlLoadMessage.SUCCESS
# with (
# mock.patch.object(mock_bragg, "_check_scan_msg") as mock_check_scan_msg,
# mock.patch.object(mock_bragg, "on_unstage"),
# ):
# scan_name = scan_status_msg.content["info"].get("scan_name", "")
# # Chek the not implemented fly scan first, should raise Mo1BraggError
# if scan_name not in [
# "xas_simple_scan",
# "xas_simple_scan_with_xrd",
# "xas_advanced_scan",
# "xas_advanced_scan_with_xrd",
# ]:
# with pytest.raises(Mo1BraggError):
# mock_bragg.stage()
# assert mock_check_scan_msg.call_count == 1
# else:
# with (
# mock.patch.object(mock_bragg, "set_xas_settings") as mock_xas_settings,
# mock.patch.object(
# mock_bragg, "set_advanced_xas_settings"
# ) as mock_advanced_xas_settings,
# mock.patch.object(mock_bragg, "set_trig_settings") as mock_trig_settings,
# mock.patch.object(
# mock_bragg, "set_scan_control_settings"
# ) as mock_set_scan_control_settings,
# ):
# # Check xas_simple_scan
# if scan_name == "xas_simple_scan":
# mock_bragg.stage()
# assert mock_xas_settings.call_args == mock.call(
# low=scan_status_msg.content["info"]["kwargs"]["start"],
# high=scan_status_msg.content["info"]["kwargs"]["stop"],
# scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
# )
# assert mock_trig_settings.call_args == mock.call(
# enable_low=False,
# enable_high=False,
# exp_time_low=0,
# exp_time_high=0,
# cycle_low=0,
# cycle_high=0,
# )
# assert mock_set_scan_control_settings.call_args == mock.call(
# mode=ScanControlMode.SIMPLE,
# scan_duration=scan_status_msg.content["info"]["kwargs"][
# "scan_duration"
# ],
# )
# # Check xas_simple_scan_with_xrd
# elif scan_name == "xas_simple_scan_with_xrd":
# mock_bragg.stage()
# assert mock_xas_settings.call_args == mock.call(
# low=scan_status_msg.content["info"]["kwargs"]["start"],
# high=scan_status_msg.content["info"]["kwargs"]["stop"],
# scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
# )
# assert mock_trig_settings.call_args == mock.call(
# enable_low=scan_status_msg.content["info"]["kwargs"]["xrd_enable_low"],
# enable_high=scan_status_msg.content["info"]["kwargs"][
# "xrd_enable_high"
# ],
# exp_time_low=scan_status_msg.content["info"]["kwargs"]["exp_time_low"],
# exp_time_high=scan_status_msg.content["info"]["kwargs"][
# "exp_time_high"
# ],
# cycle_low=scan_status_msg.content["info"]["kwargs"]["cycle_low"],
# cycle_high=scan_status_msg.content["info"]["kwargs"]["cycle_high"],
# )
# assert mock_set_scan_control_settings.call_args == mock.call(
# mode=ScanControlMode.SIMPLE,
# scan_duration=scan_status_msg.content["info"]["kwargs"][
# "scan_duration"
# ],
# )
# # Check xas_advanced_scan
# elif scan_name == "xas_advanced_scan":
# mock_bragg.stage()
# assert mock_advanced_xas_settings.call_args == mock.call(
# low=scan_status_msg.content["info"]["kwargs"]["start"],
# high=scan_status_msg.content["info"]["kwargs"]["stop"],
# scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
# p_kink=scan_status_msg.content["info"]["kwargs"]["p_kink"],
# e_kink=scan_status_msg.content["info"]["kwargs"]["e_kink"],
# )
# assert mock_trig_settings.call_args == mock.call(
# enable_low=False,
# enable_high=False,
# exp_time_low=0,
# exp_time_high=0,
# cycle_low=0,
# cycle_high=0,
# )
# assert mock_set_scan_control_settings.call_args == mock.call(
# mode=ScanControlMode.ADVANCED,
# scan_duration=scan_status_msg.content["info"]["kwargs"][
# "scan_duration"
# ],
# )
# # Check xas_advanced_scan_with_xrd
# elif scan_name == "xas_advanced_scan_with_xrd":
# mock_bragg.stage()
# assert mock_advanced_xas_settings.call_args == mock.call(
# low=scan_status_msg.content["info"]["kwargs"]["start"],
# high=scan_status_msg.content["info"]["kwargs"]["stop"],
# scan_time=scan_status_msg.content["info"]["kwargs"]["scan_time"],
# p_kink=scan_status_msg.content["info"]["kwargs"]["p_kink"],
# e_kink=scan_status_msg.content["info"]["kwargs"]["e_kink"],
# )
# assert mock_trig_settings.call_args == mock.call(
# enable_low=scan_status_msg.content["info"]["kwargs"]["xrd_enable_low"],
# enable_high=scan_status_msg.content["info"]["kwargs"][
# "xrd_enable_high"
# ],
# exp_time_low=scan_status_msg.content["info"]["kwargs"]["exp_time_low"],
# exp_time_high=scan_status_msg.content["info"]["kwargs"][
# "exp_time_high"
# ],
# cycle_low=scan_status_msg.content["info"]["kwargs"]["cycle_low"],
# cycle_high=scan_status_msg.content["info"]["kwargs"]["cycle_high"],
# )
# assert mock_set_scan_control_settings.call_args == mock.call(
# mode=ScanControlMode.ADVANCED,
# scan_duration=scan_status_msg.content["info"]["kwargs"][
# "scan_duration"
# ],
# )

View File

@@ -1,22 +1,152 @@
# pylint: skip-file
import debye_bec.devices.utils.mo1_bragg_utils as utils
import numpy as np
import debye_bec.devices.mo1_bragg.mo1_bragg_utils as utils
def test_compute_spline():
p, v, dt = utils.compute_spline(low_deg=10, high_deg=12, p_kink=50, e_kink_deg=11, scan_time=0.5)
p, v, dt = utils.compute_spline(
low_deg=10, high_deg=12, p_kink=50, e_kink_deg=11, scan_time=0.5
)
rtol = 1e-6
atol = 1e-3
p_desired = [9.98,9.98376125,9.99479,10.01270375,10.03712,10.06765625,10.10393,10.14555875,10.19216,10.24335125,10.29875,10.35797375,10.42064,10.48636625,10.55477,10.62546875,10.69808,10.77222125,10.84751,10.92356375,11.,11.07643625,11.15249,11.22777875,11.30192,11.37453125,11.44523,11.51363375,11.57936,11.64202625,11.70125,11.75664875,11.80784,11.85444125,11.89607,11.93234375,11.96288,11.98729625,12.00521,12.01623875,12.02]
v_desired = [0.,1.50156441,2.35715667,2.90783907,3.29035796,3.57019636,3.78263174,3.9483388,4.08022441,4.18675043,4.27368333,4.34507577,4.40384627,4.45213618,4.49153736,4.52324148,4.54814006,4.5668924,4.57997194,4.58769736,4.59025246,4.58769736,4.57997194,4.5668924,4.54814006,4.52324148,4.49153736,4.45213618,4.40384627,4.34507577,4.27368333,4.18675043,4.08022441,3.9483388,3.78263174,3.57019636,3.29035796,2.90783907,2.35715667,1.50156441,0.]
dt_desired = [0.,4.34081063,5.57222438,6.73882688,7.84061813,8.87759812,9.84976688,10.75712437,11.59967063,12.37740563,13.09032937,13.73844188,14.32174313,14.84023312,15.29391188,15.68277937,16.00683562,16.26608063,16.46051438,16.59013687,16.65494813,16.65494813,16.59013687,16.46051438,16.26608063,16.00683562,15.68277938,15.29391188,14.84023312,14.32174313,13.73844187,13.09032938,12.37740562,11.59967063,10.75712437,9.84976687,8.87759813,7.84061812,6.73882688,5.57222437,4.34081063]
p_desired = [
9.98,
9.98376125,
9.99479,
10.01270375,
10.03712,
10.06765625,
10.10393,
10.14555875,
10.19216,
10.24335125,
10.29875,
10.35797375,
10.42064,
10.48636625,
10.55477,
10.62546875,
10.69808,
10.77222125,
10.84751,
10.92356375,
11.0,
11.07643625,
11.15249,
11.22777875,
11.30192,
11.37453125,
11.44523,
11.51363375,
11.57936,
11.64202625,
11.70125,
11.75664875,
11.80784,
11.85444125,
11.89607,
11.93234375,
11.96288,
11.98729625,
12.00521,
12.01623875,
12.02,
]
v_desired = [
0.0,
1.50156441,
2.35715667,
2.90783907,
3.29035796,
3.57019636,
3.78263174,
3.9483388,
4.08022441,
4.18675043,
4.27368333,
4.34507577,
4.40384627,
4.45213618,
4.49153736,
4.52324148,
4.54814006,
4.5668924,
4.57997194,
4.58769736,
4.59025246,
4.58769736,
4.57997194,
4.5668924,
4.54814006,
4.52324148,
4.49153736,
4.45213618,
4.40384627,
4.34507577,
4.27368333,
4.18675043,
4.08022441,
3.9483388,
3.78263174,
3.57019636,
3.29035796,
2.90783907,
2.35715667,
1.50156441,
0.0,
]
dt_desired = [
0.0,
4.34081063,
5.57222438,
6.73882688,
7.84061813,
8.87759812,
9.84976688,
10.75712437,
11.59967063,
12.37740563,
13.09032937,
13.73844188,
14.32174313,
14.84023312,
15.29391188,
15.68277937,
16.00683562,
16.26608063,
16.46051438,
16.59013687,
16.65494813,
16.65494813,
16.59013687,
16.46051438,
16.26608063,
16.00683562,
15.68277938,
15.29391188,
14.84023312,
14.32174313,
13.73844187,
13.09032938,
12.37740562,
11.59967063,
10.75712437,
9.84976687,
8.87759813,
7.84061812,
6.73882688,
5.57222437,
4.34081063,
]
np.testing.assert_allclose(p, p_desired, rtol, atol)
np.testing.assert_allclose(v, v_desired, rtol, atol)
np.testing.assert_allclose(dt, dt_desired, rtol, atol)
assert(utils.SAFETY_FACTOR == 0.025)
assert(utils.N_SAMPLES == 41)
assert(utils.DEGREE_SPLINE == 3)
assert(utils.TIME_COMPENSATE_SPLINE == 0.0062)
assert(utils.POSITION_COMPONSATION == 0.02)
assert utils.SAFETY_FACTOR == 0.025
assert utils.N_SAMPLES == 41
assert utils.DEGREE_SPLINE == 3
assert utils.TIME_COMPENSATE_SPLINE == 0.0062
assert utils.POSITION_COMPONSATION == 0.02

View File

@@ -49,6 +49,7 @@ def get_instructions(request, ScanStubStatusMock):
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 == [
@@ -70,7 +71,7 @@ def test_xas_simple_scan(scan_assembler, ScanStubStatusMock):
"monitored": [],
"baseline": [],
"on_request": [],
"async": [],
"async": ["nidaq"],
},
"num_points": None,
"positions": [0.0, 5.0],
@@ -78,6 +79,7 @@ def test_xas_simple_scan(scan_assembler, ScanStubStatusMock):
"scan_type": "fly",
},
),
DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}),
DeviceInstructionMessage(
metadata={},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
@@ -104,7 +106,7 @@ def test_xas_simple_scan(scan_assembler, ScanStubStatusMock):
),
DeviceInstructionMessage(
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
action="pre_scan",
parameter={},
),
@@ -130,7 +132,7 @@ def test_xas_simple_scan(scan_assembler, ScanStubStatusMock):
"fake_complete",
DeviceInstructionMessage(
metadata={},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
action="unstage",
parameter={},
),
@@ -160,6 +162,7 @@ def test_xas_simple_scan_with_xrd(scan_assembler, ScanStubStatusMock):
exp_time_high=3,
cycle_high=4,
)
request.device_manager.add_device("nidaq")
reference_commands = get_instructions(request, ScanStubStatusMock)
assert reference_commands == [
@@ -181,7 +184,7 @@ def test_xas_simple_scan_with_xrd(scan_assembler, ScanStubStatusMock):
"monitored": [],
"baseline": [],
"on_request": [],
"async": [],
"async": ["nidaq"],
},
"num_points": None,
"positions": [0.0, 5.0],
@@ -189,6 +192,7 @@ def test_xas_simple_scan_with_xrd(scan_assembler, ScanStubStatusMock):
"scan_type": "fly",
},
),
DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}),
DeviceInstructionMessage(
metadata={},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
@@ -215,7 +219,7 @@ def test_xas_simple_scan_with_xrd(scan_assembler, ScanStubStatusMock):
),
DeviceInstructionMessage(
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
action="pre_scan",
parameter={},
),
@@ -241,7 +245,7 @@ def test_xas_simple_scan_with_xrd(scan_assembler, ScanStubStatusMock):
"fake_complete",
DeviceInstructionMessage(
metadata={},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
action="unstage",
parameter={},
),
@@ -265,6 +269,7 @@ def test_xas_advanced_scan(scan_assembler, ScanStubStatusMock):
p_kink=50,
e_kink=8500,
)
request.device_manager.add_device("nidaq")
reference_commands = get_instructions(request, ScanStubStatusMock)
assert reference_commands == [
@@ -286,7 +291,7 @@ def test_xas_advanced_scan(scan_assembler, ScanStubStatusMock):
"monitored": [],
"baseline": [],
"on_request": [],
"async": [],
"async": ["nidaq"],
},
"num_points": None,
"positions": [8000.0, 9000.0],
@@ -294,6 +299,7 @@ def test_xas_advanced_scan(scan_assembler, ScanStubStatusMock):
"scan_type": "fly",
},
),
DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}),
DeviceInstructionMessage(
metadata={},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
@@ -320,7 +326,7 @@ def test_xas_advanced_scan(scan_assembler, ScanStubStatusMock):
),
DeviceInstructionMessage(
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
action="pre_scan",
parameter={},
),
@@ -346,7 +352,7 @@ def test_xas_advanced_scan(scan_assembler, ScanStubStatusMock):
"fake_complete",
DeviceInstructionMessage(
metadata={},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
action="unstage",
parameter={},
),
@@ -378,6 +384,7 @@ def test_xas_advanced_scan_with_xrd(scan_assembler, ScanStubStatusMock):
exp_time_high=3,
cycle_high=4,
)
request.device_manager.add_device("nidaq")
reference_commands = get_instructions(request, ScanStubStatusMock)
assert reference_commands == [
@@ -399,7 +406,7 @@ def test_xas_advanced_scan_with_xrd(scan_assembler, ScanStubStatusMock):
"monitored": [],
"baseline": [],
"on_request": [],
"async": [],
"async": ["nidaq"],
},
"num_points": None,
"positions": [8000.0, 9000.0],
@@ -407,6 +414,7 @@ def test_xas_advanced_scan_with_xrd(scan_assembler, ScanStubStatusMock):
"scan_type": "fly",
},
),
DeviceInstructionMessage(metadata={}, device="nidaq", action="stage", parameter={}),
DeviceInstructionMessage(
metadata={},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
@@ -433,7 +441,7 @@ def test_xas_advanced_scan_with_xrd(scan_assembler, ScanStubStatusMock):
),
DeviceInstructionMessage(
metadata={"readout_priority": "monitored", "RID": "my_test_request_id"},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
action="pre_scan",
parameter={},
),
@@ -459,7 +467,7 @@ def test_xas_advanced_scan_with_xrd(scan_assembler, ScanStubStatusMock):
"fake_complete",
DeviceInstructionMessage(
metadata={},
device=["bpm4i", "eiger", "mo1_bragg", "samx"],
device=["bpm4i", "eiger", "mo1_bragg", "nidaq", "samx"],
action="unstage",
parameter={},
),