From 72b88482ca8b5104dbcf3e8a4e430497eb5fd5f8 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 30 Oct 2023 22:24:25 +0100 Subject: [PATCH 01/23] fix: bugfixes after adding tests --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 528 +++++++++++-------- 1 file changed, 315 insertions(+), 213 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 962476b..e1768ee 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -1,78 +1,60 @@ import enum -import threading import time -from typing import Any, List +import threading +from bec_lib.core.devicemanager import DeviceStatus 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 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 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_images = ADCpt(EpicsSignalWithRBV, "NumCycles") num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames") - timing_mode = ADCpt(EpicsSignalWithRBV, "TimingMode") + trigger_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 @@ -80,6 +62,8 @@ class TriggerSource(int, enum.Enum): class DetectorState(int, enum.Enum): + """Detector states for Eiger9M detector""" + IDLE = 0 ERROR = 1 WAITING = 2 @@ -105,6 +89,7 @@ class Eiger9mCsaxs(DetectorBase): """ + # Specify which functions are revealed to the user in BEC client USER_ACCESS = [ "describe", ] @@ -124,6 +109,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, @@ -133,46 +130,90 @@ 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") + # TODO check if threadlock is needed for unstage + self._lock = threading.RLock() + self._stopped = False self.name = name - self.wait_for_connection() # Make sure to be connected before talking to PVs + self.service_cfg = None + self.std_client = None + self.wait_for_connection(all_signals=True) if not sim_mode: - from bec_lib.core.bec_service import SERVICE_CONFIG - + self._update_service_config() self.device_manager = device_manager self._producer = self.device_manager.producer - self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] else: 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/"} + 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() - self._init_standard_daq() + self._init() - # self.mokev = self.device_manager.devices.mokev.read()[ - # self.device_manager.devices.mokev.name - # ]["value"] + def _update_service_config(self) -> None: + from bec_lib.core.bec_service import SERVICE_CONFIG - def _init_eiger9m(self) -> None: - """Init parameters for Eiger 9m""" + self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"] + + # TODO function for abstract class? + def _init(self) -> None: + """Initialize detector, filewriter and set default parameters""" + self._default_parameter() + 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. + At this point it is set up for gating mode (09/2023). + """ + self._stop_det() self._set_trigger(TriggerSource.GATING) - self.stop_acquisition() + + # TODO function for abstract class? + 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 put back change of e-account! and check with Leo which status to wait for + eacc = self.scaninfo.username + self._update_std_cfg("writer_user_id", int(eacc.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"] == "READY": + 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: + """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) @@ -189,71 +230,60 @@ 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 + # TODO function for abstract class? + 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. - def _prep_det(self) -> None: - self._set_det_threshold() - self._set_acquisition_params() - self._set_trigger(TriggerSource.GATING) + #TODO make sure this is fullfiled - 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 + Staging not idempotent and should raise + :obj:`RedundantStaging` if staged twice without an + intermediate :meth:`~BlueskyInterface.unstage`. """ - value = int(trigger_source) - self.cam.timing_mode.put(value) + self._stopped = False + self.scaninfo.load_scan_metadata() + self.mokev = self.device_manager.devices.mokev.obj.read()[ + self.device_manager.devices.mokev.name + ]["value"] + # TODO refactor logger.info to DEBUG mode? + self._prep_file_writer() + self._prep_det() + state = False + self._publish_file_location(done=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 + time.sleep(0.05) + return super().stage() + def _filepath_exists(self, filepath: str) -> None: + timer = 0 + while not os.path.exists(os.path.dirname(self.filepath)): + timer = time + 0.1 + time.sleep(0.1) + if timer > 3: + raise EigerError(f"Timeout of 3s reached for filepath {self.filepath}") + + # 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 + """ + timer = 0 self.filepath = self.filewriter.compile_full_filename( self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True ) - while not os.path.exists(os.path.dirname(self.filepath)): - time.sleep(0.1) - self._close_file_writer() + self._filepath_exists(self.filepath) + 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: self.std_client.start_writer_async( { @@ -267,121 +297,192 @@ class Eiger9mCsaxs(DetectorBase): raise EigerError(f"Timeout of start_writer_async with {exc}") while True: + timer = timer + 0.01 det_ctrl = self.std_client.get_status()["acquisition"]["state"] if det_ctrl == "WAITING_IMAGES": break - time.sleep(0.005) - - def _close_file_writer(self) -> None: - self.std_client.stop_writer() - pass - - 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"] - - self._prep_file_writer() - self._prep_det() - logger.info("Waiting for std daq to be armed") - logger.info("std_daq is ready") - - msg = BECMessage.FileMessage(file_path=self.filepath, done=False) - self._producer.set_and_publish( - MessageEndpoints.public_file(self.scaninfo.scanID, self.name), - msg.dumps(), - ) - msg = BECMessage.FileMessage(file_path=self.filepath, done=False) - self._producer.set_and_publish( - 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() - - @threadlocked - def unstage(self) -> List[object]: - """unstage the detector and file writer""" - logger.info("Waiting for Eiger9M to finish") - 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 - 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._stopped = False - logger.info("Eiger9M finished") - return super().unstage() - - @threadlocked - def _eiger9M_finished(self): - """Function with 10s timeout""" - timer = 0 - 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() - break - time.sleep(0.1) - timer += 0.1 + time.sleep(0.01) if timer > 5: - self._stopped == True self._close_file_writer() - self.stop_acquisition() - 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" + raise EigerError( + f"Timeout of 5s reached for std_daq start_writer_async with std_daq client status {det_ctrl}" ) - self._close_file_writer() - def arm_acquisition(self) -> None: - """Start acquisition in software trigger mode, - or arm the detector in hardware of the detector + # TODO function for abstract class? + 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 stopped and 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 """ + self._set_det_threshold() + self._set_acquisition_params() + self._set_trigger(TriggerSource.GATING) + + 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 + unit = getattr(self.cam.threshold_energy, "units", None) + if unit != None and unit == "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) + + # 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 + + Args: + trigger_source (TriggerSource): Trigger source for the detector + + """ + value = int(trigger_source) + self.cam.trigger_mode.put(value) + + 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._producer.pipeline() + if successful is None: + msg = BECMessage.FileMessage(file_path=self.filepath, done=done) + else: + 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(), pipe=pipe + ) + pipe.execute() + + # TODO function for abstract class? + def _arm_acquisition(self) -> None: + """Arm Eiger detector for acquisition""" + timer = 0 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") + time.sleep(0.01) + timer += 0.01 + if timer > 5: + self.stop() + raise EigerTimeoutError("Failed to arm the acquisition. IOC did not update.") - def stop_acquisition(self) -> None: + # 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 + + # 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 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 + old_scanID = self.scaninfo.scanID + self.scaninfo.load_scan_metadata() + if self.scaninfo.scanID != old_scanID: + self._stopped = True + if self._stopped == True: + return super().unstage() + self._finished() + state = True + self._publish_file_location(done=state, successful=state) + self._stopped = False + return super().unstage() + + # TODO function for abstract class? + # TODO necessary, how can we make this cleaner. + @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 + timer = 0 + # Check status with timeout, break out if _stopped=True + while True: + det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"] + status = self.std_client.get_status() + std_ctrl = status["acquisition"]["state"] + received_frames = status["acquisition"]["stats"]["n_write_completed"] + total_frames = int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger) + if det_ctrl == 0 and std_ctrl == "FINISHED" and total_frames == received_frames: + break + if self._stopped == True: + break + time.sleep(sleep_time) + timer += sleep_time + if timer > timeout: + self._stopped == True + 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_det() + self._stop_file_writer() + + def _stop_det(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): @@ -390,16 +491,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_det() + self._stop_file_writer() super().stop(success=success) self._stopped = True From 78ba00ce14490ac38bb0439737faced6ae7282a3 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 30 Oct 2023 22:25:15 +0100 Subject: [PATCH 02/23] test: add tests for eiger --- tests/test_eiger9m_csaxs.py | 539 ++++++++++++++++++++++++++++++++++++ 1 file changed, 539 insertions(+) create mode 100644 tests/test_eiger9m_csaxs.py diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py new file mode 100644 index 0000000..e69777d --- /dev/null +++ b/tests/test_eiger9m_csaxs.py @@ -0,0 +1,539 @@ +import pytest +from unittest import mock + +from ophyd.signal import Signal + +from bec_lib.core import BECMessage, MessageEndpoints +from bec_lib.core.devicemanager import DeviceContainer +from bec_lib.core.tests.utils import ProducerMock + + +class MockSignal(Signal): + def __init__(self, read_pv, *, name=None, parent=None, **kwargs): + self.read_pv = read_pv + super().__init__(name=name, parent=parent, **kwargs) + self._waited_for_connection = False + self._subscriptions = [] + + def wait_for_connection(self): + self._waited_for_connection = True + + def subscribe(self, method, event_type, **kw): + self._subscriptions.append((method, event_type, kw)) + + def describe_configuration(self): + return {self.name + "_conf": {"source": "SIM:test"}} + + def read_configuration(self): + return {self.name + "_conf": {"value": 0}} + + +with mock.patch("ophyd.EpicsSignal", new=MockSignal), mock.patch( + "ophyd.EpicsSignalRO", new=MockSignal +), mock.patch("ophyd.EpicsSignalWithRBV", new=MockSignal): + from ophyd_devices.epics.devices.eiger9m_csaxs import Eiger9mCsaxs + + +# TODO maybe specify here that this DeviceMock is for usage in the DeviceServer +class DeviceMock: + def __init__(self, name: str, value: float = 0.0): + self.name = name + self.read_buffer = value + self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None} + self._enabled_set = True + self._enabled = True + + def read(self): + return {self.name: {"value": self.read_buffer}} + + def readback(self): + return self.read_buffer + + @property + def enabled_set(self) -> bool: + return self._enabled_set + + @enabled_set.setter + def enabled_set(self, val: bool): + self._enabled_set = val + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, val: bool): + self._enabled = val + + @property + def user_parameter(self): + return self._config["userParameter"] + + @property + def obj(self): + return self + + +class DMMock: + """Mock for DeviceManager + + The mocked DeviceManager creates a device containert and a producer. + + """ + + def __init__(self): + self.devices = DeviceContainer() + self.producer = ProducerMock() + + def add_device(self, name: str, value: float = 0.0): + self.devices[name] = DeviceMock(name, value) + + +@pytest.fixture(scope="function") +def mock_det(): + name = "eiger" + prefix = "X12SA-ES-EIGER9M:" + sim_mode = False + dm = DMMock() + # dm.add_device("mokev", value=12.4) + with mock.patch.object(dm, "producer"): + with mock.patch( + "ophyd_devices.epics.devices.eiger9m_csaxs.BecScaninfoMixin" + ) as mixin, mock.patch( + "ophyd_devices.epics.devices.eiger9m_csaxs.FileWriterMixin" + ) as filemixin, mock.patch( + "ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9mCsaxs._update_service_config" + ) as mock_service_config: + with mock.patch.object(Eiger9mCsaxs, "_init"): + yield Eiger9mCsaxs(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + + +@pytest.mark.parametrize( + "trigger_source, stopped, detector_state, expected_exception", + [ + (2, True, 1, False), + (2, False, 0, True), + ], +) +# TODO rewrite this one, write test for init_detector, init_filewriter is tested +def test_init( + trigger_source, + stopped, + detector_state, + expected_exception, +): + """Test the _init function: + + This includes testing the functions: + - _set_default_parameter + - _init_detector + - _stop_det + - _set_trigger + --> Testing the filewriter is done in test_init_filewriter + + Validation upon setting the correct PVs + + """ + name = "eiger" + prefix = "X12SA-ES-EIGER9M:" + sim_mode = False + dm = DMMock() + # dm.add_device("mokev", value=12.4) + with mock.patch.object(dm, "producer"): + with mock.patch( + "ophyd_devices.epics.devices.eiger9m_csaxs.BecScaninfoMixin" + ) as mixin, mock.patch( + "ophyd_devices.epics.devices.eiger9m_csaxs.FileWriterMixin" + ) as filemixin, mock.patch( + "ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9mCsaxs._update_service_config" + ) as mock_service_config: + with mock.patch.object(Eiger9mCsaxs, "_init_filewriter") as mock_init_fw: + mock_det = Eiger9mCsaxs( + name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode + ) + mock_det.cam.detector_state.put(detector_state) + mock_det._stopped = stopped + if expected_exception: + with pytest.raises(Exception): + mock_det._init() + mock_init_fw.assert_called_once() + else: + mock_det._init() # call the method you want to test + assert mock_det.cam.acquire.get() == 0 + assert mock_det.cam.detector_state.get() == detector_state + assert mock_det.cam.trigger_mode.get() == trigger_source + mock_init_fw.assert_called() + assert mock_init_fw.call_count == 2 + + +@pytest.mark.parametrize( + "eacc, exp_url, daq_status, daq_cfg, expected_exception", + [ + ( + "e12345", + "http://xbl-daq-29:5000", + {"state": "READY"}, + {"writer_user_id": 12543}, + False, + ), + ( + "e12345", + "http://xbl-daq-29:5000", + {"state": "READY"}, + {"writer_user_id": 15421}, + False, + ), + ( + "e12345", + "http://xbl-daq-29:5000", + {"state": "BUSY"}, + {"writer_user_id": 15421}, + True, + ), + ( + "e12345", + "http://xbl-daq-29:5000", + {"state": "READY"}, + {"writer_ud": 12345}, + True, + ), + ], +) +def test_init_filewriter(mock_det, eacc, exp_url, daq_status, daq_cfg, expected_exception): + """Test _init_filewriter (std daq in this case) + + This includes testing the functions: + + - _update_service_config + + Validation upon checking set values in mocked std_daq instance + """ + with mock.patch("ophyd_devices.epics.devices.eiger9m_csaxs.StdDaqClient") as mock_std_daq: + instance = mock_std_daq.return_value + instance.stop_writer.return_value = None + instance.get_status.return_value = daq_status + instance.get_config.return_value = daq_cfg + mock_det.scaninfo.username = eacc + # scaninfo.username.return_value = eacc + if expected_exception: + with pytest.raises(Exception): + mock_det._init_filewriter() + else: + mock_det._init_filewriter() + + assert mock_det.std_rest_server_url == exp_url + instance.stop_writer.assert_called_once() + instance.get_status.assert_called() + instance.set_config.assert_called_once_with(daq_cfg) + + +@pytest.mark.parametrize( + "scaninfo, daq_status, daq_cfg, detector_state, stopped, expected_exception", + [ + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + "mokev": 12.4, + }, + {"state": "READY"}, + {"writer_user_id": 12543}, + 5, + False, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + "mokev": 12.4, + }, + {"state": "BUSY"}, + {"writer_user_id": 15421}, + 5, + False, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + "mokev": 18.4, + }, + {"state": "READY"}, + {"writer_user_id": 12345}, + 4, + False, + True, + ), + ], +) +def test_stage( + mock_det, + scaninfo, + daq_status, + daq_cfg, + detector_state, + stopped, + expected_exception, +): + with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( + Eiger9mCsaxs, "_publish_file_location" + ) as mock_publish_file_location: + mock_std_daq.stop_writer.return_value = None + mock_std_daq.get_status.return_value = daq_status + mock_std_daq.get_config.return_value = daq_cfg + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] + # TODO consider putting energy as variable in scaninfo + mock_det.device_manager.add_device("mokev", value=12.4) + mock_det.cam.beam_energy.put(scaninfo["mokev"]) + mock_det._stopped = stopped + mock_det.cam.detector_state.put(detector_state) + with mock.patch.object(mock_det, "_prep_file_writer") as mock_prep_fw: + mock_det.filepath = scaninfo["filepath"] + if expected_exception: + with pytest.raises(Exception): + mock_det.stage() + else: + mock_det.stage() + mock_prep_fw.assert_called_once() + # Check _prep_det + assert mock_det.cam.num_images.get() == int( + scaninfo["num_points"] * scaninfo["frames_per_trigger"] + ) + assert mock_det.cam.num_frames.get() == 1 + + mock_publish_file_location.assert_called_with(done=False) + assert mock_det.cam.acquire.get() == 1 + + +@pytest.mark.parametrize( + "scaninfo, daq_status, expected_exception", + [ + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + }, + {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + }, + {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + }, + {"state": "BUSY", "acquisition": {"state": "ERROR"}}, + True, + ), + ], +) +def test_prep_file_writer(mock_det, scaninfo, daq_status, expected_exception): + with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( + mock_det, "_filepath_exists" + ) as mock_file_path_exists, mock.patch.object( + mock_det, "_stop_file_writer" + ) as mock_stop_file_writer: + # mock_det = eiger_factory(name, prefix, sim_mode) + mock_det.std_client = mock_std_daq + mock_std_daq.start_writer_async.return_value = None + mock_std_daq.get_status.return_value = daq_status + 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"] + + if expected_exception: + with pytest.raises(Exception): + mock_det._prep_file_writer() + mock_file_path_exists.assert_called_once() + assert mock_stop_file_writer.call_count == 2 + + else: + mock_det._prep_file_writer() + mock_file_path_exists.assert_called_once() + mock_stop_file_writer.assert_called_once() + + daq_writer_call = { + "output_file": scaninfo["filepath"], + "n_images": int(scaninfo["num_points"] * scaninfo["frames_per_trigger"]), + } + mock_std_daq.start_writer_async.assert_called_with(daq_writer_call) + + +@pytest.mark.parametrize( + "stopped, expected_exception", + [ + ( + False, + False, + ), + ( + True, + True, + ), + ], +) +def test_unstage( + mock_det, + stopped, + expected_exception, +): + with mock.patch.object(mock_det, "_finished") as mock_finished, mock.patch.object( + mock_det, "_publish_file_location" + ) as mock_publish_file_location: + mock_det._stopped = stopped + if expected_exception: + mock_det.unstage() + assert mock_det._stopped == True + else: + mock_det.unstage() + mock_finished.assert_called_once() + mock_publish_file_location.assert_called_with(done=True, successful=True) + assert mock_det._stopped == False + + +def test_stop_fw(mock_det): + with mock.patch.object(mock_det, "std_client") as mock_std_daq: + mock_std_daq.stop_writer.return_value = None + mock_det.std_client = mock_std_daq + mock_det._stop_file_writer() + mock_std_daq.stop_writer.assert_called_once() + + +@pytest.mark.parametrize( + "scaninfo", + [ + ({"filepath": "test.h5", "successful": True, "done": False, "scanID": "123"}), + ({"filepath": "test.h5", "successful": False, "done": True, "scanID": "123"}), + ({"filepath": "test.h5", "successful": None, "done": True, "scanID": "123"}), + ], +) +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"]) + if scaninfo["successful"] is None: + msg = BECMessage.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() + else: + msg = BECMessage.FileMessage( + file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] + ).dumps() + expected_calls = [ + mock.call( + MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), + msg, + pipe=mock_det._producer.pipeline.return_value, + ), + mock.call( + MessageEndpoints.file_event(mock_det.name), + msg, + pipe=mock_det._producer.pipeline.return_value, + ), + ] + assert mock_det._producer.set_and_publish.call_args_list == expected_calls + + +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: + mock_det.stop() + mock_stop_det.assert_called_once() + mock_stop_file_writer.assert_called_once() + assert mock_det._stopped == True + + +@pytest.mark.parametrize( + "stopped, scaninfo, cam_state, daq_status, expected_exception", + [ + ( + False, + { + "num_points": 500, + "frames_per_trigger": 4, + }, + 0, + {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 2000}}}, + False, + ), + ( + False, + { + "num_points": 500, + "frames_per_trigger": 4, + }, + 0, + {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 1999}}}, + True, + ), + ( + False, + { + "num_points": 500, + "frames_per_trigger": 1, + }, + 1, + {"acquisition": {"state": "READY", "stats": {"n_write_completed": 500}}}, + True, + ), + ( + False, + { + "num_points": 500, + "frames_per_trigger": 1, + }, + 0, + {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 500}}}, + False, + ), + ], +) +def test_finished(mock_det, stopped, cam_state, daq_status, scaninfo, expected_exception): + with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( + mock_det, "_stop_file_writer" + ) as mock_stop_file_friter, mock.patch.object(mock_det, "_stop_det") as mock_stop_det: + mock_std_daq.get_status.return_value = daq_status + mock_det.cam.acquire.put(cam_state) + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + if expected_exception: + with pytest.raises(Exception): + mock_det._finished() + assert mock_det._stopped == stopped + mock_stop_file_friter.assert_called() + mock_stop_det.assert_called_once() + else: + mock_det._finished() + if stopped: + assert mock_det._stopped == stopped + + mock_stop_file_friter.assert_called() + mock_stop_det.assert_called_once() From 9dcf92af006ba903f2aedc970f468e470b2dd05c Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 31 Oct 2023 11:35:37 +0100 Subject: [PATCH 03/23] refactor: generalize sim_mode --- .../epics/devices/bec_scaninfo_mixin.py | 71 ++++++++++-- ophyd_devices/epics/devices/eiger9m_csaxs.py | 12 +- ophyd_devices/utils/bec_utils.py | 107 +++++++++++++---- tests/test_eiger9m_csaxs.py | 108 ++++++++++++++---- 4 files changed, 231 insertions(+), 67 deletions(-) diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index 5adf6ef..188e30f 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -6,23 +6,70 @@ from bec_lib.core import bec_logger logger = bec_logger.logger +class Bec_Info_Msg_Mock: + def __init__( + self, + mockrid: str = "mockrid1111", + mockqueueid: str = "mockqueueID111", + scan_number: int = 1, + exp_time: float = 12e-3, + num_points: int = 500, + readout_time: float = 3e-3, + scan_type: str = "fly", + num_lines: int = 1, + frames_per_trigger: int = 1, + ) -> None: + self.mockrid = mockrid + self.mockqueueid = mockqueueid + self.scan_number = scan_number + self.exp_time = exp_time + self.num_points = num_points + self.readout_time = readout_time + self.scan_type = scan_type + self.num_lines = num_lines + self.frames_per_trigger = frames_per_trigger + + def get_bec_info_msg(self) -> dict: + info_msg = { + "RID": self.mockrid, + "queueID": self.mockqueueid, + "scan_number": self.scan_number, + "exp_time": self.exp_time, + "num_points": self.num_points, + "readout_time": self.readout_time, + "scan_type": self.scan_type, + "num_lines": self.exp_time, + "frames_per_trigger": self.frames_per_trigger, + } + + return info_msg + + class BecScaninfoMixin: - def __init__(self, device_manager: DeviceManagerBase = None, sim_mode=False) -> None: + def __init__( + self, device_manager: DeviceManagerBase = None, sim_mode: bool = False, bec_info_msg=None + ) -> None: self.device_manager = device_manager self.sim_mode = sim_mode self.scan_msg = None self.scanID = None - self.bec_info_msg = { - "RID": "mockrid", - "queueID": "mockqueuid", - "scan_number": 1, - "exp_time": 12e-3, - "num_points": 500, - "readout_time": 3e-3, - "scan_type": "fly", - "num_lines": 1, - "frames_per_trigger": 1, - } + if bec_info_msg is None: + bec_info_msg_mock = Bec_Info_Msg_Mock() + self.bec_info_msg = bec_info_msg_mock.get_bec_info_msg() + else: + self.bec_info_msg = bec_info_msg + + # self.bec_info_msg = { + # "RID": "mockrid", + # "queueID": "mockqueuid", + # "scan_number": 1, + # "exp_time": 12e-3, + # "num_points": 500, + # "readout_time": 3e-3, + # "scan_type": "fly", + # "num_lines": 1, + # "frames_per_trigger": 1, + # } def get_bec_info_msg(self) -> None: return self.bec_info_msg diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index e1768ee..fa89c94 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -143,15 +143,11 @@ class Eiger9mCsaxs(DetectorBase): if not sim_mode: self._update_service_config() self.device_manager = device_manager - self._producer = self.device_manager.producer else: - self._producer = bec_utils.MockProducer() - self.device_manager = bec_utils.MockDeviceManager() - self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) - self.scaninfo.load_scan_metadata() - base_path = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" - self.service_cfg = {"base_path": base_path} - + self.device_manager = bec_utils.DMMock() + base_path = f"~/Data10/" + self.service_cfg = {"base_path": os.path.expanduser(base_path)} + self._producer = self.device_manager.producer self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self.scaninfo.load_scan_metadata() self.filewriter = FileWriterMixin(self.service_cfg) diff --git a/ophyd_devices/utils/bec_utils.py b/ophyd_devices/utils/bec_utils.py index 304a2ed..1ac07a8 100644 --- a/ophyd_devices/utils/bec_utils.py +++ b/ophyd_devices/utils/bec_utils.py @@ -1,6 +1,8 @@ import time from bec_lib.core import bec_logger +from bec_lib.core.devicemanager import DeviceContainer +from bec_lib.core.tests.utils import ProducerMock from ophyd import Signal, Kind @@ -11,33 +13,88 @@ logger = bec_logger.logger DEFAULT_EPICSSIGNAL_VALUE = object() -class MockProducer: - def set_and_publish(self, endpoint: str, msgdump: str): - logger.info(f"BECMessage to {endpoint} with msg dump {msgdump}") - - -class MockDeviceManager: - def __init__(self) -> None: - self.devices = devices() - - -class OphydObject: - def __init__(self) -> None: - self.name = "mock_mokev" - self.obj = mokev() - - -class devices: - def __init__(self): - self.mokev = OphydObject() - - -class mokev: - def __init__(self): - self.name = "mock_mokev" +# TODO maybe specify here that this DeviceMock is for usage in the DeviceServer +class DeviceMock: + def __init__(self, name: str, value: float = 0.0): + self.name = name + self.read_buffer = value + self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None} + self._enabled_set = True + self._enabled = True def read(self): - return {self.name: {"value": 16.0, "timestamp": time.time()}} + return {self.name: {"value": self.read_buffer}} + + def readback(self): + return self.read_buffer + + @property + def enabled_set(self) -> bool: + return self._enabled_set + + @enabled_set.setter + def enabled_set(self, val: bool): + self._enabled_set = val + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, val: bool): + self._enabled = val + + @property + def user_parameter(self): + return self._config["userParameter"] + + @property + def obj(self): + return self + + +class DMMock: + """Mock for DeviceManager + + The mocked DeviceManager creates a device containert and a producer. + + """ + + def __init__(self): + self.devices = DeviceContainer() + self.producer = ProducerMock() + + def add_device(self, name: str, value: float = 0.0): + self.devices[name] = DeviceMock(name, value) + + +# class MockProducer: +# def set_and_publish(self, endpoint: str, msgdump: str): +# logger.info(f"BECMessage to {endpoint} with msg dump {msgdump}") + + +# class MockDeviceManager: +# def __init__(self) -> None: +# self.devices = devices() + + +# class OphydObject: +# def __init__(self) -> None: +# self.name = "mock_mokev" +# self.obj = mokev() + + +# class devices: +# def __init__(self): +# self.mokev = OphydObject() + + +# class mokev: +# def __init__(self): +# self.name = "mock_mokev" + +# def read(self): +# return {self.name: {"value": 16.0, "timestamp": time.time()}} class ConfigSignal(Signal): diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index e69777d..de403f7 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -109,10 +109,52 @@ def mock_det(): @pytest.mark.parametrize( - "trigger_source, stopped, detector_state, expected_exception", + "trigger_source, stopped, detector_state, sim_mode, scan_status_msg, expected_exception", [ - (2, True, 1, False), - (2, False, 0, True), + ( + 2, + True, + 1, + True, + BECMessage.ScanStatusMessage( + scanID="1", + status={}, + info={ + "RID": "mockrid1111", + "queueID": "mockqueueID111", + "scan_number": 1, + "exp_time": 0.012, + "num_points": 500, + "readout_time": 0.003, + "scan_type": "fly", + "num_lines": 0.012, + "frames_per_trigger": 1, + }, + ), + False, + ), + ( + 2, + False, + 0, + False, + BECMessage.ScanStatusMessage( + scanID="1", + status={}, + info={ + "RID": "mockrid1111", + "queueID": "mockqueueID111", + "scan_number": 1, + "exp_time": 0.012, + "num_points": 500, + "readout_time": 0.003, + "scan_type": "fly", + "num_lines": 0.012, + "frames_per_trigger": 1, + }, + ), + True, + ), ], ) # TODO rewrite this one, write test for init_detector, init_filewriter is tested @@ -120,6 +162,8 @@ def test_init( trigger_source, stopped, detector_state, + sim_mode, + scan_status_msg, expected_exception, ): """Test the _init function: @@ -136,34 +180,54 @@ def test_init( """ name = "eiger" prefix = "X12SA-ES-EIGER9M:" - sim_mode = False + # sim_mode = sim_mode dm = DMMock() # dm.add_device("mokev", value=12.4) - with mock.patch.object(dm, "producer"): + with mock.patch.object(dm, "producer") as producer: with mock.patch( - "ophyd_devices.epics.devices.eiger9m_csaxs.BecScaninfoMixin" - ) as mixin, mock.patch( "ophyd_devices.epics.devices.eiger9m_csaxs.FileWriterMixin" ) as filemixin, mock.patch( "ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9mCsaxs._update_service_config" ) as mock_service_config: with mock.patch.object(Eiger9mCsaxs, "_init_filewriter") as mock_init_fw: - mock_det = Eiger9mCsaxs( - name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode - ) - mock_det.cam.detector_state.put(detector_state) - mock_det._stopped = stopped - if expected_exception: - with pytest.raises(Exception): - mock_det._init() - mock_init_fw.assert_called_once() + producer.get.return_value = scan_status_msg.dumps() + if sim_mode: + mock_det = Eiger9mCsaxs( + name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode + ) + mock_det.cam.detector_state.put(detector_state) + mock_det._stopped = stopped + if expected_exception: + with pytest.raises(Exception): + mock_det._init() + mock_init_fw.assert_called_once() + else: + mock_det._init() # call the method you want to test + assert mock_det.cam.acquire.get() == 0 + assert mock_det.cam.detector_state.get() == detector_state + assert mock_det.cam.trigger_mode.get() == trigger_source + mock_init_fw.assert_called() + assert mock_init_fw.call_count == 2 else: - mock_det._init() # call the method you want to test - assert mock_det.cam.acquire.get() == 0 - assert mock_det.cam.detector_state.get() == detector_state - assert mock_det.cam.trigger_mode.get() == trigger_source - mock_init_fw.assert_called() - assert mock_init_fw.call_count == 2 + with mock.patch( + "ophyd_devices.epics.devices.eiger9m_csaxs.BecScaninfoMixin" + ) as mixin: + mock_det = Eiger9mCsaxs( + name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode + ) + mock_det.cam.detector_state.put(detector_state) + mock_det._stopped = stopped + if expected_exception: + with pytest.raises(Exception): + mock_det._init() + mock_init_fw.assert_called_once() + else: + mock_det._init() # call the method you want to test + assert mock_det.cam.acquire.get() == 0 + assert mock_det.cam.detector_state.get() == detector_state + assert mock_det.cam.trigger_mode.get() == trigger_source + mock_init_fw.assert_called() + assert mock_init_fw.call_count == 2 @pytest.mark.parametrize( From 70ba2baedcfa64c81e5c9e70e297a02e275f57ee Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 31 Oct 2023 12:23:08 +0100 Subject: [PATCH 04/23] refactor: remove test case without sim_mode from init, fix pending --- tests/test_eiger9m_csaxs.py | 44 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index de403f7..7c2e84b 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -133,28 +133,28 @@ def mock_det(): ), False, ), - ( - 2, - False, - 0, - False, - BECMessage.ScanStatusMessage( - scanID="1", - status={}, - info={ - "RID": "mockrid1111", - "queueID": "mockqueueID111", - "scan_number": 1, - "exp_time": 0.012, - "num_points": 500, - "readout_time": 0.003, - "scan_type": "fly", - "num_lines": 0.012, - "frames_per_trigger": 1, - }, - ), - True, - ), + # ( + # 2, + # False, + # 0, + # False, + # BECMessage.ScanStatusMessage( + # scanID="1", + # status={}, + # info={ + # "RID": "mockrid1111", + # "queueID": "mockqueueID111", + # "scan_number": 1, + # "exp_time": 0.012, + # "num_points": 500, + # "readout_time": 0.003, + # "scan_type": "fly", + # "num_lines": 0.012, + # "frames_per_trigger": 1, + # }, + # ), + # True, + # ), ], ) # TODO rewrite this one, write test for init_detector, init_filewriter is tested From 256aa41690e19a165b3ee17119e380893c568a08 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 3 Nov 2023 15:52:34 +0100 Subject: [PATCH 05/23] refactor: fixed tests and mocks for refactor init --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 26 +++- tests/test_eiger9m_csaxs.py | 135 ++++++++----------- 2 files changed, 75 insertions(+), 86 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index fa89c94..95285b4 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -16,6 +16,7 @@ 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 bec_lib.core.bec_service import SERVICE_CONFIG from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin from ophyd_devices.utils import bec_utils @@ -132,30 +133,41 @@ class Eiger9mCsaxs(DetectorBase): ) if device_manager is None and not sim_mode: raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") - + self.sim_mode = sim_mode # TODO check if threadlock is needed for unstage self._lock = threading.RLock() self._stopped = False self.name = name self.service_cfg = None self.std_client = None + self.scaninfo = None + self.filewriter = None 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 = f"~/Data10/" + 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.scaninfo = BecScaninfoMixin(device_manager, sim_mode) - self.scaninfo.load_scan_metadata() - self.filewriter = FileWriterMixin(self.service_cfg) + self._update_scaninfo() + self._update_filewriter() self._init() - def _update_service_config(self) -> None: - from bec_lib.core.bec_service import SERVICE_CONFIG + 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"] # TODO function for abstract class? diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index 7c2e84b..5088b1c 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -97,23 +97,20 @@ def mock_det(): dm = DMMock() # dm.add_device("mokev", value=12.4) with mock.patch.object(dm, "producer"): - with mock.patch( - "ophyd_devices.epics.devices.eiger9m_csaxs.BecScaninfoMixin" - ) as mixin, mock.patch( + with mock.patch.object( + Eiger9mCsaxs, "_update_service_config" + ) as mock_update_service_config, mock.patch( "ophyd_devices.epics.devices.eiger9m_csaxs.FileWriterMixin" - ) as filemixin, mock.patch( - "ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9mCsaxs._update_service_config" - ) as mock_service_config: + ) as filemixin: with mock.patch.object(Eiger9mCsaxs, "_init"): yield Eiger9mCsaxs(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) @pytest.mark.parametrize( - "trigger_source, stopped, detector_state, sim_mode, scan_status_msg, expected_exception", + "trigger_source, detector_state, sim_mode, scan_status_msg, expected_exception", [ ( 2, - True, 1, True, BECMessage.ScanStatusMessage( @@ -131,36 +128,34 @@ def mock_det(): "frames_per_trigger": 1, }, ), + True, + ), + ( + 2, + 0, + False, + BECMessage.ScanStatusMessage( + scanID="1", + status={}, + info={ + "RID": "mockrid1111", + "queueID": "mockqueueID111", + "scan_number": 1, + "exp_time": 0.012, + "num_points": 500, + "readout_time": 0.003, + "scan_type": "fly", + "num_lines": 0.012, + "frames_per_trigger": 1, + }, + ), False, ), - # ( - # 2, - # False, - # 0, - # False, - # BECMessage.ScanStatusMessage( - # scanID="1", - # status={}, - # info={ - # "RID": "mockrid1111", - # "queueID": "mockqueueID111", - # "scan_number": 1, - # "exp_time": 0.012, - # "num_points": 500, - # "readout_time": 0.003, - # "scan_type": "fly", - # "num_lines": 0.012, - # "frames_per_trigger": 1, - # }, - # ), - # True, - # ), ], ) # TODO rewrite this one, write test for init_detector, init_filewriter is tested def test_init( trigger_source, - stopped, detector_state, sim_mode, scan_status_msg, @@ -180,54 +175,34 @@ def test_init( """ name = "eiger" prefix = "X12SA-ES-EIGER9M:" - # sim_mode = sim_mode + sim_mode = sim_mode dm = DMMock() - # dm.add_device("mokev", value=12.4) - with mock.patch.object(dm, "producer") as producer: - with mock.patch( - "ophyd_devices.epics.devices.eiger9m_csaxs.FileWriterMixin" - ) as filemixin, mock.patch( - "ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9mCsaxs._update_service_config" - ) as mock_service_config: - with mock.patch.object(Eiger9mCsaxs, "_init_filewriter") as mock_init_fw: - producer.get.return_value = scan_status_msg.dumps() - if sim_mode: - mock_det = Eiger9mCsaxs( - name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode - ) - mock_det.cam.detector_state.put(detector_state) - mock_det._stopped = stopped - if expected_exception: - with pytest.raises(Exception): - mock_det._init() - mock_init_fw.assert_called_once() - else: - mock_det._init() # call the method you want to test - assert mock_det.cam.acquire.get() == 0 - assert mock_det.cam.detector_state.get() == detector_state - assert mock_det.cam.trigger_mode.get() == trigger_source - mock_init_fw.assert_called() - assert mock_init_fw.call_count == 2 - else: - with mock.patch( - "ophyd_devices.epics.devices.eiger9m_csaxs.BecScaninfoMixin" - ) as mixin: - mock_det = Eiger9mCsaxs( - name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode - ) - mock_det.cam.detector_state.put(detector_state) - mock_det._stopped = stopped - if expected_exception: - with pytest.raises(Exception): - mock_det._init() - mock_init_fw.assert_called_once() - else: - mock_det._init() # call the method you want to test - assert mock_det.cam.acquire.get() == 0 - assert mock_det.cam.detector_state.get() == detector_state - assert mock_det.cam.trigger_mode.get() == trigger_source - mock_init_fw.assert_called() - assert mock_init_fw.call_count == 2 + with mock.patch.object(dm, "producer") as producer, mock.patch.object( + Eiger9mCsaxs, "_init_filewriter" + ) as mock_init_fw, mock.patch.object( + Eiger9mCsaxs, "_update_scaninfo" + ) as mock_update_scaninfo, mock.patch.object( + Eiger9mCsaxs, "_update_filewriter" + ) as mock_update_filewriter, mock.patch.object( + Eiger9mCsaxs, "_update_service_config" + ) as mock_update_service_config: + mock_det = Eiger9mCsaxs(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + mock_det.cam.detector_state.put(detector_state) + if expected_exception: + with pytest.raises(Exception): + mock_det._init() + mock_init_fw.assert_called_once() + else: + mock_det._init() # call the method you want to test + assert mock_det.cam.acquire.get() == 0 + assert mock_det.cam.detector_state.get() == detector_state + assert mock_det.cam.trigger_mode.get() == trigger_source + mock_init_fw.assert_called() + mock_update_scaninfo.assert_called_once() + mock_update_filewriter.assert_called_once() + mock_update_service_config.assert_called_once() + + assert mock_init_fw.call_count == 2 @pytest.mark.parametrize( @@ -425,7 +400,9 @@ def test_prep_file_writer(mock_det, scaninfo, daq_status, expected_exception): mock_det, "_filepath_exists" ) as mock_file_path_exists, mock.patch.object( mock_det, "_stop_file_writer" - ) as mock_stop_file_writer: + ) as mock_stop_file_writer, mock.patch.object( + mock_det, "scaninfo" + ) as mock_scaninfo: # mock_det = eiger_factory(name, prefix, sim_mode) mock_det.std_client = mock_std_daq mock_std_daq.start_writer_async.return_value = None From 5d02a13ad0fea7e07c41a2ece291df190328bc4c Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 3 Nov 2023 16:06:19 +0100 Subject: [PATCH 06/23] refactor: class renaming and minor changes in variable names --- ophyd_devices/epics/devices/__init__.py | 2 +- ophyd_devices/epics/devices/eiger9m_csaxs.py | 28 +++++++++++--------- tests/test_eiger9m_csaxs.py | 20 +++++++------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 285483a..6222eb9 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -25,7 +25,7 @@ from ophyd.quadem import QuadEM # cSAXS from .epics_motor_ex import EpicsMotorEx from .mcs_csaxs import McsCsaxs -from .eiger9m_csaxs import Eiger9mCsaxs +from .eiger9m_csaxs import Eiger9McSAXS from .pilatus_csaxs import PilatusCsaxs from .falcon_csaxs import FalconCsaxs from .DelayGeneratorDG645 import DelayGeneratorDG645 diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 95285b4..38ca436 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -30,13 +30,13 @@ class EigerError(Exception): pass -class EigerTimeoutError(Exception): +class EigerTimeoutError(EigerError): """Raised when the Eiger does not respond in time during unstage.""" pass -class SlsDetectorCam(Device): +class SLSDetectorCam(Device): """SLS Detector Camera - Eiger 9M Base class to map EPICS PVs to ophyd signals. @@ -53,7 +53,7 @@ class SlsDetectorCam(Device): detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV") -class TriggerSource(int, enum.Enum): +class TriggerSource(enum.IntEnum): """Trigger signals for Eiger9M detector""" AUTO = 0 @@ -62,7 +62,7 @@ class TriggerSource(int, enum.Enum): BURST_TRIGGER = 3 -class DetectorState(int, enum.Enum): +class DetectorState(enum.IntEnum): """Detector states for Eiger9M detector""" IDLE = 0 @@ -78,7 +78,7 @@ class DetectorState(int, enum.Enum): ABORTED = 10 -class Eiger9mCsaxs(DetectorBase): +class Eiger9McSAXS(DetectorBase): """Eiger 9M detector for CSAXS Parent class: DetectorBase @@ -95,7 +95,7 @@ class Eiger9mCsaxs(DetectorBase): "describe", ] - cam = ADCpt(SlsDetectorCam, "cam1:") + cam = ADCpt(SLSDetectorCam, "cam1:") def __init__( self, @@ -132,7 +132,9 @@ class Eiger9mCsaxs(DetectorBase): **kwargs, ) if device_manager is None and not sim_mode: - raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") + raise Exception( + 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 # TODO check if threadlock is needed for unstage self._lock = threading.RLock() @@ -142,6 +144,9 @@ class Eiger9mCsaxs(DetectorBase): self.std_client = None self.scaninfo = None self.filewriter = None + self.std_rest_server_url = ( + kwargs["file_writer_url"] if "file_writer_url" in kwargs else "http://xbl-daq-29:5000" + ) self.wait_for_connection(all_signals=True) if not sim_mode: self._update_service_config() @@ -199,7 +204,6 @@ class Eiger9mCsaxs(DetectorBase): 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 @@ -360,7 +364,7 @@ class Eiger9mCsaxs(DetectorBase): trigger_source (TriggerSource): Trigger source for the detector """ - value = int(trigger_source) + value = trigger_source self.cam.trigger_mode.put(value) def _publish_file_location(self, done: bool = False, successful: bool = None) -> None: @@ -394,7 +398,7 @@ class Eiger9mCsaxs(DetectorBase): self.cam.acquire.put(1) while True: det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] - if det_ctrl == int(DetectorState.RUNNING): + if det_ctrl == DetectorState.RUNNING: break if self._stopped == True: break @@ -493,7 +497,7 @@ class Eiger9mCsaxs(DetectorBase): # Check status while True: det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] - if det_ctrl == int(DetectorState.IDLE): + if det_ctrl == DetectorState.IDLE: break if self._stopped == True: break @@ -515,4 +519,4 @@ class Eiger9mCsaxs(DetectorBase): if __name__ == "__main__": - eiger = Eiger9mCsaxs(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True) + eiger = Eiger9McSAXS(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True) diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index 5088b1c..6710659 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -31,7 +31,7 @@ class MockSignal(Signal): with mock.patch("ophyd.EpicsSignal", new=MockSignal), mock.patch( "ophyd.EpicsSignalRO", new=MockSignal ), mock.patch("ophyd.EpicsSignalWithRBV", new=MockSignal): - from ophyd_devices.epics.devices.eiger9m_csaxs import Eiger9mCsaxs + from ophyd_devices.epics.devices.eiger9m_csaxs import Eiger9McSAXS # TODO maybe specify here that this DeviceMock is for usage in the DeviceServer @@ -98,12 +98,12 @@ def mock_det(): # dm.add_device("mokev", value=12.4) with mock.patch.object(dm, "producer"): with mock.patch.object( - Eiger9mCsaxs, "_update_service_config" + Eiger9McSAXS, "_update_service_config" ) as mock_update_service_config, mock.patch( "ophyd_devices.epics.devices.eiger9m_csaxs.FileWriterMixin" ) as filemixin: - with mock.patch.object(Eiger9mCsaxs, "_init"): - yield Eiger9mCsaxs(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + with mock.patch.object(Eiger9McSAXS, "_init"): + yield Eiger9McSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) @pytest.mark.parametrize( @@ -178,15 +178,15 @@ def test_init( sim_mode = sim_mode dm = DMMock() with mock.patch.object(dm, "producer") as producer, mock.patch.object( - Eiger9mCsaxs, "_init_filewriter" + Eiger9McSAXS, "_init_filewriter" ) as mock_init_fw, mock.patch.object( - Eiger9mCsaxs, "_update_scaninfo" + Eiger9McSAXS, "_update_scaninfo" ) as mock_update_scaninfo, mock.patch.object( - Eiger9mCsaxs, "_update_filewriter" + Eiger9McSAXS, "_update_filewriter" ) as mock_update_filewriter, mock.patch.object( - Eiger9mCsaxs, "_update_service_config" + Eiger9McSAXS, "_update_service_config" ) as mock_update_service_config: - mock_det = Eiger9mCsaxs(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + mock_det = Eiger9McSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) mock_det.cam.detector_state.put(detector_state) if expected_exception: with pytest.raises(Exception): @@ -326,7 +326,7 @@ def test_stage( expected_exception, ): with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( - Eiger9mCsaxs, "_publish_file_location" + Eiger9McSAXS, "_publish_file_location" ) as mock_publish_file_location: mock_std_daq.stop_writer.return_value = None mock_std_daq.get_status.return_value = daq_status From a80d13ae66e066b2fad6d8493b20731fb40f3677 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 3 Nov 2023 16:19:36 +0100 Subject: [PATCH 07/23] refactor: renaming --- ophyd_devices/epics/devices/__init__.py | 2 +- ophyd_devices/epics/devices/pilatus_csaxs.py | 260 ++++++++++++------- 2 files changed, 160 insertions(+), 102 deletions(-) diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 6222eb9..c2b5cf1 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -26,6 +26,6 @@ from ophyd.quadem import QuadEM from .epics_motor_ex import EpicsMotorEx from .mcs_csaxs import McsCsaxs from .eiger9m_csaxs import Eiger9McSAXS -from .pilatus_csaxs import PilatusCsaxs +from .pilatus_csaxs import PilatuscSAXS from .falcon_csaxs import FalconCsaxs from .DelayGeneratorDG645 import DelayGeneratorDG645 diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index e175eb7..b118831 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -1,36 +1,40 @@ import enum import json import os -import subprocess import time -from typing import List +from bec_lib.core.devicemanager import DeviceStatus 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 class PilatusError(Exception): + """Base class for exceptions in this module.""" + pass -class PilatusTimeoutError(Exception): +class PilatusTimeoutError(PilatusError): + """Raised when the Pilatus does not respond in time during unstage.""" + pass -class TriggerSource(int, enum.Enum): +class TriggerSource(enum.IntEnum): INTERNAL = 0 EXT_ENABLE = 1 EXT_TRIGGER = 2 @@ -38,50 +42,12 @@ 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") + Base class to map EPICS PVs to ophyd signals. + """ - # Pilatus_2 300k num_images = ADCpt(EpicsSignalWithRBV, "NumImages") num_exposures = ADCpt(EpicsSignalWithRBV, "NumExposures") delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures") @@ -104,7 +70,7 @@ class SlsDetectorCam(Device): # CamBase, FileBase): gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill") -class PilatusCsaxs(DetectorBase): +class PilatuscSAXS(DetectorBase): """Pilatus_2 300k detector for CSAXS Parent class: DetectorBase @@ -116,7 +82,12 @@ class PilatusCsaxs(DetectorBase): """ - cam = ADCpt(SlsDetectorCam, "cam1:") + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = [ + "describe", + ] + + cam = ADCpt(SLSDetectorCam, "cam1:") def __init__( self, @@ -131,6 +102,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, @@ -144,7 +127,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 @@ -152,24 +136,39 @@ 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 + self._init() - # TODO maybe needed - # self._close_file_writer() + def _init(self) -> None: + """Initialize detector, filewriter and set default parameters""" + self._default_parameter() + self._init_detector() + self._init_filewriter() - def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: - msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) - return BECMessage.ScanStatusMessage.loads(msg) + 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 _prep_det(self) -> None: # TODO slow reaction, seemed to have timeout. @@ -181,10 +180,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""" @@ -202,7 +201,7 @@ class PilatusCsaxs(DetectorBase): MULTI_TRIGGER = 3 ALGINMENT = 4 """ - value = int(trigger_source) + value = trigger_source self.cam.trigger_mode.set(value) def _prep_file_writer(self) -> None: @@ -220,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/") @@ -232,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, } ] } @@ -339,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}, ") @@ -372,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: @@ -412,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() @@ -421,19 +476,22 @@ 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 # Automatically connect to test environmenr if directly invoked if __name__ == "__main__": - pilatus_2 = PilatusCsaxs(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True) + pilatus_2 = PilatuscSAXS(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True) From a02e0f09b0f30e1b1ad73bf7f51b06c4bf9ab51c Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 3 Nov 2023 17:36:53 +0100 Subject: [PATCH 08/23] test: add first tests for pilatus --- tests/test_pilatus_csaxs.py | 474 ++++++++++++++++++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 tests/test_pilatus_csaxs.py diff --git a/tests/test_pilatus_csaxs.py b/tests/test_pilatus_csaxs.py new file mode 100644 index 0000000..6b159b7 --- /dev/null +++ b/tests/test_pilatus_csaxs.py @@ -0,0 +1,474 @@ +import pytest +from unittest import mock + +from ophyd.signal import Signal +from ophyd import Staged + +from bec_lib.core import BECMessage, MessageEndpoints +from bec_lib.core.devicemanager import DeviceContainer +from bec_lib.core.tests.utils import ProducerMock + + +class MockSignal(Signal): + def __init__(self, read_pv, *, string=False, name=None, parent=None, **kwargs): + self.read_pv = read_pv + self._string = bool(string) + super().__init__(name=name, parent=parent, **kwargs) + self._waited_for_connection = False + self._subscriptions = [] + + def wait_for_connection(self): + self._waited_for_connection = True + + def subscribe(self, method, event_type, **kw): + self._subscriptions.append((method, event_type, kw)) + + def describe_configuration(self): + return {self.name + "_conf": {"source": "SIM:test"}} + + def read_configuration(self): + return {self.name + "_conf": {"value": 0}} + + +with mock.patch("ophyd.EpicsSignal", new=MockSignal), mock.patch( + "ophyd.EpicsSignalRO", new=MockSignal +), mock.patch("ophyd.EpicsSignalWithRBV", new=MockSignal): + from ophyd_devices.epics.devices.pilatus_csaxs import PilatuscSAXS + + +# TODO maybe specify here that this DeviceMock is for usage in the DeviceServer +class DeviceMock: + def __init__(self, name: str, value: float = 0.0): + self.name = name + self.read_buffer = value + self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None} + self._enabled_set = True + self._enabled = True + + def read(self): + return {self.name: {"value": self.read_buffer}} + + def readback(self): + return self.read_buffer + + @property + def enabled_set(self) -> bool: + return self._enabled_set + + @enabled_set.setter + def enabled_set(self, val: bool): + self._enabled_set = val + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, val: bool): + self._enabled = val + + @property + def user_parameter(self): + return self._config["userParameter"] + + @property + def obj(self): + return self + + +class DMMock: + """Mock for DeviceManager + + The mocked DeviceManager creates a device containert and a producer. + + """ + + def __init__(self): + self.devices = DeviceContainer() + self.producer = ProducerMock() + + def add_device(self, name: str, value: float = 0.0): + self.devices[name] = DeviceMock(name, value) + + +@pytest.fixture(scope="function") +def mock_det(): + name = "pilatus" + prefix = "X12SA-ES-PILATUS300K:" + sim_mode = False + dm = DMMock() + # dm.add_device("mokev", value=12.4) + with mock.patch.object(dm, "producer"): + with mock.patch.object( + PilatuscSAXS, "_update_service_config" + ) as mock_update_service_config, mock.patch( + "ophyd_devices.epics.devices.pilatus_csaxs.FileWriterMixin" + ) as filemixin: + with mock.patch.object(PilatuscSAXS, "_init"): + yield PilatuscSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + + +@pytest.mark.parametrize( + "trigger_source, sim_mode, scan_status_msg, expected_exception", + [ + ( + 1, + True, + BECMessage.ScanStatusMessage( + scanID="1", + status={}, + info={ + "RID": "mockrid1111", + "queueID": "mockqueueID111", + "scan_number": 1, + "exp_time": 0.012, + "num_points": 500, + "readout_time": 0.003, + "scan_type": "fly", + "num_lines": 0.012, + "frames_per_trigger": 1, + }, + ), + True, + ), + ( + 1, + False, + BECMessage.ScanStatusMessage( + scanID="1", + status={}, + info={ + "RID": "mockrid1111", + "queueID": "mockqueueID111", + "scan_number": 1, + "exp_time": 0.012, + "num_points": 500, + "readout_time": 0.003, + "scan_type": "fly", + "num_lines": 0.012, + "frames_per_trigger": 1, + }, + ), + False, + ), + ], +) +# TODO rewrite this one, write test for init_detector, init_filewriter is tested +def test_init( + trigger_source, + sim_mode, + scan_status_msg, + expected_exception, +): + """Test the _init function: + + This includes testing the functions: + - _set_default_parameter + - _init_detector + - _stop_det + - _set_trigger + --> Testing the filewriter is done in test_init_filewriter + + Validation upon setting the correct PVs + + """ + name = "pilatus" + prefix = "X12SA-ES-PILATUS300K:" + sim_mode = sim_mode + dm = DMMock() + with mock.patch.object(dm, "producer") as producer, mock.patch.object( + PilatuscSAXS, "_init_filewriter" + ) as mock_init_fw, mock.patch.object( + PilatuscSAXS, "_update_scaninfo" + ) as mock_update_scaninfo, mock.patch.object( + PilatuscSAXS, "_update_filewriter" + ) as mock_update_filewriter, mock.patch.object( + PilatuscSAXS, "_update_service_config" + ) as mock_update_service_config: + mock_det = PilatuscSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + if expected_exception: + with pytest.raises(Exception): + mock_det._init() + mock_init_fw.assert_called_once() + else: + mock_det._init() # call the method you want to test + assert mock_det.cam.acquire.get() == 0 + assert mock_det.cam.trigger_mode.get() == trigger_source + mock_init_fw.assert_called() + mock_update_scaninfo.assert_called_once() + mock_update_filewriter.assert_called_once() + mock_update_service_config.assert_called_once() + + assert mock_init_fw.call_count == 2 + + +@pytest.mark.parametrize( + "scaninfo, stopped, expected_exception", + [ + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + "mokev": 12.4, + }, + False, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + "mokev": 12.4, + }, + True, + False, + ), + ], +) +def test_stage( + mock_det, + scaninfo, + stopped, + expected_exception, +): + with mock.patch.object(PilatuscSAXS, "_publish_file_location") as mock_publish_file_location: + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] + # TODO consider putting energy as variable in scaninfo + mock_det.device_manager.add_device("mokev", value=12.4) + mock_det._stopped = stopped + with mock.patch.object(mock_det, "_prep_file_writer") as mock_prep_fw: + mock_det.filepath = scaninfo["filepath"] + if expected_exception: + with pytest.raises(Exception): + mock_det.stage() + else: + mock_det.stage() + mock_prep_fw.assert_called_once() + # Check _prep_det + assert mock_det.cam.num_images.get() == int( + scaninfo["num_points"] * scaninfo["frames_per_trigger"] + ) + assert mock_det.cam.num_frames.get() == 1 + + mock_publish_file_location.assert_called_with(done=False) + + +def test_pre_scan(mock_det): + mock_det.pre_scan() + assert mock_det.cam.acquire.get() == 1 + + +@pytest.mark.parametrize( + "scaninfo", + [ + ({"filepath": "test.h5", "successful": True, "done": False, "scanID": "123"}), + ({"filepath": "test.h5", "successful": False, "done": True, "scanID": "123"}), + ({"filepath": "test.h5", "successful": None, "done": True, "scanID": "123"}), + ], +) +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"]) + if scaninfo["successful"] is None: + msg = BECMessage.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() + else: + msg = BECMessage.FileMessage( + file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] + ).dumps() + expected_calls = [ + mock.call( + MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), + msg, + pipe=mock_det._producer.pipeline.return_value, + ), + mock.call( + MessageEndpoints.file_event(mock_det.name), + msg, + pipe=mock_det._producer.pipeline.return_value, + ), + ] + assert mock_det._producer.set_and_publish.call_args_list == expected_calls + + +# @pytest.mark.parametrize( +# "scaninfo, daq_status, expected_exception", +# [ +# ( +# { +# "eacc": "e12345", +# "num_points": 500, +# "frames_per_trigger": 1, +# "filepath": "test.h5", +# "scanID": "123", +# }, +# {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, +# False, +# ), +# ( +# { +# "eacc": "e12345", +# "num_points": 500, +# "frames_per_trigger": 1, +# "filepath": "test.h5", +# "scanID": "123", +# }, +# {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, +# False, +# ), +# ( +# { +# "eacc": "e12345", +# "num_points": 500, +# "frames_per_trigger": 1, +# "filepath": "test.h5", +# "scanID": "123", +# }, +# {"state": "BUSY", "acquisition": {"state": "ERROR"}}, +# True, +# ), +# ], +# ) +# def test_prep_file_writer(mock_det, scaninfo, daq_status, expected_exception): +# with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( +# mock_det, "_filepath_exists" +# ) as mock_file_path_exists, mock.patch.object( +# mock_det, "_stop_file_writer" +# ) as mock_stop_file_writer, mock.patch.object( +# mock_det, "scaninfo" +# ) as mock_scaninfo: +# # mock_det = eiger_factory(name, prefix, sim_mode) +# mock_det.std_client = mock_std_daq +# mock_std_daq.start_writer_async.return_value = None +# mock_std_daq.get_status.return_value = daq_status +# 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"] + +# if expected_exception: +# with pytest.raises(Exception): +# mock_det._prep_file_writer() +# mock_file_path_exists.assert_called_once() +# assert mock_stop_file_writer.call_count == 2 + +# else: +# mock_det._prep_file_writer() +# mock_file_path_exists.assert_called_once() +# mock_stop_file_writer.assert_called_once() + +# daq_writer_call = { +# "output_file": scaninfo["filepath"], +# "n_images": int(scaninfo["num_points"] * scaninfo["frames_per_trigger"]), +# } +# mock_std_daq.start_writer_async.assert_called_with(daq_writer_call) + + +@pytest.mark.parametrize( + "stopped, expected_exception", + [ + ( + False, + False, + ), + ( + True, + True, + ), + ], +) +def test_unstage( + mock_det, + stopped, + expected_exception, +): + with mock.patch.object(mock_det, "_finished") as mock_finished, mock.patch.object( + mock_det, "_publish_file_location" + ) as mock_publish_file_location, mock.patch.object( + mock_det, "_start_h5converter" + ) as mock_start_h5converter: + mock_det._stopped = stopped + if expected_exception: + mock_det.unstage() + assert mock_det._stopped == True + else: + mock_det.unstage() + mock_finished.assert_called_once() + mock_publish_file_location.assert_called_with(done=True, successful=True) + mock_start_h5converter.assert_called_once() + assert mock_det._stopped == False + + +# def test_stop_fw(mock_det): +# with mock.patch.object(mock_det, "std_client") as mock_std_daq: +# mock_std_daq.stop_writer.return_value = None +# mock_det.std_client = mock_std_daq +# mock_det._stop_file_writer() +# mock_std_daq.stop_writer.assert_called_once() + + +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, mock.patch.object( + mock_det, "_close_file_writer" + ) as mock_close_file_writer: + mock_det.stop() + mock_stop_det.assert_called_once() + mock_stop_file_writer.assert_called_once() + mock_close_file_writer.assert_called_once() + assert mock_det._stopped == True + + +@pytest.mark.parametrize( + "stopped, mcs_stage_state, expected_exception", + [ + ( + False, + Staged.no, + False, + ), + ( + True, + Staged.no, + False, + ), + ( + False, + Staged.yes, + True, + ), + ], +) +def test_finished(mock_det, stopped, mcs_stage_state, expected_exception): + with mock.patch.object(mock_det, "device_manager") as mock_dm, mock.patch.object( + mock_det, "_stop_file_writer" + ) as mock_stop_file_friter, mock.patch.object( + mock_det, "_stop_det" + ) as mock_stop_det, mock.patch.object( + mock_det, "_close_file_writer" + ) as mock_close_file_writer: + mock_dm.devices.mcs.obj._staged = mcs_stage_state + mock_det._stopped = stopped + if expected_exception: + with pytest.raises(Exception): + mock_det._finished() + assert mock_det._stopped == stopped + mock_stop_file_friter.assert_called() + mock_stop_det.assert_called_once() + mock_close_file_writer.assert_called_once() + else: + mock_det._finished() + if stopped: + assert mock_det._stopped == stopped + + mock_stop_file_friter.assert_called() + mock_stop_det.assert_called_once() + mock_close_file_writer.assert_called_once() From d2cd6a442baced6ce8b6a7fa04a36290515fb87e Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 3 Nov 2023 18:37:36 +0100 Subject: [PATCH 09/23] refactor: refactored tests and eiger --- ophyd_devices/epics/devices/pilatus_csaxs.py | 136 +++++++++++-------- tests/test_eiger9m_csaxs.py | 3 +- 2 files changed, 78 insertions(+), 61 deletions(-) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index b118831..6d16caf 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -14,6 +14,7 @@ from ophyd import ADComponent as ADCpt from bec_lib.core import BECMessage, MessageEndpoints from bec_lib.core.file_utils import FileWriterMixin +from bec_lib.core.bec_service import SERVICE_CONFIG from bec_lib.core import bec_logger from ophyd_devices.utils import bec_utils as bec_utils @@ -49,7 +50,7 @@ class SLSDetectorCam(Device): """ num_images = ADCpt(EpicsSignalWithRBV, "NumImages") - num_exposures = ADCpt(EpicsSignalWithRBV, "NumExposures") + num_frames = ADCpt(EpicsSignalWithRBV, "NumExposures") delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures") trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode") acquire = ADCpt(EpicsSignal, "Acquire") @@ -124,30 +125,45 @@ class PilatuscSAXS(DetectorBase): **kwargs, ) if device_manager is None and not sim_mode: - raise PilatusError("Add DeviceManager to initialization or init with sim_mode=True") - + raise Exception( + 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.wait_for_connection() - # Spin up connections for simulation or BEC mode + self.service_cfg = None + self.std_client = None + self.scaninfo = None + self.filewriter = None + # TODO move url from data backend up here? + self.wait_for_connection(all_signals=True) if not sim_mode: - from bec_lib.core.bec_service import SERVICE_CONFIG - + self._update_service_config() self.device_manager = device_manager - 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": base_path} - - self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) - self.scaninfo.load_scan_metadata() - self.filewriter = FileWriterMixin(self.service_cfg) + 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() @@ -163,7 +179,7 @@ class PilatuscSAXS(DetectorBase): def _init_detector(self) -> None: """Initialize the detector""" # TODO add check if detector is running - pass + self._set_trigger(TriggerSource.EXT_ENABLE) def _init_filewriter(self) -> None: """Initialize the file writer""" @@ -178,7 +194,8 @@ class PilatuscSAXS(DetectorBase): def _set_det_threshold(self) -> None: # threshold_energy PV exists on Eiger 9M? factor = 1 - if self.cam.threshold_energy._metadata["units"] == "eV": + unit = getattr(self.cam.threshold_energy, "units", None) + if unit != None and unit == "eV": factor = 1000 setpoint = int(self.mokev * factor) threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] @@ -190,10 +207,10 @@ class PilatuscSAXS(DetectorBase): # self.cam.acquire_time.set(self.exp_time) # self.cam.acquire_period.set(self.exp_time + self.readout) self.cam.num_images.set(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) - self.cam.num_exposures.set(1) + self.cam.num_frames.set(1) self._set_trigger(TriggerSource.EXT_ENABLE) # EXT_TRIGGER) - def _set_trigger(self, trigger_source: TriggerSource) -> None: + def _set_trigger(self, trigger_source: int) -> None: """Set trigger source for the detector, either directly to value or TriggerSource.* with INTERNAL = 0 EXT_ENABLE = 1 @@ -202,7 +219,7 @@ class PilatuscSAXS(DetectorBase): ALGINMENT = 4 """ value = trigger_source - self.cam.trigger_mode.set(value) + self.cam.trigger_mode.put(value) def _prep_file_writer(self) -> None: """Prepare the file writer for pilatus_2 @@ -362,7 +379,7 @@ class PilatuscSAXS(DetectorBase): self._prep_file_writer() self._prep_det() state = False - self._publish_file_location(done=state, successful=state) + self._publish_file_location(done=state) return super().stage() # TODO might be useful for base class @@ -371,19 +388,31 @@ class PilatuscSAXS(DetectorBase): self._arm_acquisition() def _arm_acquisition(self) -> None: - self.acquire() + self.cam.acquire.put(1) + # TODO check if sleep of 1s is needed, could be that less is enough + time.sleep(1) + + 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 - 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) + if successful is None: + msg = BECMessage.FileMessage(file_path=self.filepath, done=done) + else: + 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 + MessageEndpoints.file_event(self.name), msg.dumps(), pipe=pipe ) pipe.execute() @@ -441,44 +470,31 @@ class PilatuscSAXS(DetectorBase): - _stop_det - _stop_file_writer """ + timer = 0 + sleep_time = 0.1 + # TODO this is a workaround at the moment which relies on the status of the mcs device 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: - # # rtr = self.cam.status_message_camserver.get() - # #if self.cam.acquire.get() == 0 and rtr == "Camserver returned OK": - # # if rtr == "Camserver returned OK": - # # break - # if self._stopped == True: - # break - # time.sleep(0.1) - # timer += 0.1 - # if timer > 5: - # self._close_file_writer() - # self._stop_file_writer() - # self._stopped == True - # # raise PilatusTimeoutError( - # # f"Pilatus timeout with detector state {self.cam.acquire.get()} and camserver return status: {rtr} " - # # ) + if self._stopped == True: + break + time.sleep(sleep_time) + timer = timer + sleep_time + if timer > 5: + self._stopped == True + self._stop_det() + self._stop_file_writer() + # TODO explore if sleep is needed + time.sleep(0.5) + self._close_file_writer() + raise PilatusTimeoutError(f"Timeout waiting for mcs device to unstage") self._stop_det() self._stop_file_writer() - # TODO explore if sleep is needed + # TODO explore if sleep time is needed time.sleep(0.5) self._close_file_writer() - def acquire(self) -> None: - """Start acquisition in software trigger mode, - 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) diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index 6710659..f4a2f49 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -9,8 +9,9 @@ from bec_lib.core.tests.utils import ProducerMock class MockSignal(Signal): - def __init__(self, read_pv, *, name=None, parent=None, **kwargs): + def __init__(self, read_pv, *, string=False, name=None, parent=None, **kwargs): self.read_pv = read_pv + self._string = bool(string) super().__init__(name=name, parent=parent, **kwargs) self._waited_for_connection = False self._subscriptions = [] From 4ce26b5dd5e6bdb3ff9926ffcbce17a70afe98ae Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 6 Nov 2023 14:55:42 +0100 Subject: [PATCH 10/23] refactor: add _send_requests_delete --- ophyd_devices/epics/devices/pilatus_csaxs.py | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 6d16caf..4991a4b 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -332,11 +332,32 @@ class PilatuscSAXS(DetectorBase): except Exception as exc: logger.info(f"Pilatus2 wait threw Exception: {exc}") + def _send_requests_put(self, url: str, data_msg: dict = None, headers: dict = None) -> None: + ... + + def _send_requests_delete(self, url: str, headers: dict = None) -> None: + """ + Send a delete request to the given url + + Args: + url (str): url to send the request to + headers (dict): headers to be sent with the request (optional) + """ + try: + res = requests.delete(url=url, headers=headers) + if not res.ok: + res.raise_for_status() + except Exception as exc: + logger.info(f"Pilatus2 delete threw Exception: {exc}") + def _close_file_writer(self) -> None: """Close the file writer for pilatus_2 a zmq service is running on xbl-daq-34 that is waiting for a zmq message to stop the writer for the pilatus_2 x12sa-pd-2 """ + url = "http://x12sa-pd-2:8080/stream/pilatus_2" + self._send_requests_delete(url=url) + try: res = requests.delete(url="http://x12sa-pd-2:8080/stream/pilatus_2") if not res.ok: From 13d26c65379704291526b45397e8160668aec57a Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 6 Nov 2023 15:26:43 +0100 Subject: [PATCH 11/23] refactor: requests put and delete moved to separate functions --- ophyd_devices/epics/devices/pilatus_csaxs.py | 112 +++++++++---------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 4991a4b..c26780a 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -221,16 +221,19 @@ class PilatuscSAXS(DetectorBase): value = trigger_source self.cam.trigger_mode.put(value) + def _create_directory(filepath: str) -> None: + """Create directory if it does not exist""" + os.makedirs(filepath, exist_ok=True) + def _prep_file_writer(self) -> None: - """Prepare the file writer for pilatus_2 - a zmq service is running on xbl-daq-34 that is waiting - for a zmq message to start the writer for the pilatus_2 x12sa-pd-2 """ - # TODO worked reliable with time.sleep(2) - # self._close_file_writer() - # time.sleep(2) - # self._stop_file_writer() - # time.sleep(2) + Prepare the file writer for pilatus_2 + + A zmq service is running on xbl-daq-34 that is waiting + for a zmq message to start the writer for the pilatus_2 x12sa-pd-2 + + """ + # TODO explore required sleep time here self._close_file_writer() time.sleep(0.1) self._stop_file_writer() @@ -246,6 +249,7 @@ class PilatuscSAXS(DetectorBase): self.cam.file_format.put(0) # 0: TIFF self.cam.file_template.put("%s%s_%5.5d.cbf") + # TODO remove hardcoded filepath here # compile filename basepath = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/pilatus_2/" self.filepath = os.path.join( @@ -253,8 +257,11 @@ class PilatuscSAXS(DetectorBase): self.filewriter.get_scan_directory(self.scaninfo.scan_number, 1000, 5), ) # Make directory if needed - os.makedirs(self.filepath, exist_ok=True) + self._create_directory(self.filepath) + headers = {"Content-Type": "application/json", "Accept": "application/json"} + # start the stream on x12sa-pd-2 + url = "http://x12sa-pd-2:8080/stream/pilatus_2" data_msg = { "source": [ { @@ -264,21 +271,14 @@ class PilatuscSAXS(DetectorBase): } ] } - - logger.info(data_msg) - headers = {"Content-Type": "application/json", "Accept": "application/json"} - - res = requests.put( - url="http://x12sa-pd-2:8080/stream/pilatus_2", - data=json.dumps(data_msg), - headers=headers, - ) + res = self._send_requests_put(url=url, data=data_msg, headers=headers) logger.info(f"{res.status_code} - {res.text} - {res.content}") if not res.ok: res.raise_for_status() - # prepare writer + # start the data receiver on xbl-daq-34 + url = "http://xbl-daq-34:8091/pilatus_2/run" data_msg = [ "zmqWriter", self.scaninfo.username, @@ -291,14 +291,7 @@ class PilatuscSAXS(DetectorBase): "user": self.scaninfo.username, }, ] - - res = requests.put( - url="http://xbl-daq-34:8091/pilatus_2/run", - data=json.dumps(data_msg), - headers=headers, - ) - # subprocess.run("curl -i -s -X PUT http://xbl-daq-34:8091/pilatus_2/run -d '[\"zmqWriter\",\"e20636\",{\"addr\":\"tcp://x12sa-pd-2:8888\",\"dst\":[\"file\"],\"numFrm\":10,\"timeout\":2000,\"ifType\":\"PULL\",\"user\":\"e20636\"}]'", shell=True) - + res = self._send_requests_put(url=url, data=data_msg, headers=headers) logger.info(f"{res.status_code} - {res.text} - {res.content}") if not res.ok: @@ -306,8 +299,10 @@ class PilatuscSAXS(DetectorBase): # Wait for server to become available again time.sleep(0.1) + logger.info(f"{res.status_code} -{res.text} - {res.content}") - headers = {"Content-Type": "application/json", "Accept": "application/json"} + # Sent requests.put to xbl-daq-34 to wait for data + url = "http://xbl-daq-34:8091/pilatus_2/wait" data_msg = [ "zmqWriter", self.scaninfo.username, @@ -316,15 +311,8 @@ class PilatuscSAXS(DetectorBase): "timeout": 2000, }, ] - logger.info(f"{res.status_code} -{res.text} - {res.content}") - try: - res = requests.put( - url="http://xbl-daq-34:8091/pilatus_2/wait", - data=json.dumps(data_msg), - # headers=headers, - ) - + res = self._send_requests_put(url=url, data=data_msg, headers=headers) logger.info(f"{res}") if not res.ok: @@ -332,46 +320,56 @@ class PilatuscSAXS(DetectorBase): except Exception as exc: logger.info(f"Pilatus2 wait threw Exception: {exc}") - def _send_requests_put(self, url: str, data_msg: dict = None, headers: dict = None) -> None: - ... + def _send_requests_put(self, url: str, data_msg: list = None, headers: dict = None) -> object: + """ + Send a put request to the given url - def _send_requests_delete(self, url: str, headers: dict = None) -> None: + Args: + url (str): url to send the request to + data (dict): data to be sent with the request (optional) + headers (dict): headers to be sent with the request (optional) + + Returns: + status code of the request + """ + return requests.put(url=url, data=json.dumps(data_msg), headers=headers) + + def _send_requests_delete(self, url: str, headers: dict = None) -> object: """ Send a delete request to the given url Args: url (str): url to send the request to headers (dict): headers to be sent with the request (optional) + + Returns: + status code of the request """ - try: - res = requests.delete(url=url, headers=headers) - if not res.ok: - res.raise_for_status() - except Exception as exc: - logger.info(f"Pilatus2 delete threw Exception: {exc}") + return requests.delete(url=url, headers=headers) def _close_file_writer(self) -> None: - """Close the file writer for pilatus_2 - a zmq service is running on xbl-daq-34 that is waiting - for a zmq message to stop the writer for the pilatus_2 x12sa-pd-2 + """ + Close the file writer for pilatus_2 + + Delete the data from x12sa-pd-2 + """ url = "http://x12sa-pd-2:8080/stream/pilatus_2" - self._send_requests_delete(url=url) - try: - res = requests.delete(url="http://x12sa-pd-2:8080/stream/pilatus_2") + res = self._send_requests_delete(url=url) if not res.ok: res.raise_for_status() except Exception as exc: - logger.info(f"Pilatus2 delete threw Exception: {exc}") + logger.info(f"Pilatus2 close threw Exception: {exc}") def _stop_file_writer(self) -> None: - res = requests.put( - url="http://xbl-daq-34:8091/pilatus_2/stop", - # data=json.dumps(data_msg), - # headers=headers, - ) + """ + Stop the file writer for pilatus_2 + Runs on xbl-daq-34 + """ + url = "http://xbl-daq-34:8091/pilatus_2/stop" + res = self._send_requests_put(url=url) if not res.ok: res.raise_for_status() From d3e8ece029c0d672888cc43f05cf2258684de801 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 6 Nov 2023 16:15:59 +0100 Subject: [PATCH 12/23] test: add tests for close and stop filewriter --- tests/test_pilatus_csaxs.py | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/test_pilatus_csaxs.py b/tests/test_pilatus_csaxs.py index 6b159b7..74f5322 100644 --- a/tests/test_pilatus_csaxs.py +++ b/tests/test_pilatus_csaxs.py @@ -7,6 +7,7 @@ from ophyd import Staged from bec_lib.core import BECMessage, MessageEndpoints from bec_lib.core.devicemanager import DeviceContainer from bec_lib.core.tests.utils import ProducerMock +import requests class MockSignal(Signal): @@ -299,6 +300,64 @@ def test_publish_file_location(mock_det, scaninfo): assert mock_det._producer.set_and_publish.call_args_list == expected_calls +@pytest.mark.parametrize( + "requests_state, expected_exception, url", + [ + ( + True, + False, + "http://x12sa-pd-2:8080/stream/pilatus_2", + ), + ( + False, + False, + "http://x12sa-pd-2:8080/stream/pilatus_2", + ), + ], +) +def test_close_file_writer(mock_det, requests_state, expected_exception, url): + with mock.patch.object(mock_det, "_send_requests_delete") as mock_send_requests_delete: + instance = mock_send_requests_delete.return_value + instance.ok = requests_state + if expected_exception: + mock_det._close_file_writer() + mock_send_requests_delete.assert_called_once_with(url=url) + instance.raise_for_status.called_once() + else: + mock_det._close_file_writer() + mock_send_requests_delete.assert_called_once_with(url=url) + + +@pytest.mark.parametrize( + "requests_state, expected_exception, url", + [ + ( + True, + False, + "http://xbl-daq-34:8091/pilatus_2/stop", + ), + ( + False, + True, + "http://xbl-daq-34:8091/pilatus_2/stop", + ), + ], +) +def test_stop_file_writer(mock_det, requests_state, expected_exception, url): + with mock.patch.object(mock_det, "_send_requests_put") as mock_send_requests_put: + instance = mock_send_requests_put.return_value + instance.ok = requests_state + instance.raise_for_status.side_effect = Exception + if expected_exception: + with pytest.raises(Exception): + mock_det._stop_file_writer() + mock_send_requests_put.assert_called_once_with(url=url) + instance.raise_for_status.called_once() + else: + mock_det._stop_file_writer() + mock_send_requests_put.assert_called_once_with(url=url) + + # @pytest.mark.parametrize( # "scaninfo, daq_status, expected_exception", # [ From ee77013ba29d705de3b1f499ea6c9256c3a6194b Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 6 Nov 2023 17:16:31 +0100 Subject: [PATCH 13/23] test: test init filewriter --- tests/test_pilatus_csaxs.py | 162 ++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/tests/test_pilatus_csaxs.py b/tests/test_pilatus_csaxs.py index 74f5322..2d70bfe 100644 --- a/tests/test_pilatus_csaxs.py +++ b/tests/test_pilatus_csaxs.py @@ -1,3 +1,4 @@ +import os import pytest from unittest import mock @@ -358,6 +359,167 @@ def test_stop_file_writer(mock_det, requests_state, expected_exception, url): mock_send_requests_put.assert_called_once_with(url=url) +@pytest.mark.parametrize( + "scaninfo, data_msgs, urls, requests_state, expected_exception", + [ + ( + { + "filepath_raw": "pilatus_2.h5", + "eacc": "e12345", + "scan_number": 1000, + "scan_directory": "S00000_00999", + "num_points": 500, + "frames_per_trigger": 1, + "headers": {"Content-Type": "application/json", "Accept": "application/json"}, + }, + [ + { + "source": [ + { + "searchPath": "/", + "searchPattern": "glob:*.cbf", + "destinationPath": "/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999", + } + ] + }, + [ + "zmqWriter", + "e12345", + { + "addr": "tcp://x12sa-pd-2:8888", + "dst": ["file"], + "numFrm": 500, + "timeout": 2000, + "ifType": "PULL", + "user": "e12345", + }, + ], + [ + "zmqWriter", + "e12345", + { + "frmCnt": 500, + "timeout": 2000, + }, + ], + ], + [ + "http://x12sa-pd-2:8080/stream/pilatus_2", + "http://xbl-daq-34:8091/pilatus_2/run", + "http://xbl-daq-34:8091/pilatus_2/wait", + ], + True, + False, + ), + ( + { + "filepath_raw": "pilatus_2.h5", + "eacc": "e12345", + "scan_number": 1000, + "scan_directory": "S00000_00999", + "num_points": 500, + "frames_per_trigger": 1, + "headers": {"Content-Type": "application/json", "Accept": "application/json"}, + }, + [ + { + "source": [ + { + "searchPath": "/", + "searchPattern": "glob:*.cbf", + "destinationPath": "/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999", + } + ] + }, + [ + "zmqWriter", + "e12345", + { + "addr": "tcp://x12sa-pd-2:8888", + "dst": ["file"], + "numFrm": 500, + "timeout": 2000, + "ifType": "PULL", + "user": "e12345", + }, + ], + [ + "zmqWriter", + "e12345", + { + "frmCnt": 500, + "timeout": 2000, + }, + ], + ], + [ + "http://x12sa-pd-2:8080/stream/pilatus_2", + "http://xbl-daq-34:8091/pilatus_2/run", + "http://xbl-daq-34:8091/pilatus_2/wait", + ], + False, # return of res.ok is False! + True, + ), + ], +) +def test_prep_file_writer(mock_det, scaninfo, data_msgs, urls, requests_state, expected_exception): + with mock.patch.object( + mock_det, "_close_file_writer" + ) as mock_close_file_writer, mock.patch.object( + mock_det, "_stop_file_writer" + ) as mock_stop_file_writer, mock.patch.object( + mock_det, "filewriter" + ) as mock_filewriter, mock.patch.object( + mock_det, "_create_directory" + ) as mock_create_directory, mock.patch.object( + mock_det, "_send_requests_put" + ) as mock_send_requests_put: + mock_det.scaninfo.scan_number = scaninfo["scan_number"] + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.scaninfo.username = scaninfo["eacc"] + mock_filewriter.compile_full_filename.return_value = scaninfo["filepath_raw"] + mock_filewriter.get_scan_directory.return_value = scaninfo["scan_directory"] + instance = mock_send_requests_put.return_value + instance.ok = requests_state + instance.raise_for_status.side_effect = Exception + + if expected_exception: + with pytest.raises(Exception): + mock_det._prep_file_writer() + mock_close_file_writer.assert_called_once() + mock_stop_file_writer.assert_called_once() + instance.raise_for_status.assert_called_once() + else: + mock_det._prep_file_writer() + + mock_close_file_writer.assert_called_once() + mock_stop_file_writer.assert_called_once() + + # Assert values set on detector + assert mock_det.cam.file_path.get() == "/dev/shm/zmq/" + assert ( + mock_det.cam.file_name.get() + == f"{scaninfo['eacc']}_2_{scaninfo['scan_number']:05d}" + ) + assert mock_det.cam.auto_increment.get() == 1 + assert mock_det.cam.file_number.get() == 0 + assert mock_det.cam.file_format.get() == 0 + assert mock_det.cam.file_template.get() == "%s%s_%5.5d.cbf" + # Remove last / from destinationPath + mock_create_directory.assert_called_once_with( + os.path.join(data_msgs[0]["source"][0]["destinationPath"]) + ) + assert mock_send_requests_put.call_count == 3 + + calls = [ + mock.call(url=url, data=data_msg, headers=scaninfo["headers"]) + for url, data_msg in zip(urls, data_msgs) + ] + for call, mock_call in zip(calls, mock_send_requests_put.call_args_list): + assert call == mock_call + + # @pytest.mark.parametrize( # "scaninfo, daq_status, expected_exception", # [ From ba01cf7b2da25d40611f0a059493ac22f66a36c7 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 6 Nov 2023 17:44:25 +0100 Subject: [PATCH 14/23] refactor: add min_readouttime, add complemented test cases; closes #11 #10 --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 19 ++++++++++++---- ophyd_devices/epics/devices/pilatus_csaxs.py | 16 +++++++++++-- tests/test_eiger9m_csaxs.py | 19 ++++++++++++++++ tests/test_pilatus_csaxs.py | 24 +++++++++++++++++++- 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index 38ca436..ae03f64 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -23,6 +23,8 @@ from ophyd_devices.utils import bec_utils logger = bec_logger.logger +EIGER9M_MIN_READOUT = 3e-3 + class EigerError(Exception): """Base class for exceptions in this module.""" @@ -144,6 +146,7 @@ class Eiger9McSAXS(DetectorBase): self.std_client = None self.scaninfo = None self.filewriter = None + self.readout_time_min = EIGER9M_MIN_READOUT self.std_rest_server_url = ( kwargs["file_writer_url"] if "file_writer_url" in kwargs else "http://xbl-daq-29:5000" ) @@ -182,12 +185,19 @@ 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 + """Set default parameters for Pilatus300k detector + readout (float): readout time in seconds """ - self.reduce_readout = 1e-3 + 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) # TODO function for abstract class? def _init_detector(self) -> None: @@ -354,6 +364,7 @@ class Eiger9McSAXS(DetectorBase): """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) + self._update_readout_time() # TODO function for abstract class? + call it for each scan?? def _set_trigger(self, trigger_source: TriggerSource) -> None: diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index c26780a..2aa2634 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -22,6 +22,8 @@ from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin logger = bec_logger.logger +PILATUS_MIN_READOUT = 3e-3 + class PilatusError(Exception): """Base class for exceptions in this module.""" @@ -135,6 +137,7 @@ class PilatuscSAXS(DetectorBase): self.std_client = None self.scaninfo = None self.filewriter = None + self.readout_time_min = PILATUS_MIN_READOUT # TODO move url from data backend up here? self.wait_for_connection(all_signals=True) if not sim_mode: @@ -174,7 +177,15 @@ class PilatuscSAXS(DetectorBase): """Set default parameters for Pilatus300k detector readout (float): readout time in seconds """ - self.reduce_readout = 1e-3 + 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 _init_detector(self) -> None: """Initialize the detector""" @@ -190,6 +201,7 @@ class PilatuscSAXS(DetectorBase): # TODO slow reaction, seemed to have timeout. self._set_det_threshold() self._set_acquisition_params() + self._set_trigger(TriggerSource.EXT_ENABLE) def _set_det_threshold(self) -> None: # threshold_energy PV exists on Eiger 9M? @@ -208,7 +220,7 @@ class PilatuscSAXS(DetectorBase): # self.cam.acquire_period.set(self.exp_time + self.readout) self.cam.num_images.set(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) self.cam.num_frames.set(1) - self._set_trigger(TriggerSource.EXT_ENABLE) # EXT_TRIGGER) + self._update_readout_time() def _set_trigger(self, trigger_source: int) -> None: """Set trigger source for the detector, either directly to value or TriggerSource.* with diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index f4a2f49..8efab00 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -206,6 +206,25 @@ def test_init( assert mock_init_fw.call_count == 2 +@pytest.mark.parametrize( + "readout_time, expected_value", + [ + (1e-3, 3e-3), + (3e-3, 3e-3), + (5e-3, 5e-3), + (None, 3e-3), + ], +) +def test_update_readout_time(mock_det, readout_time, expected_value): + if readout_time is None: + mock_det._update_readout_time() + assert mock_det.readout_time == expected_value + else: + mock_det.scaninfo.readout_time = readout_time + mock_det._update_readout_time() + assert mock_det.readout_time == expected_value + + @pytest.mark.parametrize( "eacc, exp_url, daq_status, daq_cfg, expected_exception", [ diff --git a/tests/test_pilatus_csaxs.py b/tests/test_pilatus_csaxs.py index 2d70bfe..f20c5fa 100644 --- a/tests/test_pilatus_csaxs.py +++ b/tests/test_pilatus_csaxs.py @@ -246,7 +246,9 @@ def test_stage( # TODO consider putting energy as variable in scaninfo mock_det.device_manager.add_device("mokev", value=12.4) mock_det._stopped = stopped - with mock.patch.object(mock_det, "_prep_file_writer") as mock_prep_fw: + with mock.patch.object(mock_det, "_prep_file_writer") as mock_prep_fw, mock.patch.object( + mock_det, "_update_readout_time" + ) as mock_update_readout_time: mock_det.filepath = scaninfo["filepath"] if expected_exception: with pytest.raises(Exception): @@ -254,6 +256,7 @@ def test_stage( else: mock_det.stage() mock_prep_fw.assert_called_once() + mock_update_readout_time.assert_called() # Check _prep_det assert mock_det.cam.num_images.get() == int( scaninfo["num_points"] * scaninfo["frames_per_trigger"] @@ -268,6 +271,25 @@ def test_pre_scan(mock_det): assert mock_det.cam.acquire.get() == 1 +@pytest.mark.parametrize( + "readout_time, expected_value", + [ + (1e-3, 3e-3), + (3e-3, 3e-3), + (5e-3, 5e-3), + (None, 3e-3), + ], +) +def test_update_readout_time(mock_det, readout_time, expected_value): + if readout_time is None: + mock_det._update_readout_time() + assert mock_det.readout_time == expected_value + else: + mock_det.scaninfo.readout_time = readout_time + mock_det._update_readout_time() + assert mock_det.readout_time == expected_value + + @pytest.mark.parametrize( "scaninfo", [ From 7e9abdb32310037b115b12bf439bdbe5f3724948 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 7 Nov 2023 13:14:10 +0100 Subject: [PATCH 15/23] test: fix test to mock PV access --- tests/test_eiger9m_csaxs.py | 921 ++++++++++++++++-------------------- tests/utils.py | 249 ++++++++++ 2 files changed, 662 insertions(+), 508 deletions(-) diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index 8efab00..43e7759 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -1,93 +1,22 @@ import pytest from unittest import mock -from ophyd.signal import Signal +import ophyd from bec_lib.core import BECMessage, MessageEndpoints -from bec_lib.core.devicemanager import DeviceContainer -from bec_lib.core.tests.utils import ProducerMock +from ophyd_devices.epics.devices.eiger9m_csaxs import Eiger9McSAXS + +from tests.utils import DMMock, MockPV -class MockSignal(Signal): - def __init__(self, read_pv, *, string=False, name=None, parent=None, **kwargs): - self.read_pv = read_pv - self._string = bool(string) - super().__init__(name=name, parent=parent, **kwargs) - self._waited_for_connection = False - self._subscriptions = [] - - def wait_for_connection(self): - self._waited_for_connection = True - - def subscribe(self, method, event_type, **kw): - self._subscriptions.append((method, event_type, kw)) - - def describe_configuration(self): - return {self.name + "_conf": {"source": "SIM:test"}} - - def read_configuration(self): - return {self.name + "_conf": {"value": 0}} - - -with mock.patch("ophyd.EpicsSignal", new=MockSignal), mock.patch( - "ophyd.EpicsSignalRO", new=MockSignal -), mock.patch("ophyd.EpicsSignalWithRBV", new=MockSignal): - from ophyd_devices.epics.devices.eiger9m_csaxs import Eiger9McSAXS - - -# TODO maybe specify here that this DeviceMock is for usage in the DeviceServer -class DeviceMock: - def __init__(self, name: str, value: float = 0.0): - self.name = name - self.read_buffer = value - self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None} - self._enabled_set = True - self._enabled = True - - def read(self): - return {self.name: {"value": self.read_buffer}} - - def readback(self): - return self.read_buffer - - @property - def enabled_set(self) -> bool: - return self._enabled_set - - @enabled_set.setter - def enabled_set(self, val: bool): - self._enabled_set = val - - @property - def enabled(self) -> bool: - return self._enabled - - @enabled.setter - def enabled(self, val: bool): - self._enabled = val - - @property - def user_parameter(self): - return self._config["userParameter"] - - @property - def obj(self): - return self - - -class DMMock: - """Mock for DeviceManager - - The mocked DeviceManager creates a device containert and a producer. - - """ - - def __init__(self): - self.devices = DeviceContainer() - self.producer = ProducerMock() - - def add_device(self, name: str, value: float = 0.0): - self.devices[name] = DeviceMock(name, value) +def patch_dual_pvs(device): + for walk in device.walk_signals(): + if not hasattr(walk.item, "_read_pv"): + continue + if not hasattr(walk.item, "_write_pv"): + continue + if walk.item._read_pv.pvname.endswith("_RBV"): + walk.item._read_pv = walk.item._write_pv @pytest.fixture(scope="function") @@ -96,76 +25,73 @@ def mock_det(): prefix = "X12SA-ES-EIGER9M:" sim_mode = False dm = DMMock() - # dm.add_device("mokev", value=12.4) with mock.patch.object(dm, "producer"): - with mock.patch.object( - Eiger9McSAXS, "_update_service_config" - ) as mock_update_service_config, mock.patch( + with mock.patch( "ophyd_devices.epics.devices.eiger9m_csaxs.FileWriterMixin" - ) as filemixin: - with mock.patch.object(Eiger9McSAXS, "_init"): - yield Eiger9McSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + ) as filemixin, mock.patch( + "ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9McSAXS._update_service_config" + ) as mock_service_config: + with mock.patch.object(ophyd, "cl") as mock_cl: + mock_cl.get_pv = MockPV + with mock.patch.object(Eiger9McSAXS, "_init"): + det = Eiger9McSAXS( + name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode + ) + patch_dual_pvs(det) + yield det + + +def test_init(): + """Test the _init function:""" + name = "eiger" + prefix = "X12SA-ES-EIGER9M:" + sim_mode = False + dm = DMMock() + with mock.patch.object(dm, "producer"): + with mock.patch( + "ophyd_devices.epics.devices.eiger9m_csaxs.FileWriterMixin" + ) as filemixin, mock.patch( + "ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9McSAXS._update_service_config" + ) as mock_service_config: + with mock.patch.object(ophyd, "cl") as mock_cl: + mock_cl.get_pv = MockPV + with mock.patch.object( + Eiger9McSAXS, "_default_parameter" + ) as mock_default, mock.patch.object( + Eiger9McSAXS, "_init_detector" + ) as mock_init_det, mock.patch.object( + Eiger9McSAXS, "_init_filewriter" + ) as mock_init_fw: + Eiger9McSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + mock_default.assert_called_once() + mock_init_det.assert_called_once() + mock_init_fw.assert_called_once() @pytest.mark.parametrize( - "trigger_source, detector_state, sim_mode, scan_status_msg, expected_exception", + "trigger_source, detector_state, expected_exception", [ ( 2, 1, True, - BECMessage.ScanStatusMessage( - scanID="1", - status={}, - info={ - "RID": "mockrid1111", - "queueID": "mockqueueID111", - "scan_number": 1, - "exp_time": 0.012, - "num_points": 500, - "readout_time": 0.003, - "scan_type": "fly", - "num_lines": 0.012, - "frames_per_trigger": 1, - }, - ), - True, ), ( 2, 0, False, - BECMessage.ScanStatusMessage( - scanID="1", - status={}, - info={ - "RID": "mockrid1111", - "queueID": "mockqueueID111", - "scan_number": 1, - "exp_time": 0.012, - "num_points": 500, - "readout_time": 0.003, - "scan_type": "fly", - "num_lines": 0.012, - "frames_per_trigger": 1, - }, - ), - False, ), ], ) -# TODO rewrite this one, write test for init_detector, init_filewriter is tested -def test_init( +def test_init_detector( + mock_det, trigger_source, detector_state, - sim_mode, - scan_status_msg, expected_exception, ): """Test the _init function: This includes testing the functions: - - _set_default_parameter - _init_detector - _stop_det - _set_trigger @@ -174,36 +100,15 @@ def test_init( Validation upon setting the correct PVs """ - name = "eiger" - prefix = "X12SA-ES-EIGER9M:" - sim_mode = sim_mode - dm = DMMock() - with mock.patch.object(dm, "producer") as producer, mock.patch.object( - Eiger9McSAXS, "_init_filewriter" - ) as mock_init_fw, mock.patch.object( - Eiger9McSAXS, "_update_scaninfo" - ) as mock_update_scaninfo, mock.patch.object( - Eiger9McSAXS, "_update_filewriter" - ) as mock_update_filewriter, mock.patch.object( - Eiger9McSAXS, "_update_service_config" - ) as mock_update_service_config: - mock_det = Eiger9McSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) - mock_det.cam.detector_state.put(detector_state) - if expected_exception: - with pytest.raises(Exception): - mock_det._init() - mock_init_fw.assert_called_once() - else: - mock_det._init() # call the method you want to test - assert mock_det.cam.acquire.get() == 0 - assert mock_det.cam.detector_state.get() == detector_state - assert mock_det.cam.trigger_mode.get() == trigger_source - mock_init_fw.assert_called() - mock_update_scaninfo.assert_called_once() - mock_update_filewriter.assert_called_once() - mock_update_service_config.assert_called_once() - - assert mock_init_fw.call_count == 2 + mock_det.cam.detector_state._read_pv.mock_data = detector_state + if expected_exception: + with pytest.raises(Exception): + mock_det._init_detector() + else: + mock_det._init_detector() # call the method you want to test + assert mock_det.cam.acquire.get() == 0 + assert mock_det.cam.detector_state.get() == detector_state + assert mock_det.cam.trigger_mode.get() == trigger_source @pytest.mark.parametrize( @@ -225,376 +130,376 @@ def test_update_readout_time(mock_det, readout_time, expected_value): assert mock_det.readout_time == expected_value -@pytest.mark.parametrize( - "eacc, exp_url, daq_status, daq_cfg, expected_exception", - [ - ( - "e12345", - "http://xbl-daq-29:5000", - {"state": "READY"}, - {"writer_user_id": 12543}, - False, - ), - ( - "e12345", - "http://xbl-daq-29:5000", - {"state": "READY"}, - {"writer_user_id": 15421}, - False, - ), - ( - "e12345", - "http://xbl-daq-29:5000", - {"state": "BUSY"}, - {"writer_user_id": 15421}, - True, - ), - ( - "e12345", - "http://xbl-daq-29:5000", - {"state": "READY"}, - {"writer_ud": 12345}, - True, - ), - ], -) -def test_init_filewriter(mock_det, eacc, exp_url, daq_status, daq_cfg, expected_exception): - """Test _init_filewriter (std daq in this case) +# @pytest.mark.parametrize( +# "eacc, exp_url, daq_status, daq_cfg, expected_exception", +# [ +# ( +# "e12345", +# "http://xbl-daq-29:5000", +# {"state": "READY"}, +# {"writer_user_id": 12543}, +# False, +# ), +# ( +# "e12345", +# "http://xbl-daq-29:5000", +# {"state": "READY"}, +# {"writer_user_id": 15421}, +# False, +# ), +# ( +# "e12345", +# "http://xbl-daq-29:5000", +# {"state": "BUSY"}, +# {"writer_user_id": 15421}, +# True, +# ), +# ( +# "e12345", +# "http://xbl-daq-29:5000", +# {"state": "READY"}, +# {"writer_ud": 12345}, +# True, +# ), +# ], +# ) +# def test_init_filewriter(mock_det, eacc, exp_url, daq_status, daq_cfg, expected_exception): +# """Test _init_filewriter (std daq in this case) - This includes testing the functions: +# This includes testing the functions: - - _update_service_config +# - _update_service_config - Validation upon checking set values in mocked std_daq instance - """ - with mock.patch("ophyd_devices.epics.devices.eiger9m_csaxs.StdDaqClient") as mock_std_daq: - instance = mock_std_daq.return_value - instance.stop_writer.return_value = None - instance.get_status.return_value = daq_status - instance.get_config.return_value = daq_cfg - mock_det.scaninfo.username = eacc - # scaninfo.username.return_value = eacc - if expected_exception: - with pytest.raises(Exception): - mock_det._init_filewriter() - else: - mock_det._init_filewriter() +# Validation upon checking set values in mocked std_daq instance +# """ +# with mock.patch("ophyd_devices.epics.devices.eiger9m_csaxs.StdDaqClient") as mock_std_daq: +# instance = mock_std_daq.return_value +# instance.stop_writer.return_value = None +# instance.get_status.return_value = daq_status +# instance.get_config.return_value = daq_cfg +# mock_det.scaninfo.username = eacc +# # scaninfo.username.return_value = eacc +# if expected_exception: +# with pytest.raises(Exception): +# mock_det._init_filewriter() +# else: +# mock_det._init_filewriter() - assert mock_det.std_rest_server_url == exp_url - instance.stop_writer.assert_called_once() - instance.get_status.assert_called() - instance.set_config.assert_called_once_with(daq_cfg) +# assert mock_det.std_rest_server_url == exp_url +# instance.stop_writer.assert_called_once() +# instance.get_status.assert_called() +# instance.set_config.assert_called_once_with(daq_cfg) -@pytest.mark.parametrize( - "scaninfo, daq_status, daq_cfg, detector_state, stopped, expected_exception", - [ - ( - { - "eacc": "e12345", - "num_points": 500, - "frames_per_trigger": 1, - "filepath": "test.h5", - "scanID": "123", - "mokev": 12.4, - }, - {"state": "READY"}, - {"writer_user_id": 12543}, - 5, - False, - False, - ), - ( - { - "eacc": "e12345", - "num_points": 500, - "frames_per_trigger": 1, - "filepath": "test.h5", - "scanID": "123", - "mokev": 12.4, - }, - {"state": "BUSY"}, - {"writer_user_id": 15421}, - 5, - False, - False, - ), - ( - { - "eacc": "e12345", - "num_points": 500, - "frames_per_trigger": 1, - "filepath": "test.h5", - "scanID": "123", - "mokev": 18.4, - }, - {"state": "READY"}, - {"writer_user_id": 12345}, - 4, - False, - True, - ), - ], -) -def test_stage( - mock_det, - scaninfo, - daq_status, - daq_cfg, - detector_state, - stopped, - expected_exception, -): - with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( - Eiger9McSAXS, "_publish_file_location" - ) as mock_publish_file_location: - mock_std_daq.stop_writer.return_value = None - mock_std_daq.get_status.return_value = daq_status - mock_std_daq.get_config.return_value = daq_cfg - mock_det.scaninfo.num_points = scaninfo["num_points"] - mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] - mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] - # TODO consider putting energy as variable in scaninfo - mock_det.device_manager.add_device("mokev", value=12.4) - mock_det.cam.beam_energy.put(scaninfo["mokev"]) - mock_det._stopped = stopped - mock_det.cam.detector_state.put(detector_state) - with mock.patch.object(mock_det, "_prep_file_writer") as mock_prep_fw: - mock_det.filepath = scaninfo["filepath"] - if expected_exception: - with pytest.raises(Exception): - mock_det.stage() - else: - mock_det.stage() - mock_prep_fw.assert_called_once() - # Check _prep_det - assert mock_det.cam.num_images.get() == int( - scaninfo["num_points"] * scaninfo["frames_per_trigger"] - ) - assert mock_det.cam.num_frames.get() == 1 +# @pytest.mark.parametrize( +# "scaninfo, daq_status, daq_cfg, detector_state, stopped, expected_exception", +# [ +# ( +# { +# "eacc": "e12345", +# "num_points": 500, +# "frames_per_trigger": 1, +# "filepath": "test.h5", +# "scanID": "123", +# "mokev": 12.4, +# }, +# {"state": "READY"}, +# {"writer_user_id": 12543}, +# 5, +# False, +# False, +# ), +# ( +# { +# "eacc": "e12345", +# "num_points": 500, +# "frames_per_trigger": 1, +# "filepath": "test.h5", +# "scanID": "123", +# "mokev": 12.4, +# }, +# {"state": "BUSY"}, +# {"writer_user_id": 15421}, +# 5, +# False, +# False, +# ), +# ( +# { +# "eacc": "e12345", +# "num_points": 500, +# "frames_per_trigger": 1, +# "filepath": "test.h5", +# "scanID": "123", +# "mokev": 18.4, +# }, +# {"state": "READY"}, +# {"writer_user_id": 12345}, +# 4, +# False, +# True, +# ), +# ], +# ) +# def test_stage( +# mock_det, +# scaninfo, +# daq_status, +# daq_cfg, +# detector_state, +# stopped, +# expected_exception, +# ): +# with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( +# Eiger9McSAXS, "_publish_file_location" +# ) as mock_publish_file_location: +# mock_std_daq.stop_writer.return_value = None +# mock_std_daq.get_status.return_value = daq_status +# mock_std_daq.get_config.return_value = daq_cfg +# mock_det.scaninfo.num_points = scaninfo["num_points"] +# mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] +# mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] +# # TODO consider putting energy as variable in scaninfo +# mock_det.device_manager.add_device("mokev", value=12.4) +# mock_det.cam.beam_energy.put(scaninfo["mokev"]) +# mock_det._stopped = stopped +# mock_det.cam.detector_state.put(detector_state) +# with mock.patch.object(mock_det, "_prep_file_writer") as mock_prep_fw: +# mock_det.filepath = scaninfo["filepath"] +# if expected_exception: +# with pytest.raises(Exception): +# mock_det.stage() +# else: +# mock_det.stage() +# mock_prep_fw.assert_called_once() +# # Check _prep_det +# assert mock_det.cam.num_images.get() == int( +# scaninfo["num_points"] * scaninfo["frames_per_trigger"] +# ) +# assert mock_det.cam.num_frames.get() == 1 - mock_publish_file_location.assert_called_with(done=False) - assert mock_det.cam.acquire.get() == 1 +# mock_publish_file_location.assert_called_with(done=False) +# assert mock_det.cam.acquire.get() == 1 -@pytest.mark.parametrize( - "scaninfo, daq_status, expected_exception", - [ - ( - { - "eacc": "e12345", - "num_points": 500, - "frames_per_trigger": 1, - "filepath": "test.h5", - "scanID": "123", - }, - {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, - False, - ), - ( - { - "eacc": "e12345", - "num_points": 500, - "frames_per_trigger": 1, - "filepath": "test.h5", - "scanID": "123", - }, - {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, - False, - ), - ( - { - "eacc": "e12345", - "num_points": 500, - "frames_per_trigger": 1, - "filepath": "test.h5", - "scanID": "123", - }, - {"state": "BUSY", "acquisition": {"state": "ERROR"}}, - True, - ), - ], -) -def test_prep_file_writer(mock_det, scaninfo, daq_status, expected_exception): - with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( - mock_det, "_filepath_exists" - ) as mock_file_path_exists, mock.patch.object( - mock_det, "_stop_file_writer" - ) as mock_stop_file_writer, mock.patch.object( - mock_det, "scaninfo" - ) as mock_scaninfo: - # mock_det = eiger_factory(name, prefix, sim_mode) - mock_det.std_client = mock_std_daq - mock_std_daq.start_writer_async.return_value = None - mock_std_daq.get_status.return_value = daq_status - 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"] +# @pytest.mark.parametrize( +# "scaninfo, daq_status, expected_exception", +# [ +# ( +# { +# "eacc": "e12345", +# "num_points": 500, +# "frames_per_trigger": 1, +# "filepath": "test.h5", +# "scanID": "123", +# }, +# {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, +# False, +# ), +# ( +# { +# "eacc": "e12345", +# "num_points": 500, +# "frames_per_trigger": 1, +# "filepath": "test.h5", +# "scanID": "123", +# }, +# {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, +# False, +# ), +# ( +# { +# "eacc": "e12345", +# "num_points": 500, +# "frames_per_trigger": 1, +# "filepath": "test.h5", +# "scanID": "123", +# }, +# {"state": "BUSY", "acquisition": {"state": "ERROR"}}, +# True, +# ), +# ], +# ) +# def test_prep_file_writer(mock_det, scaninfo, daq_status, expected_exception): +# with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( +# mock_det, "_filepath_exists" +# ) as mock_file_path_exists, mock.patch.object( +# mock_det, "_stop_file_writer" +# ) as mock_stop_file_writer, mock.patch.object( +# mock_det, "scaninfo" +# ) as mock_scaninfo: +# # mock_det = eiger_factory(name, prefix, sim_mode) +# mock_det.std_client = mock_std_daq +# mock_std_daq.start_writer_async.return_value = None +# mock_std_daq.get_status.return_value = daq_status +# 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"] - if expected_exception: - with pytest.raises(Exception): - mock_det._prep_file_writer() - mock_file_path_exists.assert_called_once() - assert mock_stop_file_writer.call_count == 2 +# if expected_exception: +# with pytest.raises(Exception): +# mock_det._prep_file_writer() +# mock_file_path_exists.assert_called_once() +# assert mock_stop_file_writer.call_count == 2 - else: - mock_det._prep_file_writer() - mock_file_path_exists.assert_called_once() - mock_stop_file_writer.assert_called_once() +# else: +# mock_det._prep_file_writer() +# mock_file_path_exists.assert_called_once() +# mock_stop_file_writer.assert_called_once() - daq_writer_call = { - "output_file": scaninfo["filepath"], - "n_images": int(scaninfo["num_points"] * scaninfo["frames_per_trigger"]), - } - mock_std_daq.start_writer_async.assert_called_with(daq_writer_call) +# daq_writer_call = { +# "output_file": scaninfo["filepath"], +# "n_images": int(scaninfo["num_points"] * scaninfo["frames_per_trigger"]), +# } +# mock_std_daq.start_writer_async.assert_called_with(daq_writer_call) -@pytest.mark.parametrize( - "stopped, expected_exception", - [ - ( - False, - False, - ), - ( - True, - True, - ), - ], -) -def test_unstage( - mock_det, - stopped, - expected_exception, -): - with mock.patch.object(mock_det, "_finished") as mock_finished, mock.patch.object( - mock_det, "_publish_file_location" - ) as mock_publish_file_location: - mock_det._stopped = stopped - if expected_exception: - mock_det.unstage() - assert mock_det._stopped == True - else: - mock_det.unstage() - mock_finished.assert_called_once() - mock_publish_file_location.assert_called_with(done=True, successful=True) - assert mock_det._stopped == False +# @pytest.mark.parametrize( +# "stopped, expected_exception", +# [ +# ( +# False, +# False, +# ), +# ( +# True, +# True, +# ), +# ], +# ) +# def test_unstage( +# mock_det, +# stopped, +# expected_exception, +# ): +# with mock.patch.object(mock_det, "_finished") as mock_finished, mock.patch.object( +# mock_det, "_publish_file_location" +# ) as mock_publish_file_location: +# mock_det._stopped = stopped +# if expected_exception: +# mock_det.unstage() +# assert mock_det._stopped == True +# else: +# mock_det.unstage() +# mock_finished.assert_called_once() +# mock_publish_file_location.assert_called_with(done=True, successful=True) +# assert mock_det._stopped == False -def test_stop_fw(mock_det): - with mock.patch.object(mock_det, "std_client") as mock_std_daq: - mock_std_daq.stop_writer.return_value = None - mock_det.std_client = mock_std_daq - mock_det._stop_file_writer() - mock_std_daq.stop_writer.assert_called_once() +# def test_stop_fw(mock_det): +# with mock.patch.object(mock_det, "std_client") as mock_std_daq: +# mock_std_daq.stop_writer.return_value = None +# mock_det.std_client = mock_std_daq +# mock_det._stop_file_writer() +# mock_std_daq.stop_writer.assert_called_once() -@pytest.mark.parametrize( - "scaninfo", - [ - ({"filepath": "test.h5", "successful": True, "done": False, "scanID": "123"}), - ({"filepath": "test.h5", "successful": False, "done": True, "scanID": "123"}), - ({"filepath": "test.h5", "successful": None, "done": True, "scanID": "123"}), - ], -) -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"]) - if scaninfo["successful"] is None: - msg = BECMessage.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() - else: - msg = BECMessage.FileMessage( - file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] - ).dumps() - expected_calls = [ - mock.call( - MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), - msg, - pipe=mock_det._producer.pipeline.return_value, - ), - mock.call( - MessageEndpoints.file_event(mock_det.name), - msg, - pipe=mock_det._producer.pipeline.return_value, - ), - ] - assert mock_det._producer.set_and_publish.call_args_list == expected_calls +# @pytest.mark.parametrize( +# "scaninfo", +# [ +# ({"filepath": "test.h5", "successful": True, "done": False, "scanID": "123"}), +# ({"filepath": "test.h5", "successful": False, "done": True, "scanID": "123"}), +# ({"filepath": "test.h5", "successful": None, "done": True, "scanID": "123"}), +# ], +# ) +# 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"]) +# if scaninfo["successful"] is None: +# msg = BECMessage.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() +# else: +# msg = BECMessage.FileMessage( +# file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] +# ).dumps() +# expected_calls = [ +# mock.call( +# MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), +# msg, +# pipe=mock_det._producer.pipeline.return_value, +# ), +# mock.call( +# MessageEndpoints.file_event(mock_det.name), +# msg, +# pipe=mock_det._producer.pipeline.return_value, +# ), +# ] +# assert mock_det._producer.set_and_publish.call_args_list == expected_calls -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: - mock_det.stop() - mock_stop_det.assert_called_once() - mock_stop_file_writer.assert_called_once() - assert mock_det._stopped == True +# 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: +# mock_det.stop() +# mock_stop_det.assert_called_once() +# mock_stop_file_writer.assert_called_once() +# assert mock_det._stopped == True -@pytest.mark.parametrize( - "stopped, scaninfo, cam_state, daq_status, expected_exception", - [ - ( - False, - { - "num_points": 500, - "frames_per_trigger": 4, - }, - 0, - {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 2000}}}, - False, - ), - ( - False, - { - "num_points": 500, - "frames_per_trigger": 4, - }, - 0, - {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 1999}}}, - True, - ), - ( - False, - { - "num_points": 500, - "frames_per_trigger": 1, - }, - 1, - {"acquisition": {"state": "READY", "stats": {"n_write_completed": 500}}}, - True, - ), - ( - False, - { - "num_points": 500, - "frames_per_trigger": 1, - }, - 0, - {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 500}}}, - False, - ), - ], -) -def test_finished(mock_det, stopped, cam_state, daq_status, scaninfo, expected_exception): - with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( - mock_det, "_stop_file_writer" - ) as mock_stop_file_friter, mock.patch.object(mock_det, "_stop_det") as mock_stop_det: - mock_std_daq.get_status.return_value = daq_status - mock_det.cam.acquire.put(cam_state) - mock_det.scaninfo.num_points = scaninfo["num_points"] - mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] - if expected_exception: - with pytest.raises(Exception): - mock_det._finished() - assert mock_det._stopped == stopped - mock_stop_file_friter.assert_called() - mock_stop_det.assert_called_once() - else: - mock_det._finished() - if stopped: - assert mock_det._stopped == stopped +# @pytest.mark.parametrize( +# "stopped, scaninfo, cam_state, daq_status, expected_exception", +# [ +# ( +# False, +# { +# "num_points": 500, +# "frames_per_trigger": 4, +# }, +# 0, +# {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 2000}}}, +# False, +# ), +# ( +# False, +# { +# "num_points": 500, +# "frames_per_trigger": 4, +# }, +# 0, +# {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 1999}}}, +# True, +# ), +# ( +# False, +# { +# "num_points": 500, +# "frames_per_trigger": 1, +# }, +# 1, +# {"acquisition": {"state": "READY", "stats": {"n_write_completed": 500}}}, +# True, +# ), +# ( +# False, +# { +# "num_points": 500, +# "frames_per_trigger": 1, +# }, +# 0, +# {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 500}}}, +# False, +# ), +# ], +# ) +# def test_finished(mock_det, stopped, cam_state, daq_status, scaninfo, expected_exception): +# with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( +# mock_det, "_stop_file_writer" +# ) as mock_stop_file_friter, mock.patch.object(mock_det, "_stop_det") as mock_stop_det: +# mock_std_daq.get_status.return_value = daq_status +# mock_det.cam.acquire.put(cam_state) +# mock_det.scaninfo.num_points = scaninfo["num_points"] +# mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] +# if expected_exception: +# with pytest.raises(Exception): +# mock_det._finished() +# assert mock_det._stopped == stopped +# mock_stop_file_friter.assert_called() +# mock_stop_det.assert_called_once() +# else: +# mock_det._finished() +# if stopped: +# assert mock_det._stopped == stopped - mock_stop_file_friter.assert_called() - mock_stop_det.assert_called_once() +# mock_stop_file_friter.assert_called() +# mock_stop_det.assert_called_once() diff --git a/tests/utils.py b/tests/utils.py index 90af2e9..047ab68 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,9 @@ +from bec_lib.core.devicemanager import DeviceContainer +from bec_lib.core.tests.utils import ProducerMock + +from unittest import mock + + class SocketMock: def __init__(self, host, port): self.host = host @@ -44,3 +50,246 @@ class SocketMock: def flush_buffer(self): self.buffer_put = [] self.buffer_recv = "" + + +class MockPV: + """ + MockPV class + + This class is used for mocking pyepics signals for testing purposes + + """ + + _fmtsca = "" + _fmtarr = "" + _fields = ( + "pvname", + "value", + "char_value", + "status", + "ftype", + "chid", + "host", + "count", + "access", + "write_access", + "read_access", + "severity", + "timestamp", + "posixseconds", + "nanoseconds", + "precision", + "units", + "enum_strs", + "upper_disp_limit", + "lower_disp_limit", + "upper_alarm_limit", + "lower_alarm_limit", + "lower_warning_limit", + "upper_warning_limit", + "upper_ctrl_limit", + "lower_ctrl_limit", + ) + + def __init__( + self, + pvname, + callback=None, + form="time", + verbose=False, + auto_monitor=None, + count=None, + connection_callback=None, + connection_timeout=None, + access_callback=None, + ): + self.pvname = pvname.strip() + self.form = form.lower() + self.verbose = verbose + self._auto_monitor = auto_monitor + self.ftype = None + self.connected = True + self.connection_timeout = connection_timeout + self._user_max_count = count + + if self.connection_timeout is None: + self.connection_timeout = 3 + self._args = {}.fromkeys(self._fields) + self._args["pvname"] = self.pvname + self._args["count"] = count + self._args["nelm"] = -1 + self._args["type"] = "unknown" + self._args["typefull"] = "unknown" + self._args["access"] = "unknown" + self._args["status"] = 0 + self.connection_callbacks = [] + self.mock_data = 0 + + if connection_callback is not None: + self.connection_callbacks = [connection_callback] + + self.access_callbacks = [] + if access_callback is not None: + self.access_callbacks = [access_callback] + + self.callbacks = {} + self._put_complete = None + self._monref = None # holder of data returned from create_subscription + self._monref_mask = None + self._conn_started = False + if isinstance(callback, (tuple, list)): + for i, thiscb in enumerate(callback): + if callable(thiscb): + self.callbacks[i] = (thiscb, {}) + elif callable(callback): + self.callbacks[0] = (callback, {}) + + self.chid = None + self.context = mock.MagicMock() + self._cache_key = (pvname, form, self.context) + self._reference_count = 0 + for conn_cb in self.connection_callbacks: + conn_cb(pvname=pvname, conn=True, pv=self) + for acc_cb in self.access_callbacks: + acc_cb(True, True, pv=self) + + def wait_for_connection(self, timeout=None): + return self.connected + + def get_all_metadata_blocking(self, timeout): + md = self._args.copy() + md.pop("value", None) + return md + + def get_all_metadata_callback(self, callback, *, timeout): + def get_metadata_thread(pvname): + md = self.get_all_metadata_blocking(timeout=timeout) + callback(pvname, md) + + get_metadata_thread(pvname=self.pvname) + + def put( + self, value, wait=False, timeout=None, use_complete=False, callback=None, callback_data=None + ): + self.mock_data = value + if callback is not None: + callback(None, None, None) + + def get_with_metadata( + self, + count=None, + as_string=False, + as_numpy=True, + timeout=None, + with_ctrlvars=False, + form=None, + use_monitor=True, + as_namespace=False, + ): + return {"value": self.mock_data} + + def get( + self, + count=None, + as_string=False, + as_numpy=True, + timeout=None, + with_ctrlvars=False, + use_monitor=True, + ): + data = self.get_with_metadata( + count=count, + as_string=as_string, + as_numpy=as_numpy, + timeout=timeout, + with_ctrlvars=with_ctrlvars, + use_monitor=use_monitor, + ) + return data["value"] if data is not None else None + + +class DeviceMock: + """Device Mock. Used for testing in combination with the DeviceManagerMock + + Args: + name (str): name of the device + value (float, optional): initial value of the device. Defaults to 0.0. + Returns: + DeviceMock: DeviceMock object + + """ + + def __init__(self, name: str, value: float = 0.0): + self.name = name + self.read_buffer = value + self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None} + self._enabled_set = True + self._enabled = True + + def read(self): + return {self.name: {"value": self.read_buffer}} + + def readback(self): + return self.read_buffer + + @property + def enabled_set(self) -> bool: + return self._enabled_set + + @enabled_set.setter + def enabled_set(self, val: bool): + self._enabled_set = val + + @property + def enabled(self) -> bool: + return self._enabled + + @enabled.setter + def enabled(self, val: bool): + self._enabled = val + + @property + def user_parameter(self): + return self._config["userParameter"] + + @property + def obj(self): + return self + + +class DMMock: + """Mock for DeviceManager + + The mocked DeviceManager creates a device containert and a producer. + + """ + + def __init__(self): + self.devices = DeviceContainer() + self.producer = ProducerMock() + + def add_device(self, name: str, value: float = 0.0): + self.devices[name] = DeviceMock(name, value) + + +# #TODO check what is the difference to SynSignal! +# class MockSignal(Signal): +# """Can mock an OphydSignal""" +# def __init__(self, read_pv, *, string=False, name=None, parent=None, **kwargs): +# self.read_pv = read_pv +# self._string = bool(string) +# super().__init__(name=name, parent=parent, **kwargs) +# self._waited_for_connection = False +# self._subscriptions = [] + +# def wait_for_connection(self): +# self._waited_for_connection = True + +# def subscribe(self, method, event_type, **kw): +# self._subscriptions.append((method, event_type, kw)) + +# def describe_configuration(self): +# return {self.name + "_conf": {"source": "SIM:test"}} + +# def read_configuration(self): +# return {self.name + "_conf": {"value": 0}} From cb49a2a2055de471200d1a26bdb762676a199708 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 7 Nov 2023 13:48:43 +0100 Subject: [PATCH 16/23] test: fixed all eiger test with updated mock PV; closes #11 --- tests/test_eiger9m_csaxs.py | 704 ++++++++++++++++++------------------ 1 file changed, 353 insertions(+), 351 deletions(-) diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index 43e7759..447feef 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -15,7 +15,9 @@ def patch_dual_pvs(device): continue if not hasattr(walk.item, "_write_pv"): continue - if walk.item._read_pv.pvname.endswith("_RBV"): + if walk.item._read_pv.pvname.endswith("_RBV") and isinstance( + walk.item, ophyd.areadetector.base.EpicsSignalWithRBV + ): walk.item._read_pv = walk.item._write_pv @@ -130,376 +132,376 @@ def test_update_readout_time(mock_det, readout_time, expected_value): assert mock_det.readout_time == expected_value -# @pytest.mark.parametrize( -# "eacc, exp_url, daq_status, daq_cfg, expected_exception", -# [ -# ( -# "e12345", -# "http://xbl-daq-29:5000", -# {"state": "READY"}, -# {"writer_user_id": 12543}, -# False, -# ), -# ( -# "e12345", -# "http://xbl-daq-29:5000", -# {"state": "READY"}, -# {"writer_user_id": 15421}, -# False, -# ), -# ( -# "e12345", -# "http://xbl-daq-29:5000", -# {"state": "BUSY"}, -# {"writer_user_id": 15421}, -# True, -# ), -# ( -# "e12345", -# "http://xbl-daq-29:5000", -# {"state": "READY"}, -# {"writer_ud": 12345}, -# True, -# ), -# ], -# ) -# def test_init_filewriter(mock_det, eacc, exp_url, daq_status, daq_cfg, expected_exception): -# """Test _init_filewriter (std daq in this case) +@pytest.mark.parametrize( + "eacc, exp_url, daq_status, daq_cfg, expected_exception", + [ + ( + "e12345", + "http://xbl-daq-29:5000", + {"state": "READY"}, + {"writer_user_id": 12543}, + False, + ), + ( + "e12345", + "http://xbl-daq-29:5000", + {"state": "READY"}, + {"writer_user_id": 15421}, + False, + ), + ( + "e12345", + "http://xbl-daq-29:5000", + {"state": "BUSY"}, + {"writer_user_id": 15421}, + True, + ), + ( + "e12345", + "http://xbl-daq-29:5000", + {"state": "READY"}, + {"writer_ud": 12345}, + True, + ), + ], +) +def test_init_filewriter(mock_det, eacc, exp_url, daq_status, daq_cfg, expected_exception): + """Test _init_filewriter (std daq in this case) -# This includes testing the functions: + This includes testing the functions: -# - _update_service_config + - _update_service_config -# Validation upon checking set values in mocked std_daq instance -# """ -# with mock.patch("ophyd_devices.epics.devices.eiger9m_csaxs.StdDaqClient") as mock_std_daq: -# instance = mock_std_daq.return_value -# instance.stop_writer.return_value = None -# instance.get_status.return_value = daq_status -# instance.get_config.return_value = daq_cfg -# mock_det.scaninfo.username = eacc -# # scaninfo.username.return_value = eacc -# if expected_exception: -# with pytest.raises(Exception): -# mock_det._init_filewriter() -# else: -# mock_det._init_filewriter() + Validation upon checking set values in mocked std_daq instance + """ + with mock.patch("ophyd_devices.epics.devices.eiger9m_csaxs.StdDaqClient") as mock_std_daq: + instance = mock_std_daq.return_value + instance.stop_writer.return_value = None + instance.get_status.return_value = daq_status + instance.get_config.return_value = daq_cfg + mock_det.scaninfo.username = eacc + # scaninfo.username.return_value = eacc + if expected_exception: + with pytest.raises(Exception): + mock_det._init_filewriter() + else: + mock_det._init_filewriter() -# assert mock_det.std_rest_server_url == exp_url -# instance.stop_writer.assert_called_once() -# instance.get_status.assert_called() -# instance.set_config.assert_called_once_with(daq_cfg) + assert mock_det.std_rest_server_url == exp_url + instance.stop_writer.assert_called_once() + instance.get_status.assert_called() + instance.set_config.assert_called_once_with(daq_cfg) -# @pytest.mark.parametrize( -# "scaninfo, daq_status, daq_cfg, detector_state, stopped, expected_exception", -# [ -# ( -# { -# "eacc": "e12345", -# "num_points": 500, -# "frames_per_trigger": 1, -# "filepath": "test.h5", -# "scanID": "123", -# "mokev": 12.4, -# }, -# {"state": "READY"}, -# {"writer_user_id": 12543}, -# 5, -# False, -# False, -# ), -# ( -# { -# "eacc": "e12345", -# "num_points": 500, -# "frames_per_trigger": 1, -# "filepath": "test.h5", -# "scanID": "123", -# "mokev": 12.4, -# }, -# {"state": "BUSY"}, -# {"writer_user_id": 15421}, -# 5, -# False, -# False, -# ), -# ( -# { -# "eacc": "e12345", -# "num_points": 500, -# "frames_per_trigger": 1, -# "filepath": "test.h5", -# "scanID": "123", -# "mokev": 18.4, -# }, -# {"state": "READY"}, -# {"writer_user_id": 12345}, -# 4, -# False, -# True, -# ), -# ], -# ) -# def test_stage( -# mock_det, -# scaninfo, -# daq_status, -# daq_cfg, -# detector_state, -# stopped, -# expected_exception, -# ): -# with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( -# Eiger9McSAXS, "_publish_file_location" -# ) as mock_publish_file_location: -# mock_std_daq.stop_writer.return_value = None -# mock_std_daq.get_status.return_value = daq_status -# mock_std_daq.get_config.return_value = daq_cfg -# mock_det.scaninfo.num_points = scaninfo["num_points"] -# mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] -# mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] -# # TODO consider putting energy as variable in scaninfo -# mock_det.device_manager.add_device("mokev", value=12.4) -# mock_det.cam.beam_energy.put(scaninfo["mokev"]) -# mock_det._stopped = stopped -# mock_det.cam.detector_state.put(detector_state) -# with mock.patch.object(mock_det, "_prep_file_writer") as mock_prep_fw: -# mock_det.filepath = scaninfo["filepath"] -# if expected_exception: -# with pytest.raises(Exception): -# mock_det.stage() -# else: -# mock_det.stage() -# mock_prep_fw.assert_called_once() -# # Check _prep_det -# assert mock_det.cam.num_images.get() == int( -# scaninfo["num_points"] * scaninfo["frames_per_trigger"] -# ) -# assert mock_det.cam.num_frames.get() == 1 +@pytest.mark.parametrize( + "scaninfo, daq_status, daq_cfg, detector_state, stopped, expected_exception", + [ + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + "mokev": 12.4, + }, + {"state": "READY"}, + {"writer_user_id": 12543}, + 5, + False, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + "mokev": 12.4, + }, + {"state": "BUSY"}, + {"writer_user_id": 15421}, + 5, + False, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + "mokev": 18.4, + }, + {"state": "READY"}, + {"writer_user_id": 12345}, + 4, + False, + True, + ), + ], +) +def test_stage( + mock_det, + scaninfo, + daq_status, + daq_cfg, + detector_state, + stopped, + expected_exception, +): + with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( + Eiger9McSAXS, "_publish_file_location" + ) as mock_publish_file_location: + mock_std_daq.stop_writer.return_value = None + mock_std_daq.get_status.return_value = daq_status + mock_std_daq.get_config.return_value = daq_cfg + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] + # TODO consider putting energy as variable in scaninfo + mock_det.device_manager.add_device("mokev", value=12.4) + mock_det.cam.beam_energy.put(scaninfo["mokev"]) + mock_det._stopped = stopped + mock_det.cam.detector_state._read_pv.mock_data = detector_state + with mock.patch.object(mock_det, "_prep_file_writer") as mock_prep_fw: + mock_det.filepath = scaninfo["filepath"] + if expected_exception: + with pytest.raises(Exception): + mock_det.stage() + else: + mock_det.stage() + mock_prep_fw.assert_called_once() + # Check _prep_det + assert mock_det.cam.num_images.get() == int( + scaninfo["num_points"] * scaninfo["frames_per_trigger"] + ) + assert mock_det.cam.num_frames.get() == 1 -# mock_publish_file_location.assert_called_with(done=False) -# assert mock_det.cam.acquire.get() == 1 + mock_publish_file_location.assert_called_with(done=False) + assert mock_det.cam.acquire.get() == 1 -# @pytest.mark.parametrize( -# "scaninfo, daq_status, expected_exception", -# [ -# ( -# { -# "eacc": "e12345", -# "num_points": 500, -# "frames_per_trigger": 1, -# "filepath": "test.h5", -# "scanID": "123", -# }, -# {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, -# False, -# ), -# ( -# { -# "eacc": "e12345", -# "num_points": 500, -# "frames_per_trigger": 1, -# "filepath": "test.h5", -# "scanID": "123", -# }, -# {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, -# False, -# ), -# ( -# { -# "eacc": "e12345", -# "num_points": 500, -# "frames_per_trigger": 1, -# "filepath": "test.h5", -# "scanID": "123", -# }, -# {"state": "BUSY", "acquisition": {"state": "ERROR"}}, -# True, -# ), -# ], -# ) -# def test_prep_file_writer(mock_det, scaninfo, daq_status, expected_exception): -# with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( -# mock_det, "_filepath_exists" -# ) as mock_file_path_exists, mock.patch.object( -# mock_det, "_stop_file_writer" -# ) as mock_stop_file_writer, mock.patch.object( -# mock_det, "scaninfo" -# ) as mock_scaninfo: -# # mock_det = eiger_factory(name, prefix, sim_mode) -# mock_det.std_client = mock_std_daq -# mock_std_daq.start_writer_async.return_value = None -# mock_std_daq.get_status.return_value = daq_status -# 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"] +@pytest.mark.parametrize( + "scaninfo, daq_status, expected_exception", + [ + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + }, + {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + }, + {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, + False, + ), + ( + { + "eacc": "e12345", + "num_points": 500, + "frames_per_trigger": 1, + "filepath": "test.h5", + "scanID": "123", + }, + {"state": "BUSY", "acquisition": {"state": "ERROR"}}, + True, + ), + ], +) +def test_prep_file_writer(mock_det, scaninfo, daq_status, expected_exception): + with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( + mock_det, "_filepath_exists" + ) as mock_file_path_exists, mock.patch.object( + mock_det, "_stop_file_writer" + ) as mock_stop_file_writer, mock.patch.object( + mock_det, "scaninfo" + ) as mock_scaninfo: + # mock_det = eiger_factory(name, prefix, sim_mode) + mock_det.std_client = mock_std_daq + mock_std_daq.start_writer_async.return_value = None + mock_std_daq.get_status.return_value = daq_status + 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"] -# if expected_exception: -# with pytest.raises(Exception): -# mock_det._prep_file_writer() -# mock_file_path_exists.assert_called_once() -# assert mock_stop_file_writer.call_count == 2 + if expected_exception: + with pytest.raises(Exception): + mock_det._prep_file_writer() + mock_file_path_exists.assert_called_once() + assert mock_stop_file_writer.call_count == 2 -# else: -# mock_det._prep_file_writer() -# mock_file_path_exists.assert_called_once() -# mock_stop_file_writer.assert_called_once() + else: + mock_det._prep_file_writer() + mock_file_path_exists.assert_called_once() + mock_stop_file_writer.assert_called_once() -# daq_writer_call = { -# "output_file": scaninfo["filepath"], -# "n_images": int(scaninfo["num_points"] * scaninfo["frames_per_trigger"]), -# } -# mock_std_daq.start_writer_async.assert_called_with(daq_writer_call) + daq_writer_call = { + "output_file": scaninfo["filepath"], + "n_images": int(scaninfo["num_points"] * scaninfo["frames_per_trigger"]), + } + mock_std_daq.start_writer_async.assert_called_with(daq_writer_call) -# @pytest.mark.parametrize( -# "stopped, expected_exception", -# [ -# ( -# False, -# False, -# ), -# ( -# True, -# True, -# ), -# ], -# ) -# def test_unstage( -# mock_det, -# stopped, -# expected_exception, -# ): -# with mock.patch.object(mock_det, "_finished") as mock_finished, mock.patch.object( -# mock_det, "_publish_file_location" -# ) as mock_publish_file_location: -# mock_det._stopped = stopped -# if expected_exception: -# mock_det.unstage() -# assert mock_det._stopped == True -# else: -# mock_det.unstage() -# mock_finished.assert_called_once() -# mock_publish_file_location.assert_called_with(done=True, successful=True) -# assert mock_det._stopped == False +@pytest.mark.parametrize( + "stopped, expected_exception", + [ + ( + False, + False, + ), + ( + True, + True, + ), + ], +) +def test_unstage( + mock_det, + stopped, + expected_exception, +): + with mock.patch.object(mock_det, "_finished") as mock_finished, mock.patch.object( + mock_det, "_publish_file_location" + ) as mock_publish_file_location: + mock_det._stopped = stopped + if expected_exception: + mock_det.unstage() + assert mock_det._stopped == True + else: + mock_det.unstage() + mock_finished.assert_called_once() + mock_publish_file_location.assert_called_with(done=True, successful=True) + assert mock_det._stopped == False -# def test_stop_fw(mock_det): -# with mock.patch.object(mock_det, "std_client") as mock_std_daq: -# mock_std_daq.stop_writer.return_value = None -# mock_det.std_client = mock_std_daq -# mock_det._stop_file_writer() -# mock_std_daq.stop_writer.assert_called_once() +def test_stop_fw(mock_det): + with mock.patch.object(mock_det, "std_client") as mock_std_daq: + mock_std_daq.stop_writer.return_value = None + mock_det.std_client = mock_std_daq + mock_det._stop_file_writer() + mock_std_daq.stop_writer.assert_called_once() -# @pytest.mark.parametrize( -# "scaninfo", -# [ -# ({"filepath": "test.h5", "successful": True, "done": False, "scanID": "123"}), -# ({"filepath": "test.h5", "successful": False, "done": True, "scanID": "123"}), -# ({"filepath": "test.h5", "successful": None, "done": True, "scanID": "123"}), -# ], -# ) -# 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"]) -# if scaninfo["successful"] is None: -# msg = BECMessage.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() -# else: -# msg = BECMessage.FileMessage( -# file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] -# ).dumps() -# expected_calls = [ -# mock.call( -# MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), -# msg, -# pipe=mock_det._producer.pipeline.return_value, -# ), -# mock.call( -# MessageEndpoints.file_event(mock_det.name), -# msg, -# pipe=mock_det._producer.pipeline.return_value, -# ), -# ] -# assert mock_det._producer.set_and_publish.call_args_list == expected_calls +@pytest.mark.parametrize( + "scaninfo", + [ + ({"filepath": "test.h5", "successful": True, "done": False, "scanID": "123"}), + ({"filepath": "test.h5", "successful": False, "done": True, "scanID": "123"}), + ({"filepath": "test.h5", "successful": None, "done": True, "scanID": "123"}), + ], +) +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"]) + if scaninfo["successful"] is None: + msg = BECMessage.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() + else: + msg = BECMessage.FileMessage( + file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"] + ).dumps() + expected_calls = [ + mock.call( + MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name), + msg, + pipe=mock_det._producer.pipeline.return_value, + ), + mock.call( + MessageEndpoints.file_event(mock_det.name), + msg, + pipe=mock_det._producer.pipeline.return_value, + ), + ] + assert mock_det._producer.set_and_publish.call_args_list == expected_calls -# 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: -# mock_det.stop() -# mock_stop_det.assert_called_once() -# mock_stop_file_writer.assert_called_once() -# assert mock_det._stopped == True +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: + mock_det.stop() + mock_stop_det.assert_called_once() + mock_stop_file_writer.assert_called_once() + assert mock_det._stopped == True -# @pytest.mark.parametrize( -# "stopped, scaninfo, cam_state, daq_status, expected_exception", -# [ -# ( -# False, -# { -# "num_points": 500, -# "frames_per_trigger": 4, -# }, -# 0, -# {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 2000}}}, -# False, -# ), -# ( -# False, -# { -# "num_points": 500, -# "frames_per_trigger": 4, -# }, -# 0, -# {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 1999}}}, -# True, -# ), -# ( -# False, -# { -# "num_points": 500, -# "frames_per_trigger": 1, -# }, -# 1, -# {"acquisition": {"state": "READY", "stats": {"n_write_completed": 500}}}, -# True, -# ), -# ( -# False, -# { -# "num_points": 500, -# "frames_per_trigger": 1, -# }, -# 0, -# {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 500}}}, -# False, -# ), -# ], -# ) -# def test_finished(mock_det, stopped, cam_state, daq_status, scaninfo, expected_exception): -# with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( -# mock_det, "_stop_file_writer" -# ) as mock_stop_file_friter, mock.patch.object(mock_det, "_stop_det") as mock_stop_det: -# mock_std_daq.get_status.return_value = daq_status -# mock_det.cam.acquire.put(cam_state) -# mock_det.scaninfo.num_points = scaninfo["num_points"] -# mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] -# if expected_exception: -# with pytest.raises(Exception): -# mock_det._finished() -# assert mock_det._stopped == stopped -# mock_stop_file_friter.assert_called() -# mock_stop_det.assert_called_once() -# else: -# mock_det._finished() -# if stopped: -# assert mock_det._stopped == stopped +@pytest.mark.parametrize( + "stopped, scaninfo, cam_state, daq_status, expected_exception", + [ + ( + False, + { + "num_points": 500, + "frames_per_trigger": 4, + }, + 0, + {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 2000}}}, + False, + ), + ( + False, + { + "num_points": 500, + "frames_per_trigger": 4, + }, + 0, + {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 1999}}}, + True, + ), + ( + False, + { + "num_points": 500, + "frames_per_trigger": 1, + }, + 1, + {"acquisition": {"state": "READY", "stats": {"n_write_completed": 500}}}, + True, + ), + ( + False, + { + "num_points": 500, + "frames_per_trigger": 1, + }, + 0, + {"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 500}}}, + False, + ), + ], +) +def test_finished(mock_det, stopped, cam_state, daq_status, scaninfo, expected_exception): + with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( + mock_det, "_stop_file_writer" + ) as mock_stop_file_friter, mock.patch.object(mock_det, "_stop_det") as mock_stop_det: + mock_std_daq.get_status.return_value = daq_status + mock_det.cam.acquire._read_pv.mock_state = cam_state + mock_det.scaninfo.num_points = scaninfo["num_points"] + mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + if expected_exception: + with pytest.raises(Exception): + mock_det._finished() + assert mock_det._stopped == stopped + mock_stop_file_friter.assert_called() + mock_stop_det.assert_called_once() + else: + mock_det._finished() + if stopped: + assert mock_det._stopped == stopped -# mock_stop_file_friter.assert_called() -# mock_stop_det.assert_called_once() + mock_stop_file_friter.assert_called() + mock_stop_det.assert_called_once() From 57a436261c46a3287320b7dfc0a29e16a9482f33 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 7 Nov 2023 14:06:38 +0100 Subject: [PATCH 17/23] refactor: remove bluesky runengine dependency from re_test.py --- ophyd_devices/utils/re_test.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ophyd_devices/utils/re_test.py b/ophyd_devices/utils/re_test.py index 4194a35..1791e4b 100644 --- a/ophyd_devices/utils/re_test.py +++ b/ophyd_devices/utils/re_test.py @@ -1,17 +1,17 @@ -from bluesky import RunEngine -from bluesky.plans import grid_scan -from bluesky.callbacks.best_effort import BestEffortCallback -from bluesky.callbacks.mpl_plotting import LivePlot +# from bluesky import RunEngine +# from bluesky.plans import grid_scan +# from bluesky.callbacks.best_effort import BestEffortCallback +# from bluesky.callbacks.mpl_plotting import LivePlot -RE = RunEngine({}) +# RE = RunEngine({}) -from bluesky.callbacks.best_effort import BestEffortCallback +# from bluesky.callbacks.best_effort import BestEffortCallback -bec = BestEffortCallback() +# bec = BestEffortCallback() -# Send all metadata/data captured to the BestEffortCallback. -RE.subscribe(bec) -# RE.subscribe(dummy) +# # Send all metadata/data captured to the BestEffortCallback. +# RE.subscribe(bec) +# # RE.subscribe(dummy) -# RE(grid_scan(dets, motor1, -10, 10, 10, motor2, -10, 10, 10)) +# # RE(grid_scan(dets, motor1, -10, 10, 10, motor2, -10, 10, 10)) From 6b0b8de8c0a869577cd233814bad48b9e1a806c4 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 7 Nov 2023 18:00:07 +0100 Subject: [PATCH 18/23] test: fixed tests for eigerl; closes #11 --- tests/test_eiger9m_csaxs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_eiger9m_csaxs.py b/tests/test_eiger9m_csaxs.py index 447feef..c98e202 100644 --- a/tests/test_eiger9m_csaxs.py +++ b/tests/test_eiger9m_csaxs.py @@ -15,9 +15,7 @@ def patch_dual_pvs(device): continue if not hasattr(walk.item, "_write_pv"): continue - if walk.item._read_pv.pvname.endswith("_RBV") and isinstance( - walk.item, ophyd.areadetector.base.EpicsSignalWithRBV - ): + if walk.item._read_pv.pvname.endswith("_RBV"): walk.item._read_pv = walk.item._write_pv From 188c8321cab0be96a2b5e435b6efbc4c39c9872d Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 7 Nov 2023 19:04:47 +0100 Subject: [PATCH 19/23] test: fixed pilatus tests; closes #10 --- ophyd_devices/epics/devices/pilatus_csaxs.py | 7 +- tests/test_pilatus_csaxs.py | 286 +++---------------- 2 files changed, 40 insertions(+), 253 deletions(-) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index 2aa2634..fe9a667 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -190,6 +190,7 @@ class PilatuscSAXS(DetectorBase): def _init_detector(self) -> None: """Initialize the detector""" # TODO add check if detector is running + self._stop_det() self._set_trigger(TriggerSource.EXT_ENABLE) def _init_filewriter(self) -> None: @@ -212,14 +213,14 @@ class PilatuscSAXS(DetectorBase): setpoint = int(self.mokev * factor) 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) + self.cam.threshold_energy.put(setpoint / 2) def _set_acquisition_params(self) -> None: """set acquisition parameters on the detector""" # self.cam.acquire_time.set(self.exp_time) # self.cam.acquire_period.set(self.exp_time + self.readout) - self.cam.num_images.set(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) - self.cam.num_frames.set(1) + self.cam.num_images.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) + self.cam.num_frames.put(1) self._update_readout_time() def _set_trigger(self, trigger_source: int) -> None: diff --git a/tests/test_pilatus_csaxs.py b/tests/test_pilatus_csaxs.py index f20c5fa..dfedb85 100644 --- a/tests/test_pilatus_csaxs.py +++ b/tests/test_pilatus_csaxs.py @@ -2,95 +2,22 @@ import os import pytest from unittest import mock -from ophyd.signal import Signal -from ophyd import Staged +import ophyd from bec_lib.core import BECMessage, MessageEndpoints -from bec_lib.core.devicemanager import DeviceContainer -from bec_lib.core.tests.utils import ProducerMock -import requests +from ophyd_devices.epics.devices.pilatus_csaxs import PilatuscSAXS + +from tests.utils import DMMock, MockPV -class MockSignal(Signal): - def __init__(self, read_pv, *, string=False, name=None, parent=None, **kwargs): - self.read_pv = read_pv - self._string = bool(string) - super().__init__(name=name, parent=parent, **kwargs) - self._waited_for_connection = False - self._subscriptions = [] - - def wait_for_connection(self): - self._waited_for_connection = True - - def subscribe(self, method, event_type, **kw): - self._subscriptions.append((method, event_type, kw)) - - def describe_configuration(self): - return {self.name + "_conf": {"source": "SIM:test"}} - - def read_configuration(self): - return {self.name + "_conf": {"value": 0}} - - -with mock.patch("ophyd.EpicsSignal", new=MockSignal), mock.patch( - "ophyd.EpicsSignalRO", new=MockSignal -), mock.patch("ophyd.EpicsSignalWithRBV", new=MockSignal): - from ophyd_devices.epics.devices.pilatus_csaxs import PilatuscSAXS - - -# TODO maybe specify here that this DeviceMock is for usage in the DeviceServer -class DeviceMock: - def __init__(self, name: str, value: float = 0.0): - self.name = name - self.read_buffer = value - self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None} - self._enabled_set = True - self._enabled = True - - def read(self): - return {self.name: {"value": self.read_buffer}} - - def readback(self): - return self.read_buffer - - @property - def enabled_set(self) -> bool: - return self._enabled_set - - @enabled_set.setter - def enabled_set(self, val: bool): - self._enabled_set = val - - @property - def enabled(self) -> bool: - return self._enabled - - @enabled.setter - def enabled(self, val: bool): - self._enabled = val - - @property - def user_parameter(self): - return self._config["userParameter"] - - @property - def obj(self): - return self - - -class DMMock: - """Mock for DeviceManager - - The mocked DeviceManager creates a device containert and a producer. - - """ - - def __init__(self): - self.devices = DeviceContainer() - self.producer = ProducerMock() - - def add_device(self, name: str, value: float = 0.0): - self.devices[name] = DeviceMock(name, value) +def patch_dual_pvs(device): + for walk in device.walk_signals(): + if not hasattr(walk.item, "_read_pv"): + continue + if not hasattr(walk.item, "_write_pv"): + continue + if walk.item._read_pv.pvname.endswith("_RBV"): + walk.item._read_pv = walk.item._write_pv @pytest.fixture(scope="function") @@ -99,73 +26,37 @@ def mock_det(): prefix = "X12SA-ES-PILATUS300K:" sim_mode = False dm = DMMock() - # dm.add_device("mokev", value=12.4) with mock.patch.object(dm, "producer"): - with mock.patch.object( - PilatuscSAXS, "_update_service_config" - ) as mock_update_service_config, mock.patch( + with mock.patch( "ophyd_devices.epics.devices.pilatus_csaxs.FileWriterMixin" - ) as filemixin: - with mock.patch.object(PilatuscSAXS, "_init"): - yield PilatuscSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) + ) as filemixin, mock.patch( + "ophyd_devices.epics.devices.pilatus_csaxs.PilatuscSAXS._update_service_config" + ) as mock_service_config: + with mock.patch.object(ophyd, "cl") as mock_cl: + mock_cl.get_pv = MockPV + with mock.patch.object(PilatuscSAXS, "_init"): + det = PilatuscSAXS( + name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode + ) + patch_dual_pvs(det) + yield det @pytest.mark.parametrize( - "trigger_source, sim_mode, scan_status_msg, expected_exception", + "trigger_source, detector_state", [ - ( - 1, - True, - BECMessage.ScanStatusMessage( - scanID="1", - status={}, - info={ - "RID": "mockrid1111", - "queueID": "mockqueueID111", - "scan_number": 1, - "exp_time": 0.012, - "num_points": 500, - "readout_time": 0.003, - "scan_type": "fly", - "num_lines": 0.012, - "frames_per_trigger": 1, - }, - ), - True, - ), - ( - 1, - False, - BECMessage.ScanStatusMessage( - scanID="1", - status={}, - info={ - "RID": "mockrid1111", - "queueID": "mockqueueID111", - "scan_number": 1, - "exp_time": 0.012, - "num_points": 500, - "readout_time": 0.003, - "scan_type": "fly", - "num_lines": 0.012, - "frames_per_trigger": 1, - }, - ), - False, - ), + (1, 0), ], ) # TODO rewrite this one, write test for init_detector, init_filewriter is tested -def test_init( +def test_init_detector( + mock_det, trigger_source, - sim_mode, - scan_status_msg, - expected_exception, + detector_state, ): """Test the _init function: This includes testing the functions: - - _set_default_parameter - _init_detector - _stop_det - _set_trigger @@ -174,34 +65,9 @@ def test_init( Validation upon setting the correct PVs """ - name = "pilatus" - prefix = "X12SA-ES-PILATUS300K:" - sim_mode = sim_mode - dm = DMMock() - with mock.patch.object(dm, "producer") as producer, mock.patch.object( - PilatuscSAXS, "_init_filewriter" - ) as mock_init_fw, mock.patch.object( - PilatuscSAXS, "_update_scaninfo" - ) as mock_update_scaninfo, mock.patch.object( - PilatuscSAXS, "_update_filewriter" - ) as mock_update_filewriter, mock.patch.object( - PilatuscSAXS, "_update_service_config" - ) as mock_update_service_config: - mock_det = PilatuscSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode) - if expected_exception: - with pytest.raises(Exception): - mock_det._init() - mock_init_fw.assert_called_once() - else: - mock_det._init() # call the method you want to test - assert mock_det.cam.acquire.get() == 0 - assert mock_det.cam.trigger_mode.get() == trigger_source - mock_init_fw.assert_called() - mock_update_scaninfo.assert_called_once() - mock_update_filewriter.assert_called_once() - mock_update_service_config.assert_called_once() - - assert mock_init_fw.call_count == 2 + mock_det._init_detector() # call the method you want to test + assert mock_det.cam.acquire.get() == detector_state + assert mock_det.cam.trigger_mode.get() == trigger_source @pytest.mark.parametrize( @@ -239,7 +105,7 @@ def test_stage( stopped, expected_exception, ): - with mock.patch.object(PilatuscSAXS, "_publish_file_location") as mock_publish_file_location: + with mock.patch.object(mock_det, "_publish_file_location") as mock_publish_file_location: mock_det.scaninfo.num_points = scaninfo["num_points"] mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] @@ -542,78 +408,6 @@ def test_prep_file_writer(mock_det, scaninfo, data_msgs, urls, requests_state, e assert call == mock_call -# @pytest.mark.parametrize( -# "scaninfo, daq_status, expected_exception", -# [ -# ( -# { -# "eacc": "e12345", -# "num_points": 500, -# "frames_per_trigger": 1, -# "filepath": "test.h5", -# "scanID": "123", -# }, -# {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, -# False, -# ), -# ( -# { -# "eacc": "e12345", -# "num_points": 500, -# "frames_per_trigger": 1, -# "filepath": "test.h5", -# "scanID": "123", -# }, -# {"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}}, -# False, -# ), -# ( -# { -# "eacc": "e12345", -# "num_points": 500, -# "frames_per_trigger": 1, -# "filepath": "test.h5", -# "scanID": "123", -# }, -# {"state": "BUSY", "acquisition": {"state": "ERROR"}}, -# True, -# ), -# ], -# ) -# def test_prep_file_writer(mock_det, scaninfo, daq_status, expected_exception): -# with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object( -# mock_det, "_filepath_exists" -# ) as mock_file_path_exists, mock.patch.object( -# mock_det, "_stop_file_writer" -# ) as mock_stop_file_writer, mock.patch.object( -# mock_det, "scaninfo" -# ) as mock_scaninfo: -# # mock_det = eiger_factory(name, prefix, sim_mode) -# mock_det.std_client = mock_std_daq -# mock_std_daq.start_writer_async.return_value = None -# mock_std_daq.get_status.return_value = daq_status -# 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"] - -# if expected_exception: -# with pytest.raises(Exception): -# mock_det._prep_file_writer() -# mock_file_path_exists.assert_called_once() -# assert mock_stop_file_writer.call_count == 2 - -# else: -# mock_det._prep_file_writer() -# mock_file_path_exists.assert_called_once() -# mock_stop_file_writer.assert_called_once() - -# daq_writer_call = { -# "output_file": scaninfo["filepath"], -# "n_images": int(scaninfo["num_points"] * scaninfo["frames_per_trigger"]), -# } -# mock_std_daq.start_writer_async.assert_called_with(daq_writer_call) - - @pytest.mark.parametrize( "stopped, expected_exception", [ @@ -649,14 +443,6 @@ def test_unstage( assert mock_det._stopped == False -# def test_stop_fw(mock_det): -# with mock.patch.object(mock_det, "std_client") as mock_std_daq: -# mock_std_daq.stop_writer.return_value = None -# mock_det.std_client = mock_std_daq -# mock_det._stop_file_writer() -# mock_std_daq.stop_writer.assert_called_once() - - 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" @@ -675,17 +461,17 @@ def test_stop(mock_det): [ ( False, - Staged.no, + ophyd.Staged.no, False, ), ( True, - Staged.no, + ophyd.Staged.no, False, ), ( False, - Staged.yes, + ophyd.Staged.yes, True, ), ], From 8a19ce1508d7d00d4999aa89b5bb0537f973bc54 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 7 Nov 2023 19:49:05 +0100 Subject: [PATCH 20/23] refactor: rename infomsgmock and add docstrings --- .../epics/devices/bec_scaninfo_mixin.py | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index 188e30f..bff1083 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -6,7 +6,12 @@ from bec_lib.core import bec_logger logger = bec_logger.logger -class Bec_Info_Msg_Mock: +class BECInfoMsgMock: + """Mock BECInfoMsg class + + This class is used for mocking BECInfoMsg for testing purposes + """ + def __init__( self, mockrid: str = "mockrid1111", @@ -46,6 +51,16 @@ class Bec_Info_Msg_Mock: class BecScaninfoMixin: + """BecScaninfoMixin class + + Args: + device_manager (DeviceManagerBase): DeviceManagerBase object + sim_mode (bool): Simulation mode flag + bec_info_msg (dict): BECInfoMsg object + Returns: + BecScaninfoMixin: BecScaninfoMixin object + """ + def __init__( self, device_manager: DeviceManagerBase = None, sim_mode: bool = False, bec_info_msg=None ) -> None: @@ -54,30 +69,25 @@ class BecScaninfoMixin: self.scan_msg = None self.scanID = None if bec_info_msg is None: - bec_info_msg_mock = Bec_Info_Msg_Mock() - self.bec_info_msg = bec_info_msg_mock.get_bec_info_msg() + BECInfoMsgMock = BECInfoMsgMock() + self.bec_info_msg = BECInfoMsgMock.get_bec_info_msg() else: self.bec_info_msg = bec_info_msg - # self.bec_info_msg = { - # "RID": "mockrid", - # "queueID": "mockqueuid", - # "scan_number": 1, - # "exp_time": 12e-3, - # "num_points": 500, - # "readout_time": 3e-3, - # "scan_type": "fly", - # "num_lines": 1, - # "frames_per_trigger": 1, - # } - def get_bec_info_msg(self) -> None: + """Get BECInfoMsg object""" return self.bec_info_msg def change_config(self, bec_info_msg: dict) -> None: + """Change BECInfoMsg object""" self.bec_info_msg = bec_info_msg def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: + """Get current scan message + + Returns: + BECMessage.ScanStatusMessage: BECMessage.ScanStatusMessage object + """ if not self.sim_mode: # TODO what if no scan info is there yet! msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) @@ -90,11 +100,16 @@ class BecScaninfoMixin: ) def get_username(self) -> str: + """Get username""" if not self.sim_mode: return self.device_manager.producer.get(MessageEndpoints.account()).decode() return os.getlogin() def load_scan_metadata(self) -> None: + """Load scan metadata + + This function loads scan metadata from the current scan message + """ self.scan_msg = scan_msg = self._get_current_scan_msg() logger.info(f"{self.scan_msg}") try: From bda859e93d171602e8fa7de1d88d8f2bfe22230f Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 7 Nov 2023 19:55:43 +0100 Subject: [PATCH 21/23] refactor: addressed comments from review; fixed docstring; add DeviceClassInitError --- ophyd_devices/epics/devices/eiger9m_csaxs.py | 9 ++++++++- ophyd_devices/epics/devices/pilatus_csaxs.py | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/ophyd_devices/epics/devices/eiger9m_csaxs.py b/ophyd_devices/epics/devices/eiger9m_csaxs.py index ae03f64..812486d 100644 --- a/ophyd_devices/epics/devices/eiger9m_csaxs.py +++ b/ophyd_devices/epics/devices/eiger9m_csaxs.py @@ -38,6 +38,13 @@ class EigerTimeoutError(EigerError): pass +class DeviceClassInitError(EigerError): + """Raised when initiation of the device class fails, + due to missing device manager or not started in sim_mode.""" + + pass + + class SLSDetectorCam(Device): """SLS Detector Camera - Eiger 9M @@ -134,7 +141,7 @@ class Eiger9McSAXS(DetectorBase): **kwargs, ) if device_manager is None and not sim_mode: - raise Exception( + raise DeviceClassInitError( 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 diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index fe9a667..228e1d9 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -37,6 +37,13 @@ class PilatusTimeoutError(PilatusError): pass +class DeviceClassInitError(PilatusError): + """Raised when initiation of the device class fails, + due to missing device manager or not started in sim_mode.""" + + pass + + class TriggerSource(enum.IntEnum): INTERNAL = 0 EXT_ENABLE = 1 @@ -127,7 +134,7 @@ class PilatuscSAXS(DetectorBase): **kwargs, ) if device_manager is None and not sim_mode: - raise Exception( + raise DeviceClassInitError( 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 @@ -416,7 +423,11 @@ class PilatuscSAXS(DetectorBase): # TODO might be useful for base class def pre_scan(self) -> None: - """ " Pre_scan gets executed right before""" + """Pre_scan is an (optional) function that is executed by BEC just before the scan core + + For the pilatus detector, it is used to arm the detector for the acquisition, + because the detector times out after ˜7-8seconds without seeing a trigger. + """ self._arm_acquisition() def _arm_acquisition(self) -> None: From cf4f195365aa3aa4bf0083a146cb1154b6feed58 Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 7 Nov 2023 20:05:20 +0100 Subject: [PATCH 22/23] test: fixed tests --- ophyd_devices/epics/devices/bec_scaninfo_mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py index bff1083..2f39ad7 100644 --- a/ophyd_devices/epics/devices/bec_scaninfo_mixin.py +++ b/ophyd_devices/epics/devices/bec_scaninfo_mixin.py @@ -69,8 +69,8 @@ class BecScaninfoMixin: self.scan_msg = None self.scanID = None if bec_info_msg is None: - BECInfoMsgMock = BECInfoMsgMock() - self.bec_info_msg = BECInfoMsgMock.get_bec_info_msg() + infomsgmock = BECInfoMsgMock() + self.bec_info_msg = infomsgmock.get_bec_info_msg() else: self.bec_info_msg = bec_info_msg From f32fdbc845b445f1254207d7a2c1780ff6f958f1 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 8 Nov 2023 11:40:56 +0100 Subject: [PATCH 23/23] test: resolved problem after merge conflict --- ophyd_devices/epics/devices/pilatus_csaxs.py | 29 -------------------- 1 file changed, 29 deletions(-) diff --git a/ophyd_devices/epics/devices/pilatus_csaxs.py b/ophyd_devices/epics/devices/pilatus_csaxs.py index f0f5d12..228e1d9 100644 --- a/ophyd_devices/epics/devices/pilatus_csaxs.py +++ b/ophyd_devices/epics/devices/pilatus_csaxs.py @@ -470,31 +470,6 @@ class PilatuscSAXS(DetectorBase): """Specify action that should be taken upon trigger signal.""" pass - 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 device. @@ -567,10 +542,6 @@ class PilatuscSAXS(DetectorBase): """Stop the detector""" self.cam.acquire.put(0) - 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._stop_det()