Skip to content

Commit

Permalink
Move trigger logic into dedicated plan stub (#175)
Browse files Browse the repository at this point in the history
This streamlines the preparation of the flyer and any detectors by delegating the responsibility for making TriggerInfo (required by detectors) and sequence tables (required by the TriggerLogic in the flyer to a plan stub. This can be called within a plan to prepare a flyer and many detectors. This is a specific use case and more will need to be made for different situations.
  • Loading branch information
abbiemery authored and evalott100 committed Apr 12, 2024
1 parent 1c5afc9 commit a76b867
Show file tree
Hide file tree
Showing 14 changed files with 484 additions and 51 deletions.
10 changes: 8 additions & 2 deletions src/ophyd_async/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
StaticDirectoryProvider,
)
from .async_status import AsyncStatus
from .detector import DetectorControl, DetectorTrigger, DetectorWriter, StandardDetector
from .detector import (
DetectorControl,
DetectorTrigger,
DetectorWriter,
StandardDetector,
TriggerInfo,
)
from .device import Device, DeviceCollector, DeviceVector
from .device_save_loader import (
get_signal_values,
Expand All @@ -17,7 +23,7 @@
set_signal_values,
walk_rw_signals,
)
from .flyer import HardwareTriggeredFlyable, TriggerInfo, TriggerLogic
from .flyer import HardwareTriggeredFlyable, TriggerLogic
from .signal import (
Signal,
SignalR,
Expand Down
29 changes: 10 additions & 19 deletions src/ophyd_async/core/flyer.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from abc import ABC, abstractmethod
from typing import Dict, Generic, Optional, Sequence, TypeVar
from typing import Dict, Generic, Sequence, TypeVar

from bluesky.protocols import Descriptor, Flyable, Preparable, Reading, Stageable

from .async_status import AsyncStatus
from .detector import TriggerInfo
from .device import Device
from .signal import SignalR
from .utils import merge_gathered_dicts
Expand All @@ -13,18 +12,18 @@


class TriggerLogic(ABC, Generic[T]):
@abstractmethod
def trigger_info(self, value: T) -> TriggerInfo:
"""Return info about triggers that will be produced for a given value"""

@abstractmethod
async def prepare(self, value: T):
"""Move to the start of the flyscan"""

@abstractmethod
async def start(self):
async def kickoff(self):
"""Start the flyscan"""

@abstractmethod
async def complete(self):
"""Block until the flyscan is done"""

@abstractmethod
async def stop(self):
"""Stop flying and wait everything to be stopped"""
Expand All @@ -45,19 +44,12 @@ def __init__(
):
self._trigger_logic = trigger_logic
self._configuration_signals = tuple(configuration_signals)
self._describe: Dict[str, Descriptor] = {}
self._fly_status: Optional[AsyncStatus] = None
self._trigger_info: Optional[TriggerInfo] = None
super().__init__(name=name)

@property
def trigger_logic(self) -> TriggerLogic[T]:
return self._trigger_logic

@property
def trigger_info(self) -> Optional[TriggerInfo]:
return self._trigger_info

@AsyncStatus.wrap
async def stage(self) -> None:
await self.unstage()
Expand All @@ -71,17 +63,16 @@ def prepare(self, value: T) -> AsyncStatus:
return AsyncStatus(self._prepare(value))

async def _prepare(self, value: T) -> None:
self._trigger_info = self._trigger_logic.trigger_info(value)
# Move to start and setup the flyscan
await self._trigger_logic.prepare(value)

@AsyncStatus.wrap
async def kickoff(self) -> None:
self._fly_status = AsyncStatus(self._trigger_logic.start())
await self._trigger_logic.kickoff()

def complete(self) -> AsyncStatus:
assert self._fly_status, "Kickoff not run"
return self._fly_status
@AsyncStatus.wrap
async def complete(self) -> None:
await self._trigger_logic.complete()

async def describe_configuration(self) -> Dict[str, Descriptor]:
return await merge_gathered_dicts(
Expand Down
17 changes: 17 additions & 0 deletions src/ophyd_async/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,20 @@ async def merge_gathered_dicts(

async def gather_list(coros: Iterable[Awaitable[T]]) -> List[T]:
return await asyncio.gather(*coros)


def in_micros(t: float) -> int:
"""
Converts between a positive number of seconds and an equivalent
number of microseconds.
Args:
t (float): A time in seconds
Raises:
ValueError: if t < 0
Returns:
t (int): A time in microseconds, rounded up to the nearest whole microsecond,
"""
if t < 0:
raise ValueError(f"Expected a positive time in seconds, got {t!r}")
return int(np.ceil(t * 1e6))
3 changes: 2 additions & 1 deletion src/ophyd_async/panda/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .panda import CommonPandABlocks, PandA, PcapBlock, PulseBlock, SeqBlock
from .panda import CommonPandABlocks, PandA, PcapBlock, PulseBlock, SeqBlock, TimeUnits
from .panda_controller import PandaPcapController
from .table import (
SeqTable,
Expand All @@ -23,4 +23,5 @@
"PandaPcapController",
"DataBlock",
"CommonPandABlocks",
"TimeUnits",
]
13 changes: 13 additions & 0 deletions src/ophyd_async/panda/panda.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from enum import Enum

from ophyd_async.core import DEFAULT_TIMEOUT, Device, DeviceVector, SignalR, SignalRW
from ophyd_async.epics.pvi import fill_pvi_entries
from ophyd_async.panda.table import SeqTable
Expand All @@ -19,9 +21,20 @@ class PulseBlock(Device):
width: SignalRW[float]


class TimeUnits(str, Enum):
min = "min"
s = "s"
ms = "ms"
us = "us"


class SeqBlock(Device):
table: SignalRW[SeqTable]
active: SignalRW[bool]
repeats: SignalRW[int]
prescale: SignalRW[float]
prescale_units: SignalRW[TimeUnits]
enable: SignalRW[str]


class PcapBlock(Device):
Expand Down
40 changes: 40 additions & 0 deletions src/ophyd_async/panda/trigger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import asyncio
from dataclasses import dataclass

from ophyd_async.core import TriggerLogic, wait_for_value
from ophyd_async.panda import SeqBlock, SeqTable, TimeUnits


@dataclass
class SeqTableInfo:
sequence_table: SeqTable
repeats: int
prescale_as_us: float = 1 # microseconds


class StaticSeqTableTriggerLogic(TriggerLogic[SeqTableInfo]):

def __init__(self, seq: SeqBlock) -> None:
self.seq = seq

async def prepare(self, value: SeqTableInfo):
await asyncio.gather(
self.seq.prescale_units.set(TimeUnits.us),
self.seq.enable.set("ZERO"),
)
await asyncio.gather(
self.seq.prescale.set(value.prescale_as_us),
self.seq.repeats.set(value.repeats),
self.seq.table.set(value.sequence_table),
)

async def kickoff(self) -> None:
await self.seq.enable.set("ONE")
await wait_for_value(self.seq.active, True, timeout=1)

async def complete(self) -> None:
await wait_for_value(self.seq.active, False, timeout=None)

async def stop(self):
await self.seq.enable.set("ZERO")
await wait_for_value(self.seq.active, False, timeout=1)
5 changes: 5 additions & 0 deletions src/ophyd_async/planstubs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .prepare_trigger_and_dets import (
prepare_static_seq_table_flyer_and_detectors_with_same_trigger,
)

__all__ = ["prepare_static_seq_table_flyer_and_detectors_with_same_trigger"]
58 changes: 58 additions & 0 deletions src/ophyd_async/planstubs/prepare_trigger_and_dets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from typing import List

import bluesky.plan_stubs as bps

from ophyd_async.core.detector import DetectorTrigger, StandardDetector, TriggerInfo
from ophyd_async.core.flyer import HardwareTriggeredFlyable
from ophyd_async.core.utils import in_micros
from ophyd_async.panda.table import SeqTable, SeqTableRow, seq_table_from_rows
from ophyd_async.panda.trigger import SeqTableInfo


def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
flyer: HardwareTriggeredFlyable[SeqTableInfo],
detectors: List[StandardDetector],
num: int,
width: float,
deadtime: float,
shutter_time: float,
repeats: int = 1,
period: float = 0.0,
):

trigger_info = TriggerInfo(
num=num * repeats,
trigger=DetectorTrigger.constant_gate,
deadtime=deadtime,
livetime=width,
)

trigger_time = num * (width + deadtime)
pre_delay = max(period - 2 * shutter_time - trigger_time, 0)

table: SeqTable = seq_table_from_rows(
# Wait for pre-delay then open shutter
SeqTableRow(
time1=in_micros(pre_delay),
time2=in_micros(shutter_time),
outa2=True,
),
# Keeping shutter open, do N triggers
SeqTableRow(
repeats=num,
time1=in_micros(width),
outa1=True,
outb1=True,
time2=in_micros(deadtime),
outa2=True,
),
# Add the shutter close
SeqTableRow(time2=in_micros(shutter_time)),
)

table_info = SeqTableInfo(table, repeats)

for det in detectors:
yield from bps.prepare(det, trigger_info, wait=False, group="prep")
yield from bps.prepare(flyer, table_info, wait=False, group="prep")
yield from bps.wait(group="prep")
32 changes: 11 additions & 21 deletions tests/core/test_flyer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,16 @@ class DummyTriggerLogic(TriggerLogic[int]):
def __init__(self):
self.state = TriggerState.null

def trigger_info(self, value: int) -> TriggerInfo:
return TriggerInfo(
num=value, trigger=DetectorTrigger.constant_gate, deadtime=2, livetime=2
)

async def prepare(self, value: int):
self.state = TriggerState.preparing
return value

async def start(self):
async def kickoff(self):
self.state = TriggerState.starting

async def complete(self):
self.state = TriggerState.null

async def stop(self):
self.state = TriggerState.stopping

Expand Down Expand Up @@ -153,6 +151,9 @@ def append_and_print(name, doc):

trigger_logic = DummyTriggerLogic()
flyer = HardwareTriggeredFlyable(trigger_logic, [], name="flyer")
trigger_info = TriggerInfo(
num=1, trigger=DetectorTrigger.constant_gate, deadtime=2, livetime=2
)

def flying_plan():
yield from bps.stage_all(*detector_list, flyer)
Expand All @@ -166,11 +167,11 @@ def flying_plan():
for detector in detector_list:
yield from bps.prepare(
detector,
flyer.trigger_info,
trigger_info,
wait=True,
)

assert trigger_logic.state == TriggerState.preparing
assert flyer._trigger_logic.state == TriggerState.preparing
for detector in detector_list:
detector.controller.disarm.assert_called_once # type: ignore

Expand All @@ -184,7 +185,7 @@ def flying_plan():
yield from bps.complete(flyer, wait=False, group="complete")
for detector in detector_list:
yield from bps.complete(detector, wait=False, group="complete")
assert trigger_logic.state == TriggerState.starting
assert flyer._trigger_logic.state == TriggerState.null

# Manually incremenet the index as if a frame was taken
for detector in detector_list:
Expand All @@ -201,9 +202,8 @@ def flying_plan():
yield from bps.collect(
*detector_list,
return_payload=False,
# name="main_stream",
name="main_stream",
)
yield from bps.sleep(0.01)
yield from bps.wait(group="complete")
yield from bps.close_run()

Expand All @@ -226,16 +226,6 @@ def flying_plan():
]


def test_flyer_has_trigger_logic_property():
flyer = HardwareTriggeredFlyable(DummyTriggerLogic(), [], name="flyer")
trigger_info = flyer.trigger_logic.trigger_info(1)
assert type(trigger_info) is TriggerInfo
assert trigger_info.num == 1
assert trigger_info.trigger == "constant_gate"
assert trigger_info.deadtime == 2
assert trigger_info.livetime == 2


# To do: Populate configuration signals
async def test_describe_configuration():
flyer = HardwareTriggeredFlyable(DummyTriggerLogic(), [], name="flyer")
Expand Down
14 changes: 7 additions & 7 deletions tests/epics/areadetector/test_scans.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,12 @@
class DummyTriggerLogic(TriggerLogic[int]):
def __init__(self): ...

def trigger_info(self, value: int) -> TriggerInfo:
return TriggerInfo(
num=value, trigger=DetectorTrigger.constant_gate, deadtime=2, livetime=2
)

async def prepare(self, value: int):
return value

async def start(self): ...
async def kickoff(self): ...

async def complete(self): ...

async def stop(self): ...

Expand Down Expand Up @@ -105,6 +102,9 @@ def test_hdf_writer_fails_on_timeout_with_flyscan(RE: RunEngine, writer: HDFWrit
trigger_logic = DummyTriggerLogic()

flyer = HardwareTriggeredFlyable(trigger_logic, [], name="flyer")
trigger_info = TriggerInfo(
num=1, trigger=DetectorTrigger.constant_gate, deadtime=2, livetime=2
)

def flying_plan():
"""NOTE: the following is a workaround to ensure tests always pass.
Expand All @@ -115,7 +115,7 @@ def flying_plan():
# Prepare the flyer first to get the trigger info for the detectors
yield from bps.prepare(flyer, 1, wait=True)
# prepare detector second.
yield from bps.prepare(detector, flyer.trigger_info, wait=True)
yield from bps.prepare(detector, trigger_info, wait=True)
yield from bps.open_run()
yield from bps.kickoff(flyer)
yield from bps.kickoff(detector)
Expand Down
Loading

0 comments on commit a76b867

Please sign in to comment.