feat: refactor falcon for psi_detector_base class; adapted eiger; added and debugged tests

This commit is contained in:
appel_c 2023-11-16 22:52:45 +01:00
parent 90cd05e68e
commit bcc3210761
5 changed files with 319 additions and 419 deletions

View File

@ -11,8 +11,10 @@ from ophyd import ADComponent as ADCpt
from std_daq_client import StdDaqClient from std_daq_client import StdDaqClient
from bec_lib.core import BECMessage, MessageEndpoints, threadlocked from bec_lib import threadlocked
from bec_lib.core import bec_logger from bec_lib.logger import bec_logger
from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from ophyd_devices.epics.devices.psi_detector_base import PSIDetectorBase, CustomDetectorMixin from ophyd_devices.epics.devices.psi_detector_base import PSIDetectorBase, CustomDetectorMixin
@ -128,13 +130,13 @@ class Eiger9MSetup(CustomDetectorMixin):
"value" "value"
], ],
DetectorState.IDLE, DetectorState.IDLE,
), )
(lambda: self.parent._stopped, True),
] ]
if not self.wait_for_signals( if not self.wait_for_signals(
signal_conditions=signal_conditions, signal_conditions=signal_conditions,
timeout=self.parent.timeout - self.parent.timeout // 2, timeout=self.parent.timeout - self.parent.timeout // 2,
check_stopped=True,
all_signals=False, all_signals=False,
): ):
# Retry stop detector and wait for remaining time # Retry stop detector and wait for remaining time
@ -142,9 +144,12 @@ class Eiger9MSetup(CustomDetectorMixin):
if not self.wait_for_signals( if not self.wait_for_signals(
signal_conditions=signal_conditions, signal_conditions=signal_conditions,
timeout=self.parent.timeout - self.parent.timeout // 2, timeout=self.parent.timeout - self.parent.timeout // 2,
check_stopped=True,
all_signals=False, all_signals=False,
): ):
raise EigerTimeoutError("Failed to stop the acquisition. IOC did not update.") raise EigerTimeoutError(
f"Failed to stop detector, detector state {signal_conditions[0][0]}"
)
def stop_detector_backend(self) -> None: def stop_detector_backend(self) -> None:
"""Close file writer""" """Close file writer"""
@ -247,35 +252,6 @@ class Eiger9MSetup(CustomDetectorMixin):
): ):
raise EigerError(f"Timeout of 3s reached for filepath {self.parent.filepath}") raise EigerError(f"Timeout of 3s reached for filepath {self.parent.filepath}")
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
"""
Publish the filepath to REDIS.
We publish two events here:
- file_event: event for the filewriter
- public_file: event for any secondary service (e.g. radial integ code)
Args:
done (bool): True if scan is finished
successful (bool): True if scan was successful
"""
pipe = self.parent._producer.pipeline()
if successful is None:
msg = BECMessage.FileMessage(file_path=self.parent.filepath, done=done)
else:
msg = BECMessage.FileMessage(
file_path=self.parent.filepath, done=done, successful=successful
)
self.parent._producer.set_and_publish(
MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name),
msg.dumps(),
pipe=pipe,
)
self.parent._producer.set_and_publish(
MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe
)
pipe.execute()
def arm_acquisition(self) -> None: def arm_acquisition(self) -> None:
"""Arm Eiger detector for acquisition""" """Arm Eiger detector for acquisition"""
self.parent.cam.acquire.put(1) self.parent.cam.acquire.put(1)
@ -293,6 +269,7 @@ class Eiger9MSetup(CustomDetectorMixin):
check_stopped=True, check_stopped=True,
all_signals=False, all_signals=False,
): ):
self.parent.stop()
raise EigerTimeoutError( raise EigerTimeoutError(
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
) )
@ -303,6 +280,35 @@ class Eiger9MSetup(CustomDetectorMixin):
if self.parent.scaninfo.scanID != old_scanID: if self.parent.scaninfo.scanID != old_scanID:
self.parent._stopped = True self.parent._stopped = True
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
"""
Publish the filepath to REDIS.
We publish two events here:
- file_event: event for the filewriter
- public_file: event for any secondary service (e.g. radial integ code)
Args:
done (bool): True if scan is finished
successful (bool): True if scan was successful
"""
pipe = self.parent._producer.pipeline()
if successful is None:
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
else:
msg = messages.FileMessage(
file_path=self.parent.filepath, done=done, successful=successful
)
self.parent._producer.set_and_publish(
MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name),
msg.dumps(),
pipe=pipe,
)
self.parent._producer.set_and_publish(
MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe
)
pipe.execute()
@threadlocked @threadlocked
def finished(self): def finished(self):
"""Check if acquisition is finished.""" """Check if acquisition is finished."""
@ -375,6 +381,19 @@ class DetectorState(enum.IntEnum):
class Eiger9McSAXS(PSIDetectorBase): class Eiger9McSAXS(PSIDetectorBase):
"""
Eiger 9M detector class for cSAXS
Parent class: PSIDetectorBase
class attributes:
custom_prepare_cls (Eiger9MSetup): Custom detector setup class for cSAXS,
inherits from CustomDetectorMixin
cam (SLSDetectorCam): Detector camera
MIN_READOUT (float): Minimum readout time for the detector
"""
custom_prepare_cls = Eiger9MSetup custom_prepare_cls = Eiger9MSetup
cam = ADCpt(SLSDetectorCam, "cam1:") cam = ADCpt(SLSDetectorCam, "cam1:")
MIN_READOUT = 3e-3 MIN_READOUT = 3e-3

