From 3f59ce090dd5c5f94f761b4b80a61cbd34d90564 Mon Sep 17 00:00:00 2001 From: DiamondJoseph <53935796+DiamondJoseph@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:46:30 +0100 Subject: [PATCH] Update to ophyd-async 0.6.0 (#796) * Update to ophyd-async 0.6.0 * Add tests to cover TetrAMM triggering, use asyncio wrapper for standard TimeoutError --- pyproject.toml | 2 +- src/dodal/devices/i24/pmac.py | 6 +- src/dodal/devices/tetramm.py | 27 +++---- tests/devices/unit_tests/test_gridscan.py | 2 +- tests/devices/unit_tests/test_tetramm.py | 96 +++++++++++++++++------ tests/devices/unit_tests/test_xspress3.py | 4 +- 6 files changed, 88 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2e0da3a2b6..18155c1580 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ description = "Ophyd devices and other utils that could be used across DLS beaml dependencies = [ "click", "ophyd", - "ophyd-async>=0.5.2", + "ophyd-async>=0.6,<0.7", "bluesky", "pyepics", "dataclasses-json", diff --git a/src/dodal/devices/i24/pmac.py b/src/dodal/devices/i24/pmac.py index 4a385897f6..d50bf89cd9 100644 --- a/src/dodal/devices/i24/pmac.py +++ b/src/dodal/devices/i24/pmac.py @@ -4,9 +4,9 @@ from bluesky.protocols import Triggerable from ophyd_async.core import ( + CALCULATE_TIMEOUT, DEFAULT_TIMEOUT, AsyncStatus, - CalculateTimeout, SignalBackend, SignalR, SignalRW, @@ -90,7 +90,7 @@ async def set( self, value: LaserSettings, wait=True, - timeout=CalculateTimeout, + timeout=CALCULATE_TIMEOUT, ): await self.signal.set(value.value, wait, timeout) @@ -113,7 +113,7 @@ async def set( self, value: EncReset, wait=True, - timeout=CalculateTimeout, + timeout=CALCULATE_TIMEOUT, ): await self.signal.set(value.value, wait, timeout) diff --git a/src/dodal/devices/tetramm.py b/src/dodal/devices/tetramm.py index 4c7d86076b..20e35e4afd 100644 --- a/src/dodal/devices/tetramm.py +++ b/src/dodal/devices/tetramm.py @@ -3,13 +3,13 @@ from bluesky.protocols import Hints from ophyd_async.core import ( - AsyncStatus, DatasetDescriber, DetectorControl, DetectorTrigger, Device, PathProvider, StandardDetector, + TriggerInfo, set_and_wait_for_value, soft_signal_r_and_setter, ) @@ -113,29 +113,24 @@ def get_deadtime(self, exposure: float | None) -> float: # 2 internal clock cycles. Best effort approximation return 2 / self.base_sample_rate - async def arm( - self, - num: int, - trigger: DetectorTrigger = DetectorTrigger.edge_trigger, - exposure: float | None = None, - ) -> AsyncStatus: - if exposure is None: - raise ValueError( - "Tetramm does not support arm without exposure time. " - "Is this a software scan? Tetramm only supports hardware scans." - ) - self._validate_trigger(trigger) + async def prepare(self, trigger_info: TriggerInfo): + self._validate_trigger(trigger_info.trigger) + assert trigger_info.livetime is not None # trigger mode must be set first and on its own! await self._drv.trigger_mode.set(TetrammTrigger.ExtTrigger) await asyncio.gather( - self._drv.averaging_time.set(exposure), self.set_exposure(exposure) + self._drv.averaging_time.set(trigger_info.livetime), + self.set_exposure(trigger_info.livetime), ) - status = await set_and_wait_for_value(self._drv.acquire, True) + async def arm(self): + self._arm_status = await set_and_wait_for_value(self._drv.acquire, True) - return status + async def wait_for_idle(self): + if self._arm_status: + await self._arm_status def _validate_trigger(self, trigger: DetectorTrigger) -> None: supported_trigger_types = { diff --git a/tests/devices/unit_tests/test_gridscan.py b/tests/devices/unit_tests/test_gridscan.py index f69d829488..586201d424 100644 --- a/tests/devices/unit_tests/test_gridscan.py +++ b/tests/devices/unit_tests/test_gridscan.py @@ -1,4 +1,4 @@ -from asyncio import wait_for +from asyncio import TimeoutError, wait_for from dataclasses import dataclass import numpy as np diff --git a/tests/devices/unit_tests/test_tetramm.py b/tests/devices/unit_tests/test_tetramm.py index 9ee73dfab0..9bf3479f24 100644 --- a/tests/devices/unit_tests/test_tetramm.py +++ b/tests/devices/unit_tests/test_tetramm.py @@ -48,6 +48,7 @@ async def tetramm(static_path_provider: PathProvider) -> TetrammDetector: static_path_provider, name=TEST_TETRAMM_NAME, ) + set_mock_value(tetramm.hdf.file_path_exists, True) return tetramm @@ -66,8 +67,9 @@ def supported_trigger_info() -> TriggerInfo: async def test_max_frame_rate_is_calculated_correctly( tetramm_controller: TetrammController, ): - status = await tetramm_controller.arm(1, DetectorTrigger.edge_trigger, 2.0) - await status + await tetramm_controller.prepare( + TriggerInfo(number=1, trigger=DetectorTrigger.edge_trigger, livetime=2.0) + ) assert tetramm_controller.minimum_exposure == 0.1 assert tetramm_controller.max_frame_rate == 10.0 @@ -106,13 +108,15 @@ async def test_min_exposure_is_calculated_correctly( ) # 100_000 / 17 ~ 5800; 5800 * 0.01 = 58; 58 << tetramm_controller.maximum_readings_per_frame - status = await tetramm_controller.arm(1, DetectorTrigger.edge_trigger, 0.01) - await status + await tetramm_controller.prepare( + TriggerInfo(number=1, trigger=DetectorTrigger.edge_trigger, livetime=0.01) + ) assert tetramm_controller.readings_per_frame == int(readings_per_time * 0.01) # 100_000 / 17 ~ 5800; 5800 * 0.2 = 1160; 1160 > tetramm_controller.maximum_readings_per_frame - status = await tetramm_controller.arm(1, DetectorTrigger.edge_trigger, 0.2) - await status + await tetramm_controller.prepare( + TriggerInfo(number=1, trigger=DetectorTrigger.edge_trigger, livetime=0.2) + ) assert ( tetramm_controller.readings_per_frame == tetramm_controller.maximum_readings_per_frame @@ -120,12 +124,14 @@ async def test_min_exposure_is_calculated_correctly( # 100_000 / 17 ~ 5800; 5800 * 0.2 = 1160; 1160 < 1200 tetramm_controller.maximum_readings_per_frame = 1200 - status = await tetramm_controller.arm(1, DetectorTrigger.edge_trigger, 0.1) - await status + await tetramm_controller.prepare( + TriggerInfo(number=1, trigger=DetectorTrigger.edge_trigger, livetime=0.1) + ) assert tetramm_controller.readings_per_frame == int(readings_per_time * 0.1) VALID_TEST_EXPOSURE_TIME = 1 / 19 +VALID_TEST_DEADTIME = 1 / 100 async def test_set_exposure_updates_values_per_reading( @@ -153,7 +159,9 @@ async def test_set_invalid_exposure_for_number_of_values_per_reading( ValueError, match="Tetramm exposure time must be at least 5e-05s, asked to set it to 4e-05s", ): - await (await tetramm_controller.arm(-1, DetectorTrigger.edge_trigger, 4e-5)) + await tetramm_controller.prepare( + TriggerInfo(number=0, trigger=DetectorTrigger.edge_trigger, livetime=4e-5) + ) @pytest.mark.parametrize( @@ -211,10 +219,13 @@ async def test_arm_raises_value_error_for_invalid_trigger_type( f"types: {accepted_types} but was asked to " f"use {trigger_type}", ): - await tetramm_controller.arm( - -1, - trigger_type, - VALID_TEST_EXPOSURE_TIME, + await tetramm_controller.prepare( + TriggerInfo( + number=0, + trigger=trigger_type, + livetime=VALID_TEST_EXPOSURE_TIME, + deadtime=VALID_TEST_DEADTIME, + ) ) @@ -226,29 +237,36 @@ async def test_arm_raises_value_error_for_invalid_trigger_type( ], ) async def test_arm_sets_signals_correctly_given_valid_inputs( - tetramm_controller: TetrammController, - tetramm_driver: TetrammDriver, + tetramm: TetrammDetector, trigger_type: DetectorTrigger, ): - arm_status = await tetramm_controller.arm( - -1, trigger_type, VALID_TEST_EXPOSURE_TIME + await tetramm.prepare( + TriggerInfo( + number=0, + trigger=trigger_type, + livetime=VALID_TEST_EXPOSURE_TIME, + deadtime=VALID_TEST_DEADTIME, + ) ) - await arm_status - await assert_armed(tetramm_driver) + await assert_armed(tetramm.drv) async def test_disarm_disarms_driver( - tetramm_controller: TetrammController, - tetramm_driver: TetrammDriver, + tetramm: TetrammDetector, ): + tetramm_driver = tetramm.drv assert (await tetramm_driver.acquire.get_value()) == 0 - arm_status = await tetramm_controller.arm( - -1, DetectorTrigger.edge_trigger, VALID_TEST_EXPOSURE_TIME + await tetramm.prepare( + TriggerInfo( + number=0, + trigger=DetectorTrigger.edge_trigger, + livetime=VALID_TEST_EXPOSURE_TIME, + deadtime=VALID_TEST_DEADTIME, + ) ) - await arm_status assert (await tetramm_driver.acquire.get_value()) == 1 - await tetramm_controller.disarm() + await tetramm.controller.disarm() assert (await tetramm_driver.acquire.get_value()) == 0 @@ -328,7 +346,33 @@ async def test_stage_sets_up_accurate_describe_output( async def test_error_if_armed_without_exposure(tetramm_controller: TetrammController): with pytest.raises(ValueError): - await tetramm_controller.arm(10, DetectorTrigger.internal) + await tetramm_controller.prepare( + TriggerInfo(number=10, trigger=DetectorTrigger.internal) + ) + + +async def test_pilatus_controller( + RE, + tetramm: TetrammDetector, +): + controller = tetramm.controller + driver = tetramm.drv + await controller.prepare( + TriggerInfo( + number=1, + trigger=DetectorTrigger.constant_gate, + livetime=VALID_TEST_EXPOSURE_TIME, + deadtime=VALID_TEST_DEADTIME, + ) + ) + await controller.arm() + await controller.wait_for_idle() + + assert await driver.acquire.get_value() is True + + await controller.disarm() + + assert await driver.acquire.get_value() is False async def assert_armed(driver: TetrammDriver) -> None: diff --git a/tests/devices/unit_tests/test_xspress3.py b/tests/devices/unit_tests/test_xspress3.py index a718e1400b..cee4a0a75b 100644 --- a/tests/devices/unit_tests/test_xspress3.py +++ b/tests/devices/unit_tests/test_xspress3.py @@ -68,7 +68,7 @@ async def test_stage_fail_on_detector_not_busy_state( set_mock_value(mock_xspress3mini.acquire_rbv, AcquireRBVState.DONE) set_mock_value(mock_xspress3mini.detector_state, DetectorState.IDLE) mock_xspress3mini.timeout = 0.1 - with pytest.raises(TimeoutError): + with pytest.raises(asyncio.TimeoutError): await mock_xspress3mini.stage() with pytest.raises(FailedStatus): RE(bps.stage(mock_xspress3mini, wait=True)) @@ -84,7 +84,7 @@ async def test_stage_fail_to_acquire_timeout( set_mock_value(mock_xspress3mini.detector_state, DetectorState.ACQUIRE) set_mock_value(mock_xspress3mini.acquire_rbv, AcquireRBVState.DONE) mock_xspress3mini.timeout = 0.1 - with pytest.raises(TimeoutError): + with pytest.raises(asyncio.TimeoutError): await mock_xspress3mini.stage() with pytest.raises(FailedStatus): RE(bps.stage(mock_xspress3mini, wait=True))