Skip to content

Commit

Permalink
Merge branch 'main' into 570-make-table-subclass-enums-be-sequence-en…
Browse files Browse the repository at this point in the history
…um-rather-than-numpy-string
  • Loading branch information
evalott100 committed Sep 16, 2024
2 parents 46c9d2a + 5b67581 commit b49ba75
Show file tree
Hide file tree
Showing 26 changed files with 404 additions and 202 deletions.
92 changes: 53 additions & 39 deletions src/ophyd_async/core/_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,24 @@ class TriggerInfo(BaseModel):
"""Minimal set of information required to setup triggering on a detector"""

#: Number of triggers that will be sent, 0 means infinite
number: int = Field(gt=0)
number: int = Field(ge=0)
#: Sort of triggers that will be sent
trigger: DetectorTrigger = Field()
trigger: DetectorTrigger = Field(default=DetectorTrigger.internal)
#: What is the minimum deadtime between triggers
deadtime: float | None = Field(ge=0)
deadtime: float | None = Field(default=None, ge=0)
#: What is the maximum high time of the triggers
livetime: float | None = Field(ge=0)
livetime: float | None = Field(default=None, ge=0)
#: What is the maximum timeout on waiting for a frame
frame_timeout: float | None = Field(None, gt=0)
frame_timeout: float | None = Field(default=None, gt=0)
#: How many triggers make up a single StreamDatum index, to allow multiple frames
#: from a faster detector to be zipped with a single frame from a slow detector
#: e.g. if num=10 and multiplier=5 then the detector will take 10 frames,
#: but publish 2 indices, and describe() will show a shape of (5, h, w)
multiplier: int = 1
#: The number of times the detector can go through a complete cycle of kickoff and
#: complete without needing to re-arm. This is important for detectors where the
#: process of arming is expensive in terms of time
iteration: int = 1


class DetectorControl(ABC):
Expand All @@ -78,27 +82,35 @@ def get_deadtime(self, exposure: float | None) -> float:
"""For a given exposure, how long should the time between exposures be"""

@abstractmethod
async def arm(
self,
num: int,
trigger: DetectorTrigger = DetectorTrigger.internal,
exposure: Optional[float] = None,
) -> AsyncStatus:
async def prepare(self, trigger_info: TriggerInfo):
"""
Arm detector, do all necessary steps to prepare detector for triggers.
Do all necessary steps to prepare the detector for triggers.
Args:
num: Expected number of frames
trigger: Type of trigger for which to prepare the detector. Defaults to
DetectorTrigger.internal.
exposure: Exposure time with which to set up the detector. Defaults to None
if not applicable or the detector is expected to use its previously-set
exposure time.
trigger_info: This is a Pydantic model which contains
number Expected number of frames.
trigger Type of trigger for which to prepare the detector. Defaults
to DetectorTrigger.internal.
livetime Livetime / Exposure time with which to set up the detector.
Defaults to None
if not applicable or the detector is expected to use its previously-set
exposure time.
deadtime Defaults to None. This is the minimum deadtime between
triggers.
multiplier The number of triggers grouped into a single StreamDatum
index.
"""

Returns:
AsyncStatus: Status representing the arm operation. This function returning
represents the start of the arm. The returned status completing means
the detector is now armed.
@abstractmethod
async def arm(self) -> None:
"""
Arm the detector
"""

@abstractmethod
async def wait_for_idle(self):
"""
This will wait on the internal _arm_status and wait for it to get disarmed/idle
"""

@abstractmethod
Expand Down Expand Up @@ -186,7 +198,7 @@ def __init__(
self._watchers: List[Callable] = []
self._fly_status: Optional[WatchableAsyncStatus] = None
self._fly_start: float

self._iterations_completed: int = 0
self._intial_frame: int
self._last_frame: int
super().__init__(name)
Expand Down Expand Up @@ -248,15 +260,15 @@ async def trigger(self) -> None:
trigger=DetectorTrigger.internal,
deadtime=None,
livetime=None,
frame_timeout=None,
)
)
assert self._trigger_info
assert self._trigger_info.trigger is DetectorTrigger.internal
# Arm the detector and wait for it to finish.
indices_written = await self.writer.get_indices_written()
written_status = await self.controller.arm(
num=self._trigger_info.number,
trigger=self._trigger_info.trigger,
)
await written_status
await self.controller.arm()
await self.controller.wait_for_idle()
end_observation = indices_written + 1

