From bcc321076153ccd6ae8419b95553b5b4916e82ad Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 16 Nov 2023 22:52:45 +0100 Subject: [PATCH] feat: refactor falcon for psi_detector_base class; adapted eiger; added and debugged tests --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 87 +-- ophyd_devices/epics/devices/falcon_csaxs.py | 572 +++++++----------- .../epics/devices/psi_detector_base.py | 2 +- tests/test_eiger9m_csaxs.py | 9 + tests/test_falcon_csaxs.py | 68 ++- 5 files changed, 319 insertions(+), 419 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index d362c7a..9c11a15 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -11,8 +11,10 @@ from ophyd import ADComponent as ADCpt from std_daq_client import StdDaqClient -from bec_lib.core import BECMessage, MessageEndpoints, threadlocked -from bec_lib.core import bec_logger +from bec_lib import threadlocked +from bec_lib.logger import bec_logger +from bec_lib import messages +from bec_lib.endpoints import MessageEndpoints from ophyd_devices.epics.devices.psi_detector_base import PSIDetectorBase, CustomDetectorMixin @@ -128,13 +130,13 @@ class Eiger9MSetup(CustomDetectorMixin): "value" ], DetectorState.IDLE, - ), - (lambda: self.parent._stopped, True), + ) ] if not self.wait_for_signals( signal_conditions=signal_conditions, timeout=self.parent.timeout - self.parent.timeout // 2, + check_stopped=True, all_signals=False, ): # Retry stop detector and wait for remaining time @@ -142,9 +144,12 @@ class Eiger9MSetup(CustomDetectorMixin): if not self.wait_for_signals( signal_conditions=signal_conditions, timeout=self.parent.timeout - self.parent.timeout // 2, + check_stopped=True, all_signals=False, ): - raise EigerTimeoutError("Failed to stop the acquisition. IOC did not update.") + raise EigerTimeoutError( + f"Failed to stop detector, detector state {signal_conditions[0][0]}" + ) def stop_detector_backend(self) -> None: """Close file writer""" @@ -247,35 +252,6 @@ class Eiger9MSetup(CustomDetectorMixin): ): raise EigerError(f"Timeout of 3s reached for filepath {self.parent.filepath}") - def publish_file_location(self, done: bool = False, successful: bool = None) -> None: - """ - Publish the filepath to REDIS. - - We publish two events here: - - file_event: event for the filewriter - - public_file: event for any secondary service (e.g. radial integ code) - - Args: - done (bool): True if scan is finished - successful (bool): True if scan was successful - """ - pipe = self.parent._producer.pipeline() - if successful is None: - msg = BECMessage.FileMessage(file_path=self.parent.filepath, done=done) - else: - msg = BECMessage.FileMessage( - file_path=self.parent.filepath, done=done, successful=successful - ) - self.parent._producer.set_and_publish( - MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), - msg.dumps(), - pipe=pipe, - ) - self.parent._producer.set_and_publish( - MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe - ) - pipe.execute() - def arm_acquisition(self) -> None: """Arm Eiger detector for acquisition""" self.parent.cam.acquire.put(1) @@ -293,6 +269,7 @@ class Eiger9MSetup(CustomDetectorMixin): check_stopped=True, all_signals=False, ): + self.parent.stop() raise EigerTimeoutError( f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" ) @@ -303,6 +280,35 @@ class Eiger9MSetup(CustomDetectorMixin): if self.parent.scaninfo.scanID != old_scanID: self.parent._stopped = True + def publish_file_location(self, done: bool = False, successful: bool = None) -> None: + """ + Publish the filepath to REDIS. + + We publish two events here: + - file_event: event for the filewriter + - public_file: event for any secondary service (e.g. radial integ code) + + Args: + done (bool): True if scan is finished + successful (bool): True if scan was successful + """ + pipe = self.parent._producer.pipeline() + if successful is None: + msg = messages.FileMessage(file_path=self.parent.filepath, done=done) + else: + msg = messages.FileMessage( + file_path=self.parent.filepath, done=done, successful=successful + ) + self.parent._producer.set_and_publish( + MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), + msg.dumps(), + pipe=pipe, + ) + self.parent._producer.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe + ) + pipe.execute() + @threadlocked def finished(self): """Check if acquisition is finished.""" @@ -375,6 +381,19 @@ class DetectorState(enum.IntEnum): class Eiger9McSAXS(PSIDetectorBase): + """ + Eiger 9M detector class for cSAXS + + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (Eiger9MSetup): Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + cam (SLSDetectorCam): Detector camera + MIN_READOUT (float): Minimum readout time for the detector + + """ + custom_prepare_cls = Eiger9MSetup cam = ADCpt(SLSDetectorCam, "cam1:") MIN_READOUT = 3e-3 diff --git a/ophyd_devices/epics/devices/falcon_csaxs.py b/ophyd_devices/epics/devices/falcon_csaxs.py index 517fa06..4caa645 100644 --- a/ophyd_devices/epics/devices/falcon_csaxs.py +++ b/ophyd_devices/epics/devices/falcon_csaxs.py @@ -1,26 +1,21 @@ import enum import os -import time - -from typing import List from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt from ophyd.mca import EpicsMCARecord from ophyd import Device -from bec_lib import MessageEndpoints, messages, bec_logger -from bec_lib.file_utils import FileWriterMixin -from bec_lib.devicemanager import DeviceStatus -from bec_lib.bec_service import SERVICE_CONFIG +from bec_lib.endpoints import MessageEndpoints +from bec_lib import messages +from bec_lib.logger import bec_logger + + +from ophyd_devices.epics.devices.psi_detector_base import PSIDetectorBase, CustomDetectorMixin -from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin -from ophyd_devices.utils import bec_utils logger = bec_logger.logger -FALCON_MIN_READOUT = 3e-3 - class FalconError(Exception): """Base class for exceptions in this module.""" @@ -34,13 +29,6 @@ class FalconTimeoutError(FalconError): pass -class FalconInitError(FalconError): - """Raised when initiation of the device class fails, - due to missing device manager or not started in sim_mode.""" - - pass - - class DetectorState(enum.IntEnum): """Detector states for Falcon detector""" @@ -113,29 +101,226 @@ class FalconHDF5Plugins(Device): array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config") -class FalconcSAXS(Device): - """Falcon Sitoro detector for CSAXS +class FalconSetup(CustomDetectorMixin): + """Falcon setup class for cSAXS - Parent class: Device - Device classes: EpicsDXPFalcon dxp1:, EpicsMCARecord mca1, FalconHDF5Plugins HDF1: - - Attributes: - name str: 'falcon' - prefix (str): PV prefix ("X12SA-SITORO:) + Parent class: CustomDetectorMixin """ - # Specify which functions are revealed to the user in BEC client - USER_ACCESS = [ - "describe", - ] + def __init__(self, parent: Device = None, *args, **kwargs) -> None: + super().__init__(parent=parent, *args, **kwargs) + + def initialize_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.parent._value_pixel_per_buffer = 20 + 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.readout_time_min + ) + self.parent.readout_time = max(readout_time, self.parent.readout_time_min) + + def initialize_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.stop_detector() + self.stop_detector_backend() + self.parent.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + self.parent.preset_mode.put(1) # 1 Realtime + self.parent.input_logic_polarity.put(0) # 0 Normal, 1 Inverted + self.parent.auto_pixels_per_buffer.put(0) # 0 Manual 1 Auto + self.parent.pixels_per_buffer.put(self.parent._value_pixel_per_buffer) + + def stop_detector(self) -> None: + """Stops detector""" + + self.parent.stop_all.put(1) + self.parent.erase_all.put(1) + + signal_conditions = [ + ( + lambda: self.parent.state.read()[self.parent.state.name]["value"], + DetectorState.DONE, + ), + ] + + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout - self.parent.timeout // 2, + all_signals=False, + ): + # Retry stop detector and wait for remaining time + raise FalconTimeoutError( + f"Failed to stop detector, timeout with state {signal_conditions[0][0]}" + ) + + def stop_detector_backend(self) -> None: + """Stop the detector backend""" + self.parent.hdf5.capture.put(0) + + def initialize_detector_backend(self) -> None: + """Initialize the detector backend for Falcon.""" + self.parent.hdf5.enable.put(1) + # file location of h5 layout for cSAXS + self.parent.hdf5.xml_file_name.put("layout.xml") + # TODO Check if lazy open is needed and wanted! + self.parent.hdf5.lazy_open.put(1) + self.parent.hdf5.temp_suffix.put("") + # size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost + self.parent.hdf5.queue_size.put(2000) + # Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate + self.parent.nd_array_mode.put(1) + + def prepare_detector(self) -> None: + """Prepare detector for acquisition""" + self.parent.set_trigger( + mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 + ) + self.parent.preset_real.put(self.parent.scaninfo.exp_time) + self.parent.pixels_per_run.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + + def prepare_data_backend(self) -> None: + """Prepare data backend for acquisition""" + self.parent.filepath = self.parent.filewriter.compile_full_filename( + self.parent.scaninfo.scan_number, f"{self.parent.name}.h5", 1000, 5, True + ) + file_path, file_name = os.path.split(self.parent.filepath) + self.parent.hdf5.file_path.put(file_path) + self.parent.hdf5.file_name.put(file_name) + self.parent.hdf5.file_template.put(f"%s%s") + self.parent.hdf5.num_capture.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + self.parent.hdf5.file_write_mode.put(2) + # Reset spectrum counter in filewriter, used for indexing & identifying missing triggers + self.parent.hdf5.array_counter.put(0) + # Start file writing + self.parent.hdf5.capture.put(1) + + def arm_acquisition(self) -> None: + """Arm Eiger detector for acquisition""" + self.parent.start_all.put(1) + signal_conditions = [ + ( + lambda: self.parent.state.read()[self.parent.state.name]["value"], + DetectorState.ACQUIRING, + ) + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=True, + all_signals=False, + ): + self.parent.stop() + raise FalconTimeoutError( + f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" + ) + + def check_scanID(self) -> None: + old_scanID = self.parent.scaninfo.scanID + self.parent.scaninfo.load_scan_metadata() + if self.parent.scaninfo.scanID != old_scanID: + self.parent._stopped = True + + def publish_file_location(self, done: bool = False, successful: bool = None) -> None: + """ + Publish the filepath to REDIS. + + We publish two events here: + - file_event: event for the filewriter + - public_file: event for any secondary service (e.g. radial integ code) + + Args: + done (bool): True if scan is finished + successful (bool): True if scan was successful + """ + pipe = self.parent._producer.pipeline() + if successful is None: + msg = messages.FileMessage(file_path=self.parent.filepath, done=done) + else: + msg = messages.FileMessage( + file_path=self.parent.filepath, done=done, successful=successful + ) + self.parent._producer.set_and_publish( + MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name), + msg.dumps(), + pipe=pipe, + ) + self.parent._producer.set_and_publish( + MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe + ) + pipe.execute() + + def finished(self) -> None: + """Check if acquisition is finished. + + For the Falcon we accept that it misses a trigger since we can reconstruct it from the data. + """ + signal_conditions = [ + ( + lambda: self.parent.dxp.current_pixel.get(), + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger), + ), + ( + lambda: self.parent.hdf5.array_counter.get(), + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger), + ), + ] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=True, + all_signals=True, + ): + logger.debug( + f"Falcon missed a trigger: received trigger {received_frames}, send data {written_frames} from total_frames {total_frames}" + ) + self.stop_detector() + self.stop_detector_backend() + + +class FalconcSAXS(PSIDetectorBase): + """ + Falcon Sitoro detector for CSAXS + + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS, + inherits from CustomDetectorMixin + dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector + mca (EpicsMCARecord) : MCA parameters for Falcon detector + hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector + MIN_READOUT (float) : Minimum readout time for the detector + """ + + custom_prepare_cls = FalconSetup + MIN_READOUT = 3e-3 dxp = Cpt(EpicsDXPFalcon, "dxp1:") mca = Cpt(EpicsMCARecord, "mca1") hdf5 = Cpt(FalconHDF5Plugins, "HDF1:") - # 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") @@ -157,157 +342,7 @@ class FalconcSAXS(Device): pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun") nd_array_mode = Cpt(EpicsSignal, "NDArrayMode") - def __init__( - self, - prefix="", - *, - name, - kind=None, - read_attrs=None, - configuration_attrs=None, - parent=None, - device_manager=None, - 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, - kind=kind, - read_attrs=read_attrs, - configuration_attrs=configuration_attrs, - parent=parent, - **kwargs, - ) - if device_manager is None and not sim_mode: - raise FalconInitError( - f"No device manager for device: {name}, and not started sim_mode: {sim_mode}. Add DeviceManager to initialization or init with sim_mode=True" - ) - self.sim_mode = sim_mode - self._stopped = False - self.name = name - self.service_cfg = None - self.scaninfo = None - self.filewriter = None - self.readout_time_min = FALCON_MIN_READOUT - self._value_pixel_per_buffer = None - self.readout_time = None - self.timeout = 5 - self.wait_for_connection(all_signals=True) - if not sim_mode: - self._update_service_config() - self.device_manager = device_manager - else: - self.device_manager = bec_utils.DMMock() - base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/" - self.service_cfg = {"base_path": os.path.expanduser(base_path)} - self._producer = self.device_manager.producer - self._update_scaninfo() - self._update_filewriter() - self._init() - - def _update_filewriter(self) -> None: - """Update filewriter with service config""" - self.filewriter = FileWriterMixin(self.service_cfg) - - def _update_scaninfo(self) -> None: - """Update scaninfo from BecScaninfoMixing - This depends on device manager and operation/sim_mode - """ - self.scaninfo = BecScaninfoMixin(self.device_manager, self.sim_mode) - self.scaninfo.load_scan_metadata() - - def _update_service_config(self) -> None: - """Update service config from BEC service config""" - self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] - - 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._value_pixel_per_buffer = 20 - self._update_readout_time() - - def _update_readout_time(self) -> None: - readout_time = ( - self.scaninfo.readout_time - if hasattr(self.scaninfo, "readout_time") - else self.readout_time_min - ) - self.readout_time = max(readout_time, self.readout_time_min) - - def _stop_det(self) -> None: - """ "Stop detector""" - self.stop_all.put(1) - self.erase_all.put(1) - det_ctrl = self.state.read()[self.state.name]["value"] - timer = 0 - while True: - det_ctrl = self.state.read()[self.state.name]["value"] - if det_ctrl == DetectorState.DONE: - break - if self._stopped == True: - break - time.sleep(0.01) - timer += 0.01 - if timer > self.timeout: - raise FalconTimeoutError("Failed to stop the detector. IOC did not update.") - - 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) - # file location of h5 layout for cSAXS - self.hdf5.xml_file_name.put("layout.xml") - # Potentially not needed, means a temp data file is created first, could be 0 - self.hdf5.lazy_open.put(1) - self.hdf5.temp_suffix.put("") - # size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost - self.hdf5.queue_size.put(2000) - # Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate - self.nd_array_mode.put(1) - - 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._stop_det() - self._stop_file_writer() - self._set_trigger( - mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 - ) - self.preset_mode.put(1) # 1 Realtime - self.input_logic_polarity.put(0) # 0 Normal, 1 Inverted - self.auto_pixels_per_buffer.put(0) # 0 Manual 1 Auto - self.pixels_per_buffer.put(self._value_pixel_per_buffer) # - - def _set_trigger( + def set_trigger( self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0 ) -> None: """ @@ -325,177 +360,6 @@ class FalconcSAXS(Device): self.pixel_advance_mode.put(trigger) self.ignore_gate.put(ignore_gate) - 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 (at least): - - _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._prep_file_writer() - self._prep_det() - state = False - self._publish_file_location(done=state) - self._arm_acquisition() - return super().stage() - - def _prep_det(self) -> None: - """Prepare detector for acquisition""" - self._set_trigger( - mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0 - ) - self.preset_real.put(self.scaninfo.exp_time) - 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 these settings together with Controls put vs set - - """ - self.filepath = self.filewriter.compile_full_filename( - self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True - ) - 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") - self.hdf5.num_capture.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) - self.hdf5.file_write_mode.put(2) - # Reset spectrum counter in filewriter, used for indexing & identifying missing triggers - self.hdf5.array_counter.put(0) - # Start file writing - self.hdf5.capture.put(1) - - def _publish_file_location(self, done: bool = False, successful: bool = False) -> None: - """Publish the filepath to REDIS. - We publish two events here: - - file_event: event for the filewriter - - public_file: event for any secondary service (e.g. radial integ code) - - Args: - done (bool): True if scan is finished - successful (bool): True if scan was successful - - """ - pipe = self._producer.pipeline() - if successful is None: - msg = messages.FileMessage(file_path=self.filepath, done=done) - else: - msg = messages.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(), pipe=pipe - ) - pipe.execute() - - def _arm_acquisition(self) -> None: - """Arm Falcon detector for acquisition""" - timer = 0 - self.start_all.put(1) - while True: - det_ctrl = self.state.read()[self.state.name]["value"] - if det_ctrl == DetectorState.ACQUIRING: - break - if self._stopped == True: - break - time.sleep(0.01) - timer += 0.01 - if timer > self.timeout: - self.stop() - raise FalconTimeoutError("Failed to arm the acquisition. IOC did not update.") - - # 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. - - At cSAXS with DDGs triggering the devices, we do nothing upon the trigger signal - """ - pass - - def unstage(self) -> List[object]: - """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}, ") - if self.scaninfo.scanID != old_scanID: - self._stopped = True - if self._stopped: - return super().unstage() - self._finished() - state = True - self._publish_file_location(done=state, successful=state) - self._stopped = False - return super().unstage() - - 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 - timer = 0 - while True: - det_ctrl = self.state.read()[self.state.name]["value"] - writer_ctrl = self.hdf5.capture.get() - 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 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(sleep_time) - timer += sleep_time - if timer > self.timeout: - # self._stop_det() - # self._stop_file_writer() - logger.info( - f"Falcon missed a trigger: received trigger {received_frames}, send data {written_frames} from total_frames {total_frames}" - ) - break - self._stop_det() - self._stop_file_writer() - - def stop(self, *, success=False) -> None: - """Stop the scan, with camera and file writer""" - self._stop_det() - self._stop_file_writer() - super().stop(success=success) - self._stopped = True - if __name__ == "__main__": falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True) diff --git a/ophyd_devices/epics/devices/psi_detector_base.py b/ophyd_devices/epics/devices/psi_detector_base.py index 6c77db9..a170db9 100644 --- a/ophyd_devices/epics/devices/psi_detector_base.py +++ b/ophyd_devices/epics/devices/psi_detector_base.py @@ -1,6 +1,6 @@ import time import threading -from bec_lib.core.devicemanager import DeviceStatus +from bec_lib.devicemanager import DeviceStatus import os from typing import List diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index c33d03f..0dc53e9 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -114,6 +114,15 @@ def test_initialize_detector( assert mock_det.cam.trigger_mode.get() == trigger_source +def test_trigger(mock_det): + """Test the trigger function: + Validate that trigger calls the custom_prepare.on_trigger() function + """ + with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger: + mock_det.trigger() + mock_on_trigger.assert_called_once() + + @pytest.mark.parametrize( "readout_time, expected_value", [ diff --git a/tests/test_falcon_csaxs.py b/tests/test_falcon_csaxs.py index 9d5ddc1..c9b2b2b 100644 --- a/tests/test_falcon_csaxs.py +++ b/tests/test_falcon_csaxs.py @@ -28,9 +28,9 @@ def mock_det(): dm = DMMock() with mock.patch.object(dm, "producer"): with mock.patch( - "ophyd_devices.epics.devices.falcon_csaxs.FileWriterMixin" + "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin" ) as filemixin, mock.patch( - "ophyd_devices.epics.devices.falcon_csaxs.FalconcSAXS._update_service_config" + "ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config" ) as mock_service_config: with mock.patch.object(ophyd, "cl") as mock_cl: mock_cl.get_pv = MockPV @@ -76,9 +76,9 @@ def test_init_detector( if expected_exception: with pytest.raises(FalconTimeoutError): mock_det.timeout = 0.1 - mock_det._init_detector() + mock_det.custom_prepare.initialize_detector() else: - mock_det._init_detector() # call the method you want to test + mock_det.custom_prepare.initialize_detector() assert mock_det.state.get() == detector_state assert mock_det.collect_mode.get() == mapping_source assert mock_det.pixel_advance_mode.get() == trigger_source @@ -103,17 +103,19 @@ def test_init_detector( def test_update_readout_time(mock_det, readout_time, expected_value): # mock_det.scaninfo.readout_time = readout_time if readout_time is None: - mock_det._update_readout_time() + mock_det.custom_prepare.update_readout_time() assert mock_det.readout_time == expected_value else: mock_det.scaninfo.readout_time = readout_time - mock_det._update_readout_time() + mock_det.custom_prepare.update_readout_time() assert mock_det.readout_time == expected_value -def test_default_parameter(mock_det): - with mock.patch.object(mock_det, "_update_readout_time") as mock_update_readout_time: - mock_det._default_parameter() +def test_initialize_default_parameter(mock_det): + with mock.patch.object( + mock_det.custom_prepare, "update_readout_time" + ) as mock_update_readout_time: + mock_det.custom_prepare.initialize_default_parameter() assert mock_det._value_pixel_per_buffer == 20 mock_update_readout_time.assert_called_once() @@ -139,12 +141,12 @@ def test_stage(mock_det, scaninfo): This includes testing _prep_det """ - with mock.patch.object(mock_det, "_set_trigger") as mock_set_trigger, mock.patch.object( - mock_det, "_prep_file_writer" - ) as mock_prep_file_writer, mock.patch.object( - mock_det, "_publish_file_location" + with mock.patch.object(mock_det, "set_trigger") as mock_set_trigger, mock.patch.object( + mock_det.custom_prepare, "prepare_data_backend" + ) as mock_prep_data_backend, mock.patch.object( + mock_det.custom_prepare, "publish_file_location" ) as mock_publish_file_location, mock.patch.object( - mock_det, "_arm_acquisition" + mock_det.custom_prepare, "arm_acquisition" ) as mock_arm_acquisition: mock_det.scaninfo.exp_time = scaninfo["exp_time"] mock_det.scaninfo.num_points = scaninfo["num_points"] @@ -155,7 +157,7 @@ def test_stage(mock_det, scaninfo): assert mock_det.pixels_per_run.get() == int( scaninfo["num_points"] * scaninfo["frames_per_trigger"] ) - mock_prep_file_writer.assert_called_once() + mock_prep_data_backend.assert_called_once() mock_publish_file_location.assert_called_once_with(done=False) mock_arm_acquisition.assert_called_once() @@ -179,12 +181,12 @@ def test_stage(mock_det, scaninfo): ), ], ) -def test_prep_file_writer(mock_det, scaninfo): +def test_prepare_data_backend(mock_det, scaninfo): mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] mock_det.scaninfo.num_points = scaninfo["num_points"] mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] mock_det.scaninfo.scan_number = 1 - mock_det._prep_file_writer() + mock_det.custom_prepare.prepare_data_backend() file_path, file_name = os.path.split(scaninfo["filepath"]) assert mock_det.hdf5.file_path.get() == file_path assert mock_det.hdf5.file_name.get() == file_name @@ -208,7 +210,9 @@ def test_prep_file_writer(mock_det, scaninfo): def test_publish_file_location(mock_det, scaninfo): mock_det.scaninfo.scanID = scaninfo["scanID"] mock_det.filepath = scaninfo["filepath"] - mock_det._publish_file_location(done=scaninfo["done"], successful=scaninfo["successful"]) + mock_det.custom_prepare.publish_file_location( + done=scaninfo["done"], successful=scaninfo["successful"] + ) if scaninfo["successful"] is None: msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() else: @@ -243,15 +247,15 @@ def test_arm_acquisition(mock_det, detector_state, expected_exception): if expected_exception: with pytest.raises(FalconTimeoutError): mock_det.timeout = 0.1 - mock_det._arm_acquisition() + mock_det.custom_prepare.arm_acquisition() mock_stop.assert_called_once() else: - mock_det._arm_acquisition() + mock_det.custom_prepare.arm_acquisition() assert mock_det.start_all.get() == 1 def test_trigger(mock_det): - with mock.patch.object(mock_det, "_on_trigger") as mock_on_trigger: + with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger: mock_det.trigger() mock_on_trigger.assert_called_once() @@ -274,8 +278,8 @@ def test_unstage( stopped, expected_abort, ): - with mock.patch.object(mock_det, "_finished") as mock_finished, mock.patch.object( - mock_det, "_publish_file_location" + with mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished, mock.patch.object( + mock_det.custom_prepare, "publish_file_location" ) as mock_publish_file_location: mock_det._stopped = stopped if expected_abort: @@ -290,12 +294,14 @@ def test_unstage( def test_stop(mock_det): - with mock.patch.object(mock_det, "_stop_det") as mock_stop_det, mock.patch.object( - mock_det, "_stop_file_writer" - ) as mock_stop_file_writer: + with mock.patch.object( + mock_det.custom_prepare, "stop_detector" + ) as mock_stop_det, mock.patch.object( + mock_det.custom_prepare, "stop_detector_backend" + ) as mock_stop_detector_backend: mock_det.stop() mock_stop_det.assert_called_once() - mock_stop_file_writer.assert_called_once() + mock_stop_detector_backend.assert_called_once() assert mock_det._stopped == True @@ -307,8 +313,10 @@ def test_stop(mock_det): ], ) def test_finished(mock_det, stopped, scaninfo): - with mock.patch.object(mock_det, "_stop_det") as mock_stop_det, mock.patch.object( - mock_det, "_stop_file_writer" + with mock.patch.object( + mock_det.custom_prepare, "stop_detector" + ) as mock_stop_det, mock.patch.object( + mock_det.custom_prepare, "stop_detector_backend" ) as mock_stop_file_writer: mock_det._stopped = stopped mock_det.dxp.current_pixel._read_pv.mock_data = int( @@ -319,7 +327,7 @@ def test_finished(mock_det, stopped, scaninfo): ) mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] mock_det.scaninfo.num_points = scaninfo["num_points"] - mock_det._finished() + mock_det.custom_prepare.finished() assert mock_det._stopped == stopped mock_stop_det.assert_called_once() mock_stop_file_writer.assert_called_once()