diff --git a/src/ophyd_async/core/_detector.py b/src/ophyd_async/core/_detector.py index ca9f756fe6..adea471475 100644 --- a/src/ophyd_async/core/_detector.py +++ b/src/ophyd_async/core/_detector.py @@ -98,6 +98,9 @@ class TriggerInfo(ConfinedModel): number_of_events: NonNegativeInt = Field(default=1) """Number of bluesky events that will be emitted, (0 means infinite).""" + images_per_exposure: PositiveInt = Field(default=1) + """Number of frames the detector should produce for every trigger sent to it.""" + exposure_timeout: float = Field( default_factory=lambda d: d["livetime"] + d["deadtime"] + DEFAULT_TIMEOUT, gt=0, @@ -166,10 +169,11 @@ async def prepare_internal(self, num: int, livetime: float, deadtime: float): """ raise NotImplementedError(self) - async def prepare_edge(self, num: int, livetime: float): + async def prepare_edge(self, num: int, images_per_exposure: int, livetime: float): """Prepare the detector to take external edge triggered exposures. :param num: the number of exposures to take + :param images_per_trigger: the number frames each exposure should produce :param livetime: how long the exposure should be, 0 means what is currently set """ raise NotImplementedError(self) @@ -601,6 +605,7 @@ async def prepare(self, value: TriggerInfo) -> None: case DetectorTrigger.EXTERNAL_EDGE: await self._trigger_logic.prepare_edge( num=value.number_of_exposures, + images_per_exposure=value.images_per_exposure, livetime=value.livetime, ) case DetectorTrigger.EXTERNAL_LEVEL: diff --git a/src/ophyd_async/epics/adandor.py b/src/ophyd_async/epics/adandor.py index 5e1feef409..9c1ef01126 100644 --- a/src/ophyd_async/epics/adandor.py +++ b/src/ophyd_async/epics/adandor.py @@ -77,7 +77,7 @@ async def prepare_internal(self, num: int, livetime: float, deadtime: float): await self.driver.trigger_mode.set(Andor2TriggerMode.INTERNAL) await prepare_exposures(self.driver, num or _MAX_NUM_IMAGE, livetime, deadtime) - async def prepare_edge(self, num: int, livetime: float): + async def prepare_edge(self, num: int, images_per_exposure, livetime: float): await self.driver.trigger_mode.set(Andor2TriggerMode.EXT_TRIGGER) await prepare_exposures(self.driver, num or _MAX_NUM_IMAGE, livetime) diff --git a/src/ophyd_async/epics/adaravis.py b/src/ophyd_async/epics/adaravis.py index 695c78e886..f156b05db1 100644 --- a/src/ophyd_async/epics/adaravis.py +++ b/src/ophyd_async/epics/adaravis.py @@ -75,7 +75,7 @@ async def prepare_internal(self, num: int, livetime: float, deadtime: float): await self.driver.trigger_mode.set(OnOff.OFF) await prepare_exposures(self.driver, num, livetime, deadtime) - async def prepare_edge(self, num: int, livetime: float): + async def prepare_edge(self, num: int, images_per_exposure, livetime: float): # Trigger on the rising edge of Line1 # trigger mode must be set first and on its own! # Hardware race condition in Aravis firmware requires setting trigger mode diff --git a/src/ophyd_async/epics/adkinetix.py b/src/ophyd_async/epics/adkinetix.py index ca855bb581..68ce9eeb37 100644 --- a/src/ophyd_async/epics/adkinetix.py +++ b/src/ophyd_async/epics/adkinetix.py @@ -75,7 +75,7 @@ async def prepare_internal(self, num: int, livetime: float, deadtime: float): await self.driver.trigger_mode.set(KinetixTriggerMode.INTERNAL) await prepare_exposures(self.driver, num, livetime, deadtime) - async def prepare_edge(self, num: int, livetime: float): + async def prepare_edge(self, num: int, images_per_exposure, livetime: float): await self.driver.trigger_mode.set(KinetixTriggerMode.EDGE) await prepare_exposures(self.driver, num, livetime) diff --git a/src/ophyd_async/epics/admerlin.py b/src/ophyd_async/epics/admerlin.py index 0ace7baa64..0a7ae0a570 100644 --- a/src/ophyd_async/epics/admerlin.py +++ b/src/ophyd_async/epics/admerlin.py @@ -103,7 +103,7 @@ async def prepare_internal(self, num: int, livetime: float, deadtime: float): await self.driver.trigger_mode.set(MerlinTriggerMode.INTERNAL) await prepare_exposures(self.driver, num, livetime, deadtime) - async def prepare_edge(self, num: int, livetime: float): + async def prepare_edge(self, num: int, images_per_exposure, livetime: float): # Is this the right trigger mode? await self.driver.trigger_mode.set(MerlinTriggerMode.TRIGGER_START_RISING) await prepare_exposures(self.driver, num, livetime) diff --git a/src/ophyd_async/epics/adpilatus.py b/src/ophyd_async/epics/adpilatus.py index a5209a0276..15e6099c4f 100644 --- a/src/ophyd_async/epics/adpilatus.py +++ b/src/ophyd_async/epics/adpilatus.py @@ -116,7 +116,7 @@ async def prepare_internal(self, num: int, livetime: float, deadtime: float): await self.driver.trigger_mode.set(PilatusTriggerMode.INTERNAL) await prepare_exposures(self.driver, num or _MAX_NUM_IMAGE, livetime, deadtime) - async def prepare_edge(self, num: int, livetime: float): + async def prepare_edge(self, num: int, images_per_exposure, livetime: float): await self.driver.trigger_mode.set(self.edge_trigger_mode) await prepare_exposures(self.driver, num or _MAX_NUM_IMAGE, livetime) diff --git a/src/ophyd_async/epics/advimba.py b/src/ophyd_async/epics/advimba.py index f6afd41ba9..17bac5b4a7 100644 --- a/src/ophyd_async/epics/advimba.py +++ b/src/ophyd_async/epics/advimba.py @@ -116,7 +116,7 @@ async def prepare_internal(self, num: int, livetime: float, deadtime: float): ) await prepare_exposures(self.driver, num, livetime, deadtime) - async def prepare_edge(self, num: int, livetime: float): + async def prepare_edge(self, num: int, images_per_exposure, livetime: float): await asyncio.gather( self.driver.trigger_mode.set(OnOff.ON), self.driver.exposure_mode.set(VimbaExposeOutMode.TIMED), diff --git a/src/ophyd_async/fastcs/eiger/_trigger_logic.py b/src/ophyd_async/fastcs/eiger/_trigger_logic.py index e4f7a14c57..f4fd458b71 100644 --- a/src/ophyd_async/fastcs/eiger/_trigger_logic.py +++ b/src/ophyd_async/fastcs/eiger/_trigger_logic.py @@ -10,11 +10,13 @@ async def _prepare_detector( detector: EigerDetectorIO, trigger_mode: EigerTriggerMode, num: int, + images_per_exposure: int, livetime: float = 0.0, ): coros = [ detector.trigger_mode.set(trigger_mode), - detector.nimages.set(num), + detector.ntrigger.set(num), + detector.nimages.set(images_per_exposure), ] if livetime: coros += [ @@ -33,10 +35,14 @@ def get_deadtime(self, config_values: SignalDict) -> float: return 0.0001 async def prepare_internal(self, num: int, livetime: float, deadtime: float): - await _prepare_detector(self.detector, EigerTriggerMode.INTERNAL, num, livetime) + await _prepare_detector( + self.detector, EigerTriggerMode.INTERNAL, 1, num, livetime + ) - async def prepare_edge(self, num: int, livetime: float): - await _prepare_detector(self.detector, EigerTriggerMode.EDGE, num, livetime) + async def prepare_edge(self, num: int, images_per_exposure: int, livetime: float): + await _prepare_detector( + self.detector, EigerTriggerMode.EDGE, num, images_per_exposure, livetime + ) async def prepare_level(self, num: int): - await _prepare_detector(self.detector, EigerTriggerMode.GATE, num) + await _prepare_detector(self.detector, EigerTriggerMode.GATE, 1, num) diff --git a/src/ophyd_async/fastcs/jungfrau/_trigger_logic.py b/src/ophyd_async/fastcs/jungfrau/_trigger_logic.py index b06ae1f7cc..6bc7ea1cb0 100644 --- a/src/ophyd_async/fastcs/jungfrau/_trigger_logic.py +++ b/src/ophyd_async/fastcs/jungfrau/_trigger_logic.py @@ -104,7 +104,7 @@ async def prepare_internal(self, num: int, livetime: float, deadtime: float): self.detector, JungfrauTriggerMode.INTERNAL, num, livetime, deadtime ) - async def prepare_edge(self, num: int, livetime: float): + async def prepare_edge(self, num: int, images_per_exposure, livetime: float): acquisition_type = await get_and_reset_acquisition_type(self.acquisition_type) if acquisition_type == AcquisitionType.PEDESTAL: raise ValueError( diff --git a/tests/unit_tests/core/test_standard_detector.py b/tests/unit_tests/core/test_standard_detector.py index 0c9b9ab814..1fbf6315aa 100644 --- a/tests/unit_tests/core/test_standard_detector.py +++ b/tests/unit_tests/core/test_standard_detector.py @@ -63,7 +63,7 @@ async def prepare_internal(self, num: int, livetime: float, deadtime: float): self.trigger = DetectorTrigger.INTERNAL self.num, self.livetime, self.deadtime = num, livetime, deadtime - async def prepare_edge(self, num: int, livetime: float): + async def prepare_edge(self, num: int, images_per_exposure, livetime: float): self.trigger = DetectorTrigger.EXTERNAL_EDGE self.num, self.livetime = num, livetime @@ -1071,7 +1071,7 @@ async def test_trigger_logic_not_implemented_errors(): await logic.prepare_internal(1, 0.1, 0.01) with pytest.raises(NotImplementedError): - await logic.prepare_edge(1, 0.1) + await logic.prepare_edge(1, 1, 0.1) with pytest.raises(NotImplementedError): await logic.prepare_level(1) diff --git a/tests/unit_tests/fastcs/eiger/test_eiger_detector.py b/tests/unit_tests/fastcs/eiger/test_eiger_detector.py index 2570f72f28..f6b624394a 100644 --- a/tests/unit_tests/fastcs/eiger/test_eiger_detector.py +++ b/tests/unit_tests/fastcs/eiger/test_eiger_detector.py @@ -40,6 +40,7 @@ async def test_prepare_internal_calls_correct_parameters( detector, [ call.detector.trigger_mode.put(EigerTriggerMode.INTERNAL), + call.detector.ntrigger.put(1), call.detector.nimages.put(10), call.detector.count_time.put(0.1), call.detector.frame_time.put(0.1), @@ -57,6 +58,69 @@ async def test_prepare_internal_calls_correct_parameters( ) +async def test_prepare_edge_calls_correct_parameters(detector: EigerDetector, tmp_path): + await detector.prepare( + TriggerInfo( + number_of_events=10, + images_per_exposure=5, + livetime=0.1, + trigger=DetectorTrigger.EXTERNAL_EDGE, + ) + ) + assert_has_calls( + detector, + [ + call.detector.trigger_mode.put(EigerTriggerMode.EDGE), + call.detector.ntrigger.put(10), + call.detector.nimages.put(5), + call.detector.count_time.put(0.1), + call.detector.frame_time.put(0.1), + call.od.acquisition_id.put(""), + call.od.file_prefix.put("filename"), + call.od.file_path.put(str(tmp_path)), + call.od.fp.data_compression.put("BSLZ4"), + call.od.fp.data_datatype.put("uint16"), + call.od.fp.frames.put(0), + call.od.block_size.put(100000), + call.pixel_mask.put(str(tmp_path / "filename_meta.h5") + "//mask"), + call.od.fp.start_writing.execute(), + call.arm_when_ready.execute(), + ], + reset_after=False, + ) + + +async def test_prepare_level_calls_correct_parameters( + detector: EigerDetector, tmp_path +): + await detector.prepare( + TriggerInfo( + number_of_events=10, + livetime=0.1, + trigger=DetectorTrigger.EXTERNAL_LEVEL, + ) + ) + assert_has_calls( + detector, + [ + call.detector.trigger_mode.put(EigerTriggerMode.GATE), + call.detector.ntrigger.put(1), + call.detector.nimages.put(10), + call.od.acquisition_id.put(""), + call.od.file_prefix.put("filename"), + call.od.file_path.put(str(tmp_path)), + call.od.fp.data_compression.put("BSLZ4"), + call.od.fp.data_datatype.put("uint16"), + call.od.fp.frames.put(0), + call.od.block_size.put(100000), + call.pixel_mask.put(str(tmp_path / "filename_meta.h5") + "//mask"), + call.od.fp.start_writing.execute(), + call.arm_when_ready.execute(), + ], + reset_after=False, + ) + + async def test_deadtime_correct(detector: EigerDetector): supported_triggers, deadtime = await detector.get_trigger_deadtime() assert supported_triggers == {