async for index in self.writer.observe_indices_written(
Expand All @@ -283,35 +295,35 @@ async def prepare(self, value: TriggerInfo) -> None:
Args:
value: TriggerInfo describing how to trigger the detector
"""
self._trigger_info = value
if value.trigger != DetectorTrigger.internal:
assert (
value.deadtime
), "Deadtime must be supplied when in externally triggered mode"
if value.deadtime:
required = self.controller.get_deadtime(self._trigger_info.livetime)
required = self.controller.get_deadtime(value.livetime)
assert required <= value.deadtime, (
f"Detector {self.controller} needs at least {required}s deadtime, "
f"but trigger logic provides only {value.deadtime}s"
)
self._trigger_info = value
self._initial_frame = await self.writer.get_indices_written()
self._last_frame = self._initial_frame + self._trigger_info.number
self._arm_status = await self.controller.arm(
num=self._trigger_info.number,
trigger=self._trigger_info.trigger,
exposure=self._trigger_info.livetime,
self._describe, _ = await asyncio.gather(
self.writer.open(value.multiplier), self.controller.prepare(value)
)
self._fly_start = time.monotonic()
self._describe = await self.writer.open(value.multiplier)
if value.trigger != DetectorTrigger.internal:
await self.controller.arm()
self._fly_start = time.monotonic()

@AsyncStatus.wrap
async def kickoff(self):
if not self._arm_status:
raise Exception("Detector not armed!")
assert self._trigger_info, "Prepare must be called before kickoff!"
if self._iterations_completed >= self._trigger_info.iteration:
raise Exception(f"Kickoff called more than {self._trigger_info.iteration}")
self._iterations_completed += 1

@WatchableAsyncStatus.wrap
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._trigger_info.frame_timeout
Expand All @@ -332,6 +344,8 @@ async def complete(self):
)
if index >= self._trigger_info.number:
break
if self._iterations_completed == self._trigger_info.iteration:
await self.controller.wait_for_idle()

async def describe_collect(self) -> Dict[str, DataKey]:
return self._describe
Expand Down
2 changes: 1 addition & 1 deletion src/ophyd_async/core/_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ async def wait_for_value(self, signal: SignalR[T], timeout: Optional[float]):
try:
await asyncio.wait_for(self._wait_for_value(signal), timeout)
except asyncio.TimeoutError as e:
raise TimeoutError(
raise asyncio.TimeoutError(
f"{signal.name} didn't match {self._matcher_name} in {timeout}s, "
f"last value {self._last_value!r}"
) from e
Expand Down
27 changes: 14 additions & 13 deletions src/ophyd_async/epics/adaravis/_aravis_controller.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import asyncio
from typing import Literal, Optional, Tuple
from typing import Literal, Tuple

from ophyd_async.core import (
AsyncStatus,
DetectorControl,
DetectorTrigger,
TriggerInfo,
set_and_wait_for_value,
)
from ophyd_async.core._status import AsyncStatus
from ophyd_async.epics import adcore

from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
Expand All @@ -23,24 +24,20 @@ class AravisController(DetectorControl):
def __init__(self, driver: AravisDriverIO, gpio_number: GPIO_NUMBER) -> None:
self._drv = driver
self.gpio_number = gpio_number
self._arm_status: AsyncStatus | None = None

def get_deadtime(self, exposure: float) -> float:
return _HIGHEST_POSSIBLE_DEADTIME

async def arm(
self,
num: int = 0,
trigger: DetectorTrigger = DetectorTrigger.internal,
exposure: Optional[float] = None,
) -> AsyncStatus:
if num == 0:
async def prepare(self, trigger_info: TriggerInfo):
if (num := trigger_info.number) == 0:
image_mode = adcore.ImageMode.continuous
else:
image_mode = adcore.ImageMode.multiple
if exposure is not None:
if (exposure := trigger_info.livetime) is not None:
await self._drv.acquire_time.set(exposure)

trigger_mode, trigger_source = self._get_trigger_info(trigger)
trigger_mode, trigger_source = self._get_trigger_info(trigger_info.trigger)
# trigger mode must be set first and on it's own!
await self._drv.trigger_mode.set(trigger_mode)

Expand All @@ -50,8 +47,12 @@ async def arm(
self._drv.image_mode.set(image_mode),
)

status = await set_and_wait_for_value(self._drv.acquire, True)
return status
async def arm(self):
self._arm_status = await set_and_wait_for_value(self._drv.acquire, True)

async def wait_for_idle(self):
if self._arm_status:
await self._arm_status

def _get_trigger_info(
self, trigger: DetectorTrigger
Expand Down
31 changes: 18 additions & 13 deletions src/ophyd_async/epics/adkinetix/_kinetix_controller.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import asyncio
from typing import Optional

from ophyd_async.core import AsyncStatus, DetectorControl, DetectorTrigger
from ophyd_async.core import DetectorControl, DetectorTrigger
from ophyd_async.core._detector import TriggerInfo
from ophyd_async.core._status import AsyncStatus
from ophyd_async.epics import adcore

from ._kinetix_io import KinetixDriverIO, KinetixTriggerMode
Expand All @@ -20,27 +21,31 @@ def __init__(
driver: KinetixDriverIO,
) -> None:
self._drv = driver
self._arm_status: AsyncStatus | None = None

def get_deadtime(self, exposure: float) -> float:
return 0.001

async def arm(
self,
num: int,
trigger: DetectorTrigger = DetectorTrigger.internal,
exposure: Optional[float] = None,
) -> AsyncStatus:
async def prepare(self, trigger_info: TriggerInfo):
await asyncio.gather(
self._drv.trigger_mode.set(KINETIX_TRIGGER_MODE_MAP[trigger]),
self._drv.num_images.set(num),
self._drv.trigger_mode.set(KINETIX_TRIGGER_MODE_MAP[trigger_info.trigger]),
self._drv.num_images.set(trigger_info.number),
self._drv.image_mode.set(adcore.ImageMode.multiple),
)
if exposure is not None and trigger not in [
if trigger_info.livetime is not None and trigger_info.trigger not in [
DetectorTrigger.variable_gate,
DetectorTrigger.constant_gate,
]:
await self._drv.acquire_time.set(exposure)
return await adcore.start_acquiring_driver_and_ensure_status(self._drv)
await self._drv.acquire_time.set(trigger_info.livetime)

async def arm(self):
self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
self._drv
)

async def wait_for_idle(self):
if self._arm_status:
await self._arm_status

async def disarm(self):
await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
32 changes: 17 additions & 15 deletions src/ophyd_async/epics/adpilatus/_pilatus_controller.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import asyncio
from typing import Optional

from ophyd_async.core import (
DEFAULT_TIMEOUT,
AsyncStatus,
DetectorControl,
DetectorTrigger,
wait_for_value,
)
from ophyd_async.core._detector import TriggerInfo
from ophyd_async.core._status import AsyncStatus
from ophyd_async.epics import adcore

from ._pilatus_io import PilatusDriverIO, PilatusTriggerMode
Expand All @@ -27,29 +27,29 @@ def __init__(
) -> None:
self._drv = driver
self._readout_time = readout_time
self._arm_status: AsyncStatus | None = None

def get_deadtime(self, exposure: float) -> float:
return self._readout_time

async def arm(
self,
num: int,
trigger: DetectorTrigger = DetectorTrigger.internal,
exposure: Optional[float] = None,
) -> AsyncStatus:
if exposure is not None:
async def prepare(self, trigger_info: TriggerInfo):
if trigger_info.livetime is not None:
await adcore.set_exposure_time_and_acquire_period_if_supplied(
self, self._drv, exposure
self, self._drv, trigger_info.livetime
)
await asyncio.gather(
self._drv.trigger_mode.set(self._get_trigger_mode(trigger)),
self._drv.num_images.set(999_999 if num == 0 else num),
self._drv.trigger_mode.set(self._get_trigger_mode(trigger_info.trigger)),
self._drv.num_images.set(
999_999 if trigger_info.number == 0 else trigger_info.number
),
self._drv.image_mode.set(adcore.ImageMode.multiple),
)

async def arm(self):
# Standard arm the detector and wait for the acquire PV to be True
idle_status = await adcore.start_acquiring_driver_and_ensure_status(self._drv)

self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
self._drv
)
# The pilatus has an additional PV that goes True when the camserver
# is actually ready. Should wait for that too or we risk dropping
# a frame
Expand All @@ -59,7 +59,9 @@ async def arm(
timeout=DEFAULT_TIMEOUT,
)

return idle_status
async def wait_for_idle(self):
if self._arm_status:
await self._arm_status

@classmethod
def _get_trigger_mode(cls, trigger: DetectorTrigger) -> PilatusTriggerMode:
Expand Down
Loading

0 comments on commit b49ba75

Please sign in to comment.