View File

@ -1,26 +1,21 @@
import enum import enum
import os import os
import time
from typing import List
from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt
from ophyd.mca import EpicsMCARecord from ophyd.mca import EpicsMCARecord
from ophyd import Device from ophyd import Device
from bec_lib import MessageEndpoints, messages, bec_logger from bec_lib.endpoints import MessageEndpoints
from bec_lib.file_utils import FileWriterMixin from bec_lib import messages
from bec_lib.devicemanager import DeviceStatus from bec_lib.logger import bec_logger
from bec_lib.bec_service import SERVICE_CONFIG
from ophyd_devices.epics.devices.psi_detector_base import PSIDetectorBase, CustomDetectorMixin
from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
from ophyd_devices.utils import bec_utils
logger = bec_logger.logger logger = bec_logger.logger
FALCON_MIN_READOUT = 3e-3
class FalconError(Exception): class FalconError(Exception):
"""Base class for exceptions in this module.""" """Base class for exceptions in this module."""
@ -34,13 +29,6 @@ class FalconTimeoutError(FalconError):
pass pass
class FalconInitError(FalconError):
"""Raised when initiation of the device class fails,
due to missing device manager or not started in sim_mode."""
pass
class DetectorState(enum.IntEnum): class DetectorState(enum.IntEnum):
"""Detector states for Falcon detector""" """Detector states for Falcon detector"""
@ -113,29 +101,226 @@ class FalconHDF5Plugins(Device):
array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config") array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config")
class FalconcSAXS(Device): class FalconSetup(CustomDetectorMixin):
"""Falcon Sitoro detector for CSAXS """Falcon setup class for cSAXS
Parent class: Device Parent class: CustomDetectorMixin
Device classes: EpicsDXPFalcon dxp1:, EpicsMCARecord mca1, FalconHDF5Plugins HDF1:
Attributes:
name str: 'falcon'
prefix (str): PV prefix ("X12SA-SITORO:)
""" """
# Specify which functions are revealed to the user in BEC client def __init__(self, parent: Device = None, *args, **kwargs) -> None:
USER_ACCESS = [ super().__init__(parent=parent, *args, **kwargs)
"describe",
] def initialize_default_parameter(self) -> None:
"""Set default parameters for Falcon
readout (float): readout time in seconds
_value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro
"""
self.parent._value_pixel_per_buffer = 20
self.update_readout_time()
def update_readout_time(self) -> None:
"""Set readout time for Eiger9M detector"""
readout_time = (
self.parent.scaninfo.readout_time
if hasattr(self.parent.scaninfo, "readout_time")
else self.parent.readout_time_min
)
self.parent.readout_time = max(readout_time, self.parent.readout_time_min)
def initialize_detector(self) -> None:
"""
Initialize Falcon detector.
The detector is operated in MCA mapping mode.
Parameters here affect the triggering, gating etc.
This includes also the readout chunk size and whether data is segmented into spectra in EPICS.
"""
self.stop_detector()
self.stop_detector_backend()
self.parent.set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
self.parent.preset_mode.put(1) # 1 Realtime
self.parent.input_logic_polarity.put(0) # 0 Normal, 1 Inverted
self.parent.auto_pixels_per_buffer.put(0) # 0 Manual 1 Auto
self.parent.pixels_per_buffer.put(self.parent._value_pixel_per_buffer)
def stop_detector(self) -> None:
"""Stops detector"""
self.parent.stop_all.put(1)
self.parent.erase_all.put(1)
signal_conditions = [
(
lambda: self.parent.state.read()[self.parent.state.name]["value"],
DetectorState.DONE,
),
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout - self.parent.timeout // 2,
all_signals=False,
):
# Retry stop detector and wait for remaining time
raise FalconTimeoutError(
f"Failed to stop detector, timeout with state {signal_conditions[0][0]}"
)
def stop_detector_backend(self) -> None:
"""Stop the detector backend"""
self.parent.hdf5.capture.put(0)
def initialize_detector_backend(self) -> None:
"""Initialize the detector backend for Falcon."""
self.parent.hdf5.enable.put(1)
# file location of h5 layout for cSAXS
self.parent.hdf5.xml_file_name.put("layout.xml")
# TODO Check if lazy open is needed and wanted!
self.parent.hdf5.lazy_open.put(1)
self.parent.hdf5.temp_suffix.put("")
# size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost
self.parent.hdf5.queue_size.put(2000)
# Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
self.parent.nd_array_mode.put(1)
def prepare_detector(self) -> None:
"""Prepare detector for acquisition"""
self.parent.set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
self.parent.preset_real.put(self.parent.scaninfo.exp_time)
self.parent.pixels_per_run.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
def prepare_data_backend(self) -> None:
"""Prepare data backend for acquisition"""
self.parent.filepath = self.parent.filewriter.compile_full_filename(
self.parent.scaninfo.scan_number, f"{self.parent.name}.h5", 1000, 5, True
)
file_path, file_name = os.path.split(self.parent.filepath)
self.parent.hdf5.file_path.put(file_path)
self.parent.hdf5.file_name.put(file_name)
self.parent.hdf5.file_template.put(f"%s%s")
self.parent.hdf5.num_capture.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
self.parent.hdf5.file_write_mode.put(2)
# Reset spectrum counter in filewriter, used for indexing & identifying missing triggers
self.parent.hdf5.array_counter.put(0)
# Start file writing
self.parent.hdf5.capture.put(1)
def arm_acquisition(self) -> None:
"""Arm Eiger detector for acquisition"""
self.parent.start_all.put(1)
signal_conditions = [
(
lambda: self.parent.state.read()[self.parent.state.name]["value"],
DetectorState.ACQUIRING,
)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
check_stopped=True,
all_signals=False,
):
self.parent.stop()
raise FalconTimeoutError(
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
)
def check_scanID(self) -> None:
old_scanID = self.parent.scaninfo.scanID
self.parent.scaninfo.load_scan_metadata()
if self.parent.scaninfo.scanID != old_scanID:
self.parent._stopped = True
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
"""
Publish the filepath to REDIS.
We publish two events here:
- file_event: event for the filewriter
- public_file: event for any secondary service (e.g. radial integ code)
Args:
done (bool): True if scan is finished
successful (bool): True if scan was successful
"""
pipe = self.parent._producer.pipeline()
if successful is None:
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
else:
msg = messages.FileMessage(
file_path=self.parent.filepath, done=done, successful=successful
)
self.parent._producer.set_and_publish(
MessageEndpoints.public_file(self.parent.scaninfo.scanID, self.parent.name),
msg.dumps(),
pipe=pipe,
)
self.parent._producer.set_and_publish(
MessageEndpoints.file_event(self.parent.name), msg.dumps(), pipe=pipe
)
pipe.execute()
def finished(self) -> None:
"""Check if acquisition is finished.
For the Falcon we accept that it misses a trigger since we can reconstruct it from the data.
"""
signal_conditions = [
(
lambda: self.parent.dxp.current_pixel.get(),
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger),
),
(
lambda: self.parent.hdf5.array_counter.get(),
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger),
),
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.timeout,
check_stopped=True,
all_signals=True,
):
logger.debug(
f"Falcon missed a trigger: received trigger {received_frames}, send data {written_frames} from total_frames {total_frames}"
)
self.stop_detector()
self.stop_detector_backend()
class FalconcSAXS(PSIDetectorBase):
"""
Falcon Sitoro detector for CSAXS
Parent class: PSIDetectorBase
class attributes:
custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS,
inherits from CustomDetectorMixin
dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector
mca (EpicsMCARecord) : MCA parameters for Falcon detector
hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector
MIN_READOUT (float) : Minimum readout time for the detector
"""
custom_prepare_cls = FalconSetup
MIN_READOUT = 3e-3
dxp = Cpt(EpicsDXPFalcon, "dxp1:") dxp = Cpt(EpicsDXPFalcon, "dxp1:")
mca = Cpt(EpicsMCARecord, "mca1") mca = Cpt(EpicsMCARecord, "mca1")
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:") hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
# specify Epics PVs for Falcon
# TODO consider moving this outside of this class!
stop_all = Cpt(EpicsSignal, "StopAll") stop_all = Cpt(EpicsSignal, "StopAll")
erase_all = Cpt(EpicsSignal, "EraseAll") erase_all = Cpt(EpicsSignal, "EraseAll")
start_all = Cpt(EpicsSignal, "StartAll") start_all = Cpt(EpicsSignal, "StartAll")
@ -157,157 +342,7 @@ class FalconcSAXS(Device):
pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun") pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun")
nd_array_mode = Cpt(EpicsSignal, "NDArrayMode") nd_array_mode = Cpt(EpicsSignal, "NDArrayMode")
def __init__( def set_trigger(
self,
prefix="",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
device_manager=None,
sim_mode=False,
**kwargs,
):
"""Initialize Falcon detector
Args:
#TODO add here the parameters for kind, read_attrs, configuration_attrs, parent
prefix (str): PV prefix ("X12SA-SITORO:)
name (str): 'falcon'
kind (str):
read_attrs (list):
configuration_attrs (list):
parent (object):
device_manager (object): BEC device manager
sim_mode (bool): simulation mode to start the detector without BEC, e.g. from ipython shell
"""
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
if device_manager is None and not sim_mode:
raise FalconInitError(
f"No device manager for device: {name}, and not started sim_mode: {sim_mode}. Add DeviceManager to initialization or init with sim_mode=True"
)
self.sim_mode = sim_mode
self._stopped = False
self.name = name
self.service_cfg = None
self.scaninfo = None
self.filewriter = None
self.readout_time_min = FALCON_MIN_READOUT
self._value_pixel_per_buffer = None
self.readout_time = None
self.timeout = 5
self.wait_for_connection(all_signals=True)
if not sim_mode:
self._update_service_config()
self.device_manager = device_manager
else:
self.device_manager = bec_utils.DMMock()
base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/"
self.service_cfg = {"base_path": os.path.expanduser(base_path)}
self._producer = self.device_manager.producer
self._update_scaninfo()
self._update_filewriter()
self._init()
def _update_filewriter(self) -> None:
"""Update filewriter with service config"""
self.filewriter = FileWriterMixin(self.service_cfg)
def _update_scaninfo(self) -> None:
"""Update scaninfo from BecScaninfoMixing
This depends on device manager and operation/sim_mode
"""
self.scaninfo = BecScaninfoMixin(self.device_manager, self.sim_mode)
self.scaninfo.load_scan_metadata()
def _update_service_config(self) -> None:
"""Update service config from BEC service config"""
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
def _init(self) -> None:
"""Initialize detector, filewriter and set default parameters"""
self._default_parameter()
self._init_detector()
self._init_filewriter()
def _default_parameter(self) -> None:
"""Set default parameters for Falcon
readout (float): readout time in seconds
_value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro
"""
self._value_pixel_per_buffer = 20
self._update_readout_time()
def _update_readout_time(self) -> None:
readout_time = (
self.scaninfo.readout_time
if hasattr(self.scaninfo, "readout_time")
else self.readout_time_min
)
self.readout_time = max(readout_time, self.readout_time_min)
def _stop_det(self) -> None:
""" "Stop detector"""
self.stop_all.put(1)
self.erase_all.put(1)
det_ctrl = self.state.read()[self.state.name]["value"]
timer = 0
while True:
det_ctrl = self.state.read()[self.state.name]["value"]
if det_ctrl == DetectorState.DONE:
break
if self._stopped == True:
break
time.sleep(0.01)
timer += 0.01
if timer > self.timeout:
raise FalconTimeoutError("Failed to stop the detector. IOC did not update.")
def _stop_file_writer(self) -> None:
""" "Stop the file writer"""
self.hdf5.capture.put(0)
def _init_filewriter(self) -> None:
"""Initialize file writer for Falcon.
This includes setting variables for the HDF5 plugin (EPICS) that is used to write the data.
"""
self.hdf5.enable.put(1)
# file location of h5 layout for cSAXS
self.hdf5.xml_file_name.put("layout.xml")
# Potentially not needed, means a temp data file is created first, could be 0
self.hdf5.lazy_open.put(1)
self.hdf5.temp_suffix.put("")
# size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost
self.hdf5.queue_size.put(2000)
# Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
self.nd_array_mode.put(1)
def _init_detector(self) -> None:
"""Initialize Falcon detector.
The detector is operated in MCA mapping mode.
Parameters here affect the triggering, gating etc.
This includes also the readout chunk size and whether data is segmented into spectra in EPICS.
"""
self._stop_det()
self._stop_file_writer()
self._set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
self.preset_mode.put(1) # 1 Realtime
self.input_logic_polarity.put(0) # 0 Normal, 1 Inverted
self.auto_pixels_per_buffer.put(0) # 0 Manual 1 Auto
self.pixels_per_buffer.put(self._value_pixel_per_buffer) #
def _set_trigger(
self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0 self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0
) -> None: ) -> None:
""" """
@ -325,177 +360,6 @@ class FalconcSAXS(Device):
self.pixel_advance_mode.put(trigger) self.pixel_advance_mode.put(trigger)
self.ignore_gate.put(ignore_gate) self.ignore_gate.put(ignore_gate)
def stage(self) -> List[object]:
"""Stage command, called from BEC in preparation of a scan.
This will iniate the preparation of detector and file writer.
The following functuions are called (at least):
- _prep_file_writer
- _prep_det
- _publish_file_location
The device returns a List[object] from the Ophyd Device class.
#TODO make sure this is fullfiled
Staging not idempotent and should raise
:obj:`RedundantStaging` if staged twice without an
intermediate :meth:`~BlueskyInterface.unstage`.
"""
self._stopped = False
self.scaninfo.load_scan_metadata()
self._prep_file_writer()
self._prep_det()
state = False
self._publish_file_location(done=state)
self._arm_acquisition()
return super().stage()
def _prep_det(self) -> None:
"""Prepare detector for acquisition"""
self._set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
self.preset_real.put(self.scaninfo.exp_time)
self.pixels_per_run.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
def _prep_file_writer(self) -> None:
"""Prepare filewriting from HDF5 plugin
#TODO check these settings together with Controls put vs set
"""
self.filepath = self.filewriter.compile_full_filename(
self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True
)
file_path, file_name = os.path.split(self.filepath)
self.hdf5.file_path.put(file_path)
self.hdf5.file_name.put(file_name)
self.hdf5.file_template.put(f"%s%s")
self.hdf5.num_capture.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
self.hdf5.file_write_mode.put(2)
# Reset spectrum counter in filewriter, used for indexing & identifying missing triggers
self.hdf5.array_counter.put(0)
# Start file writing
self.hdf5.capture.put(1)
def _publish_file_location(self, done: bool = False, successful: bool = False) -> None:
"""Publish the filepath to REDIS.
We publish two events here:
- file_event: event for the filewriter
- public_file: event for any secondary service (e.g. radial integ code)
Args:
done (bool): True if scan is finished
successful (bool): True if scan was successful
"""
pipe = self._producer.pipeline()
if successful is None:
msg = messages.FileMessage(file_path=self.filepath, done=done)
else:
msg = messages.FileMessage(file_path=self.filepath, done=done, successful=successful)
self._producer.set_and_publish(
MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe
)
self._producer.set_and_publish(
MessageEndpoints.file_event(self.name), msg.dumps(), pipe=pipe
)
pipe.execute()
def _arm_acquisition(self) -> None:
"""Arm Falcon detector for acquisition"""
timer = 0
self.start_all.put(1)
while True:
det_ctrl = self.state.read()[self.state.name]["value"]
if det_ctrl == DetectorState.ACQUIRING:
break
if self._stopped == True:
break
time.sleep(0.01)
timer += 0.01
if timer > self.timeout:
self.stop()
raise FalconTimeoutError("Failed to arm the acquisition. IOC did not update.")
# TODO function for abstract class?
def trigger(self) -> DeviceStatus:
"""Trigger the detector, called from BEC."""
self._on_trigger()
return super().trigger()
# TODO function for abstract class?
def _on_trigger(self):
"""Specify action that should be taken upon trigger signal.
At cSAXS with DDGs triggering the devices, we do nothing upon the trigger signal
"""
pass
def unstage(self) -> List[object]:
"""Unstage the device.
This method must be idempotent, multiple calls (without a new
call to 'stage') have no effect.
Functions called:
- _finished
- _publish_file_location
"""
old_scanID = self.scaninfo.scanID
self.scaninfo.load_scan_metadata()
logger.info(f"Old scanID: {old_scanID}, ")
if self.scaninfo.scanID != old_scanID:
self._stopped = True
if self._stopped:
return super().unstage()
self._finished()
state = True
self._publish_file_location(done=state, successful=state)
self._stopped = False
return super().unstage()
def _finished(self):
"""Check if acquisition is finished.
This function is called from unstage and stop
and will check detector and file backend status.
Timeouts after given time
Functions called:
- _stop_det
- _stop_file_writer
"""
sleep_time = 0.1
timer = 0
while True:
det_ctrl = self.state.read()[self.state.name]["value"]
writer_ctrl = self.hdf5.capture.get()
received_frames = self.dxp.current_pixel.get()
written_frames = self.hdf5.array_counter.get()
total_frames = int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)
# TODO Could check state of detector (det_ctrl) and file writer (writer_ctrl)
if total_frames == received_frames and total_frames == written_frames:
break
if self._stopped == True:
break
time.sleep(sleep_time)
timer += sleep_time
if timer > self.timeout:
# self._stop_det()
# self._stop_file_writer()
logger.info(
f"Falcon missed a trigger: received trigger {received_frames}, send data {written_frames} from total_frames {total_frames}"
)
break
self._stop_det()
self._stop_file_writer()
def stop(self, *, success=False) -> None:
"""Stop the scan, with camera and file writer"""
self._stop_det()
self._stop_file_writer()
super().stop(success=success)
self._stopped = True
if __name__ == "__main__": if __name__ == "__main__":
falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True) falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True)

