Merge branch 'eiger_refactor' into 'master'

refactoring and tests for eiger and pilatus detectors

Closes #11 and #10

See merge request bec/ophyd_devices!36
This commit is contained in:
appel_c 2023-11-08 19:21:38 +01:00
commit 47d3a5a65b
9 changed files with 1726 additions and 228 deletions

View File

@ -25,7 +25,7 @@ from ophyd.quadem import QuadEM
# cSAXS # cSAXS
from .epics_motor_ex import EpicsMotorEx from .epics_motor_ex import EpicsMotorEx
from .mcs_csaxs import McsCsaxs from .mcs_csaxs import McsCsaxs
from .eiger9m_csaxs import Eiger9mCsaxs from .eiger9m_csaxs import Eiger9McSAXS
from .pilatus_csaxs import PilatusCsaxs from .pilatus_csaxs import PilatuscSAXS
from .falcon_csaxs import FalconCsaxs from .falcon_csaxs import FalconCsaxs
from .DelayGeneratorDG645 import DelayGeneratorDG645 from .DelayGeneratorDG645 import DelayGeneratorDG645

View File

@ -6,31 +6,88 @@ from bec_lib.core import bec_logger
logger = bec_logger.logger logger = bec_logger.logger
class BECInfoMsgMock:
"""Mock BECInfoMsg class
This class is used for mocking BECInfoMsg for testing purposes
"""
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: class BecScaninfoMixin:
def __init__(self, device_manager: DeviceManagerBase = None, sim_mode=False) -> None: """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:
self.device_manager = device_manager self.device_manager = device_manager
self.sim_mode = sim_mode self.sim_mode = sim_mode
self.scan_msg = None self.scan_msg = None
self.scanID = None self.scanID = None
self.bec_info_msg = { if bec_info_msg is None:
"RID": "mockrid", infomsgmock = BECInfoMsgMock()
"queueID": "mockqueuid", self.bec_info_msg = infomsgmock.get_bec_info_msg()
"scan_number": 1, else:
"exp_time": 12e-3, self.bec_info_msg = bec_info_msg
"num_points": 500,
"readout_time": 3e-3,
"scan_type": "fly",
"num_lines": 1,
"frames_per_trigger": 1,
}
def get_bec_info_msg(self) -> None: def get_bec_info_msg(self) -> None:
"""Get BECInfoMsg object"""
return self.bec_info_msg return self.bec_info_msg
def change_config(self, bec_info_msg: dict) -> None: def change_config(self, bec_info_msg: dict) -> None:
"""Change BECInfoMsg object"""
self.bec_info_msg = bec_info_msg self.bec_info_msg = bec_info_msg
def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage: def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage:
"""Get current scan message
Returns:
BECMessage.ScanStatusMessage: BECMessage.ScanStatusMessage object
"""
if not self.sim_mode: if not self.sim_mode:
# TODO what if no scan info is there yet! # TODO what if no scan info is there yet!
msg = self.device_manager.producer.get(MessageEndpoints.scan_status()) msg = self.device_manager.producer.get(MessageEndpoints.scan_status())
@ -43,11 +100,16 @@ class BecScaninfoMixin:
) )
def get_username(self) -> str: def get_username(self) -> str:
"""Get username"""
if not self.sim_mode: if not self.sim_mode:
return self.device_manager.producer.get(MessageEndpoints.account()).decode() return self.device_manager.producer.get(MessageEndpoints.account()).decode()
return os.getlogin() return os.getlogin()
def load_scan_metadata(self) -> None: 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() self.scan_msg = scan_msg = self._get_current_scan_msg()
logger.info(f"{self.scan_msg}") logger.info(f"{self.scan_msg}")
try: try:

View File

@ -1,6 +1,6 @@
import enum import enum
import threading
import time import time
import threading
from bec_lib.core.devicemanager import DeviceStatus from bec_lib.core.devicemanager import DeviceStatus
import numpy as np import numpy as np
import os import os
@ -16,12 +16,15 @@ from std_daq_client import StdDaqClient
from bec_lib.core import BECMessage, MessageEndpoints, threadlocked from bec_lib.core import BECMessage, MessageEndpoints, threadlocked
from bec_lib.core.file_utils import FileWriterMixin from bec_lib.core.file_utils import FileWriterMixin
from bec_lib.core import bec_logger 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.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
from ophyd_devices.utils import bec_utils from ophyd_devices.utils import bec_utils
logger = bec_logger.logger logger = bec_logger.logger
EIGER9M_MIN_READOUT = 3e-3
class EigerError(Exception): class EigerError(Exception):
"""Base class for exceptions in this module.""" """Base class for exceptions in this module."""
@ -29,13 +32,20 @@ class EigerError(Exception):
pass pass
class EigerTimeoutError(Exception): class EigerTimeoutError(EigerError):
"""Raised when the Eiger does not respond in time during unstage.""" """Raised when the Eiger does not respond in time during unstage."""
pass pass
class SlsDetectorCam(Device): 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 """SLS Detector Camera - Eiger 9M
Base class to map EPICS PVs to ophyd signals. Base class to map EPICS PVs to ophyd signals.
@ -52,7 +62,7 @@ class SlsDetectorCam(Device):
detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV") detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV")
class TriggerSource(int, enum.Enum): class TriggerSource(enum.IntEnum):
"""Trigger signals for Eiger9M detector""" """Trigger signals for Eiger9M detector"""
AUTO = 0 AUTO = 0
@ -61,7 +71,7 @@ class TriggerSource(int, enum.Enum):
BURST_TRIGGER = 3 BURST_TRIGGER = 3
class DetectorState(int, enum.Enum): class DetectorState(enum.IntEnum):
"""Detector states for Eiger9M detector""" """Detector states for Eiger9M detector"""
IDLE = 0 IDLE = 0
@ -77,7 +87,7 @@ class DetectorState(int, enum.Enum):
ABORTED = 10 ABORTED = 10
class Eiger9mCsaxs(DetectorBase): class Eiger9McSAXS(DetectorBase):
"""Eiger 9M detector for CSAXS """Eiger 9M detector for CSAXS
Parent class: DetectorBase Parent class: DetectorBase
@ -94,7 +104,7 @@ class Eiger9mCsaxs(DetectorBase):
"describe", "describe",
] ]
cam = ADCpt(SlsDetectorCam, "cam1:") cam = ADCpt(SLSDetectorCam, "cam1:")
def __init__( def __init__(
self, self,
@ -131,34 +141,50 @@ class Eiger9mCsaxs(DetectorBase):
**kwargs, **kwargs,
) )
if device_manager is None and not sim_mode: if device_manager is None and not sim_mode:
raise EigerError("Add DeviceManager to initialization or init with sim_mode=True") 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"
# Not sure if this is needed, comment it for now! )
# self._lock = threading.RLock() self.sim_mode = sim_mode
# TODO check if threadlock is needed for unstage
self._lock = threading.RLock()
self._stopped = False self._stopped = False
self.name = name self.name = name
self.wait_for_connection() self.service_cfg = None
# Spin up connections for simulation or BEC mode self.std_client = None
# TODO check if sim_mode still works. Is it needed? I believe filewriting might be handled properly 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"
)
self.wait_for_connection(all_signals=True)
if not sim_mode: if not sim_mode:
from bec_lib.core.bec_service import SERVICE_CONFIG self._update_service_config()
self.device_manager = device_manager self.device_manager = device_manager
self._producer = self.device_manager.producer
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
else: else:
base_path = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" self.device_manager = bec_utils.DMMock()
self._producer = bec_utils.MockProducer() base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/"
self.device_manager = bec_utils.MockDeviceManager() self.service_cfg = {"base_path": os.path.expanduser(base_path)}
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self._producer = self.device_manager.producer
self.scaninfo.load_scan_metadata() self._update_scaninfo()
self.service_cfg = {"base_path": base_path} self._update_filewriter()
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
self.scaninfo.load_scan_metadata()
self.filewriter = FileWriterMixin(self.service_cfg)
self._init() 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"]
# TODO function for abstract class? # TODO function for abstract class?
def _init(self) -> None: def _init(self) -> None:
"""Initialize detector, filewriter and set default parameters""" """Initialize detector, filewriter and set default parameters"""
@ -166,12 +192,19 @@ class Eiger9mCsaxs(DetectorBase):
self._init_detector() self._init_detector()
self._init_filewriter() self._init_filewriter()
# TODO function for abstract class?
def _default_parameter(self) -> None: def _default_parameter(self) -> None:
"""Set default parameters for Eiger 9M """Set default parameters for Pilatus300k detector
readout (float): readout time in seconds 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? # TODO function for abstract class?
def _init_detector(self) -> None: def _init_detector(self) -> None:
@ -188,20 +221,19 @@ class Eiger9mCsaxs(DetectorBase):
For the Eiger9M, the data backend is std_daq client. 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. 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 = StdDaqClient(url_base=self.std_rest_server_url)
self.std_client.stop_writer() self.std_client.stop_writer()
timeout = 0 timeout = 0
# TODO changing e-account was not possible during beamtimes. # TODO put back change of e-account! and check with Leo which status to wait for
# self._update_std_cfg("writer_user_id", int(self.scaninfo.username.strip(" e"))) eacc = self.scaninfo.username
# time.sleep(5) self._update_std_cfg("writer_user_id", int(eacc.strip(" e")))
# TODO is this the only state to wait for or should we wait for more from the std_daq client? time.sleep(5)
while not self.std_client.get_status()["state"] == "READY": while not self.std_client.get_status()["state"] == "READY":
time.sleep(0.1) time.sleep(0.1)
timeout = timeout + 0.1 timeout = timeout + 0.1
logger.info("Waiting for std_daq init.") logger.info("Waiting for std_daq init.")
if timeout > 5: if timeout > 5:
if not self.std_client.get_status()["state"]: if not self.std_client.get_status()["state"] == "READY":
raise EigerError( raise EigerError(
f"Std client not in READY state, returns: {self.std_client.get_status()}" f"Std client not in READY state, returns: {self.std_client.get_status()}"
) )
@ -252,26 +284,32 @@ class Eiger9mCsaxs(DetectorBase):
self._prep_file_writer() self._prep_file_writer()
self._prep_det() self._prep_det()
state = False state = False
self._publish_file_location(done=state, successful=state) self._publish_file_location(done=state)
self._arm_acquisition() self._arm_acquisition()
# TODO Fix should take place in EPICS or directly on the hardware! # TODO Fix should take place in EPICS or directly on the hardware!
# We observed that the detector missed triggers in the beginning in case BEC was to fast. Adding 50ms delay solved this # 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) time.sleep(0.05)
return super().stage() 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? # TODO function for abstract class?
def _prep_file_writer(self) -> None: def _prep_file_writer(self) -> None:
"""Prepare file writer for scan """Prepare file writer for scan
self.filewriter is a FileWriterMixin object that hosts logic for compiling the filepath self.filewriter is a FileWriterMixin object that hosts logic for compiling the filepath
""" """
timer = 0
self.filepath = self.filewriter.compile_full_filename( self.filepath = self.filewriter.compile_full_filename(
self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True
) )
# TODO needed, should be checked from the filerwriter mixin right? self._filepath_exists(self.filepath)
while not os.path.exists(os.path.dirname(self.filepath)):
time.sleep(0.1)
self._stop_file_writer() self._stop_file_writer()
logger.info(f" std_daq output filepath {self.filepath}") logger.info(f" std_daq output filepath {self.filepath}")
# TODO Discuss with Leo if this is needed, or how to start the async writing best # TODO Discuss with Leo if this is needed, or how to start the async writing best
@ -288,16 +326,22 @@ class Eiger9mCsaxs(DetectorBase):
raise EigerError(f"Timeout of start_writer_async with {exc}") raise EigerError(f"Timeout of start_writer_async with {exc}")
while True: while True:
timer = timer + 0.01
det_ctrl = self.std_client.get_status()["acquisition"]["state"] det_ctrl = self.std_client.get_status()["acquisition"]["state"]
if det_ctrl == "WAITING_IMAGES": if det_ctrl == "WAITING_IMAGES":
break break
time.sleep(0.005) time.sleep(0.01)
if timer > 5:
self._close_file_writer()
raise EigerError(
f"Timeout of 5s reached for std_daq start_writer_async with std_daq client status {det_ctrl}"
)
# TODO function for abstract class? # TODO function for abstract class?
def _stop_file_writer(self) -> None: def _stop_file_writer(self) -> None:
"""Close file writer""" """Close file writer"""
self.std_client.stop_writer() self.std_client.stop_writer()
# TODO can I wait for a status message here maybe? To ensure writer returned # TODO can I wait for a status message here maybe? To ensure writer stopped and returned
# TODO function for abstract class? # TODO function for abstract class?
def _prep_det(self) -> None: def _prep_det(self) -> None:
@ -312,7 +356,8 @@ class Eiger9mCsaxs(DetectorBase):
"""Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance""" """Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance"""
# threshold energy might be in eV or keV # threshold energy might be in eV or keV
factor = 1 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 factor = 1000
setpoint = int(self.mokev * factor) setpoint = int(self.mokev * factor)
energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"] energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"]
@ -326,40 +371,60 @@ class Eiger9mCsaxs(DetectorBase):
"""Set acquisition parameters for the detector""" """Set acquisition parameters for the detector"""
self.cam.num_images.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)) self.cam.num_images.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
self.cam.num_frames.put(1) self.cam.num_frames.put(1)
self._update_readout_time()
# TODO function for abstract class? + call it for each scan?? # TODO function for abstract class? + call it for each scan??
def _set_trigger(self, trigger_source: TriggerSource) -> None: def _set_trigger(self, trigger_source: TriggerSource) -> None:
"""Set trigger source for the detector. """Set trigger source for the detector.
Check the TriggerSource enum for possible values Check the TriggerSource enum for possible values
Args:
trigger_source (TriggerSource): Trigger source for the detector
""" """
value = int(trigger_source) value = trigger_source
self.cam.trigger_mode.put(value) self.cam.trigger_mode.put(value)
def _publish_file_location(self, done=False, successful=False) -> None: def _publish_file_location(self, done: bool = False, successful: bool = None) -> None:
"""Publish the filepath to REDIS """Publish the filepath to REDIS.
First msg for file writer and the second one for other listeners (e.g. radial integ) 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() 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) msg = BECMessage.FileMessage(file_path=self.filepath, done=done, successful=successful)
self._producer.set_and_publish( self._producer.set_and_publish(
MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe
) )
self._producer.set_and_publish( 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() pipe.execute()
# TODO function for abstract class? # TODO function for abstract class?
def _arm_acquisition(self) -> None: def _arm_acquisition(self) -> None:
"""Arm Eiger detector for acquisition""" """Arm Eiger detector for acquisition"""
timer = 0
self.cam.acquire.put(1) self.cam.acquire.put(1)
while True: while True:
det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] 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 break
if self._stopped == True: if self._stopped == True:
break break
time.sleep(0.005) time.sleep(0.01)
timer += 0.01
if timer > 5:
self.stop()
raise EigerTimeoutError("Failed to arm the acquisition. IOC did not update.")
# TODO function for abstract class? # TODO function for abstract class?
def trigger(self) -> DeviceStatus: def trigger(self) -> DeviceStatus:
@ -419,15 +484,13 @@ class Eiger9mCsaxs(DetectorBase):
# Check status with timeout, break out if _stopped=True # Check status with timeout, break out if _stopped=True
while True: while True:
det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"] det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"]
std_ctrl = self.std_client.get_status()["acquisition"]["state"]
status = self.std_client.get_status() status = self.std_client.get_status()
std_ctrl = status["acquisition"]["state"]
received_frames = status["acquisition"]["stats"]["n_write_completed"] received_frames = status["acquisition"]["stats"]["n_write_completed"]
total_frames = int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger) 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: if det_ctrl == 0 and std_ctrl == "FINISHED" and total_frames == received_frames:
break break
if self._stopped == True: if self._stopped == True:
self._stop_det()
self._stop_file_writer()
break break
time.sleep(sleep_time) time.sleep(sleep_time)
timer += sleep_time timer += sleep_time
@ -452,7 +515,7 @@ class Eiger9mCsaxs(DetectorBase):
# Check status # Check status
while True: while True:
det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"] 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 break
if self._stopped == True: if self._stopped == True:
break break
@ -474,4 +537,4 @@ class Eiger9mCsaxs(DetectorBase):
if __name__ == "__main__": 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)

