From 217c27bfdb38e7b1141565317b69c4955d5b6df4 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 14:57:23 +0200 Subject: [PATCH 01/24] refactor: cleanup import for detectors --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 11 ++++++----- ophyd_devices/epics/devices/falcon_csaxs.py | 8 ++++---- ophyd_devices/epics/devices/pilatus_csaxs.py | 7 +++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 962476b..108ac00 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -1,22 +1,23 @@ import enum import threading import time -from typing import Any, List import numpy as np import os + +from typing import Any, List + from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV from ophyd import DetectorBase, Device from ophyd import ADComponent as ADCpt +from std_daq_client import StdDaqClient + from bec_lib.core import BECMessage, MessageEndpoints, threadlocked from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import bec_logger -from ophyd_devices.utils import bec_utils as bec_utils - -from std_daq_client import StdDaqClient from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin - +from ophyd_devices.utils import bec_utils logger = bec_logger.logger diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index f52f87d..2fe3b7e 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -1,17 +1,17 @@ import enum import os import time -from typing import List -from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt, Device +from typing import List + +from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt, Device from ophyd.mca import EpicsMCARecord -from ophyd.areadetector.plugins import HDF5Plugin_V21, FilePlugin_V22 from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import MessageEndpoints, BECMessage from bec_lib.core import bec_logger -from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin +from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin from ophyd_devices.utils import bec_utils logger = bec_logger.logger diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index e175eb7..0ec8780 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -1,22 +1,21 @@ import enum import json import os -import subprocess import time -from typing import List import requests import numpy as np +from typing import List + from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV from ophyd import DetectorBase, Device, Staged from ophyd import ADComponent as ADCpt -from ophyd_devices.utils import bec_utils as bec_utils from bec_lib.core import BECMessage, MessageEndpoints from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import bec_logger - +from ophyd_devices.utils import bec_utils as bec_utils from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin logger = bec_logger.logger From 88d3b92b33c9076311a3ff4ce012de94cf6de758 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 14:59:41 +0200 Subject: [PATCH 02/24] refactor: add docstrings to errors --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 2 ++ ophyd_devices/epics/devices/falcon_csaxs.py | 2 ++ ophyd_devices/epics/devices/pilatus_csaxs.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 108ac00..6783bd6 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -23,10 +23,12 @@ logger = bec_logger.logger class EigerError(Exception): + '''Base class for exceptions in this module.''' pass class EigerTimeoutError(Exception): + '''Raised when the Eiger does not respond in time during unstage.''' pass diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 2fe3b7e..440499b 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -18,10 +18,12 @@ logger = bec_logger.logger class FalconError(Exception): + '''Base class for exceptions in this module.''' pass class FalconTimeoutError(Exception): + '''Raised when the Falcon does not respond in time during unstage.''' pass diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 0ec8780..46fe0b0 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -22,10 +22,12 @@ logger = bec_logger.logger class PilatusError(Exception): + """Base class for exceptions in this module.""" pass class PilatusTimeoutError(Exception): + '''Raised when the Pilatus does not respond in time during unstage.''' pass From 702b212f50629d9ccf36071d38cc001fc155c1da Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 15:13:57 +0200 Subject: [PATCH 03/24] refactor: add docstrings and clean cam classes; dxp and hdf for falcon --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 37 +++------------ ophyd_devices/epics/devices/falcon_csaxs.py | 15 ++++--- ophyd_devices/epics/devices/pilatus_csaxs.py | 47 ++------------------ 3 files changed, 20 insertions(+), 79 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 6783bd6..c4d01cd 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -33,49 +33,23 @@ class EigerTimeoutError(Exception): class SlsDetectorCam(Device): - # detector_type = ADCpt(EpicsSignalRO, "DetectorType_RBV") - # setting = ADCpt(EpicsSignalWithRBV, "Setting") - # delay_time = ADCpt(EpicsSignalWithRBV, "DelayTime") + """SLS Detector Camera - Eiger 9M + + Base class to map EPICS PVs to ophyd signals. + """ threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy") - # enable_trimbits = ADCpt(EpicsSignalWithRBV, "Trimbits") bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth") - # num_gates = ADCpt(EpicsSignalWithRBV, "NumGates") num_cycles = ADCpt(EpicsSignalWithRBV, "NumCycles") num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames") timing_mode = ADCpt(EpicsSignalWithRBV, "TimingMode") trigger_software = ADCpt(EpicsSignal, "TriggerSoftware") - # high_voltage = ADCpt(EpicsSignalWithRBV, "HighVoltage") - # Receiver and data callback - # receiver_mode = ADCpt(EpicsSignalWithRBV, "ReceiverMode") - # receiver_stream = ADCpt(EpicsSignalWithRBV, "ReceiverStream") - # enable_data = ADCpt(EpicsSignalWithRBV, "UseDataCallback") - # missed_packets = ADCpt(EpicsSignalRO, "ReceiverMissedPackets_RBV") - # Direct settings access - # setup_file = ADCpt(EpicsSignal, "SetupFile") - # load_setup = ADCpt(EpicsSignal, "LoadSetup") - # command = ADCpt(EpicsSignal, "Command") - # Mythen 3 - # counter_mask = ADCpt(EpicsSignalWithRBV, "CounterMask") - # counter1_threshold = ADCpt(EpicsSignalWithRBV, "Counter1Threshold") - # counter2_threshold = ADCpt(EpicsSignalWithRBV, "Counter2Threshold") - # counter3_threshold = ADCpt(EpicsSignalWithRBV, "Counter3Threshold") - # gate1_delay = ADCpt(EpicsSignalWithRBV, "Gate1Delay") - # gate1_width = ADCpt(EpicsSignalWithRBV, "Gate1Width") - # gate2_delay = ADCpt(EpicsSignalWithRBV, "Gate2Delay") - # gate2_width = ADCpt(EpicsSignalWithRBV, "Gate2Width") - # gate3_delay = ADCpt(EpicsSignalWithRBV, "Gate3Delay") - # gate3_width = ADCpt(EpicsSignalWithRBV, "Gate3Width") - # # Moench - # json_frame_mode = ADCpt(EpicsSignalWithRBV, "JsonFrameMode") - # json_detector_mode = ADCpt(EpicsSignalWithRBV, "JsonDetectorMode") - - # fixes due to missing PVs from CamBase acquire = ADCpt(EpicsSignal, "Acquire") detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV") class TriggerSource(int, enum.Enum): + """Trigger signals for Eiger9M detector""" AUTO = 0 TRIGGER = 1 GATING = 2 @@ -83,6 +57,7 @@ class TriggerSource(int, enum.Enum): class DetectorState(int, enum.Enum): + """ Detector states for Eiger9M detector""" IDLE = 0 ERROR = 1 WAITING = 2 diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 440499b..a2748f8 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -28,12 +28,16 @@ class FalconTimeoutError(Exception): class DetectorState(int, enum.Enum): + """Detector states for Falcon detector""" DONE = 0 ACQUIRING = 1 class EpicsDXPFalcon(Device): - """All high-level DXP parameters for each channel""" + """DXP parameters for Falcon detector + + Base class to map EPICS PVs from DXP parameters to ophyd signals. + """ elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime") elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime") @@ -53,15 +57,16 @@ class EpicsDXPFalcon(Device): current_pixel = Cpt(EpicsSignalRO, "CurrentPixel") -class FalconHDF5Plugins(Device): # HDF5Plugin_V21, FilePlugin_V22): +class FalconHDF5Plugins(Device): + """HDF5 parameters for Falcon detector + + Base class to map EPICS PVs from HDF5 Plugin to ophyd signals. + """ capture = Cpt(EpicsSignalWithRBV, "Capture") enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config") xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config") lazy_open = Cpt(EpicsSignalWithRBV, "LazyOpen", string=True, doc="0='No' 1='Yes'") temp_suffix = Cpt(EpicsSignalWithRBV, "TempSuffix", string=True) - # file_path = Cpt( - # EpicsSignalWithRBV, "FilePath", string=True, kind="config", path_semantics="posix" - # ) file_path = Cpt(EpicsSignalWithRBV, "FilePath", string=True, kind="config") file_name = Cpt(EpicsSignalWithRBV, "FileName", string=True, kind="config") file_template = Cpt(EpicsSignalWithRBV, "FileTemplate", string=True, kind="config") diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 46fe0b0..b1c066a 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -39,50 +39,11 @@ class TriggerSource(int, enum.Enum): ALGINMENT = 4 -class SlsDetectorCam(Device): # CamBase, FileBase): - # detector_type = ADCpt(EpicsSignalRO, "DetectorType_RBV") - # setting = ADCpt(EpicsSignalWithRBV, "Setting") - # beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy") - # enable_trimbits = ADCpt(EpicsSignalWithRBV, "Trimbits") - # bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth") - # trigger_software = ADCpt(EpicsSignal, "TriggerSoftware") - # high_voltage = ADCpt(EpicsSignalWithRBV, "HighVoltage") - # Receiver and data callback - # receiver_mode = ADCpt(EpicsSignalWithRBV, "ReceiverMode") - # receiver_stream = ADCpt(EpicsSignalWithRBV, "ReceiverStream") - # enable_data = ADCpt(EpicsSignalWithRBV, "UseDataCallback") - # missed_packets = ADCpt(EpicsSignalRO, "ReceiverMissedPackets_RBV") - # # Direct settings access - # setup_file = ADCpt(EpicsSignal, "SetupFile") - # load_setup = ADCpt(EpicsSignal, "LoadSetup") - # command = ADCpt(EpicsSignal, "Command") - # Mythen 3 - # counter_mask = ADCpt(EpicsSignalWithRBV, "CounterMask") - # counter1_threshold = ADCpt(EpicsSignalWithRBV, "Counter1Threshold") - # counter2_threshold = ADCpt(EpicsSignalWithRBV, "Counter2Threshold") - # counter3_threshold = ADCpt(EpicsSignalWithRBV, "Counter3Threshold") - # gate1_delay = ADCpt(EpicsSignalWithRBV, "Gate1Delay") - # gate1_width = ADCpt(EpicsSignalWithRBV, "Gate1Width") - # gate2_delay = ADCpt(EpicsSignalWithRBV, "Gate2Delay") - # gate2_width = ADCpt(EpicsSignalWithRBV, "Gate2Width") - # gate3_delay = ADCpt(EpicsSignalWithRBV, "Gate3Delay") - # gate3_width = ADCpt(EpicsSignalWithRBV, "Gate3Width") - # Moench - # json_frame_mode = ADCpt(EpicsSignalWithRBV, "JsonFrameMode") - # json_detector_mode = ADCpt(EpicsSignalWithRBV, "JsonDetectorMode") +class SlsDetectorCam(Device): + """SLS Detector Camera - Pilatus - # Eiger9M - # delay_time = ADCpt(EpicsSignalWithRBV, "DelayTime") - # num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames") - # acquire = ADCpt(EpicsSignal, "Acquire") - # acquire_time = ADCpt(EpicsSignal, 'AcquireTime') - # detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV") - # threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") - # num_gates = ADCpt(EpicsSignalWithRBV, "NumGates") - # num_cycles = ADCpt(EpicsSignalWithRBV, "NumCycles") - # timing_mode = ADCpt(EpicsSignalWithRBV, "TimingMode") - - # Pilatus_2 300k + Base class to map EPICS PVs to ophyd signals. + """ num_images = ADCpt(EpicsSignalWithRBV, "NumImages") num_exposures = ADCpt(EpicsSignalWithRBV, "NumExposures") delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures") From 22e63c4976eb2c19acc3234ccb8296dbed1b3d22 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 15:55:47 +0200 Subject: [PATCH 04/24] refactor: add documentation, clean up init function and unify classes --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 32 +++++++++--- ophyd_devices/epics/devices/falcon_csaxs.py | 51 +++++++++++++++----- ophyd_devices/epics/devices/pilatus_csaxs.py | 27 +++++++++-- 3 files changed, 87 insertions(+), 23 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index c4d01cd..049fdba 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -83,6 +83,7 @@ class Eiger9mCsaxs(DetectorBase): """ + # Specify which functions are revealed to the user in BEC client USER_ACCESS = [ "describe", ] @@ -102,6 +103,18 @@ class Eiger9mCsaxs(DetectorBase): sim_mode=False, **kwargs, ): + """Initialize the Eiger9M detector + Args: + #TODO add here the parameters for kind, read_attrs, configuration_attrs, parent + prefix (str): PV prefix (X12SA-ES-EIGER9M:) + name (str): 'eiger' + kind (str): + read_attrs (list): + configuration_attrs (list): + parent (object): + device_manager (object): BEC device manager + sim_mode (bool): simulation mode to start the detector without BEC, e.g. from ipython shell + """ super().__init__( prefix=prefix, name=name, @@ -111,13 +124,16 @@ class Eiger9mCsaxs(DetectorBase): parent=parent, **kwargs, ) - self._stopped = False - self._lock = threading.RLock() if device_manager is None and not sim_mode: raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") - + + # Not sure if this is needed, comment it for now! + #self._lock = threading.RLock() + self._stopped = False self.name = name - self.wait_for_connection() # Make sure to be connected before talking to PVs + self.wait_for_connection() + # Spin up connections for simulation or BEC mode + # TODO check if sim_mode still works. Is it needed? I believe filewriting might be handled properly if not sim_mode: from bec_lib.core.bec_service import SERVICE_CONFIG @@ -125,17 +141,17 @@ class Eiger9mCsaxs(DetectorBase): self._producer = self.device_manager.producer self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] else: + base_path = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self.scaninfo.load_scan_metadata() - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.service_cfg = {"base_path": base_path} + self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self.scaninfo.load_scan_metadata() - # TODO - self.filepath = "" - self.filewriter = FileWriterMixin(self.service_cfg) + self.reduce_readout = 1e-3 # 3 ms self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered self._init_eiger9m() diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index a2748f8..f753110 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -77,29 +77,40 @@ class FalconHDF5Plugins(Device): class FalconCsaxs(Device): - """FalxonX1 with HDF5 writer""" + """Falcon Sitoro detector for CSAXS + + Parent class: Device + Device classes: EpicsDXPFalcon dxp1:, EpicsMCARecord mca1, FalconHDF5Plugins HDF1: + + Attributes: + name str: 'falcon' + prefix (str): PV prefix ("X12SA-SITORO:) + + """ + + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = [ + "describe", + ] dxp = Cpt(EpicsDXPFalcon, "dxp1:") mca = Cpt(EpicsMCARecord, "mca1") hdf5 = Cpt(FalconHDF5Plugins, "HDF1:") - # Control + # specify Epics PVs for Falcon + # TODO consider moving this outside of this class! stop_all = Cpt(EpicsSignal, "StopAll") erase_all = Cpt(EpicsSignal, "EraseAll") start_all = Cpt(EpicsSignal, "StartAll") state = Cpt(EpicsSignal, "Acquiring") - # Preset options preset_mode = Cpt(EpicsSignal, "PresetMode") # 0 No preset 1 Real time 2 Events 3 Triggers preset_real = Cpt(EpicsSignal, "PresetReal") preset_events = Cpt(EpicsSignal, "PresetEvents") preset_triggers = Cpt(EpicsSignal, "PresetTriggers") - # read-only diagnostics triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True) events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True) input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True) output_count_rate = Cpt(EpicsSignalRO, "MaxOutputCountRate", lazy=True) - - # Mapping control collect_mode = Cpt(EpicsSignal, "CollectMode") # 0 MCA spectra, 1 MCA mapping pixel_advance_mode = Cpt(EpicsSignal, "PixelAdvanceMode") ignore_gate = Cpt(EpicsSignal, "IgnoreGate") @@ -109,8 +120,6 @@ class FalconCsaxs(Device): pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun") nd_array_mode = Cpt(EpicsSignal, "NDArrayMode") - # HDF5 - def __init__( self, prefix="", @@ -124,6 +133,18 @@ class FalconCsaxs(Device): sim_mode=False, **kwargs, ): + """Initialize Falcon detector + Args: + #TODO add here the parameters for kind, read_attrs, configuration_attrs, parent + prefix (str): PV prefix ("X12SA-SITORO:) + name (str): 'falcon' + kind (str): + read_attrs (list): + configuration_attrs (list): + parent (object): + device_manager (object): BEC device manager + sim_mode (bool): simulation mode to start the detector without BEC, e.g. from ipython shell + """ super().__init__( prefix=prefix, name=name, @@ -137,7 +158,8 @@ class FalconCsaxs(Device): raise FalconError("Add DeviceManager to initialization or init with sim_mode=True") self._stopped = False self.name = name - self.wait_for_connection() # Make sure to be connected before talking to PVs + self.wait_for_connection() + # Spin up connections for simulation or BEC mode if not sim_mode: from bec_lib.core.bec_service import SERVICE_CONFIG @@ -145,15 +167,21 @@ class FalconCsaxs(Device): self._producer = self.device_manager.producer self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] else: + base_path = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self.scaninfo.load_scan_metadata() - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.service_cfg = {"base_path": base_path} + self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self.scaninfo.load_scan_metadata() self.filewriter = FileWriterMixin(self.service_cfg) - + self._init_detector() + + def _init_detector(self) -> None: + """Set up detector parameters, init detector, init filewriter + """ self.readout = 0.003 # 3 ms self._value_pixel_per_buffer = 20 # 16 self._clean_up() @@ -162,6 +190,7 @@ class FalconCsaxs(Device): def _clean_up(self) -> None: """Clean up""" + #TODO clarify when to use put and when to use set! self.hdf5.capture.put(0) self.stop_all.put(1) self.erase_all.put(1) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index b1c066a..61849f3 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -78,6 +78,11 @@ class PilatusCsaxs(DetectorBase): """ + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = [ + "describe", + ] + cam = ADCpt(SlsDetectorCam, "cam1:") def __init__( @@ -93,6 +98,18 @@ class PilatusCsaxs(DetectorBase): sim_mode=False, **kwargs, ): + """Initialize the Pilatus detector + Args: + #TODO add here the parameters for kind, read_attrs, configuration_attrs, parent + prefix (str): PV prefix ("X12SA-ES-PILATUS300K:) + name (str): 'pilatus_2' + kind (str): + read_attrs (list): + configuration_attrs (list): + parent (object): + device_manager (object): BEC device manager + sim_mode (bool): simulation mode to start the detector without BEC, e.g. from ipython shell + """ super().__init__( prefix=prefix, name=name, @@ -106,7 +123,8 @@ class PilatusCsaxs(DetectorBase): raise PilatusError("Add DeviceManager to initialization or init with sim_mode=True") self.name = name - self.wait_for_connection() # Make sure to be connected before talking to PVs + self.wait_for_connection() + # Spin up connections for simulation or BEC mode if not sim_mode: from bec_lib.core.bec_service import SERVICE_CONFIG @@ -114,16 +132,17 @@ class PilatusCsaxs(DetectorBase): self._producer = self.device_manager.producer self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] else: + base_path = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" self._producer = bec_utils.MockProducer() self.device_manager = bec_utils.MockDeviceManager() self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self.scaninfo.load_scan_metadata() - self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"} + self.service_cfg = {"base_path": base_path} self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) - self.filepath_h5 = "" - + self.scaninfo.load_scan_metadata() self.filewriter = FileWriterMixin(self.service_cfg) + self.readout = 1e-3 # 3 ms # TODO maybe needed From 55d20a0ed056b0f2855a89b14672d1ee8f3a99a7 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 16:05:19 +0200 Subject: [PATCH 05/24] refactor: add _init function to all classes --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 16 ++++++++-------- ophyd_devices/epics/devices/falcon_csaxs.py | 8 ++++---- ophyd_devices/epics/devices/pilatus_csaxs.py | 9 +++++---- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 049fdba..008b83b 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -42,7 +42,7 @@ class SlsDetectorCam(Device): bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth") num_cycles = ADCpt(EpicsSignalWithRBV, "NumCycles") num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames") - timing_mode = ADCpt(EpicsSignalWithRBV, "TimingMode") + trigger_mode = ADCpt(EpicsSignalWithRBV, "TimingMode") trigger_software = ADCpt(EpicsSignal, "TriggerSoftware") acquire = ADCpt(EpicsSignal, "Acquire") detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV") @@ -151,16 +151,16 @@ class Eiger9mCsaxs(DetectorBase): self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self.scaninfo.load_scan_metadata() self.filewriter = FileWriterMixin(self.service_cfg) + self._init() - self.reduce_readout = 1e-3 # 3 ms - self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered + #TODO function for abstract class? + def _init(self) -> None: + """Initialize detector, filewriter and set default parameters + """ + self.reduce_readout = 1e-3 self._init_eiger9m() self._init_standard_daq() - # self.mokev = self.device_manager.devices.mokev.read()[ - # self.device_manager.devices.mokev.name - # ]["value"] - def _init_eiger9m(self) -> None: """Init parameters for Eiger 9m""" self._set_trigger(TriggerSource.GATING) @@ -238,7 +238,7 @@ class Eiger9mCsaxs(DetectorBase): BURST_TRIGGER = 3 """ value = int(trigger_source) - self.cam.timing_mode.put(value) + self.cam.trigger_mode.put(value) def _prep_file_writer(self) -> None: self.filepath = self.filewriter.compile_full_filename( diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index f753110..bf85e32 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -177,12 +177,12 @@ class FalconCsaxs(Device): self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self.scaninfo.load_scan_metadata() self.filewriter = FileWriterMixin(self.service_cfg) - self._init_detector() + self._init() - def _init_detector(self) -> None: - """Set up detector parameters, init detector, init filewriter + def _init(self) -> None: + """Initialize detector, filewriter and set default parameters """ - self.readout = 0.003 # 3 ms + self.readout = 1e-3 self._value_pixel_per_buffer = 20 # 16 self._clean_up() self._init_hdf5_saving() diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 61849f3..a857b0d 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -142,11 +142,12 @@ class PilatusCsaxs(DetectorBase): self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self.scaninfo.load_scan_metadata() self.filewriter = FileWriterMixin(self.service_cfg) - - self.readout = 1e-3 # 3 ms + self._init() - # TODO maybe needed - # self._close_file_writer() + def _init(self) -> None: + """Initialize detector, filewriter and set default parameters + """ + self.reduce_readout = 1e-3 def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) From 920d7bb2b108e8f48d8b28ae3b5e35fcb9113e05 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 16:16:36 +0200 Subject: [PATCH 06/24] refactor: change _init filewriter and detector for eiger9m --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 60 ++++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 008b83b..be87631 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -152,19 +152,47 @@ class Eiger9mCsaxs(DetectorBase): self.scaninfo.load_scan_metadata() self.filewriter = FileWriterMixin(self.service_cfg) self._init() - + #TODO function for abstract class? def _init(self) -> None: """Initialize detector, filewriter and set default parameters """ self.reduce_readout = 1e-3 - self._init_eiger9m() - self._init_standard_daq() + self._init_detector() + self._init_filewriter() - def _init_eiger9m(self) -> None: - """Init parameters for Eiger 9m""" - self._set_trigger(TriggerSource.GATING) + def _init_detector(self) -> None: + """Init parameters for Eiger 9m. + Depends on hardware configuration and delay generators. + At this point it is set up for gating mode (09/2023). + """ self.stop_acquisition() + self._set_trigger(TriggerSource.GATING) + + def _init_filewriter(self) -> None: + """Init parameters for filewriter. + For the Eiger9M, the data backend is std_daq client. + Setting up these parameters depends on the backend, and would need to change upon changes in the backend. + """ + self.std_rest_server_url = "http://xbl-daq-29:5000" + self.std_client = StdDaqClient(url_base=self.std_rest_server_url) + self.std_client.stop_writer() + timeout = 0 + # TODO changing e-account was not possible during beamtimes. + # self._update_std_cfg("writer_user_id", int(self.scaninfo.username.strip(" e"))) + # time.sleep(5) + #TODO is this the only state to wait for or should we wait for more from the std_daq client? + while not self.std_client.get_status()["state"] == "READY": + time.sleep(0.1) + timeout = timeout + 0.1 + logger.info("Waiting for std_daq init.") + if timeout > 5: + if not self.std_client.get_status()["state"]: + raise EigerError( + f"Std client not in READY state, returns: {self.std_client.get_status()}" + ) + else: + return def _update_std_cfg(self, cfg_key: str, value: Any) -> None: cfg = self.std_client.get_config() @@ -183,25 +211,7 @@ class Eiger9mCsaxs(DetectorBase): logger.info(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}") self.std_client.set_config(cfg) - def _init_standard_daq(self) -> None: - self.std_rest_server_url = "http://xbl-daq-29:5000" - self.std_client = StdDaqClient(url_base=self.std_rest_server_url) - self.std_client.stop_writer() - timeout = 0 - # TODO put back change of e-account! - # self._update_std_cfg("writer_user_id", int(self.scaninfo.username.strip(" e"))) - # time.sleep(5) - while not self.std_client.get_status()["state"] == "READY": - time.sleep(0.1) - timeout = timeout + 0.1 - logger.info("Waiting for std_daq init.") - if timeout > 5: - if not self.std_client.get_status()["state"]: - raise EigerError( - f"Std client not in READY state, returns: {self.std_client.get_status()}" - ) - else: - return + def _prep_det(self) -> None: self._set_det_threshold() From 4c6e99af118c0987a6b01c28cce503ecfa63d0a5 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 16:25:22 +0200 Subject: [PATCH 07/24] refactor: add comment to loggers in _update_std_cfg --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index be87631..ebe339e 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -157,9 +157,15 @@ class Eiger9mCsaxs(DetectorBase): def _init(self) -> None: """Initialize detector, filewriter and set default parameters """ - self.reduce_readout = 1e-3 + self._default_parameter() self._init_detector() self._init_filewriter() + + def _default_parameter(self) -> None: + """Set default parameters for Eiger 9M + readout (float) : readout time in seconds + """ + self.reduce_readout = 1e-3 def _init_detector(self) -> None: """Init parameters for Eiger 9m. @@ -195,6 +201,8 @@ class Eiger9mCsaxs(DetectorBase): return def _update_std_cfg(self, cfg_key: str, value: Any) -> None: + """Update std_daq config with new e-account for the current beamtime""" + #TODO Do we need all the loggers here, should this be properly refactored with a DEBUG mode? cfg = self.std_client.get_config() old_value = cfg.get(cfg_key) logger.info(old_value) From 6f49be47758a269e8f5f8a8ca9661f8e79dfafda Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 16:29:19 +0200 Subject: [PATCH 08/24] refactor: change _init for falcon detector --- ophyd_devices/epics/devices/falcon_csaxs.py | 34 ++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index bf85e32..a6091da 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -178,15 +178,21 @@ class FalconCsaxs(Device): self.scaninfo.load_scan_metadata() self.filewriter = FileWriterMixin(self.service_cfg) self._init() - + def _init(self) -> None: """Initialize detector, filewriter and set default parameters """ + self._default_parameter() + self._init_detector() + self._init_filewriter() + + def _default_parameter(self) -> None: + """Set default parameters for Falcon + readout (float): readout time in seconds + _value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro""" self.readout = 1e-3 self._value_pixel_per_buffer = 20 # 16 - self._clean_up() - self._init_hdf5_saving() - self._init_mapping_mode() + self._clean_up() def _clean_up(self) -> None: """Clean up""" @@ -195,16 +201,22 @@ class FalconCsaxs(Device): self.stop_all.put(1) self.erase_all.put(1) - def _init_hdf5_saving(self) -> None: - """Set up hdf5 save parameters""" + def _init_filewriter(self) -> None: + """Initialize file writer for Falcon. + This includes setting variables for the HDF5 plugin (EPICS) that is used to write the data. + """ self.hdf5.enable.put(1) # EnableCallbacks self.hdf5.xml_file_name.put("layout.xml") # Points to hardcopy of HDF5 Layout xml file - self.hdf5.lazy_open.put(1) # Yes -> To be checked how to add FilePlugin_V21+ + self.hdf5.lazy_open.put(1) # Potentially not needed, means a temp data file is created first, could be 0 self.hdf5.temp_suffix.put("") # -> To be checked how to add FilePlugin_V22+ - self.hdf5.queue_size.put(2000) + self.hdf5.queue_size.put(2000) # size of queue for spectra in the buffer - def _init_mapping_mode(self) -> None: - """Set up mapping mode params""" + def _init_detector(self) -> None: + """Initialize Falcon detector. + The detector is operated in MCA mapping mode. + Parameters here affect the triggering, gating etc. + This includes also the readout chunk size and whether data is segmented into spectra in EPICS. + """ self.collect_mode.put(1) # 1 MCA Mapping, 0 MCA Spectrum self.preset_mode.put(1) # 1 Realtime self.input_logic_polarity.put(0) # 0 Normal, 1 Inverted @@ -212,7 +224,7 @@ class FalconCsaxs(Device): self.ignore_gate.put(0) # 1 Yes, 0 No self.auto_pixels_per_buffer.put(0) # 0 Manual 1 Auto self.pixels_per_buffer.put(self._value_pixel_per_buffer) # - self.nd_array_mode.put(1) + self.nd_array_mode.put(1) # Segmentation happens in EPICS def _prep_det(self) -> None: """Prepare detector for acquisition""" From c5951b3c5bea572a8962d920c780870a78e98d39 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 16:32:19 +0200 Subject: [PATCH 09/24] refactor: change _init for pilatus --- ophyd_devices/epics/devices/pilatus_csaxs.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index a857b0d..212336a 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -147,7 +147,25 @@ class PilatusCsaxs(DetectorBase): def _init(self) -> None: """Initialize detector, filewriter and set default parameters """ - self.reduce_readout = 1e-3 + self._default_parameter() + self._init_detector() + self._init_filewriter() + + def _default_parameter(self) -> None: + """Set default parameters for Pilatus300k detector + readout (float): readout time in seconds + """ + self.reduce_readout = 1e-3 + + def _init_detector(self) -> None: + """Initialize the detector""" + #TODO add check if detector is running + pass + + def _init_filewriter(self) -> None: + """Initialize the file writer""" + #TODO in case the data backend is rewritten, add check if it is ready! + pass def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) From 6dae767c5e21cd1e6ce3e045e6d99bf984098c2c Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 16:56:56 +0200 Subject: [PATCH 10/24] refactor: eiger9m stage function, refactoring --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 135 +++++++++++-------- 1 file changed, 76 insertions(+), 59 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index ebe339e..620d811 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -40,7 +40,7 @@ class SlsDetectorCam(Device): threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy") bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth") - num_cycles = ADCpt(EpicsSignalWithRBV, "NumCycles") + num_images = ADCpt(EpicsSignalWithRBV, "NumCycles") num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames") trigger_mode = ADCpt(EpicsSignalWithRBV, "TimingMode") trigger_software = ADCpt(EpicsSignal, "TriggerSoftware") @@ -161,12 +161,14 @@ class Eiger9mCsaxs(DetectorBase): self._init_detector() self._init_filewriter() + #TODO function for abstract class? def _default_parameter(self) -> None: """Set default parameters for Eiger 9M readout (float) : readout time in seconds """ self.reduce_readout = 1e-3 + #TODO function for abstract class? def _init_detector(self) -> None: """Init parameters for Eiger 9m. Depends on hardware configuration and delay generators. @@ -175,6 +177,7 @@ class Eiger9mCsaxs(DetectorBase): self.stop_acquisition() self._set_trigger(TriggerSource.GATING) + #TODO function for abstract class? def _init_filewriter(self) -> None: """Init parameters for filewriter. For the Eiger9M, the data backend is std_daq client. @@ -219,53 +222,49 @@ class Eiger9mCsaxs(DetectorBase): logger.info(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}") self.std_client.set_config(cfg) - - - def _prep_det(self) -> None: - self._set_det_threshold() - self._set_acquisition_params() - self._set_trigger(TriggerSource.GATING) - - def _set_det_threshold(self) -> None: - # threshold_energy PV exists on Eiger 9M? - factor = 1 - if self.cam.threshold_energy._metadata["units"] == "eV": - factor = 1000 - setp_energy = int(self.mokev * factor) - energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"] - if setp_energy != energy: - self.cam.beam_energy.set(setp_energy) # .wait() - threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] - if not np.isclose(setp_energy / 2, threshold, rtol=0.05): - self.cam.threshold_energy.set(setp_energy / 2) # .wait() - - def _set_acquisition_params(self) -> None: - # self.cam.acquire_time.set(self.scaninfo.exp_time) - # Set acquisition parameters slightly shorter then cycle - # self.cam.acquire_period.set( - # self.scaninfo.exp_time + (self.scaninfo.readout_time - self.reduce_readout) - # ) - self.cam.num_cycles.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) - self.cam.num_frames.put(1) - - def _set_trigger(self, trigger_source: TriggerSource) -> None: - """Set trigger source for the detector, either directly to value or TriggerSource.* with - AUTO = 0 - TRIGGER = 1 - GATING = 2 - BURST_TRIGGER = 3 + #TODO function for abstract class? + def stage(self) -> List[object]: + """Stage command, called from BEC in preparation of a scan. + The device needs to return with a state once it is ready to start the scan! """ - value = int(trigger_source) - self.cam.trigger_mode.put(value) + # Set parameters for scan interuption and if acquisition is done + self._stopped = False + self._acquisition_done = False + # Get parameters for scan + self.scaninfo.load_scan_metadata() + self.mokev = self.device_manager.devices.mokev.obj.read()[ + self.device_manager.devices.mokev.name + ]["value"] + # Prepare file writer and detector + self._prep_file_writer() + self._prep_det() + #TODO refactor logger.info to DEBUG mode? + logger.info("Waiting for std daq to be armed") + logger.info("std_daq is ready") + + self._publish_file_location() + self.arm_acquisition() + #TODO Fix should take place in EPICS or directly on the hardware! + # We observed that the detector missed triggers in the beginning in case BEC was to fast. Adding 50ms delay solved this + time.sleep(0.05) + return super().stage() + def _prep_file_writer(self) -> None: + """Prepare file writer for scan + + self.filewriter is a FileWriterMixin object that hosts logic for compiling the filepath + """ self.filepath = self.filewriter.compile_full_filename( self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True ) + # TODO needed, should be checked from the filerwriter mixin right? while not os.path.exists(os.path.dirname(self.filepath)): time.sleep(0.1) + self._close_file_writer() logger.info(f" std_daq output filepath {self.filepath}") + #TODO Discuss with Leo if this is needed, or how to start the async writing best try: self.std_client.start_writer_async( { @@ -283,25 +282,48 @@ class Eiger9mCsaxs(DetectorBase): if det_ctrl == "WAITING_IMAGES": break time.sleep(0.005) - + def _close_file_writer(self) -> None: + """Close file writer""" self.std_client.stop_writer() - pass + #TODO can I wait for a status message here maybe? To ensure writer returned - def stage(self) -> List[object]: - """stage the detector and file writer""" - self._stopped = False - self._acquisition_done = False - self.scaninfo.load_scan_metadata() - self.mokev = self.device_manager.devices.mokev.obj.read()[ - self.device_manager.devices.mokev.name - ]["value"] + def _prep_det(self) -> None: + """Prepare detector for scan. + Includes checking the detector threshold, setting the acquisition parameters and setting the trigger source + """ + self._set_det_threshold() + self._set_acquisition_params() + self._set_trigger(TriggerSource.GATING) - self._prep_file_writer() - self._prep_det() - logger.info("Waiting for std daq to be armed") - logger.info("std_daq is ready") + def _set_det_threshold(self) -> None: + """Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance""" + # threshold energy might be in eV or keV + factor = 1 + if self.cam.threshold_energy._metadata["units"] == "eV": + factor = 1000 + setpoint = int(self.mokev * factor) + energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"] + if setpoint != energy: + self.cam.beam_energy.set(setpoint) + threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] + if not np.isclose(setpoint / 2, threshold, rtol=0.05): + self.cam.threshold_energy.set(setpoint / 2) + def _set_acquisition_params(self) -> None: + """Set acquisition parameters for the detector""" + self.cam.num_images.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) + self.cam.num_frames.put(1) + + def _set_trigger(self, trigger_source: TriggerSource) -> None: + """Set trigger source for the detector. + Check the TriggerSource enum for possible values + """ + value = int(trigger_source) + self.cam.trigger_mode.put(value) + + def _publish_file_location(self) -> None: + """Publish the filepath to REDIS for file writer""" msg = BECMessage.FileMessage(file_path=self.filepath, done=False) self._producer.set_and_publish( MessageEndpoints.public_file(self.scaninfo.scanID, self.name), @@ -312,13 +334,8 @@ class Eiger9mCsaxs(DetectorBase): MessageEndpoints.file_event(self.name), msg.dumps(), ) - self.arm_acquisition() - - self._stopped = False - # We see that we miss a trigger occasionally, it seems that status msg from the ioc are not realiable - time.sleep(0.05) - return super().stage() - + + #TODO needed? if yes why only for the eiger9m? @threadlocked def unstage(self) -> List[object]: """unstage the detector and file writer""" From ce8616a9798f191659e8dd1afa52d9038e4cff84 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 16:58:44 +0200 Subject: [PATCH 11/24] refactor: reworked arm to --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 32 +++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 620d811..1cdb4a9 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -250,6 +250,7 @@ class Eiger9mCsaxs(DetectorBase): time.sleep(0.05) return super().stage() + #TODO function for abstract class? def _prep_file_writer(self) -> None: """Prepare file writer for scan @@ -283,11 +284,13 @@ class Eiger9mCsaxs(DetectorBase): break time.sleep(0.005) + #TODO function for abstract class? def _close_file_writer(self) -> None: """Close file writer""" self.std_client.stop_writer() #TODO can I wait for a status message here maybe? To ensure writer returned + #TODO function for abstract class? def _prep_det(self) -> None: """Prepare detector for scan. Includes checking the detector threshold, setting the acquisition parameters and setting the trigger source @@ -334,6 +337,20 @@ class Eiger9mCsaxs(DetectorBase): MessageEndpoints.file_event(self.name), msg.dumps(), ) + + def arm_acquisition(self) -> None: + """Arm detector for acquisition + """ + self.cam.acquire.put(1) + logger.info("Waiting for Eiger9m to be armed") + while True: + det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] + if det_ctrl == int(DetectorState.RUNNING): + break + if self._stopped == True: + break + time.sleep(0.005) + logger.info("Eiger9m is armed") #TODO needed? if yes why only for the eiger9m? @threadlocked @@ -389,21 +406,6 @@ class Eiger9mCsaxs(DetectorBase): ) self._close_file_writer() - def arm_acquisition(self) -> None: - """Start acquisition in software trigger mode, - or arm the detector in hardware of the detector - """ - self.cam.acquire.put(1) - logger.info("Waiting for Eiger9m to be armed") - while True: - det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] - if det_ctrl == int(DetectorState.RUNNING): - break - if self._stopped == True: - break - time.sleep(0.005) - logger.info("Eiger9m is armed") - def stop_acquisition(self) -> None: """Stop the detector and wait for the proper status message""" logger.info("Waiting for Eiger9m to be armed") From c2e4bbc4067427113f0a1ec1ede69a2c84a381e5 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 17:08:38 +0200 Subject: [PATCH 12/24] refactor: small change on eiger arm --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 1cdb4a9..fbecb68 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -229,20 +229,18 @@ class Eiger9mCsaxs(DetectorBase): """ # Set parameters for scan interuption and if acquisition is done self._stopped = False - self._acquisition_done = False # Get parameters for scan self.scaninfo.load_scan_metadata() self.mokev = self.device_manager.devices.mokev.obj.read()[ self.device_manager.devices.mokev.name ]["value"] - # Prepare file writer and detector - self._prep_file_writer() - self._prep_det() #TODO refactor logger.info to DEBUG mode? - logger.info("Waiting for std daq to be armed") - logger.info("std_daq is ready") - + #logger.info("Waiting for std daq to be armed") + self._prep_file_writer() + #logger.info("std_daq is ready") + self._prep_det() + #logger.info("Eiger9m is ready") self._publish_file_location() self.arm_acquisition() #TODO Fix should take place in EPICS or directly on the hardware! @@ -326,7 +324,9 @@ class Eiger9mCsaxs(DetectorBase): self.cam.trigger_mode.put(value) def _publish_file_location(self) -> None: - """Publish the filepath to REDIS for file writer""" + """Publish the filepath to REDIS for file writer + file_event #TODO what was the purpose of this again? + """ msg = BECMessage.FileMessage(file_path=self.filepath, done=False) self._producer.set_and_publish( MessageEndpoints.public_file(self.scaninfo.scanID, self.name), @@ -343,6 +343,7 @@ class Eiger9mCsaxs(DetectorBase): """ self.cam.acquire.put(1) logger.info("Waiting for Eiger9m to be armed") + #TODO add here timeout? while True: det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] if det_ctrl == int(DetectorState.RUNNING): From 4c120b0d4f4f1bd6a5a2d55a96d8ee7b85eb97a4 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Fri, 20 Oct 2023 17:29:21 +0200 Subject: [PATCH 13/24] refactor: prep detector and filewriter for falcon; stage refactored --- ophyd_devices/epics/devices/falcon_csaxs.py | 58 +++++++++++--------- ophyd_devices/epics/devices/pilatus_csaxs.py | 4 -- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index a6091da..3b89177 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -226,17 +226,38 @@ class FalconCsaxs(Device): self.pixels_per_buffer.put(self._value_pixel_per_buffer) # self.nd_array_mode.put(1) # Segmentation happens in EPICS + def stage(self) -> List[object]: + """Stage command, called from BEC in preparation of a scan. + The device needs to return with a state once it is ready to start the scan! + """ + # Set parameters for scan interuption and if acquisition is done + self._stopped = False + # Get parameters for scan + self.scaninfo.load_scan_metadata() + self.mokev = self.device_manager.devices.mokev.obj.read()[ + self.device_manager.devices.mokev.name + ]["value"] + + # Prepare file writer and detector + #TODO refactor logger.info to DEBUG mode? + #logger.info("Waiting for falcon filewriter to be ready") + self._prep_file_writer() + #logger.info("falcon file writer ready") + self._prep_det() + #logger.info("falcon is ready") + self._publish_file_location() + self.arm_acquisition() + return super().stage() + def _prep_det(self) -> None: """Prepare detector for acquisition""" self.collect_mode.put(1) self.preset_real.put(self.scaninfo.exp_time) self.pixels_per_run.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) - # self.auto_pixels_per_buffer.put(0) - # self.pixels_per_buffer.put(self._value_pixel_per_buffer) def _prep_file_writer(self) -> None: - """Prep HDF5 weriting""" - # TODO creta filename and destination path from filepath + """Prepare filewriting from HDF5 plugin + #TODO check file_write_mode value in EPICs for details""" self.destination_path = self.filewriter.compile_full_filename( self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True ) @@ -250,29 +271,18 @@ class FalconCsaxs(Device): self.hdf5.array_counter.put(0) self.hdf5.capture.put(1) - def stage(self) -> List[object]: - """stage the detector and file writer""" - # TODO clean up needed? - self._stopped = False - self.scaninfo.load_scan_metadata() - self.mokev = self.device_manager.devices.mokev.obj.read()[ - self.device_manager.devices.mokev.name - ]["value"] - - logger.info("Waiting for pilatus2 to be armed") - self._prep_det() - logger.info("Pilatus2 armed") - logger.info("Waiting for pilatus2 zmq stream to be ready") - self._prep_file_writer() - logger.info("Pilatus2 zmq ready") - - msg = BECMessage.FileMessage(file_path=self.destination_path, done=False) + def _publish_file_location(self) -> None: + """Publish the filepath to REDIS for file writer""" + msg = BECMessage.FileMessage(file_path=self.filepath, done=False) self._producer.set_and_publish( MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), ) - self.arm_acquisition() + + def arm_acquisition(self) -> None: + self.start_all.put(1) logger.info("Waiting for Falcon to be armed") + #TODO add here timeout? while True: det_ctrl = self.state.read()[self.state.name]["value"] if det_ctrl == int(DetectorState.ACQUIRING): @@ -281,10 +291,6 @@ class FalconCsaxs(Device): break time.sleep(0.005) logger.info("Falcon is armed") - return super().stage() - - def arm_acquisition(self) -> None: - self.start_all.put(1) def unstage(self) -> List[object]: logger.info("Waiting for Falcon to return from acquisition") diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 212336a..cb6ca40 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -167,10 +167,6 @@ class PilatusCsaxs(DetectorBase): #TODO in case the data backend is rewritten, add check if it is ready! pass - def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: - msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) - return BECMessage.ScanStatusMessage.loads(msg) - def _prep_det(self) -> None: # TODO slow reaction, seemed to have timeout. self._set_det_threshold() From 78765100bada2afebea8f3b6c266859d53af341d Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Mon, 23 Oct 2023 10:45:34 +0200 Subject: [PATCH 14/24] refactor: pilatus bugfix --- ophyd_devices/epics/devices/pilatus_csaxs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index cb6ca40..58930a0 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -177,10 +177,10 @@ class PilatusCsaxs(DetectorBase): factor = 1 if self.cam.threshold_energy._metadata["units"] == "eV": factor = 1000 - setp_energy = int(self.mokev * factor) + setpoint = int(self.mokev * factor) threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] - if not np.isclose(setp_energy / 2, threshold, rtol=0.05): - self.cam.threshold_energy.set(setp_energy / 2) + if not np.isclose(setpoint / 2, threshold, rtol=0.05): + self.cam.threshold_energy.set(setpoint / 2) def _set_acquisition_params(self) -> None: """set acquisition parameters on the detector""" From e6d05c9d022a13fa93d2642ef8560a0c38a425ac Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Tue, 24 Oct 2023 10:55:09 +0200 Subject: [PATCH 15/24] refactor: eiger, add trigger function --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 91 +++++++++++--------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index fbecb68..86df31e 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -17,18 +17,20 @@ from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core import bec_logger from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin -from ophyd_devices.utils import bec_utils +from ophyd_devices.utils import bec_utils logger = bec_logger.logger class EigerError(Exception): - '''Base class for exceptions in this module.''' + """Base class for exceptions in this module.""" + pass class EigerTimeoutError(Exception): - '''Raised when the Eiger does not respond in time during unstage.''' + """Raised when the Eiger does not respond in time during unstage.""" + pass @@ -37,6 +39,7 @@ class SlsDetectorCam(Device): Base class to map EPICS PVs to ophyd signals. """ + threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy") bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth") @@ -50,6 +53,7 @@ class SlsDetectorCam(Device): class TriggerSource(int, enum.Enum): """Trigger signals for Eiger9M detector""" + AUTO = 0 TRIGGER = 1 GATING = 2 @@ -57,7 +61,8 @@ class TriggerSource(int, enum.Enum): class DetectorState(int, enum.Enum): - """ Detector states for Eiger9M detector""" + """Detector states for Eiger9M detector""" + IDLE = 0 ERROR = 1 WAITING = 2 @@ -108,10 +113,10 @@ class Eiger9mCsaxs(DetectorBase): #TODO add here the parameters for kind, read_attrs, configuration_attrs, parent prefix (str): PV prefix (X12SA-ES-EIGER9M:) name (str): 'eiger' - kind (str): - read_attrs (list): - configuration_attrs (list): - parent (object): + kind (str): + read_attrs (list): + configuration_attrs (list): + parent (object): device_manager (object): BEC device manager sim_mode (bool): simulation mode to start the detector without BEC, e.g. from ipython shell """ @@ -126,9 +131,9 @@ class Eiger9mCsaxs(DetectorBase): ) if device_manager is None and not sim_mode: raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") - + # Not sure if this is needed, comment it for now! - #self._lock = threading.RLock() + # self._lock = threading.RLock() self._stopped = False self.name = name self.wait_for_connection() @@ -153,31 +158,30 @@ class Eiger9mCsaxs(DetectorBase): self.filewriter = FileWriterMixin(self.service_cfg) self._init() - #TODO function for abstract class? + # TODO function for abstract class? def _init(self) -> None: - """Initialize detector, filewriter and set default parameters - """ + """Initialize detector, filewriter and set default parameters""" self._default_parameter() self._init_detector() self._init_filewriter() - - #TODO function for abstract class? + + # TODO function for abstract class? def _default_parameter(self) -> None: """Set default parameters for Eiger 9M readout (float) : readout time in seconds """ - self.reduce_readout = 1e-3 + self.reduce_readout = 1e-3 - #TODO function for abstract class? + # TODO function for abstract class? def _init_detector(self) -> None: """Init parameters for Eiger 9m. - Depends on hardware configuration and delay generators. + Depends on hardware configuration and delay generators. At this point it is set up for gating mode (09/2023). """ self.stop_acquisition() self._set_trigger(TriggerSource.GATING) - #TODO function for abstract class? + # TODO function for abstract class? def _init_filewriter(self) -> None: """Init parameters for filewriter. For the Eiger9M, the data backend is std_daq client. @@ -187,10 +191,10 @@ class Eiger9mCsaxs(DetectorBase): self.std_client = StdDaqClient(url_base=self.std_rest_server_url) self.std_client.stop_writer() timeout = 0 - # TODO changing e-account was not possible during beamtimes. + # TODO changing e-account was not possible during beamtimes. # self._update_std_cfg("writer_user_id", int(self.scaninfo.username.strip(" e"))) # time.sleep(5) - #TODO is this the only state to wait for or should we wait for more from the std_daq client? + # TODO is this the only state to wait for or should we wait for more from the std_daq client? while not self.std_client.get_status()["state"] == "READY": time.sleep(0.1) timeout = timeout + 0.1 @@ -205,7 +209,7 @@ class Eiger9mCsaxs(DetectorBase): def _update_std_cfg(self, cfg_key: str, value: Any) -> None: """Update std_daq config with new e-account for the current beamtime""" - #TODO Do we need all the loggers here, should this be properly refactored with a DEBUG mode? + # TODO Do we need all the loggers here, should this be properly refactored with a DEBUG mode? cfg = self.std_client.get_config() old_value = cfg.get(cfg_key) logger.info(old_value) @@ -222,7 +226,7 @@ class Eiger9mCsaxs(DetectorBase): logger.info(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}") self.std_client.set_config(cfg) - #TODO function for abstract class? + # TODO function for abstract class? def stage(self) -> List[object]: """Stage command, called from BEC in preparation of a scan. The device needs to return with a state once it is ready to start the scan! @@ -235,23 +239,23 @@ class Eiger9mCsaxs(DetectorBase): self.device_manager.devices.mokev.name ]["value"] # Prepare file writer and detector - #TODO refactor logger.info to DEBUG mode? - #logger.info("Waiting for std daq to be armed") + # TODO refactor logger.info to DEBUG mode? + # logger.info("Waiting for std daq to be armed") self._prep_file_writer() - #logger.info("std_daq is ready") + # logger.info("std_daq is ready") self._prep_det() - #logger.info("Eiger9m is ready") + # logger.info("Eiger9m is ready") self._publish_file_location() self.arm_acquisition() - #TODO Fix should take place in EPICS or directly on the hardware! + # TODO Fix should take place in EPICS or directly on the hardware! # We observed that the detector missed triggers in the beginning in case BEC was to fast. Adding 50ms delay solved this time.sleep(0.05) return super().stage() - - #TODO function for abstract class? + + # TODO function for abstract class? def _prep_file_writer(self) -> None: """Prepare file writer for scan - + self.filewriter is a FileWriterMixin object that hosts logic for compiling the filepath """ self.filepath = self.filewriter.compile_full_filename( @@ -263,7 +267,7 @@ class Eiger9mCsaxs(DetectorBase): self._close_file_writer() logger.info(f" std_daq output filepath {self.filepath}") - #TODO Discuss with Leo if this is needed, or how to start the async writing best + # TODO Discuss with Leo if this is needed, or how to start the async writing best try: self.std_client.start_writer_async( { @@ -281,14 +285,14 @@ class Eiger9mCsaxs(DetectorBase): if det_ctrl == "WAITING_IMAGES": break time.sleep(0.005) - - #TODO function for abstract class? + + # TODO function for abstract class? def _close_file_writer(self) -> None: """Close file writer""" self.std_client.stop_writer() - #TODO can I wait for a status message here maybe? To ensure writer returned + # TODO can I wait for a status message here maybe? To ensure writer returned - #TODO function for abstract class? + # TODO function for abstract class? def _prep_det(self) -> None: """Prepare detector for scan. Includes checking the detector threshold, setting the acquisition parameters and setting the trigger source @@ -316,6 +320,7 @@ class Eiger9mCsaxs(DetectorBase): self.cam.num_images.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) self.cam.num_frames.put(1) + # TODO function for abstract class? + call it for each scan?? def _set_trigger(self, trigger_source: TriggerSource) -> None: """Set trigger source for the detector. Check the TriggerSource enum for possible values @@ -339,11 +344,10 @@ class Eiger9mCsaxs(DetectorBase): ) def arm_acquisition(self) -> None: - """Arm detector for acquisition - """ + """Arm detector for acquisition""" self.cam.acquire.put(1) logger.info("Waiting for Eiger9m to be armed") - #TODO add here timeout? + # TODO add here timeout? while True: det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] if det_ctrl == int(DetectorState.RUNNING): @@ -352,8 +356,13 @@ class Eiger9mCsaxs(DetectorBase): break time.sleep(0.005) logger.info("Eiger9m is armed") - - #TODO needed? if yes why only for the eiger9m? + + # TODO is this correct? -> for hardware triggering, nothing should happen upon trigger signal + # Comment this otherwise! + def trigger(self) -> DeviceStatus: + return super().trigger() + + # TODO threadlocked needed? if yes why only for the eiger9m? @threadlocked def unstage(self) -> List[object]: """unstage the detector and file writer""" From cbeb6794784a116a03f5a95858320965e6a86b2a Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Tue, 24 Oct 2023 13:18:30 +0200 Subject: [PATCH 16/24] refactor: eiger, add documentation for stage --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 28 ++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 86df31e..fdfe1f9 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -1,6 +1,7 @@ import enum import threading import time +from bec_lib.core.devicemanager import DeviceStatus import numpy as np import os @@ -229,24 +230,24 @@ class Eiger9mCsaxs(DetectorBase): # TODO function for abstract class? def stage(self) -> List[object]: """Stage command, called from BEC in preparation of a scan. - The device needs to return with a state once it is ready to start the scan! + This will iniate the preparation of detector and file writer. + The following functuions are called: + - _prep_file_writer + - _prep_det + - _publish_file_location + - _arm_acquisition + The device returns a List[object] from the Ophyd Device class. """ - # Set parameters for scan interuption and if acquisition is done self._stopped = False - # Get parameters for scan self.scaninfo.load_scan_metadata() self.mokev = self.device_manager.devices.mokev.obj.read()[ self.device_manager.devices.mokev.name ]["value"] - # Prepare file writer and detector # TODO refactor logger.info to DEBUG mode? - # logger.info("Waiting for std daq to be armed") self._prep_file_writer() - # logger.info("std_daq is ready") self._prep_det() - # logger.info("Eiger9m is ready") self._publish_file_location() - self.arm_acquisition() + self._arm_acquisition() # TODO Fix should take place in EPICS or directly on the hardware! # We observed that the detector missed triggers in the beginning in case BEC was to fast. Adding 50ms delay solved this time.sleep(0.05) @@ -343,7 +344,7 @@ class Eiger9mCsaxs(DetectorBase): msg.dumps(), ) - def arm_acquisition(self) -> None: + def _arm_acquisition(self) -> None: """Arm detector for acquisition""" self.cam.acquire.put(1) logger.info("Waiting for Eiger9m to be armed") @@ -357,11 +358,16 @@ class Eiger9mCsaxs(DetectorBase): time.sleep(0.005) logger.info("Eiger9m is armed") - # TODO is this correct? -> for hardware triggering, nothing should happen upon trigger signal - # Comment this otherwise! + # TODO function for abstract class? def trigger(self) -> DeviceStatus: + """Trigger the detector, called from BEC.""" + self.on_trigger() return super().trigger() + def on_trigger(self): + """Specify action that should be taken upon trigger signal.""" + pass + # TODO threadlocked needed? if yes why only for the eiger9m? @threadlocked def unstage(self) -> List[object]: From 8eb60a980aa209796a8c0f5d87de4c1624ef34ef Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Tue, 24 Oct 2023 13:19:08 +0200 Subject: [PATCH 17/24] refactor: eiger, fix _on_trigger --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index fdfe1f9..c7bba15 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -361,10 +361,10 @@ class Eiger9mCsaxs(DetectorBase): # TODO function for abstract class? def trigger(self) -> DeviceStatus: """Trigger the detector, called from BEC.""" - self.on_trigger() + self._on_trigger() return super().trigger() - def on_trigger(self): + def _on_trigger(self): """Specify action that should be taken upon trigger signal.""" pass From 7346f5d76aa82c2fe34b1d8df18f95cb37b664d7 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Tue, 24 Oct 2023 14:25:04 +0200 Subject: [PATCH 18/24] refactor: eiger, adapt publish file --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 41 +++++++++++--------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index c7bba15..5afc034 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -237,6 +237,11 @@ class Eiger9mCsaxs(DetectorBase): - _publish_file_location - _arm_acquisition The device returns a List[object] from the Ophyd Device class. + + #TODO make sure this is fullfiled + Staging not idempotent and should raise + :obj:`RedundantStaging` if staged twice without an + intermediate :meth:`~BlueskyInterface.unstage`. """ self._stopped = False self.scaninfo.load_scan_metadata() @@ -246,7 +251,8 @@ class Eiger9mCsaxs(DetectorBase): # TODO refactor logger.info to DEBUG mode? self._prep_file_writer() self._prep_det() - self._publish_file_location() + state = False + self._publish_file_location(done=state, successful=state) self._arm_acquisition() # TODO Fix should take place in EPICS or directly on the hardware! # We observed that the detector missed triggers in the beginning in case BEC was to fast. Adding 50ms delay solved this @@ -329,20 +335,19 @@ class Eiger9mCsaxs(DetectorBase): value = int(trigger_source) self.cam.trigger_mode.put(value) - def _publish_file_location(self) -> None: - """Publish the filepath to REDIS for file writer - file_event #TODO what was the purpose of this again? + def _publish_file_location(self, done=False, successful=False) -> None: + """Publish the filepath to REDIS + First msg for file writer and the second one for other listeners (e.g. radial integ) """ - msg = BECMessage.FileMessage(file_path=self.filepath, done=False) + pipe = self._producer.pipeline() + msg = BECMessage.FileMessage(file_path=self.filepath, done=done) self._producer.set_and_publish( - MessageEndpoints.public_file(self.scaninfo.scanID, self.name), - msg.dumps(), + MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe ) - msg = BECMessage.FileMessage(file_path=self.filepath, done=False) self._producer.set_and_publish( - MessageEndpoints.file_event(self.name), - msg.dumps(), + MessageEndpoints.file_event(self.name), msg.dumps(), pip=pipe ) + pipe.execute() def _arm_acquisition(self) -> None: """Arm detector for acquisition""" @@ -368,10 +373,15 @@ class Eiger9mCsaxs(DetectorBase): """Specify action that should be taken upon trigger signal.""" pass - # TODO threadlocked needed? if yes why only for the eiger9m? + # TODO threadlocked was attached, in principle unstage needs to be fast and should possibly called multiple times @threadlocked def unstage(self) -> List[object]: - """unstage the detector and file writer""" + """Unstage the device, this means detector and file writer. + + + This method must be idempotent, multiple calls (without a new + call to 'stage') have no effect. + """ logger.info("Waiting for Eiger9M to finish") old_scanID = self.scaninfo.scanID self.scaninfo.load_scan_metadata() @@ -383,12 +393,7 @@ class Eiger9mCsaxs(DetectorBase): self._eiger9M_finished() # Message to BEC state = True - - msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state) - self._producer.set_and_publish( - MessageEndpoints.public_file(self.scaninfo.scanID, self.name), - msg.dumps(), - ) + self._publish_file_location(done=state, successful=state) self._stopped = False logger.info("Eiger9M finished") return super().unstage() From d9606a47075847219a0e0a0bd63f7b533e1606fa Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Tue, 24 Oct 2023 14:44:37 +0200 Subject: [PATCH 19/24] refactor: eiger, refactoring done of unstage, stop and closing det and filewriter --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 67 ++++++++++++-------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 5afc034..cf1d580 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -179,7 +179,7 @@ class Eiger9mCsaxs(DetectorBase): Depends on hardware configuration and delay generators. At this point it is set up for gating mode (09/2023). """ - self.stop_acquisition() + self._stop_detector() self._set_trigger(TriggerSource.GATING) # TODO function for abstract class? @@ -272,7 +272,7 @@ class Eiger9mCsaxs(DetectorBase): while not os.path.exists(os.path.dirname(self.filepath)): time.sleep(0.1) - self._close_file_writer() + self._stop_file_writer() logger.info(f" std_daq output filepath {self.filepath}") # TODO Discuss with Leo if this is needed, or how to start the async writing best try: @@ -294,7 +294,7 @@ class Eiger9mCsaxs(DetectorBase): time.sleep(0.005) # TODO function for abstract class? - def _close_file_writer(self) -> None: + def _stop_file_writer(self) -> None: """Close file writer""" self.std_client.stop_writer() # TODO can I wait for a status message here maybe? To ensure writer returned @@ -349,6 +349,7 @@ class Eiger9mCsaxs(DetectorBase): ) pipe.execute() + # TODO function for abstract class? def _arm_acquisition(self) -> None: """Arm detector for acquisition""" self.cam.acquire.put(1) @@ -369,71 +370,80 @@ class Eiger9mCsaxs(DetectorBase): self._on_trigger() return super().trigger() + # TODO function for abstract class? def _on_trigger(self): """Specify action that should be taken upon trigger signal.""" pass + # TODO function for abstract class? # TODO threadlocked was attached, in principle unstage needs to be fast and should possibly called multiple times @threadlocked def unstage(self) -> List[object]: - """Unstage the device, this means detector and file writer. - + """Unstage the device, detector and file writer. This method must be idempotent, multiple calls (without a new call to 'stage') have no effect. """ - logger.info("Waiting for Eiger9M to finish") + # TODO solution for multiple calls of the function to avoid calling the finished loop. + # Loop to avoid calling the finished loop multiple times old_scanID = self.scaninfo.scanID self.scaninfo.load_scan_metadata() - logger.info(f"Old scanID: {old_scanID}, ") if self.scaninfo.scanID != old_scanID: self._stopped = True if self._stopped == True: return super().unstage() - self._eiger9M_finished() - # Message to BEC + self._finished() state = True self._publish_file_location(done=state, successful=state) self._stopped = False - logger.info("Eiger9M finished") return super().unstage() + # TODO function for abstract class? + # TODO necessary, how can we make this cleaner. @threadlocked - def _eiger9M_finished(self): - """Function with 10s timeout""" + def _finished(self): + """Check if acquisition is finished. + This function is called from unstage and stop + and will check detector and file backend status. + Timeouts after given time + """ + sleep_time = 0.1 + timeout = 5 timer = 0 + # Check status with timeout, break out if _stopped=True while True: det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"] - # det_ctrl = 0 std_ctrl = self.std_client.get_status()["acquisition"]["state"] status = self.std_client.get_status() received_frames = status["acquisition"]["stats"]["n_write_completed"] total_frames = int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger) - # TODO if no writing was performed before if det_ctrl == 0 and std_ctrl == "FINISHED" and total_frames == received_frames: break if self._stopped == True: - self.stop_acquisition() - self._close_file_writer() + self._stop_detector() + self._stop_file_writer() break - time.sleep(0.1) - timer += 0.1 - if timer > 5: + time.sleep(sleep_time) + timer += sleep_time + if timer > timeout: self._stopped == True - self._close_file_writer() - self.stop_acquisition() + self._stop_detector() + self._stop_file_writer() raise EigerTimeoutError( f"Reached timeout with detector state {det_ctrl}, std_daq state {std_ctrl} and received frames of {received_frames} for the file writer" ) - self._close_file_writer() + self._stop_detector() + self._stop_file_writer() - def stop_acquisition(self) -> None: + def _stop_detector(self) -> None: """Stop the detector and wait for the proper status message""" - logger.info("Waiting for Eiger9m to be armed") elapsed_time = 0 sleep_time = 0.01 + timeout = 5 + # Stop acquisition self.cam.acquire.put(0) retry = False + # Check status while True: det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] if det_ctrl == int(DetectorState.IDLE): @@ -442,16 +452,17 @@ class Eiger9mCsaxs(DetectorBase): break time.sleep(sleep_time) elapsed_time += sleep_time - if elapsed_time > 2 and not retry: + if elapsed_time > timeout // 2 and not retry: retry = True + # Retry to stop acquisition self.cam.acquire.put(0) - if elapsed_time > 5: + if elapsed_time > timeout: raise EigerTimeoutError("Failed to stop the acquisition. IOC did not update.") def stop(self, *, success=False) -> None: """Stop the scan, with camera and file writer""" - self.stop_acquisition() - self._close_file_writer() + self._stop_detector() + self._stop_file_writer() super().stop(success=success) self._stopped = True From 583c61ff411bb87349297c9163453a7ba8f5876c Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Tue, 24 Oct 2023 14:48:41 +0200 Subject: [PATCH 20/24] refactor: eiger, small bugfix --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index cf1d580..9d7ab73 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -340,7 +340,7 @@ class Eiger9mCsaxs(DetectorBase): First msg for file writer and the second one for other listeners (e.g. radial integ) """ pipe = self._producer.pipeline() - msg = BECMessage.FileMessage(file_path=self.filepath, done=done) + msg = BECMessage.FileMessage(file_path=self.filepath, done=done, successful=successful) self._producer.set_and_publish( MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe ) From 0f5fe04e59ae7054567b72fdfeec4dd8d3f96d78 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Tue, 24 Oct 2023 15:25:42 +0200 Subject: [PATCH 21/24] refactor: eiger, small refactoring of docs and names --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 29 ++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 9d7ab73..2267659 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -179,7 +179,7 @@ class Eiger9mCsaxs(DetectorBase): Depends on hardware configuration and delay generators. At this point it is set up for gating mode (09/2023). """ - self._stop_detector() + self._stop_det() self._set_trigger(TriggerSource.GATING) # TODO function for abstract class? @@ -239,6 +239,7 @@ class Eiger9mCsaxs(DetectorBase): The device returns a List[object] from the Ophyd Device class. #TODO make sure this is fullfiled + Staging not idempotent and should raise :obj:`RedundantStaging` if staged twice without an intermediate :meth:`~BlueskyInterface.unstage`. @@ -351,10 +352,8 @@ class Eiger9mCsaxs(DetectorBase): # TODO function for abstract class? def _arm_acquisition(self) -> None: - """Arm detector for acquisition""" + """Arm Eiger detector for acquisition""" self.cam.acquire.put(1) - logger.info("Waiting for Eiger9m to be armed") - # TODO add here timeout? while True: det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] if det_ctrl == int(DetectorState.RUNNING): @@ -362,7 +361,6 @@ class Eiger9mCsaxs(DetectorBase): if self._stopped == True: break time.sleep(0.005) - logger.info("Eiger9m is armed") # TODO function for abstract class? def trigger(self) -> DeviceStatus: @@ -379,10 +377,14 @@ class Eiger9mCsaxs(DetectorBase): # TODO threadlocked was attached, in principle unstage needs to be fast and should possibly called multiple times @threadlocked def unstage(self) -> List[object]: - """Unstage the device, detector and file writer. + """Unstage the device. This method must be idempotent, multiple calls (without a new call to 'stage') have no effect. + + Functions called: + - _finished + - _publish_file_location """ # TODO solution for multiple calls of the function to avoid calling the finished loop. # Loop to avoid calling the finished loop multiple times @@ -403,9 +405,14 @@ class Eiger9mCsaxs(DetectorBase): @threadlocked def _finished(self): """Check if acquisition is finished. + This function is called from unstage and stop and will check detector and file backend status. Timeouts after given time + + Functions called: + - _stop_det + - _stop_file_writer """ sleep_time = 0.1 timeout = 5 @@ -420,22 +427,22 @@ class Eiger9mCsaxs(DetectorBase): if det_ctrl == 0 and std_ctrl == "FINISHED" and total_frames == received_frames: break if self._stopped == True: - self._stop_detector() + self._stop_det() self._stop_file_writer() break time.sleep(sleep_time) timer += sleep_time if timer > timeout: self._stopped == True - self._stop_detector() + self._stop_det() self._stop_file_writer() raise EigerTimeoutError( f"Reached timeout with detector state {det_ctrl}, std_daq state {std_ctrl} and received frames of {received_frames} for the file writer" ) - self._stop_detector() + self._stop_det() self._stop_file_writer() - def _stop_detector(self) -> None: + def _stop_det(self) -> None: """Stop the detector and wait for the proper status message""" elapsed_time = 0 sleep_time = 0.01 @@ -461,7 +468,7 @@ class Eiger9mCsaxs(DetectorBase): def stop(self, *, success=False) -> None: """Stop the scan, with camera and file writer""" - self._stop_detector() + self._stop_det() self._stop_file_writer() super().stop(success=success) self._stopped = True From 0dec88eda59e33de5763199ea6db2f98d8f71a2d Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Tue, 24 Oct 2023 15:27:08 +0200 Subject: [PATCH 22/24] refactor: falcon, adapt to eiger refactoring --- ophyd_devices/epics/devices/falcon_csaxs.py | 162 ++++++++++++-------- 1 file changed, 95 insertions(+), 67 deletions(-) diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 3b89177..a15fddc 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -18,17 +18,20 @@ logger = bec_logger.logger class FalconError(Exception): - '''Base class for exceptions in this module.''' + """Base class for exceptions in this module.""" + pass class FalconTimeoutError(Exception): - '''Raised when the Falcon does not respond in time during unstage.''' + """Raised when the Falcon does not respond in time during unstage.""" + pass class DetectorState(int, enum.Enum): """Detector states for Falcon detector""" + DONE = 0 ACQUIRING = 1 @@ -59,9 +62,10 @@ class EpicsDXPFalcon(Device): class FalconHDF5Plugins(Device): """HDF5 parameters for Falcon detector - + Base class to map EPICS PVs from HDF5 Plugin to ophyd signals. """ + capture = Cpt(EpicsSignalWithRBV, "Capture") enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config") xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config") @@ -98,7 +102,7 @@ class FalconCsaxs(Device): hdf5 = Cpt(FalconHDF5Plugins, "HDF1:") # specify Epics PVs for Falcon - # TODO consider moving this outside of this class! + # TODO consider moving this outside of this class! stop_all = Cpt(EpicsSignal, "StopAll") erase_all = Cpt(EpicsSignal, "EraseAll") start_all = Cpt(EpicsSignal, "StartAll") @@ -138,10 +142,10 @@ class FalconCsaxs(Device): #TODO add here the parameters for kind, read_attrs, configuration_attrs, parent prefix (str): PV prefix ("X12SA-SITORO:) name (str): 'falcon' - kind (str): - read_attrs (list): - configuration_attrs (list): - parent (object): + kind (str): + read_attrs (list): + configuration_attrs (list): + parent (object): device_manager (object): BEC device manager sim_mode (bool): simulation mode to start the detector without BEC, e.g. from ipython shell """ @@ -159,7 +163,7 @@ class FalconCsaxs(Device): self._stopped = False self.name = name self.wait_for_connection() - # Spin up connections for simulation or BEC mode + # Spin up connections for simulation or BEC mode if not sim_mode: from bec_lib.core.bec_service import SERVICE_CONFIG @@ -178,43 +182,47 @@ class FalconCsaxs(Device): self.scaninfo.load_scan_metadata() self.filewriter = FileWriterMixin(self.service_cfg) self._init() - + def _init(self) -> None: - """Initialize detector, filewriter and set default parameters - """ + """Initialize detector, filewriter and set default parameters""" self._default_parameter() self._init_detector() self._init_filewriter() - + def _default_parameter(self) -> None: """Set default parameters for Falcon readout (float): readout time in seconds _value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro""" self.readout = 1e-3 self._value_pixel_per_buffer = 20 # 16 - self._clean_up() + self._stop_det() + self._stop_file_writer() - def _clean_up(self) -> None: - """Clean up""" - #TODO clarify when to use put and when to use set! - self.hdf5.capture.put(0) + def _stop_det(self) -> None: + """ "Stop detector""" self.stop_all.put(1) self.erase_all.put(1) + def _stop_file_writer(self) -> None: + """ "Stop the file writer""" + self.hdf5.capture.put(0) + def _init_filewriter(self) -> None: """Initialize file writer for Falcon. This includes setting variables for the HDF5 plugin (EPICS) that is used to write the data. """ self.hdf5.enable.put(1) # EnableCallbacks self.hdf5.xml_file_name.put("layout.xml") # Points to hardcopy of HDF5 Layout xml file - self.hdf5.lazy_open.put(1) # Potentially not needed, means a temp data file is created first, could be 0 + self.hdf5.lazy_open.put( + 1 + ) # Potentially not needed, means a temp data file is created first, could be 0 self.hdf5.temp_suffix.put("") # -> To be checked how to add FilePlugin_V22+ - self.hdf5.queue_size.put(2000) # size of queue for spectra in the buffer + self.hdf5.queue_size.put(2000) # size of queue for spectra in the buffer def _init_detector(self) -> None: """Initialize Falcon detector. - The detector is operated in MCA mapping mode. - Parameters here affect the triggering, gating etc. + The detector is operated in MCA mapping mode. + Parameters here affect the triggering, gating etc. This includes also the readout chunk size and whether data is segmented into spectra in EPICS. """ self.collect_mode.put(1) # 1 MCA Mapping, 0 MCA Spectrum @@ -224,29 +232,34 @@ class FalconCsaxs(Device): self.ignore_gate.put(0) # 1 Yes, 0 No self.auto_pixels_per_buffer.put(0) # 0 Manual 1 Auto self.pixels_per_buffer.put(self._value_pixel_per_buffer) # - self.nd_array_mode.put(1) # Segmentation happens in EPICS + self.nd_array_mode.put(1) # Segmentation happens in EPICS def stage(self) -> List[object]: """Stage command, called from BEC in preparation of a scan. - The device needs to return with a state once it is ready to start the scan! + This will iniate the preparation of detector and file writer. + The following functuions are called: + - _prep_file_writer + - _prep_det + - _publish_file_location + - _arm_acquisition + The device returns a List[object] from the Ophyd Device class. + + #TODO make sure this is fullfiled + + Staging not idempotent and should raise + :obj:`RedundantStaging` if staged twice without an + intermediate :meth:`~BlueskyInterface.unstage`. """ - # Set parameters for scan interuption and if acquisition is done self._stopped = False - # Get parameters for scan self.scaninfo.load_scan_metadata() self.mokev = self.device_manager.devices.mokev.obj.read()[ self.device_manager.devices.mokev.name ]["value"] - - # Prepare file writer and detector - #TODO refactor logger.info to DEBUG mode? - #logger.info("Waiting for falcon filewriter to be ready") self._prep_file_writer() - #logger.info("falcon file writer ready") self._prep_det() - #logger.info("falcon is ready") - self._publish_file_location() - self.arm_acquisition() + state = False + self._publish_file_location(done=state, successful=state) + self._arm_acquisition() return super().stage() def _prep_det(self) -> None: @@ -256,13 +269,11 @@ class FalconCsaxs(Device): self.pixels_per_run.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) def _prep_file_writer(self) -> None: - """Prepare filewriting from HDF5 plugin - #TODO check file_write_mode value in EPICs for details""" - self.destination_path = self.filewriter.compile_full_filename( + """Prepare filewriting from HDF5 plugin""" + self.filepath = self.filewriter.compile_full_filename( self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True ) - # self.hdf5.file_path.set(self.destination_path) - file_path, file_name = os.path.split(self.destination_path) + file_path, file_name = os.path.split(self.filepath) self.hdf5.file_path.put(file_path) self.hdf5.file_name.put(file_name) self.hdf5.file_template.put(f"%s%s") @@ -271,18 +282,23 @@ class FalconCsaxs(Device): self.hdf5.array_counter.put(0) self.hdf5.capture.put(1) - def _publish_file_location(self) -> None: - """Publish the filepath to REDIS for file writer""" - msg = BECMessage.FileMessage(file_path=self.filepath, done=False) + def _publish_file_location(self, done=False, successful=False) -> None: + """Publish the filepath to REDIS + First msg for file writer and the second one for other listeners (e.g. radial integ) + """ + pipe = self._producer.pipeline() + msg = BECMessage.FileMessage(file_path=self.filepath, done=done, successful=successful) self._producer.set_and_publish( - MessageEndpoints.public_file(self.scaninfo.scanID, self.name), - msg.dumps(), + MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe ) + self._producer.set_and_publish( + MessageEndpoints.file_event(self.name), msg.dumps(), pip=pipe + ) + pipe.execute() - def arm_acquisition(self) -> None: + def _arm_acquisition(self) -> None: + """Arm Falcon detector for acquisition""" self.start_all.put(1) - logger.info("Waiting for Falcon to be armed") - #TODO add here timeout? while True: det_ctrl = self.state.read()[self.state.name]["value"] if det_ctrl == int(DetectorState.ACQUIRING): @@ -290,10 +306,17 @@ class FalconCsaxs(Device): if self._stopped == True: break time.sleep(0.005) - logger.info("Falcon is armed") def unstage(self) -> List[object]: - logger.info("Waiting for Falcon to return from acquisition") + """Unstage the device. + + This method must be idempotent, multiple calls (without a new + call to 'stage') have no effect. + + Functions called: + - _finished + - _publish_file_location + """ old_scanID = self.scaninfo.scanID self.scaninfo.load_scan_metadata() logger.info(f"Old scanID: {old_scanID}, ") @@ -301,20 +324,25 @@ class FalconCsaxs(Device): self._stopped = True if self._stopped: return super().unstage() - self._falcon_finished() - self._clean_up() + self._finished() state = True - msg = BECMessage.FileMessage(file_path=self.destination_path, done=True, successful=state) - self._producer.set_and_publish( - MessageEndpoints.public_file(self.scaninfo.metadata["scanID"], self.name), - msg.dumps(), - ) + self._publish_file_location(done=state, successful=state) self._stopped = False - logger.info("Falcon done") return super().unstage() - def _falcon_finished(self): - """Function with 10s timeout""" + def _finished(self): + """Check if acquisition is finished. + + This function is called from unstage and stop + and will check detector and file backend status. + Timeouts after given time + + Functions called: + - _stop_det + - _stop_file_writer + """ + sleep_time = 0.1 + timeout = 5 timer = 0 while True: det_ctrl = self.state.read()[self.state.name]["value"] @@ -322,25 +350,25 @@ class FalconCsaxs(Device): received_frames = self.dxp.current_pixel.get() written_frames = self.hdf5.array_counter.get() total_frames = int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger) - # TODO if no writing was performed before + # TODO Could check state of detector (det_ctrl) and file writer (writer_ctrl) if total_frames == received_frames and total_frames == written_frames: break if self._stopped == True: break - time.sleep(0.1) - timer += 0.1 - if timer > 5: + time.sleep(sleep_time) + timer += sleep_time + if timer > timeout: logger.info( f"Falcon missed a trigger: received trigger {received_frames}, send data {written_frames} from total_frames {total_frames}" ) break - # raise FalconTimeoutError - # f"Reached timeout with detector state {det_ctrl}, falcon state {writer_ctrl}, received trigger {received_frames} and files written {written_frames}" - # ) + self._stop_det() + self._stop_file_writer() def stop(self, *, success=False) -> None: """Stop the scan, with camera and file writer""" - self._clean_up() + self._stop_det() + self._stop_file_writer() super().stop(success=success) self._stopped = True From 7f4082a6e9071c853c826d84aa5de1b2f46bb1a5 Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Tue, 24 Oct 2023 15:33:01 +0200 Subject: [PATCH 23/24] refactor: falcon, add trigger function --- ophyd_devices/epics/devices/falcon_csaxs.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index a15fddc..bc43e82 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -307,6 +307,17 @@ class FalconCsaxs(Device): break time.sleep(0.005) + # TODO function for abstract class? + def trigger(self) -> DeviceStatus: + """Trigger the detector, called from BEC.""" + self._on_trigger() + return super().trigger() + + # TODO function for abstract class? + def _on_trigger(self): + """Specify action that should be taken upon trigger signal.""" + pass + def unstage(self) -> List[object]: """Unstage the device. From 08e35df0f2acb44b657318c0c499f149fbbadb8a Mon Sep 17 00:00:00 2001 From: Christian Appel Date: Tue, 24 Oct 2023 16:50:19 +0200 Subject: [PATCH 24/24] refactor: pilatus changes from stage and minor changes for eiger and falcon --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 3 +- ophyd_devices/epics/devices/falcon_csaxs.py | 4 +- ophyd_devices/epics/devices/pilatus_csaxs.py | 156 +++++++++++++------ 3 files changed, 112 insertions(+), 51 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 2267659..3037e32 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -231,11 +231,10 @@ class Eiger9mCsaxs(DetectorBase): def stage(self) -> List[object]: """Stage command, called from BEC in preparation of a scan. This will iniate the preparation of detector and file writer. - The following functuions are called: + The following functuions are called (at least): - _prep_file_writer - _prep_det - _publish_file_location - - _arm_acquisition The device returns a List[object] from the Ophyd Device class. #TODO make sure this is fullfiled diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index bc43e82..4a5bd1b 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -3,6 +3,7 @@ import os import time from typing import List +from bec_lib.core.devicemanager import DeviceStatus from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt, Device from ophyd.mca import EpicsMCARecord @@ -237,11 +238,10 @@ class FalconCsaxs(Device): def stage(self) -> List[object]: """Stage command, called from BEC in preparation of a scan. This will iniate the preparation of detector and file writer. - The following functuions are called: + The following functuions are called (at least): - _prep_file_writer - _prep_det - _publish_file_location - - _arm_acquisition The device returns a List[object] from the Ophyd Device class. #TODO make sure this is fullfiled diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 58930a0..1982d86 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -2,6 +2,7 @@ import enum import json import os import time +from bec_lib.core.devicemanager import DeviceStatus import requests import numpy as np @@ -23,11 +24,13 @@ logger = bec_logger.logger class PilatusError(Exception): """Base class for exceptions in this module.""" + pass class PilatusTimeoutError(Exception): - '''Raised when the Pilatus does not respond in time during unstage.''' + """Raised when the Pilatus does not respond in time during unstage.""" + pass @@ -39,11 +42,12 @@ class TriggerSource(int, enum.Enum): ALGINMENT = 4 -class SlsDetectorCam(Device): +class SlsDetectorCam(Device): """SLS Detector Camera - Pilatus Base class to map EPICS PVs to ophyd signals. """ + num_images = ADCpt(EpicsSignalWithRBV, "NumImages") num_exposures = ADCpt(EpicsSignalWithRBV, "NumExposures") delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures") @@ -103,13 +107,13 @@ class PilatusCsaxs(DetectorBase): #TODO add here the parameters for kind, read_attrs, configuration_attrs, parent prefix (str): PV prefix ("X12SA-ES-PILATUS300K:) name (str): 'pilatus_2' - kind (str): - read_attrs (list): - configuration_attrs (list): - parent (object): + kind (str): + read_attrs (list): + configuration_attrs (list): + parent (object): device_manager (object): BEC device manager sim_mode (bool): simulation mode to start the detector without BEC, e.g. from ipython shell - """ + """ super().__init__( prefix=prefix, name=name, @@ -145,8 +149,7 @@ class PilatusCsaxs(DetectorBase): self._init() def _init(self) -> None: - """Initialize detector, filewriter and set default parameters - """ + """Initialize detector, filewriter and set default parameters""" self._default_parameter() self._init_detector() self._init_filewriter() @@ -159,12 +162,12 @@ class PilatusCsaxs(DetectorBase): def _init_detector(self) -> None: """Initialize the detector""" - #TODO add check if detector is running + # TODO add check if detector is running pass def _init_filewriter(self) -> None: """Initialize the file writer""" - #TODO in case the data backend is rewritten, add check if it is ready! + # TODO in case the data backend is rewritten, add check if it is ready! pass def _prep_det(self) -> None: @@ -216,7 +219,7 @@ class PilatusCsaxs(DetectorBase): self._stop_file_writer() time.sleep(0.1) - self.filepath_h5 = self.filewriter.compile_full_filename( + self.filepath_raw = self.filewriter.compile_full_filename( self.scaninfo.scan_number, "pilatus_2.h5", 1000, 5, True ) self.cam.file_path.put(f"/dev/shm/zmq/") @@ -228,19 +231,19 @@ class PilatusCsaxs(DetectorBase): # compile filename basepath = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/pilatus_2/" - self.destination_path = os.path.join( + self.filepath = os.path.join( basepath, self.filewriter.get_scan_directory(self.scaninfo.scan_number, 1000, 5), ) # Make directory if needed - os.makedirs(self.destination_path, exist_ok=True) + os.makedirs(self.filepath, exist_ok=True) data_msg = { "source": [ { "searchPath": "/", "searchPattern": "glob:*.cbf", - "destinationPath": self.destination_path, + "destinationPath": self.filepath, } ] } @@ -335,32 +338,76 @@ class PilatusCsaxs(DetectorBase): res.raise_for_status() def stage(self) -> List[object]: - """stage the detector and file writer""" - self._acquisition_done = False + """Stage command, called from BEC in preparation of a scan. + This will iniate the preparation of detector and file writer. + The following functuions are called: + - _prep_file_writer + - _prep_det + - _publish_file_location + + The device returns a List[object] from the Ophyd Device class. + + #TODO make sure this is fullfiled + + Staging not idempotent and should raise + :obj:`RedundantStaging` if staged twice without an + intermediate :meth:`~BlueskyInterface.unstage`. + """ self._stopped = False self.scaninfo.load_scan_metadata() self.mokev = self.device_manager.devices.mokev.obj.read()[ self.device_manager.devices.mokev.name ]["value"] - - logger.info("Waiting for pilatus2 to be armed") - self._prep_det() - logger.info("Pilatus2 armed") - logger.info("Waiting for pilatus2 zmq stream to be ready") + # TODO refactor logger.info to DEBUG mode? self._prep_file_writer() - logger.info("Pilatus2 zmq ready") - msg = BECMessage.FileMessage( - file_path=self.filepath_h5, done=False, metadata={"input_path": self.destination_path} - ) + self._prep_det() + state = False + self._publish_file_location(done=state, successful=state) return super().stage() + # TODO might be useful for base class def pre_scan(self) -> None: + """ " Pre_scan gets executed right before""" + self._arm_acquisition() + + def _arm_acquisition(self) -> None: self.acquire() + def _publish_file_location(self, done=False, successful=False) -> None: + """Publish the filepath to REDIS + First msg for file writer and the second one for other listeners (e.g. radial integ) + """ + pipe = self._producer.pipeline() + msg = BECMessage.FileMessage(file_path=self.filepath, done=done, successful=successful) + self._producer.set_and_publish( + MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe + ) + self._producer.set_and_publish( + MessageEndpoints.file_event(self.name), msg.dumps(), pip=pipe + ) + pipe.execute() + + # TODO function for abstract class? + def trigger(self) -> DeviceStatus: + """Trigger the detector, called from BEC.""" + self._on_trigger() + return super().trigger() + + # TODO function for abstract class? + def _on_trigger(self): + """Specify action that should be taken upon trigger signal.""" + pass + def unstage(self) -> List[object]: - """unstage the detector and file writer""" - # Reset to software trigger - logger.info("Waiting for Pilatus to return from acquisition") + """Unstage the device. + + This method must be idempotent, multiple calls (without a new + call to 'stage') have no effect. + + Functions called: + - _finished + - _publish_file_location + """ old_scanID = self.scaninfo.scanID self.scaninfo.load_scan_metadata() logger.info(f"Old scanID: {old_scanID}, ") @@ -368,27 +415,37 @@ class PilatusCsaxs(DetectorBase): self._stopped = True if self._stopped: return super().unstage() - self._pilatus_finished() - msg = BECMessage.FileMessage( - file_path=self.filepath_h5, done=True, metadata={"input_path": self.destination_path} - ) - self._producer.set_and_publish( - MessageEndpoints.public_file(self.scaninfo.scanID, self.name), - msg.dumps(), - ) - self._producer.set_and_publish( - MessageEndpoints.file_event(self.name), - msg.dumps(), - ) - logger.info("Pilatus2 done") + self._finished() + state = True + self._publish_file_location(done=state, successful=state) + self._start_h5converter(done=state) return super().unstage() - def _pilatus_finished(self) -> None: - # time.sleep(2) + def _start_h5converter(self, done=False) -> None: + """Start the h5converter""" + msg = BECMessage.FileMessage( + file_path=self.filepath_raw, done=done, metadata={"input_path": self.filepath} + ) + self._producer.set_and_publish( + MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps() + ) + + def _finished(self) -> None: + """Check if acquisition is finished. + + This function is called from unstage and stop + and will check detector and file backend status. + Timeouts after given time + + Functions called: + - _stop_det + - _stop_file_writer + """ while True: if self.device_manager.devices.mcs.obj._staged != Staged.yes: break time.sleep(0.1) + # TODO implement a waiting function or not # time.sleep(2) # timer = 0 # while True: @@ -408,7 +465,9 @@ class PilatusCsaxs(DetectorBase): # # f"Pilatus timeout with detector state {self.cam.acquire.get()} and camserver return status: {rtr} " # # ) + self._stop_det() self._stop_file_writer() + # TODO explore if sleep is needed time.sleep(0.5) self._close_file_writer() @@ -417,15 +476,18 @@ class PilatusCsaxs(DetectorBase): or arm the detector in hardware of the detector """ self.cam.acquire.put(1) + # TODO check if sleep of 1s is needed, could be that less is enough time.sleep(1) + def _stop_det(self) -> None: + """Stop the detector""" + self.cam.acquire.put(0) + def stop(self, *, success=False) -> None: """Stop the scan, with camera and file writer""" - self.cam.acquire.put(0) + self._stop_det() self._stop_file_writer() - # TODO maybe needed self._close_file_writer() - # self.unstage() super().stop(success=success) self._stopped = True