diff --git a/src/ophyd_async/core/_detector.py b/src/ophyd_async/core/_detector.py index 374c38adc6..d17a94c4d8 100644 --- a/src/ophyd_async/core/_detector.py +++ b/src/ophyd_async/core/_detector.py @@ -50,8 +50,16 @@ class DetectorTrigger(str, Enum): 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(ge=0) + #: Number of triggers that will be sent, (0 means infinite) Can be: + # - A single interger or + # - A list of intergers for mulitple triggers + # Example for tomography: TriggerInfo(number=[2,3,100,3]) + #: This would trigger: + #: - 2 times for dark field images + #: - 3 times for initial flat field images + #: - 1000 times for projections + #: - 3 times for final flat field images + number: int | list[int] #: Sort of triggers that will be sent trigger: DetectorTrigger = Field(default=DetectorTrigger.internal) #: What is the minimum deadtime between triggers @@ -65,10 +73,6 @@ class TriggerInfo(BaseModel): #: 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): @@ -201,6 +205,8 @@ def __init__( self._iterations_completed: int = 0 self._intial_frame: int self._last_frame: int + self._total_frames: int + super().__init__(name) @property @@ -307,7 +313,18 @@ async def prepare(self, value: TriggerInfo) -> None: ) self._trigger_info = value self._initial_frame = await self.writer.get_indices_written() - self._last_frame = self._initial_frame + self._trigger_info.number + self._scan_index: int = 0 + if isinstance(self._trigger_info.number, list): + assert all( + frame >= 0 and type(frame) is int for frame in self._trigger_info.number + ), "Number of frames can only be greater than or equal to 0" + self._total_frames = sum(self._trigger_info.number) + else: + assert ( + self._trigger_info.number >= 0 + ), "Number of frames can only be greater than or equal to 0" + self._total_frames = self._trigger_info.number + self._last_frame = self._initial_frame + self._total_frames self._describe, _ = await asyncio.gather( self.writer.open(value.multiplier), self.controller.prepare(value) ) @@ -318,8 +335,8 @@ async def prepare(self, value: TriggerInfo) -> None: @AsyncStatus.wrap async def kickoff(self): 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}") + if self._iterations_completed >= self._total_frames: + raise Exception(f"Kickoff called more than {self._total_frames}") self._iterations_completed += 1 @WatchableAsyncStatus.wrap @@ -342,9 +359,14 @@ async def complete(self): precision=0, time_elapsed=time.monotonic() - self._fly_start, ) - if index >= self._trigger_info.number: - break - if self._iterations_completed == self._trigger_info.iteration: + if isinstance(self._trigger_info.number, list): + if index >= self._trigger_info.number[self._scan_index]: + self._scan_index += 1 + break + else: + if index >= self._trigger_info.number: + break + if self._iterations_completed == self._total_frames: await self.controller.wait_for_idle() async def describe_collect(self) -> Dict[str, DataKey]: diff --git a/src/ophyd_async/plan_stubs/_fly.py b/src/ophyd_async/plan_stubs/_fly.py index 2cf6f5499e..80180e27ef 100644 --- a/src/ophyd_async/plan_stubs/_fly.py +++ b/src/ophyd_async/plan_stubs/_fly.py @@ -40,13 +40,12 @@ def prepare_static_pcomp_flyer_and_detectors( def prepare_static_seq_table_flyer_and_detectors_with_same_trigger( flyer: StandardFlyer[SeqTableInfo], detectors: List[StandardDetector], - number_of_frames: int, + number_of_frames: int | list[int], exposure: float, shutter_time: float, repeats: int = 1, period: float = 0.0, frame_timeout: Optional[float] = None, - iteration: int = 1, ): """Prepare a hardware triggered flyable and one or more detectors. @@ -62,16 +61,24 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger( raise ValueError("No detectors provided. There must be at least one.") deadtime = max(det.controller.get_deadtime(exposure) for det in detectors) + if isinstance(number_of_frames, list): + assert ( + [number_of_frames[0]] * len(number_of_frames) == number_of_frames + ), "In fly scan number of frames should be same for each iteration" + number_of_frames = [frames * repeats for frames in number_of_frames] + single_frame = number_of_frames[0] + else: + number_of_frames = number_of_frames * repeats + single_frame = number_of_frames trigger_info = TriggerInfo( - number=number_of_frames * repeats, + number=number_of_frames, trigger=DetectorTrigger.constant_gate, deadtime=deadtime, livetime=exposure, frame_timeout=frame_timeout, - iteration=iteration, ) - trigger_time = number_of_frames * (exposure + deadtime) + trigger_time = single_frame * (exposure + deadtime) pre_delay = max(period - 2 * shutter_time - trigger_time, 0) table = ( @@ -84,7 +91,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger( + # Keeping shutter open, do N triggers SeqTable.row( - repeats=number_of_frames, + repeats=single_frame, time1=in_micros(exposure), outa1=True, outb1=True, diff --git a/tests/fastcs/panda/test_hdf_panda.py b/tests/fastcs/panda/test_hdf_panda.py index ab5acd5c20..091ad51e2c 100644 --- a/tests/fastcs/panda/test_hdf_panda.py +++ b/tests/fastcs/panda/test_hdf_panda.py @@ -220,10 +220,9 @@ def flying_plan(): yield from prepare_static_seq_table_flyer_and_detectors_with_same_trigger( # noqa: E501 flyer, [mock_hdf_panda], - number_of_frames=1, + number_of_frames=[1] * iteration, exposure=exposure, shutter_time=shutter_time, - iteration=iteration, ) yield from bps.declare_stream(mock_hdf_panda, name="main_stream", collect=True)