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

Interface change of StandardDetector and Standard Controller #568

Merged
merged 15 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 51 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,34 @@ 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
"""

async def wait_for_idle(self):
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved
"""
This will wait on the internal _arm_status and wait for it to get disarmed
"""

@abstractmethod
Expand Down Expand Up @@ -186,7 +197,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 +259,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 +294,34 @@ 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._fly_start = time.monotonic()
await self.controller.prepare(value)
self._describe = await self.writer.open(value.multiplier)
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved
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 +342,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
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
)
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved

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,33 @@ 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
)

async def wait_for_idle(self):
if self._arm_status:
await self._arm_status
# 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,8 +63,6 @@ async def arm(
timeout=DEFAULT_TIMEOUT,
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved
)

return idle_status

@classmethod
def _get_trigger_mode(cls, trigger: DetectorTrigger) -> PilatusTriggerMode:
if trigger not in cls._supported_trigger_types.keys():
Expand Down
Loading
Loading