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 8 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
62 changes: 34 additions & 28 deletions src/ophyd_async/core/_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ 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,
Expand All @@ -78,22 +78,29 @@ 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.
"""

@abstractmethod
async def arm(self) -> AsyncStatus:
"""
Arm detector

Returns:
AsyncStatus: Status representing the arm operation. This function returning
Expand Down Expand Up @@ -248,14 +255,14 @@ 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,
)
written_status = await self.controller.arm()
await written_status
end_observation = indices_written + 1

Expand Down Expand Up @@ -296,12 +303,10 @@ async def prepare(self, value: TriggerInfo) -> None:
)
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)
if self._trigger_info.trigger is not DetectorTrigger.internal:
self._arm_status = await self.controller.arm()
self._fly_start = time.monotonic()
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved
self._describe = await self.writer.open(value.multiplier)

@AsyncStatus.wrap
Expand Down Expand Up @@ -332,6 +337,7 @@ async def complete(self):
)
if index >= self._trigger_info.number:
break
self._arm_status = None

async def describe_collect(self) -> Dict[str, DataKey]:
return self._describe
Expand Down
20 changes: 8 additions & 12 deletions src/ophyd_async/epics/adaravis/_aravis_controller.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
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.epics import adcore
Expand All @@ -27,20 +28,15 @@ def __init__(self, driver: AravisDriverIO, gpio_number: GPIO_NUMBER) -> 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 +46,8 @@ 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) -> AsyncStatus:
return await set_and_wait_for_value(self._drv.acquire, True)
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved

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

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

from ._kinetix_io import KinetixDriverIO, KinetixTriggerMode
Expand All @@ -24,22 +24,19 @@ def __init__(
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)
await self._drv.acquire_time.set(trigger_info.livetime)

async def arm(self) -> AsyncStatus:
return await adcore.start_acquiring_driver_and_ensure_status(self._drv)

async def disarm(self):
Expand Down
20 changes: 9 additions & 11 deletions src/ophyd_async/epics/adpilatus/_pilatus_controller.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import asyncio
from typing import Optional

from ophyd_async.core import (
DEFAULT_TIMEOUT,
Expand All @@ -8,6 +7,7 @@
DetectorTrigger,
wait_for_value,
)
from ophyd_async.core._detector import TriggerInfo
from ophyd_async.epics import adcore

from ._pilatus_io import PilatusDriverIO, PilatusTriggerMode
Expand All @@ -31,22 +31,20 @@ def __init__(
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) -> AsyncStatus:
# 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)

Expand Down
23 changes: 12 additions & 11 deletions src/ophyd_async/epics/adsimdetector/_sim_controller.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import asyncio
from typing import Optional, Set
from typing import Set

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


Expand All @@ -18,26 +19,26 @@ def __init__(
) -> None:
self.driver = driver
self.good_states = good_states
self.frame_timeout: float

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

async def arm(
self,
num: int,
trigger: DetectorTrigger = DetectorTrigger.internal,
exposure: Optional[float] = None,
) -> AsyncStatus:
async def prepare(self, trigger_info: TriggerInfo):
assert (
trigger == DetectorTrigger.internal
trigger_info.trigger == DetectorTrigger.internal
), "fly scanning (i.e. external triggering) is not supported for this device"
frame_timeout = DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
self.frame_timeout = (
DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
)
await asyncio.gather(
self.driver.num_images.set(num),
self.driver.num_images.set(trigger_info.number),
self.driver.image_mode.set(adcore.ImageMode.multiple),
)

async def arm(self) -> AsyncStatus:
return await adcore.start_acquiring_driver_and_ensure_status(
self.driver, good_states=self.good_states, timeout=frame_timeout
self.driver, good_states=self.good_states, timeout=self.frame_timeout
)

async def disarm(self):
Expand Down
25 changes: 12 additions & 13 deletions src/ophyd_async/epics/advimba/_vimba_controller.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
from typing import Optional

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

from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
Expand Down Expand Up @@ -31,27 +31,26 @@ def __init__(
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(TRIGGER_MODE[trigger]),
self._drv.exposure_mode.set(EXPOSE_OUT_MODE[trigger]),
self._drv.num_images.set(num),
self._drv.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
self._drv.exposure_mode.set(EXPOSE_OUT_MODE[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)
if trigger != DetectorTrigger.internal:
await self._drv.acquire_time.set(trigger_info.livetime)
if trigger_info.trigger != DetectorTrigger.internal:
self._drv.trigger_source.set(VimbaTriggerSource.line1)
else:
self._drv.trigger_source.set(VimbaTriggerSource.freerun)

async def arm(
self,
) -> AsyncStatus:
return await adcore.start_acquiring_driver_and_ensure_status(self._drv)

async def disarm(self):
Expand Down
26 changes: 13 additions & 13 deletions src/ophyd_async/epics/eiger/_eiger_controller.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import asyncio
from typing import Optional

from ophyd_async.core import (
DEFAULT_TIMEOUT,
Expand All @@ -8,6 +7,7 @@
DetectorTrigger,
set_and_wait_for_other_value,
)
from ophyd_async.core._detector import TriggerInfo

from ._eiger_io import EigerDriverIO, EigerTriggerMode

Expand Down Expand Up @@ -37,26 +37,26 @@ async def set_energy(self, energy: float, tolerance: float = 0.1):
if abs(current_energy - energy) > tolerance:
await self._drv.photon_energy.set(energy)

@AsyncStatus.wrap
async def arm(
self,
num: int,
trigger: DetectorTrigger = DetectorTrigger.internal,
exposure: Optional[float] = None,
):
async def prepare(self, trigger_info: TriggerInfo):
coros = [
self._drv.trigger_mode.set(EIGER_TRIGGER_MODE_MAP[trigger].value),
self._drv.num_images.set(num),
self._drv.trigger_mode.set(
EIGER_TRIGGER_MODE_MAP[trigger_info.trigger].value
),
self._drv.num_images.set(trigger_info.number),
]
if exposure is not None:
if trigger_info.livetime is not None:
coros.extend(
[
self._drv.acquire_time.set(exposure),
self._drv.acquire_period.set(exposure),
self._drv.acquire_time.set(trigger_info.livetime),
self._drv.acquire_period.set(trigger_info.livetime),
]
)
await asyncio.gather(*coros)

@AsyncStatus.wrap
async def arm(
self,
):
# TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
await set_and_wait_for_other_value(
self._drv.arm, 1, self._drv.state, "ready", timeout=DEFAULT_TIMEOUT
Expand Down
Loading
Loading