Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add frame_timeout to trigger info #348

Merged
merged 11 commits into from
Jun 4, 2024
25 changes: 15 additions & 10 deletions src/ophyd_async/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ class TriggerInfo:
deadtime: float
#: What is the maximum high time of the triggers
livetime: float
#: What is the maximum timeout on waiting for a frame
frame_timeout: float | None = None


class DetectorControl(ABC):
Expand Down Expand Up @@ -162,7 +164,6 @@ def __init__(
writer: DetectorWriter,
config_sigs: Sequence[AsyncReadable] = (),
name: str = "",
writer_timeout: float = DEFAULT_TIMEOUT,
) -> None:
"""
Constructor
Expand All @@ -173,16 +174,11 @@ def __init__(
config_sigs: Signals to read when describe and read
configuration are called. Defaults to ().
name: Device name. Defaults to "".
writer_timeout: Timeout for frame writing to start, if the
timeout is reached, ophyd-async assumes the detector
has a problem and raises an error.
Defaults to DEFAULT_TIMEOUT.
"""
self._controller = controller
self._writer = writer
self._describe: Dict[str, DataKey] = {}
self._config_sigs = list(config_sigs)
self._frame_writing_timeout = writer_timeout
# For prepare
self._arm_status: Optional[AsyncStatus] = None
self._trigger_info: Optional[TriggerInfo] = None
Expand Down Expand Up @@ -245,17 +241,21 @@ async def describe(self) -> Dict[str, DataKey]:

@AsyncStatus.wrap
async def trigger(self) -> None:
# set default trigger_info
self._trigger_info = TriggerInfo(
num=1, trigger=DetectorTrigger.internal, deadtime=0.0, livetime=0.0
)
Comment on lines +245 to +247
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you have previously run prepare(TriggerInfo(trigger=DetectorTrigger.internal, livetime=0.5)) as a way of setting up for a step scan with 0.5s exposure?

# Arm the detector and wait for it to finish.
indices_written = await self.writer.get_indices_written()
written_status = await self.controller.arm(
num=1,
trigger=DetectorTrigger.internal,
num=self._trigger_info.num,
trigger=self._trigger_info.trigger,
)
await written_status
end_observation = indices_written + 1

async for index in self.writer.observe_indices_written(
self._frame_writing_timeout
DEFAULT_TIMEOUT + self._trigger_info.livetime + self._trigger_info.deadtime
):
if index >= end_observation:
break
Expand Down Expand Up @@ -309,7 +309,12 @@ async def complete(self):
assert self._arm_status, "Prepare not run"
assert self._trigger_info
async for index in self.writer.observe_indices_written(
self._frame_writing_timeout
self._trigger_info.frame_timeout
or (
DEFAULT_TIMEOUT
+ self._trigger_info.livetime
+ self._trigger_info.deadtime
)
):
yield WatcherUpdate(
name=self.name,
Expand Down
1 change: 0 additions & 1 deletion src/ophyd_async/panda/_hdf_panda.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ def __init__(
writer=writer,
config_sigs=config_sigs,
name=name,
writer_timeout=DEFAULT_TIMEOUT,
)

async def connect(
Expand Down
4 changes: 4 additions & 0 deletions src/ophyd_async/plan_stubs/fly.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
shutter_time: float,
repeats: int = 1,
period: float = 0.0,
frame_timeout: float | None = None,
):
"""Prepare a hardware triggered flyable and one or more detectors.

Expand All @@ -39,6 +40,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
trigger=DetectorTrigger.constant_gate,
deadtime=deadtime,
livetime=exposure,
frame_timeout=frame_timeout,
)
trigger_time = number_of_frames * (exposure + deadtime)
pre_delay = max(period - 2 * shutter_time - trigger_time, 0)
Expand Down Expand Up @@ -120,6 +122,7 @@ def time_resolved_fly_and_collect_with_static_seq_table(
shutter_time: float,
repeats: int = 1,
period: float = 0.0,
frame_timeout: float | None = None,
):
"""Run a scan wth a flyer and multiple detectors.

Expand All @@ -144,6 +147,7 @@ def time_resolved_fly_and_collect_with_static_seq_table(
shutter_time=shutter_time,
repeats=repeats,
period=period,
frame_timeout=frame_timeout,
)
# Run the fly scan
yield from fly_and_collect(stream_name, flyer, detectors)
2 changes: 0 additions & 2 deletions src/ophyd_async/sim/sim_pattern_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ def __init__(
path: Path,
config_sigs: Sequence[AsyncReadable] = [],
name: str = "sim_pattern_detector",
writer_timeout: float = 1,
) -> None:
self.directory_provider: DirectoryProvider = StaticDirectoryProvider(path)
self.pattern_generator = PatternGenerator()
Expand All @@ -33,5 +32,4 @@ def __init__(
writer=writer,
config_sigs=config_sigs,
name=name,
writer_timeout=writer_timeout,
)
2 changes: 0 additions & 2 deletions tests/core/test_flyer.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,11 @@ async def dummy_arm_2(self=None, trigger=None, num=0, exposure=None):
Mock(spec=DetectorControl, get_deadtime=lambda num: num, arm=dummy_arm_1),
writers[0],
name="detector_1",
writer_timeout=3,
)
detector_2: StandardDetector[Any] = StandardDetector(
Mock(spec=DetectorControl, get_deadtime=lambda num: num, arm=dummy_arm_2),
writers[1],
name="detector_2",
writer_timeout=3,
)