View File

@ -14,6 +14,7 @@ from ophyd import ADComponent as ADCpt
from bec_lib.core import BECMessage, MessageEndpoints from bec_lib.core import BECMessage, MessageEndpoints
from bec_lib.core.file_utils import FileWriterMixin 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 bec_lib.core import bec_logger
from ophyd_devices.utils import bec_utils as bec_utils from ophyd_devices.utils import bec_utils as bec_utils
@ -21,6 +22,8 @@ from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
logger = bec_logger.logger logger = bec_logger.logger
PILATUS_MIN_READOUT = 3e-3
class PilatusError(Exception): class PilatusError(Exception):
"""Base class for exceptions in this module.""" """Base class for exceptions in this module."""
@ -28,13 +31,20 @@ class PilatusError(Exception):
pass pass
class PilatusTimeoutError(Exception): class PilatusTimeoutError(PilatusError):
"""Raised when the Pilatus does not respond in time during unstage.""" """Raised when the Pilatus does not respond in time during unstage."""
pass pass
class TriggerSource(int, enum.Enum): 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 INTERNAL = 0
EXT_ENABLE = 1 EXT_ENABLE = 1
EXT_TRIGGER = 2 EXT_TRIGGER = 2
@ -42,14 +52,14 @@ class TriggerSource(int, enum.Enum):
ALGINMENT = 4 ALGINMENT = 4
class SlsDetectorCam(Device): class SLSDetectorCam(Device):
"""SLS Detector Camera - Pilatus """SLS Detector Camera - Pilatus
Base class to map EPICS PVs to ophyd signals. Base class to map EPICS PVs to ophyd signals.
""" """
num_images = ADCpt(EpicsSignalWithRBV, "NumImages") num_images = ADCpt(EpicsSignalWithRBV, "NumImages")
num_exposures = ADCpt(EpicsSignalWithRBV, "NumExposures") num_frames = ADCpt(EpicsSignalWithRBV, "NumExposures")
delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures") delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures")
trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode") trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode")
acquire = ADCpt(EpicsSignal, "Acquire") acquire = ADCpt(EpicsSignal, "Acquire")
@ -70,7 +80,7 @@ class SlsDetectorCam(Device):
gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill") gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill")
class PilatusCsaxs(DetectorBase): class PilatuscSAXS(DetectorBase):
"""Pilatus_2 300k detector for CSAXS """Pilatus_2 300k detector for CSAXS
Parent class: DetectorBase Parent class: DetectorBase
@ -87,7 +97,7 @@ class PilatusCsaxs(DetectorBase):
"describe", "describe",
] ]
cam = ADCpt(SlsDetectorCam, "cam1:") cam = ADCpt(SLSDetectorCam, "cam1:")
def __init__( def __init__(
self, self,
@ -124,30 +134,46 @@ class PilatusCsaxs(DetectorBase):
**kwargs, **kwargs,
) )
if device_manager is None and not sim_mode: if device_manager is None and not sim_mode:
raise PilatusError("Add DeviceManager to initialization or init with sim_mode=True") 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
self._stopped = False
self.name = name self.name = name
self.wait_for_connection() self.service_cfg = None
# Spin up connections for simulation or BEC mode 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: if not sim_mode:
from bec_lib.core.bec_service import SERVICE_CONFIG self._update_service_config()
self.device_manager = device_manager self.device_manager = device_manager
self._producer = self.device_manager.producer
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
else: else:
base_path = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/" self.device_manager = bec_utils.DMMock()
self._producer = bec_utils.MockProducer() base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/"
self.device_manager = bec_utils.MockDeviceManager() self.service_cfg = {"base_path": os.path.expanduser(base_path)}
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode) self._producer = self.device_manager.producer
self.scaninfo.load_scan_metadata() self._update_scaninfo()
self.service_cfg = {"base_path": base_path} self._update_filewriter()
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
self.scaninfo.load_scan_metadata()
self.filewriter = FileWriterMixin(self.service_cfg)
self._init() 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: def _init(self) -> None:
"""Initialize detector, filewriter and set default parameters""" """Initialize detector, filewriter and set default parameters"""
self._default_parameter() self._default_parameter()
@ -158,12 +184,21 @@ class PilatusCsaxs(DetectorBase):
"""Set default parameters for Pilatus300k detector """Set default parameters for Pilatus300k detector
readout (float): readout time in seconds 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: def _init_detector(self) -> None:
"""Initialize the detector""" """Initialize the detector"""
# TODO add check if detector is running # TODO add check if detector is running
pass self._stop_det()
self._set_trigger(TriggerSource.EXT_ENABLE)
def _init_filewriter(self) -> None: def _init_filewriter(self) -> None:
"""Initialize the file writer""" """Initialize the file writer"""
@ -174,26 +209,28 @@ class PilatusCsaxs(DetectorBase):
# TODO slow reaction, seemed to have timeout. # TODO slow reaction, seemed to have timeout.
self._set_det_threshold() self._set_det_threshold()
self._set_acquisition_params() self._set_acquisition_params()
self._set_trigger(TriggerSource.EXT_ENABLE)
def _set_det_threshold(self) -> None: def _set_det_threshold(self) -> None:
# threshold_energy PV exists on Eiger 9M? # threshold_energy PV exists on Eiger 9M?
factor = 1 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 factor = 1000
setpoint = int(self.mokev * factor) setpoint = int(self.mokev * factor)
threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"] threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"]
if not np.isclose(setpoint / 2, threshold, rtol=0.05): 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: def _set_acquisition_params(self) -> None:
"""set acquisition parameters on the detector""" """set acquisition parameters on the detector"""
# self.cam.acquire_time.set(self.exp_time) # self.cam.acquire_time.set(self.exp_time)
# self.cam.acquire_period.set(self.exp_time + self.readout) # 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_images.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
self.cam.num_exposures.set(1) self.cam.num_frames.put(1)
self._set_trigger(TriggerSource.EXT_ENABLE) # EXT_TRIGGER) self._update_readout_time()
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 """Set trigger source for the detector, either directly to value or TriggerSource.* with
INTERNAL = 0 INTERNAL = 0
EXT_ENABLE = 1 EXT_ENABLE = 1
@ -201,19 +238,22 @@ class PilatusCsaxs(DetectorBase):
MULTI_TRIGGER = 3 MULTI_TRIGGER = 3
ALGINMENT = 4 ALGINMENT = 4
""" """
value = int(trigger_source) value = trigger_source
self.cam.trigger_mode.set(value) 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: 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) Prepare the file writer for pilatus_2
# self._close_file_writer()
# time.sleep(2) A zmq service is running on xbl-daq-34 that is waiting
# self._stop_file_writer() for a zmq message to start the writer for the pilatus_2 x12sa-pd-2
# time.sleep(2)
"""
# TODO explore required sleep time here
self._close_file_writer() self._close_file_writer()
time.sleep(0.1) time.sleep(0.1)
self._stop_file_writer() self._stop_file_writer()
@ -229,6 +269,7 @@ class PilatusCsaxs(DetectorBase):
self.cam.file_format.put(0) # 0: TIFF self.cam.file_format.put(0) # 0: TIFF
self.cam.file_template.put("%s%s_%5.5d.cbf") self.cam.file_template.put("%s%s_%5.5d.cbf")
# TODO remove hardcoded filepath here
# compile filename # compile filename
basepath = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/pilatus_2/" basepath = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/pilatus_2/"
self.filepath = os.path.join( self.filepath = os.path.join(
@ -236,8 +277,11 @@ class PilatusCsaxs(DetectorBase):
self.filewriter.get_scan_directory(self.scaninfo.scan_number, 1000, 5), self.filewriter.get_scan_directory(self.scaninfo.scan_number, 1000, 5),
) )
# Make directory if needed # 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 = { data_msg = {
"source": [ "source": [
{ {
@ -247,21 +291,14 @@ class PilatusCsaxs(DetectorBase):
} }
] ]
} }
res = self._send_requests_put(url=url, data=data_msg, headers=headers)
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,
)
logger.info(f"{res.status_code} - {res.text} - {res.content}") logger.info(f"{res.status_code} - {res.text} - {res.content}")
if not res.ok: if not res.ok:
res.raise_for_status() 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 = [ data_msg = [
"zmqWriter", "zmqWriter",
self.scaninfo.username, self.scaninfo.username,
@ -274,14 +311,7 @@ class PilatusCsaxs(DetectorBase):
"user": self.scaninfo.username, "user": self.scaninfo.username,
}, },
] ]
res = self._send_requests_put(url=url, data=data_msg, headers=headers)
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)
logger.info(f"{res.status_code} - {res.text} - {res.content}") logger.info(f"{res.status_code} - {res.text} - {res.content}")
if not res.ok: if not res.ok:
@ -289,8 +319,10 @@ class PilatusCsaxs(DetectorBase):
# Wait for server to become available again # Wait for server to become available again
time.sleep(0.1) 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 = [ data_msg = [
"zmqWriter", "zmqWriter",
self.scaninfo.username, self.scaninfo.username,
@ -299,15 +331,8 @@ class PilatusCsaxs(DetectorBase):
"timeout": 2000, "timeout": 2000,
}, },
] ]
logger.info(f"{res.status_code} -{res.text} - {res.content}")
try: try:
res = requests.put( res = self._send_requests_put(url=url, data=data_msg, headers=headers)
url="http://xbl-daq-34:8091/pilatus_2/wait",
data=json.dumps(data_msg),
# headers=headers,
)
logger.info(f"{res}") logger.info(f"{res}")
if not res.ok: if not res.ok:
@ -315,25 +340,56 @@ class PilatusCsaxs(DetectorBase):
except Exception as exc: except Exception as exc:
logger.info(f"Pilatus2 wait threw Exception: {exc}") logger.info(f"Pilatus2 wait threw Exception: {exc}")
def _close_file_writer(self) -> None: def _send_requests_put(self, url: str, data_msg: list = None, headers: dict = None) -> object:
"""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
""" """
Send a put request to the given url
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
"""
return requests.delete(url=url, headers=headers)
def _close_file_writer(self) -> None:
"""
Close the file writer for pilatus_2
Delete the data from x12sa-pd-2
"""
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
try: try:
res = requests.delete(url="http://x12sa-pd-2:8080/stream/pilatus_2") res = self._send_requests_delete(url=url)
if not res.ok: if not res.ok:
res.raise_for_status() res.raise_for_status()
except Exception as exc: 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: def _stop_file_writer(self) -> None:
res = requests.put( """
url="http://xbl-daq-34:8091/pilatus_2/stop", Stop the file writer for pilatus_2
# data=json.dumps(data_msg),
# headers=headers,
)
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: if not res.ok:
res.raise_for_status() res.raise_for_status()
@ -362,28 +418,44 @@ class PilatusCsaxs(DetectorBase):
self._prep_file_writer() self._prep_file_writer()
self._prep_det() self._prep_det()
state = False state = False
self._publish_file_location(done=state, successful=state) self._publish_file_location(done=state)
return super().stage() return super().stage()
# TODO might be useful for base class # TODO might be useful for base class
def pre_scan(self) -> None: 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() self._arm_acquisition()
def _arm_acquisition(self) -> None: 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() 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) msg = BECMessage.FileMessage(file_path=self.filepath, done=done, successful=successful)
self._producer.set_and_publish( self._producer.set_and_publish(
MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe
) )
self._producer.set_and_publish( 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() pipe.execute()
@ -441,43 +513,30 @@ class PilatusCsaxs(DetectorBase):
- _stop_det - _stop_det
- _stop_file_writer - _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: while True:
if self.device_manager.devices.mcs.obj._staged != Staged.yes: if self.device_manager.devices.mcs.obj._staged != Staged.yes:
break break
time.sleep(0.1) if self._stopped == True:
# TODO implement a waiting function or not break
# time.sleep(2) time.sleep(sleep_time)
# timer = 0 timer = timer + sleep_time
# while True: if timer > 5:
# # rtr = self.cam.status_message_camserver.get() self._stopped == True
# #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} "
# # )
self._stop_det() self._stop_det()
self._stop_file_writer() self._stop_file_writer()
# TODO explore if sleep is needed # TODO explore if sleep is needed
time.sleep(0.5) time.sleep(0.5)
self._close_file_writer() self._close_file_writer()
raise PilatusTimeoutError(f"Timeout waiting for mcs device to unstage")
def acquire(self) -> None: self._stop_det()
"""Start acquisition in software trigger mode, self._stop_file_writer()
or arm the detector in hardware of the detector # TODO explore if sleep time is needed
""" time.sleep(0.5)
self.cam.acquire.put(1) self._close_file_writer()
# TODO check if sleep of 1s is needed, could be that less is enough
time.sleep(1)
def _stop_det(self) -> None: def _stop_det(self) -> None:
"""Stop the detector""" """Stop the detector"""
@ -494,4 +553,4 @@ class PilatusCsaxs(DetectorBase):
# Automatically connect to test environmenr if directly invoked # Automatically connect to test environmenr if directly invoked
if __name__ == "__main__": 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)

View File

@ -1,6 +1,8 @@
import time import time
from bec_lib.core import bec_logger 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 from ophyd import Signal, Kind
@ -11,33 +13,88 @@ logger = bec_logger.logger
DEFAULT_EPICSSIGNAL_VALUE = object() DEFAULT_EPICSSIGNAL_VALUE = object()
class MockProducer: # TODO maybe specify here that this DeviceMock is for usage in the DeviceServer
def set_and_publish(self, endpoint: str, msgdump: str): class DeviceMock:
logger.info(f"BECMessage to {endpoint} with msg dump {msgdump}") def __init__(self, name: str, value: float = 0.0):
self.name = name
self.read_buffer = value
class MockDeviceManager: self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None}
def __init__(self) -> None: self._enabled_set = True
self.devices = devices() self._enabled = True
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): 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): class ConfigSignal(Signal):

View File

@ -1,17 +1,17 @@
from bluesky import RunEngine # from bluesky import RunEngine
from bluesky.plans import grid_scan # from bluesky.plans import grid_scan
from bluesky.callbacks.best_effort import BestEffortCallback # from bluesky.callbacks.best_effort import BestEffortCallback
from bluesky.callbacks.mpl_plotting import LivePlot # 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. # # Send all metadata/data captured to the BestEffortCallback.
RE.subscribe(bec) # RE.subscribe(bec)
# RE.subscribe(dummy) # # 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))

505
tests/test_eiger9m_csaxs.py Normal file
View File

@ -0,0 +1,505 @@
import pytest
from unittest import mock
import ophyd
from bec_lib.core import BECMessage, MessageEndpoints
from ophyd_devices.epics.devices.eiger9m_csaxs import Eiger9McSAXS
from tests.utils import DMMock, MockPV
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")
def mock_det():
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, "_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, expected_exception",
[
(
2,
1,
True,
),
(
2,
0,
False,
),
],
)
def test_init_detector(
mock_det,
trigger_source,
detector_state,
expected_exception,
):
"""Test the _init function:
This includes testing the functions:
- _init_detector
- _stop_det
- _set_trigger
--> Testing the filewriter is done in test_init_filewriter
Validation upon setting the correct PVs
"""
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(
"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",
[
(
"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._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
@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_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._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()

503
tests/test_pilatus_csaxs.py Normal file
View File

@ -0,0 +1,503 @@
import os
import pytest
from unittest import mock
import ophyd
from bec_lib.core import BECMessage, MessageEndpoints
from ophyd_devices.epics.devices.pilatus_csaxs import PilatuscSAXS
from tests.utils import DMMock, MockPV
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")
def mock_det():
name = "pilatus"
prefix = "X12SA-ES-PILATUS300K:"
sim_mode = False
dm = DMMock()
with mock.patch.object(dm, "producer"):
with mock.patch(
"ophyd_devices.epics.devices.pilatus_csaxs.FileWriterMixin"
) 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, detector_state",
[
(1, 0),
],
)
# TODO rewrite this one, write test for init_detector, init_filewriter is tested
def test_init_detector(
mock_det,
trigger_source,
detector_state,
):
"""Test the _init function:
This includes testing the functions:
- _init_detector
- _stop_det
- _set_trigger
--> Testing the filewriter is done in test_init_filewriter
Validation upon setting the correct PVs
"""
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(
"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(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"]
# 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.patch.object(
mock_det, "_update_readout_time"
) as mock_update_readout_time:
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()
mock_update_readout_time.assert_called()
# 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(
"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",
[
({"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(
"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, 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(
"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(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,
ophyd.Staged.no,
False,
),
(
True,
ophyd.Staged.no,
False,
),
(
False,
ophyd.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()

View File

@ -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: class SocketMock:
def __init__(self, host, port): def __init__(self, host, port):
self.host = host self.host = host
@ -44,3 +50,246 @@ class SocketMock:
def flush_buffer(self): def flush_buffer(self):
self.buffer_put = [] self.buffer_put = []
self.buffer_recv = "" self.buffer_recv = ""
class MockPV:
"""
MockPV class
This class is used for mocking pyepics signals for testing purposes
"""
_fmtsca = "<PV '%(pvname)s', count=%(count)i, type=%(typefull)s, access=%(access)s>"
_fmtarr = "<PV '%(pvname)s', count=%(count)i/%(nelm)i, type=%(typefull)s, access=%(access)s>"
_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}}