diff --git a/csaxs_bec/devices/epics/eiger9m_csaxs.py b/csaxs_bec/devices/epics/eiger9m_csaxs.py deleted file mode 100644 index cf3e31d..0000000 --- a/csaxs_bec/devices/epics/eiger9m_csaxs.py +++ /dev/null @@ -1,381 +0,0 @@ -import enum -import os -import threading -import time -from typing import Any - -import numpy as np -from bec_lib.logger import bec_logger -from ophyd import ADComponent as ADCpt -from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV -from ophyd_devices.interfaces.base_classes.psi_detector_base import ( - CustomDetectorMixin, - PSIDetectorBase, -) -from std_daq_client import StdDaqClient - -logger = bec_logger.logger - - -class EigerError(Exception): - """Base class for exceptions in this module.""" - - -class EigerTimeoutError(EigerError): - """Raised when the Eiger does not respond in time.""" - - -class Eiger9MSetup(CustomDetectorMixin): - """Eiger setup class - - Parent class: CustomDetectorMixin - - """ - - def __init__(self, *args, parent: Device = None, **kwargs) -> None: - super().__init__(*args, parent=parent, **kwargs) - self.std_rest_server_url = ( - kwargs["file_writer_url"] if "file_writer_url" in kwargs else "http://xbl-daq-29:5000" - ) - self.std_client = None - self._lock = threading.RLock() - - def on_init(self) -> None: - """Initialize the detector""" - self.initialize_default_parameter() - self.initialize_detector() - self.initialize_detector_backend() - - def initialize_detector(self) -> None: - """Initialize detector""" - self.stop_detector() - self.parent.cam.trigger_mode.put(TriggerSource.GATING) - - def initialize_default_parameter(self) -> None: - """Set default parameters for Eiger9M detector""" - self.update_readout_time() - - def update_readout_time(self) -> None: - """Set readout time for Eiger9M detector""" - readout_time = ( - self.parent.scaninfo.readout_time - if hasattr(self.parent.scaninfo, "readout_time") - else self.parent.MIN_READOUT - ) - self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT) - - def initialize_detector_backend(self) -> None: - """Initialize detector backend""" - - self.std_client = StdDaqClient(url_base=self.std_rest_server_url) - self.std_client.stop_writer() - eacc = self.parent.scaninfo.username - self.update_std_cfg("writer_user_id", int(eacc.strip(" e"))) - - signal_conditions = [(lambda: self.std_client.get_status()["state"], "READY")] - if not self.wait_for_signals( - signal_conditions=signal_conditions, - timeout=self.parent.TIMEOUT_FOR_SIGNALS, - all_signals=True, - ): - raise EigerTimeoutError( - f"Std client not in READY state, returns: {self.std_client.get_status()}" - ) - - def update_std_cfg(self, cfg_key: str, value: Any) -> None: - """ - Update std_daq config - - Checks that the new value matches the type of the former entry. - - Args: - cfg_key (str) : config key of value to be updated - value (Any) : value to be updated for the specified key - - Raises: - Raises EigerError if the key was not in the config before and if the new value does not match the type of the old value - - """ - - cfg = self.std_client.get_config() - old_value = cfg.get(cfg_key) - if old_value is None: - raise EigerError( - f"Tried to change entry for key {cfg_key} in std_config that does not exist" - ) - if not isinstance(value, type(old_value)): - raise EigerError( - f"Type of new value {type(value)}:{value} does not match old value" - f" {type(old_value)}:{old_value}" - ) - - cfg.update({cfg_key: value}) - logger.debug(cfg) - self.std_client.set_config(cfg) - logger.debug(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}") - - def on_stage(self) -> None: - """Prepare the detector for scan""" - self.prepare_detector() - self.prepare_data_backend() - self.publish_file_location(done=False, successful=False) - self.arm_acquisition() - - def prepare_detector(self) -> None: - """Prepare detector for scan""" - self.set_detector_threshold() - self.set_acquisition_params() - self.parent.cam.trigger_mode.put(TriggerSource.GATING) - - def set_detector_threshold(self) -> None: - """ - Set the detector threshold - - The function sets the detector threshold automatically to 1/2 of the beam energy. - """ - mokev = self.parent.device_manager.devices.mokev.obj.read()[ - self.parent.device_manager.devices.mokev.name - ]["value"] - factor = 1 - unit = getattr(self.parent.cam.threshold_energy, "units", None) - - if unit is not None and unit == "eV": - factor = 1000 - setpoint = int(mokev * factor) - energy = self.parent.cam.beam_energy.read()[self.parent.cam.beam_energy.name]["value"] - - if setpoint != energy: - self.parent.cam.beam_energy.set(setpoint) - - threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][ - "value" - ] - if not np.isclose(setpoint / 2, threshold, rtol=0.05): - self.parent.cam.threshold_energy.set(setpoint / 2) - - def set_acquisition_params(self) -> None: - """Set acquisition parameters for the detector""" - self.parent.cam.num_images.put( - int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) - ) - self.parent.cam.num_frames.put(1) - self.update_readout_time() - - def prepare_data_backend(self) -> None: - """Prepare the data backend for the scan""" - self.parent.filepath.set( - self.parent.filewriter.compile_full_filename(f"{self.parent.name}.h5") - ).wait() - self.filepath_exists(self.parent.filepath.get()) - self.stop_detector_backend() - try: - self.std_client.start_writer_async( - { - "output_file": self.parent.filepath.get(), - "n_images": int( - self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger - ), - } - ) - except Exception as exc: - time.sleep(5) - if self.std_client.get_status()["state"] == "READY": - raise EigerTimeoutError(f"Timeout of start_writer_async with {exc}") from exc - - signal_conditions = [ - (lambda: self.std_client.get_status()["acquisition"]["state"], "WAITING_IMAGES") - ] - if not self.wait_for_signals( - signal_conditions=signal_conditions, - timeout=self.parent.TIMEOUT_FOR_SIGNALS, - check_stopped=False, - all_signals=True, - ): - raise EigerTimeoutError( - "Timeout of 5s reached for std_daq start_writer_async with std_daq client status" - f" {self.std_client.get_status()}" - ) - - def on_unstage(self) -> None: - """Unstage the detector""" - pass - - def on_complete(self) -> None: - """Complete the detector""" - self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS) - self.publish_file_location(done=True, successful=True) - - def on_stop(self) -> None: - """Stop the detector""" - self.stop_detector() - self.stop_detector_backend() - - def stop_detector(self) -> None: - """Stop the detector""" - - # Stop detector - self.parent.cam.acquire.put(0) - signal_conditions = [ - ( - lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][ - "value" - ], - DetectorState.IDLE, - ) - ] - - if not self.wait_for_signals( - signal_conditions=signal_conditions, - timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2, - check_stopped=True, - all_signals=False, - ): - # Retry stop detector and wait for remaining time - self.parent.cam.acquire.put(0) - if not self.wait_for_signals( - signal_conditions=signal_conditions, - timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2, - check_stopped=True, - all_signals=False, - ): - raise EigerTimeoutError( - f"Failed to stop detector, detector state {signal_conditions[0][0]}" - ) - - def stop_detector_backend(self) -> None: - """Close file writer""" - self.std_client.stop_writer() - - def filepath_exists(self, filepath: str) -> None: - """Check if filepath exists""" - signal_conditions = [(lambda: os.path.exists(os.path.dirname(filepath)), True)] - if not self.wait_for_signals( - signal_conditions=signal_conditions, - timeout=self.parent.TIMEOUT_FOR_SIGNALS, - check_stopped=False, - all_signals=True, - ): - raise EigerError(f"Timeout of 3s reached for filepath {filepath}") - - def arm_acquisition(self) -> None: - """Arm Eiger detector for acquisition""" - self.parent.cam.acquire.put(1) - signal_conditions = [ - ( - lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][ - "value" - ], - DetectorState.RUNNING, - ) - ] - if not self.wait_for_signals( - signal_conditions=signal_conditions, - timeout=self.parent.TIMEOUT_FOR_SIGNALS, - check_stopped=True, - all_signals=False, - ): - raise EigerTimeoutError( - f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" - ) - - def finished(self, timeout: int = 5) -> None: - """Check if acquisition is finished.""" - with self._lock: - signal_conditions = [ - ( - lambda: self.parent.cam.acquire.read()[self.parent.cam.acquire.name]["value"], - DetectorState.IDLE, - ), - (lambda: self.std_client.get_status()["acquisition"]["state"], "FINISHED"), - ( - lambda: self.std_client.get_status()["acquisition"]["stats"][ - "n_write_completed" - ], - int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger), - ), - ] - if not self.wait_for_signals( - signal_conditions=signal_conditions, - timeout=timeout, - check_stopped=True, - all_signals=True, - ): - raise EigerTimeoutError( - f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state" - f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for" - " the file writer" - ) - self.stop_detector() - self.stop_detector_backend() - - -class SLSDetectorCam(Device): - """ - SLS Detector Camera - Eiger9M - - Base class to map EPICS PVs to ophyd signals. - """ - - threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy") - beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy") - bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth") - num_images = ADCpt(EpicsSignalWithRBV, "NumCycles") - num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames") - trigger_mode = ADCpt(EpicsSignalWithRBV, "TimingMode") - trigger_software = ADCpt(EpicsSignal, "TriggerSoftware") - 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 - BURST_TRIGGER = 3 - - -class DetectorState(int, enum.Enum): - """Detector states for Eiger9M detector""" - - IDLE = 0 - ERROR = 1 - WAITING = 2 - FINISHED = 3 - TRANSMITTING = 4 - RUNNING = 5 - STOPPED = 6 - STILL_WAITING = 7 - INITIALIZING = 8 - DISCONNECTED = 9 - ABORTED = 10 - - -class Eiger9McSAXS(PSIDetectorBase): - """ - Eiger9M detector for CSAXS - - Parent class: PSIDetectorBase - - class attributes: - custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS, - inherits from CustomDetectorMixin - PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector - Various EpicsPVs for controlling the detector - """ - - # Specify which functions are revealed to the user in BEC client - USER_ACCESS = [] - - # specify Setup class - custom_prepare_cls = Eiger9MSetup - # specify minimum readout time for detector and timeout for checks after unstage - MIN_READOUT = 3e-3 - TIMEOUT_FOR_SIGNALS = 5 - # specify class attributes - cam = ADCpt(SLSDetectorCam, "cam1:") - - -if __name__ == "__main__": - eiger = Eiger9McSAXS(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True)