return (detector_1, detector_2)
Expand Down
8 changes: 5 additions & 3 deletions tests/epics/areadetector/test_scans.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
from pathlib import Path
from typing import Any, Optional
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, patch

import bluesky.plan_stubs as bps
import bluesky.plans as bp
Expand Down Expand Up @@ -76,14 +76,15 @@ def writer(RE, tmp_path: Path) -> HDFWriter:
)


@patch("ophyd_async.core.detector.DEFAULT_TIMEOUT", 0.1)
async def test_hdf_writer_fails_on_timeout_with_stepscan(
RE: RunEngine,
writer: HDFWriter,
controller: ADSimController,
):
set_mock_value(writer.hdf.file_path_exists, True)
detector: StandardDetector[Any] = StandardDetector(
controller, writer, name="detector", writer_timeout=0.01
controller, writer, name="detector"
)

with pytest.raises(Exception) as exc:
Expand All @@ -92,12 +93,13 @@ async def test_hdf_writer_fails_on_timeout_with_stepscan(
assert isinstance(exc.value.__cause__, asyncio.TimeoutError)


@patch("ophyd_async.core.detector.DEFAULT_TIMEOUT", 0.1)
def test_hdf_writer_fails_on_timeout_with_flyscan(RE: RunEngine, writer: HDFWriter):
controller = DummyController()
set_mock_value(writer.hdf.file_path_exists, True)

detector: StandardDetector[Optional[TriggerInfo]] = StandardDetector(
controller, writer, writer_timeout=0.01
controller, writer
)
trigger_logic = DummyTriggerLogic()

Expand Down
50 changes: 45 additions & 5 deletions tests/plan_stubs/test_fly.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __init__(self, name: str, shape: Sequence[int]):
self._file: Optional[ComposeStreamResourceBundle] = None
self._last_emitted = 0
self.index = 0
self.observe_indices_written_timeout_log = []

async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
return {
Expand All @@ -54,6 +55,7 @@ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
async def observe_indices_written(
self, timeout=DEFAULT_TIMEOUT
) -> AsyncGenerator[int, None]:
self.observe_indices_written_timeout_log.append(timeout)
num_captured: int
async for num_captured in observe_value(self.dummy_signal, timeout):
yield num_captured
Expand Down Expand Up @@ -102,17 +104,21 @@ def __init__(
writer: DetectorWriter,
config_sigs: Sequence[AsyncReadable] = [],
name: str = "",
writer_timeout: float = 1,
) -> None:
super().__init__(controller, writer, config_sigs, name, writer_timeout)
super().__init__(controller, writer, config_sigs, name)

@WatchableAsyncStatus.wrap
async def complete(self):
assert self._arm_status, "Prepare not run"
assert self._trigger_info
self.writer.increment_index()
async for index in self.writer.observe_indices_written(
self._frame_writing_timeout
self._trigger_info.frame_timeout
or (
DEFAULT_TIMEOUT
+ self._trigger_info.livetime
+ self._trigger_info.deadtime
)
):
yield WatcherUpdate(
name=self.name,
Expand Down Expand Up @@ -143,13 +149,11 @@ async def dummy_arm_2(self=None, trigger=None, num=0, exposure=None):
Mock(spec=DetectorControl, get_deadtime=lambda num: num, arm=dummy_arm_1),
writers[0],
name="detector_1",
writer_timeout=3,
)
detector_2 = MockDetector(
Mock(spec=DetectorControl, get_deadtime=lambda num: num, arm=dummy_arm_2),
writers[1],
name="detector_2",
writer_timeout=3,
)
return (detector_1, detector_2)

Expand Down Expand Up @@ -374,3 +378,39 @@ def fly():
with pytest.raises(ValueError) as exc:
RE(fly())
assert str(exc) == "No detectors provided. There must be at least one."


@pytest.mark.parametrize("timeout_setting,expected_timeout", [(None, 12), (5.0, 5.0)])
async def test_trigger_sets_or_defaults_timeout(
RE: RunEngine,
flyer: HardwareTriggeredFlyable,
detectors: tuple[StandardDetector, ...],
timeout_setting: float | None,
expected_timeout: float,
):
detector_list = list(detectors)

# Trigger parameters
number_of_frames = 1
exposure = 1
shutter_time = 0.004

def fly():
yield from bps.stage_all(*detector_list, flyer)
yield from bps.open_run()
yield from time_resolved_fly_and_collect_with_static_seq_table(
stream_name="stream1",
flyer=flyer,
detectors=detector_list,
number_of_frames=number_of_frames,
exposure=exposure,
shutter_time=shutter_time,
frame_timeout=timeout_setting,
)
yield from bps.close_run()
yield from bps.unstage_all(flyer, *detector_list)

RE(fly())

for detector in detectors:
assert detector.writer.observe_indices_written_timeout_log == [expected_timeout]
Loading