refactor(pilatus): update config, add live mode

This commit is contained in:
2025-09-04 17:57:48 +02:00
parent 8e5bdd230d
commit 09c3e395de
2 changed files with 71 additions and 43 deletions

View File

@@ -1,9 +1,34 @@
pilatus:
pilatus:
readoutPriority: async
description: Pilatus
deviceClass: debye_bec.devices.pilatus.PilatusDetector
deviceClass: debye_bec.devices.pilatus.pilatus.Pilatus
deviceTags:
- detector
deviceConfig:
prefix: "X01DA-ES2-PIL:"
onFailure: retry
enabled: true
softwareTrigger: false
softwareTrigger: true
samx:
readoutPriority: baseline
deviceClass: ophyd_devices.SimPositioner
deviceConfig:
delay: 1
limits:
- -50
- 50
tolerance: 0.01
update_frequency: 400
deviceTags:
- user motors
enabled: true
readOnly: false
bpm4i:
readoutPriority: monitored
deviceClass: ophyd_devices.SimMonitor
deviceConfig:
deviceTags:
- beamline
enabled: true
readOnly: false

View File

@@ -15,6 +15,7 @@ from ophyd import Component as Cpt
from ophyd import EpicsSignal, Kind
from ophyd.areadetector.cam import ADBase, PilatusDetectorCam
from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin
from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin
from ophyd.status import WaitTimeoutError
from ophyd_devices import (
AndStatusWithList,
@@ -23,9 +24,7 @@ from ophyd_devices import (
FileEventSignal,
PreviewSignal,
)
from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35 as ImagePlugin
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from ophyd_devices.utils.psi_device_base_utils import TaskStatus
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
@@ -171,17 +170,19 @@ class Pilatus(PSIDeviceBase, ADBase):
self.device_manager = device_manager
self._readout_time = PILATUS_READOUT_TIME
self._full_path = ""
self._poll_thread = threading.Thread(
target=self._poll_array_data, daemon=True, name=f"{self.name}_poll_thread"
)
self._poll_thread_stop_event = threading.Event()
self._task_status: TaskStatus | None = None
self._poll_rate = 1 # 1Hz
self._poll_rate = 1 # Poll rate in Hz
########################################
# Custom Beamline Methods #
########################################
def _poll_array_data(self):
"""Poll the array data for preview updates."""
while not self._poll_thread_stop_event.wait(1 / self._poll_rate):
logger.debug("Polling Pilatus array data for preview...")
try:
value = self.image1.array_data.get()
if value is None:
@@ -191,14 +192,16 @@ class Pilatus(PSIDeviceBase, ADBase):
# Geometry correction for the image
data = np.reshape(value, (height, width))
last_image: DevicePreviewMessage = self.preview.get()
if last_image is None:
return
elif np.array_equal(data, last_image.data):
# No update if image is the same, ~2.5ms on 2400x2400 image (6M)
logger.debug(
f"Pilatus preview image for {self.name} is the same as last one, not updating."
)
return
if last_image is not None:
if np.array_equal(data, last_image.data):
# No update if image is the same, ~2.5ms on 2400x2400 image (6M)
logger.debug(
f"Pilatus preview image for {self.name} is the same as last one, not updating."
)
continue
logger.debug(f"Setting preview data for {self.name}")
self.preview.put(data)
except Exception: # pylint: disable=broad-except
content = traceback.format_exc()
@@ -243,13 +246,14 @@ class Pilatus(PSIDeviceBase, ADBase):
self.hdf.lazy_open.set(1).wait(5)
self.hdf.compression.set(COMPRESSIONALGORITHM.NONE.value).wait(5) # To test which to use
# Start polling thread...
self._task_status = self.task_handler.submit_task(task=self._poll_array_data, run=True)
self._poll_thread.start()
def on_stage(self) -> DeviceStatus | None:
"""
Called while staging the device.
Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object.
Information about the upcoming scan can be accessed from the scan_info
(self.scan_info.msg) object.
"""
scan_msg: ScanStatusMessage = self.scan_info.msg
if scan_msg.scan_name.startswith("xas"):
@@ -327,32 +331,30 @@ class Pilatus(PSIDeviceBase, ADBase):
if status.success:
status.device.file_event.put(
file_path=status.device._full_path, done=True, successful=True
)
) # pylint: disable:protected-access
else:
status.device.file_event.put(
file_path=status.device._full_path, done=True, successful=False
)
) # pylint: disable:protected-access
def on_complete(self) -> DeviceStatus | None:
"""Called to inquire if a device has completed a scans."""
if self.scan_info.msg.scan_name.startswith("xas"):
# TODO implement logic for 'xas' scans
return None
else:
status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value)
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value)
num_images = self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters.get(
"frames_per_trigger", 1
)
status_img_written = CompareStatus(self.hdf.num_captured, num_images)
status = AndStatusWithList(
device=self,
status_list=[status_hdf, status_cam, status_img_written, status_cam_server],
)
status.add_callback(self._complete_callback) # Callback that writing was successful
self.cancel_on_stop(status)
return status
status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value)
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value)
num_images = self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters.get(
"frames_per_trigger", 1
)
status_img_written = CompareStatus(self.hdf.num_captured, num_images)
status = AndStatusWithList(
device=self, status_list=[status_hdf, status_cam, status_img_written, status_cam_server]
)
status.add_callback(self._complete_callback) # Callback that writing was successful
self.cancel_on_stop(status)
return status
def on_kickoff(self) -> None:
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
@@ -364,19 +366,20 @@ class Pilatus(PSIDeviceBase, ADBase):
def on_destroy(self) -> None:
"""Called when the device is destroyed. Cleanup resources here."""
self.on_stop()
self._poll_thread_stop_event.set()
# TODO do we need to clean the poll thread ourselves?
self.on_stop()
if __name__ == "__main__":
try:
pilatus = Pilatus(name="pilatus", prefix="X01DA-ES2-PIL:")
logger.info(f"Calling wait for connection")
logger.info("Calling wait for connection")
# pilatus.wait_for_connection(all_signals=True, timeout=20)
logger.info(f"Connecting to pilatus...")
logger.info("Connecting to pilatus...")
pilatus.on_connected()
for exp_time, scan_number, n_pnts in zip([0.5, 1.0, 2.0], [1, 2, 3], [30, 20, 10]):
logger.info(f"Sleeping for 5s")
logger.info("Sleeping for 5s")
time.sleep(5)
pilatus.scan_info.msg.num_points = n_pnts
pilatus.scan_info.msg.scan_parameters["exp_time"] = exp_time
@@ -386,9 +389,9 @@ if __name__ == "__main__":
"h5",
)
pilatus.on_stage()
logger.info(f"Stage done")
logger.info("Stage done")
pilatus.on_pre_scan().wait(timeout=5)
logger.info(f"Pre-scan done")
logger.info("Pre-scan done")
for ii in range(pilatus.scan_info.msg.num_points):
# if ii == 0:
# time.sleep(1)
@@ -401,8 +404,8 @@ if __name__ == "__main__":
f"Preview shape: {p.data.shape}, max: {np.max(p.data)}, min: {np.min(p.data)}, mean: {np.mean(p.data)}"
)
pilatus.on_complete().wait(timeout=5)
logger.info(f"Complete done")
logger.info("Complete done")
pilatus.on_unstage()
logger.info(f"Unstage done")
logger.info("Unstage done")
finally:
pilatus.on_destroy()