feat(falcon): add prime() to arm the HDF5 plugin after an IOC restart #307
@@ -3,6 +3,7 @@
|
||||
import enum
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
from bec_lib.file_utils import get_full_path
|
||||
@@ -65,6 +66,9 @@ class FalconcSAXS(PSIDeviceBase, FalconControl):
|
||||
MIN_READOUT (float) : Minimum readout time for the detector
|
||||
"""
|
||||
|
||||
# Methods exposed to the BEC client, i.e. callable as dev.falcon.<name>()
|
||||
USER_ACCESS = ["prime", "is_primed"]
|
||||
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 3e-3
|
||||
_pv_timeout = 3 # Timeout for PV operations in seconds
|
||||
@@ -127,6 +131,110 @@ class FalconcSAXS(PSIDeviceBase, FalconControl):
|
||||
# Sets the number of pixels/spectra in the buffer
|
||||
self.pixels_per_buffer.put(self._value_pixel_per_buffer)
|
||||
|
||||
def is_primed(self) -> bool:
|
||||
"""
|
||||
Check whether the HDF5 plugin has already been primed.
|
||||
|
||||
The plugin only knows the array dimensions and data type once a single NDArray
|
||||
has passed through it. Before that, its ArraySize readbacks are all zero and
|
||||
ophyd refuses to stage it.
|
||||
|
||||
Returns:
|
||||
bool: True if the plugin is primed, False otherwise.
|
||||
"""
|
||||
return sum(self.hdf5.array_size.get()) > 0
|
||||
|
||||
def prime(self, timeout: float = 5) -> None:
|
||||
"""
|
||||
Prime the HDF5 plugin by pushing a single spectrum through it.
|
||||
|
||||
The areaDetector file plugin refuses to be staged until it has seen one NDArray,
|
||||
raising ophyd's UnprimedPlugin. That state lives in the IOC, so this has to be
|
||||
done once after every restart of the SITORO IOC, and survives restarts of BEC.
|
||||
|
||||
The generic HDF5Plugin.warmup() cannot be used here as it drives 'parent.cam',
|
||||
which the Falcon (an ADBase, not a DetectorBase) does not have. Instead the
|
||||
detector is briefly switched to user-advanced pixels, so that no external gate
|
||||
signal is needed and a missing trigger cable cannot mask the priming. No file
|
||||
is written.
|
||||
|
||||
The trigger configuration is restored afterwards, also if the priming fails.
|
||||
|
||||
Args:
|
||||
timeout (float): Time in seconds to wait for the array to reach the plugin.
|
||||
Defaults to 5.
|
||||
|
||||
Raises:
|
||||
FalconError: If the plugin is still unprimed after the timeout.
|
||||
"""
|
||||
if self.is_primed():
|
||||
logger.info(f"HDF5 plugin of {self.name} is already primed.")
|
||||
return
|
||||
|
||||
# NOTE on_stage does not touch the trigger configuration, only on_connected does.
|
||||
# Restoring it here is therefore not optional; a Falcon left in USER advance mode
|
||||
# silently ignores the gate and starves every subsequent scan.
|
||||
collect_mode = self.collect_mode.get()
|
||||
pixel_advance_mode = self.pixel_advance_mode.get()
|
||||
ignore_gate = self.ignore_gate.get()
|
||||
pixels_per_buffer = self.pixels_per_buffer.get()
|
||||
|
||||
with self._lock:
|
||||
try:
|
||||
self.on_stop()
|
||||
self.hdf5.enable.put(1)
|
||||
# Prime only; no file should be written.
|
||||
self.hdf5.capture.put(0)
|
||||
|
||||
self.set_trigger(
|
||||
mapping_mode=MappingSource.MAPPING,
|
||||
trigger_source=TriggerSource.USER,
|
||||
ignore_gate=1,
|
||||
)
|
||||
self.pixels_per_buffer.put(1)
|
||||
self.pixels_per_run.put(1)
|
||||
self.preset_real_time.put(0.1)
|
||||
|
||||
self.start_all.put(1)
|
||||
# The buffer is only emitted once it is full. The IOC may refuse a buffer
|
||||
# size of 1, so advance as many pixels as it actually accepted.
|
||||
for _ in range(int(self.pixels_per_buffer.get()) + 1):
|
||||
self.next_pixel.put(1)
|
||||
|
||||
if not self._wait_for_primed(timeout):
|
||||
raise FalconError(
|
||||
f"HDF5 plugin of {self.name} is still unprimed after {timeout}s. "
|
||||
"Check that the HDF5 plugin is enabled and that nd_array_mode is set."
|
||||
)
|
||||
logger.info(
|
||||
f"HDF5 plugin of {self.name} primed: array_size={self.hdf5.array_size.get()}"
|
||||
)
|
||||
finally:
|
||||
self.stop_all.put(1)
|
||||
self.set_trigger(
|
||||
mapping_mode=collect_mode,
|
||||
trigger_source=pixel_advance_mode,
|
||||
ignore_gate=ignore_gate,
|
||||
)
|
||||
self.pixels_per_buffer.put(pixels_per_buffer)
|
||||
self.erase_all.put(1)
|
||||
|
||||
def _wait_for_primed(self, timeout: float) -> bool:
|
||||
"""Poll the plugin's array size until it reports a non-zero shape.
|
||||
|
||||
Args:
|
||||
timeout (float): Maximum time to wait in seconds.
|
||||
|
||||
Returns:
|
||||
bool: True if the plugin reported a non-zero array size within the timeout.
|
||||
"""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
if self.is_primed():
|
||||
return True
|
||||
time.sleep(0.1)
|
||||
return self.is_primed()
|
||||
|
||||
def _initialize_detector_backend(self) -> None:
|
||||
"""Initialize the detector backend for Falcon."""
|
||||
# Enable HDF5 plugin
|
||||
|
||||
@@ -16,6 +16,7 @@ from ophyd_devices.tests.utils import patched_device
|
||||
from csaxs_bec.devices.epics.falcon_csaxs import (
|
||||
ACQUIRESTATUS,
|
||||
FalconcSAXS,
|
||||
FalconError,
|
||||
MappingSource,
|
||||
TriggerSource,
|
||||
)
|
||||
@@ -230,3 +231,75 @@ def test_falcon_complete(mock_det: FalconcSAXS):
|
||||
hinted_h5_entries=None,
|
||||
metadata={},
|
||||
)
|
||||
|
||||
|
||||
def test_falcon_is_primed(mock_det: FalconcSAXS):
|
||||
"""The plugin counts as primed as soon as any array dimension is non-zero."""
|
||||
falcon = mock_det
|
||||
|
||||
with mock.patch.object(falcon.hdf5.array_size, "get", return_value=(0, 0, 0)):
|
||||
assert falcon.is_primed() is False
|
||||
|
||||
with mock.patch.object(falcon.hdf5.array_size, "get", return_value=(0, 1, 3000)):
|
||||
assert falcon.is_primed() is True
|
||||
|
||||
|
||||
def test_falcon_prime_skips_when_already_primed(mock_det: FalconcSAXS):
|
||||
"""Priming an already primed plugin must not touch the detector."""
|
||||
falcon = mock_det
|
||||
|
||||
with (
|
||||
mock.patch.object(falcon, "is_primed", return_value=True),
|
||||
mock.patch.object(falcon, "set_trigger") as mock_set_trigger,
|
||||
mock.patch.object(falcon, "on_stop") as mock_on_stop,
|
||||
):
|
||||
falcon.prime()
|
||||
mock_set_trigger.assert_not_called()
|
||||
mock_on_stop.assert_not_called()
|
||||
|
||||
|
||||
def test_falcon_prime(mock_det: FalconcSAXS):
|
||||
"""Priming advances a pixel by hand and restores the trigger configuration."""
|
||||
falcon = mock_det
|
||||
|
||||
falcon.set_trigger(
|
||||
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
|
||||
)
|
||||
falcon.pixels_per_buffer.put(20)
|
||||
|
||||
with (
|
||||
mock.patch.object(falcon, "is_primed", side_effect=[False, True]),
|
||||
mock.patch.object(falcon.next_pixel, "put") as mock_next_pixel,
|
||||
):
|
||||
falcon.prime()
|
||||
|
||||
# A gate signal must not be required, so the pixel is advanced from software.
|
||||
assert mock_next_pixel.call_count > 0
|
||||
assert falcon.hdf5.capture.get() == 0
|
||||
|
||||
# Trigger configuration restored, otherwise the next scan silently starves.
|
||||
assert falcon.collect_mode.get() == MappingSource.MAPPING
|
||||
assert falcon.pixel_advance_mode.get() == TriggerSource.GATE
|
||||
assert falcon.ignore_gate.get() == 0
|
||||
assert falcon.pixels_per_buffer.get() == 20
|
||||
|
||||
|
||||
def test_falcon_prime_restores_config_on_failure(mock_det: FalconcSAXS):
|
||||
"""A failed priming must still leave the detector in its original trigger config."""
|
||||
falcon = mock_det
|
||||
|
||||
falcon.set_trigger(
|
||||
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
|
||||
)
|
||||
falcon.pixels_per_buffer.put(20)
|
||||
|
||||
with (
|
||||
mock.patch.object(falcon, "is_primed", return_value=False),
|
||||
mock.patch.object(falcon, "_wait_for_primed", return_value=False),
|
||||
):
|
||||
with pytest.raises(FalconError):
|
||||
falcon.prime()
|
||||
|
||||
assert falcon.pixel_advance_mode.get() == TriggerSource.GATE
|
||||
assert falcon.ignore_gate.get() == 0
|
||||
assert falcon.pixels_per_buffer.get() == 20
|
||||
|
||||
Reference in New Issue
Block a user