View File

@ -1,6 +1,6 @@
import time import time
import threading import threading
from bec_lib.core.devicemanager import DeviceStatus from bec_lib.devicemanager import DeviceStatus
import os import os
from typing import List from typing import List

View File

@ -114,6 +114,15 @@ def test_initialize_detector(
assert mock_det.cam.trigger_mode.get() == trigger_source assert mock_det.cam.trigger_mode.get() == trigger_source
def test_trigger(mock_det):
"""Test the trigger function:
Validate that trigger calls the custom_prepare.on_trigger() function
"""
with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger:
mock_det.trigger()
mock_on_trigger.assert_called_once()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"readout_time, expected_value", "readout_time, expected_value",
[ [

View File

@ -28,9 +28,9 @@ def mock_det():
dm = DMMock() dm = DMMock()
with mock.patch.object(dm, "producer"): with mock.patch.object(dm, "producer"):
with mock.patch( with mock.patch(
"ophyd_devices.epics.devices.falcon_csaxs.FileWriterMixin" "ophyd_devices.epics.devices.psi_detector_base.FileWriterMixin"
) as filemixin, mock.patch( ) as filemixin, mock.patch(
"ophyd_devices.epics.devices.falcon_csaxs.FalconcSAXS._update_service_config" "ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config"
) as mock_service_config: ) as mock_service_config:
with mock.patch.object(ophyd, "cl") as mock_cl: with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV mock_cl.get_pv = MockPV
@ -76,9 +76,9 @@ def test_init_detector(
if expected_exception: if expected_exception:
with pytest.raises(FalconTimeoutError): with pytest.raises(FalconTimeoutError):
mock_det.timeout = 0.1 mock_det.timeout = 0.1
mock_det._init_detector() mock_det.custom_prepare.initialize_detector()
else: else:
mock_det._init_detector() # call the method you want to test mock_det.custom_prepare.initialize_detector()
assert mock_det.state.get() == detector_state assert mock_det.state.get() == detector_state
assert mock_det.collect_mode.get() == mapping_source assert mock_det.collect_mode.get() == mapping_source
assert mock_det.pixel_advance_mode.get() == trigger_source assert mock_det.pixel_advance_mode.get() == trigger_source
@ -103,17 +103,19 @@ def test_init_detector(
def test_update_readout_time(mock_det, readout_time, expected_value): def test_update_readout_time(mock_det, readout_time, expected_value):
# mock_det.scaninfo.readout_time = readout_time # mock_det.scaninfo.readout_time = readout_time
if readout_time is None: if readout_time is None:
mock_det._update_readout_time() mock_det.custom_prepare.update_readout_time()
assert mock_det.readout_time == expected_value assert mock_det.readout_time == expected_value
else: else:
mock_det.scaninfo.readout_time = readout_time mock_det.scaninfo.readout_time = readout_time
mock_det._update_readout_time() mock_det.custom_prepare.update_readout_time()
assert mock_det.readout_time == expected_value assert mock_det.readout_time == expected_value
def test_default_parameter(mock_det): def test_initialize_default_parameter(mock_det):
with mock.patch.object(mock_det, "_update_readout_time") as mock_update_readout_time: with mock.patch.object(
mock_det._default_parameter() mock_det.custom_prepare, "update_readout_time"
) as mock_update_readout_time:
mock_det.custom_prepare.initialize_default_parameter()
assert mock_det._value_pixel_per_buffer == 20 assert mock_det._value_pixel_per_buffer == 20
mock_update_readout_time.assert_called_once() mock_update_readout_time.assert_called_once()
@ -139,12 +141,12 @@ def test_stage(mock_det, scaninfo):
This includes testing _prep_det This includes testing _prep_det
""" """
with mock.patch.object(mock_det, "_set_trigger") as mock_set_trigger, mock.patch.object( with mock.patch.object(mock_det, "set_trigger") as mock_set_trigger, mock.patch.object(
mock_det, "_prep_file_writer" mock_det.custom_prepare, "prepare_data_backend"
) as mock_prep_file_writer, mock.patch.object( ) as mock_prep_data_backend, mock.patch.object(
mock_det, "_publish_file_location" mock_det.custom_prepare, "publish_file_location"
) as mock_publish_file_location, mock.patch.object( ) as mock_publish_file_location, mock.patch.object(
mock_det, "_arm_acquisition" mock_det.custom_prepare, "arm_acquisition"
) as mock_arm_acquisition: ) as mock_arm_acquisition:
mock_det.scaninfo.exp_time = scaninfo["exp_time"] mock_det.scaninfo.exp_time = scaninfo["exp_time"]
mock_det.scaninfo.num_points = scaninfo["num_points"] mock_det.scaninfo.num_points = scaninfo["num_points"]
@ -155,7 +157,7 @@ def test_stage(mock_det, scaninfo):
assert mock_det.pixels_per_run.get() == int( assert mock_det.pixels_per_run.get() == int(
scaninfo["num_points"] * scaninfo["frames_per_trigger"] scaninfo["num_points"] * scaninfo["frames_per_trigger"]
) )
mock_prep_file_writer.assert_called_once() mock_prep_data_backend.assert_called_once()
mock_publish_file_location.assert_called_once_with(done=False) mock_publish_file_location.assert_called_once_with(done=False)
mock_arm_acquisition.assert_called_once() mock_arm_acquisition.assert_called_once()
@ -179,12 +181,12 @@ def test_stage(mock_det, scaninfo):
), ),
], ],
) )
def test_prep_file_writer(mock_det, scaninfo): def test_prepare_data_backend(mock_det, scaninfo):
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"] mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
mock_det.scaninfo.num_points = scaninfo["num_points"] mock_det.scaninfo.num_points = scaninfo["num_points"]
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
mock_det.scaninfo.scan_number = 1 mock_det.scaninfo.scan_number = 1
mock_det._prep_file_writer() mock_det.custom_prepare.prepare_data_backend()
file_path, file_name = os.path.split(scaninfo["filepath"]) file_path, file_name = os.path.split(scaninfo["filepath"])
assert mock_det.hdf5.file_path.get() == file_path assert mock_det.hdf5.file_path.get() == file_path
assert mock_det.hdf5.file_name.get() == file_name assert mock_det.hdf5.file_name.get() == file_name
@ -208,7 +210,9 @@ def test_prep_file_writer(mock_det, scaninfo):
def test_publish_file_location(mock_det, scaninfo): def test_publish_file_location(mock_det, scaninfo):
mock_det.scaninfo.scanID = scaninfo["scanID"] mock_det.scaninfo.scanID = scaninfo["scanID"]
mock_det.filepath = scaninfo["filepath"] mock_det.filepath = scaninfo["filepath"]
mock_det._publish_file_location(done=scaninfo["done"], successful=scaninfo["successful"]) mock_det.custom_prepare.publish_file_location(
done=scaninfo["done"], successful=scaninfo["successful"]
)
if scaninfo["successful"] is None: if scaninfo["successful"] is None:
msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps() msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps()
else: else:
@ -243,15 +247,15 @@ def test_arm_acquisition(mock_det, detector_state, expected_exception):
if expected_exception: if expected_exception:
with pytest.raises(FalconTimeoutError): with pytest.raises(FalconTimeoutError):
mock_det.timeout = 0.1 mock_det.timeout = 0.1
mock_det._arm_acquisition() mock_det.custom_prepare.arm_acquisition()
mock_stop.assert_called_once() mock_stop.assert_called_once()
else: else:
mock_det._arm_acquisition() mock_det.custom_prepare.arm_acquisition()
assert mock_det.start_all.get() == 1 assert mock_det.start_all.get() == 1
def test_trigger(mock_det): def test_trigger(mock_det):
with mock.patch.object(mock_det, "_on_trigger") as mock_on_trigger: with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger:
mock_det.trigger() mock_det.trigger()
mock_on_trigger.assert_called_once() mock_on_trigger.assert_called_once()
@ -274,8 +278,8 @@ def test_unstage(
stopped, stopped,
expected_abort, expected_abort,
): ):
with mock.patch.object(mock_det, "_finished") as mock_finished, mock.patch.object( with mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished, mock.patch.object(
mock_det, "_publish_file_location" mock_det.custom_prepare, "publish_file_location"
) as mock_publish_file_location: ) as mock_publish_file_location:
mock_det._stopped = stopped mock_det._stopped = stopped
if expected_abort: if expected_abort:
@ -290,12 +294,14 @@ def test_unstage(
def test_stop(mock_det): def test_stop(mock_det):
with mock.patch.object(mock_det, "_stop_det") as mock_stop_det, mock.patch.object( with mock.patch.object(
mock_det, "_stop_file_writer" mock_det.custom_prepare, "stop_detector"
) as mock_stop_file_writer: ) as mock_stop_det, mock.patch.object(
mock_det.custom_prepare, "stop_detector_backend"
) as mock_stop_detector_backend:
mock_det.stop() mock_det.stop()
mock_stop_det.assert_called_once() mock_stop_det.assert_called_once()
mock_stop_file_writer.assert_called_once() mock_stop_detector_backend.assert_called_once()
assert mock_det._stopped == True assert mock_det._stopped == True
@ -307,8 +313,10 @@ def test_stop(mock_det):
], ],
) )
def test_finished(mock_det, stopped, scaninfo): def test_finished(mock_det, stopped, scaninfo):
with mock.patch.object(mock_det, "_stop_det") as mock_stop_det, mock.patch.object( with mock.patch.object(
mock_det, "_stop_file_writer" mock_det.custom_prepare, "stop_detector"
) as mock_stop_det, mock.patch.object(
mock_det.custom_prepare, "stop_detector_backend"
) as mock_stop_file_writer: ) as mock_stop_file_writer:
mock_det._stopped = stopped mock_det._stopped = stopped
mock_det.dxp.current_pixel._read_pv.mock_data = int( mock_det.dxp.current_pixel._read_pv.mock_data = int(
@ -319,7 +327,7 @@ def test_finished(mock_det, stopped, scaninfo):
) )
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
mock_det.scaninfo.num_points = scaninfo["num_points"] mock_det.scaninfo.num_points = scaninfo["num_points"]
mock_det._finished() mock_det.custom_prepare.finished()
assert mock_det._stopped == stopped assert mock_det._stopped == stopped
mock_stop_det.assert_called_once() mock_stop_det.assert_called_once()
mock_stop_file_writer.assert_called_once() mock_stop_file_writer.assert_called_once()