From 59d30c8d3a978abfe7c8048d910de859702c6943 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 17 Feb 2026 11:42:38 +0000 Subject: [PATCH 1/4] ElectronAnalyser no longer depends directly on sequence file path to work with BlueAPI --- .../electron_analyser/base/base_detector.py | 44 +++--- .../electron_analyser/specs/specs_detector.py | 16 +-- .../vgscienta/vgscienta_detector.py | 13 +- .../testing/electron_analyser/__init__.py | 6 - .../electron_analyser/device_factory.py | 57 -------- .../base/test_base_controller.py | 107 ++------------- .../base/test_base_detector.py | 71 +++------- .../base/test_base_driver_io.py | 25 +--- .../base/test_base_region.py | 5 +- tests/devices/electron_analyser/conftest.py | 126 ++++++++++++------ .../helper_util/assert_func.py | 2 +- .../electron_analyser/helper_util/sequence.py | 41 ++++-- .../specs/test_specs_detector.py | 27 +--- .../specs/test_specs_driver_io.py | 18 +-- .../specs/test_specs_region.py | 11 +- .../vgscienta/test_vgscienta_detector.py | 24 +--- .../vgscienta/test_vgscienta_driver_io.py | 19 +-- .../vgscienta/test_vgsicenta_region.py | 6 +- 18 files changed, 212 insertions(+), 406 deletions(-) delete mode 100644 src/dodal/testing/electron_analyser/__init__.py delete mode 100644 src/dodal/testing/electron_analyser/device_factory.py diff --git a/src/dodal/devices/electron_analyser/base/base_detector.py b/src/dodal/devices/electron_analyser/base/base_detector.py index 320acd97357..8fcab34fb63 100644 --- a/src/dodal/devices/electron_analyser/base/base_detector.py +++ b/src/dodal/devices/electron_analyser/base/base_detector.py @@ -10,7 +10,6 @@ TriggerInfo, ) -from dodal.common.data_util import load_json_file_to_class from dodal.devices.electron_analyser.base.base_controller import ( ElectronAnalyserController, ) @@ -20,9 +19,7 @@ ) from dodal.devices.electron_analyser.base.base_region import ( GenericRegion, - GenericSequence, TAbstractBaseRegion, - TAbstractBaseSequence, ) @@ -111,6 +108,9 @@ async def trigger(self) -> None: await super().trigger() +# Used in sm-bluesky, but will hopefully be removed along with +# ElectronAnalyserRegionDetector in future. Blocked by: +# https://github.com/bluesky/bluesky/pull/1978 GenericElectronAnalyserRegionDetector = ElectronAnalyserRegionDetector[ GenericAnalyserDriverIO, GenericRegion ] @@ -123,7 +123,7 @@ async def trigger(self) -> None: class ElectronAnalyserDetector( BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO, TAbstractBaseRegion], Stageable, - Generic[TAbstractBaseSequence, TAbstractAnalyserDriverIO, TAbstractBaseRegion], + Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion], ): """Electron analyser detector with the additional functionality to load a sequence file and create a list of temporary ElectronAnalyserRegionDetector objects. These @@ -132,13 +132,13 @@ class ElectronAnalyserDetector( def __init__( self, - sequence_class: type[TAbstractBaseSequence], controller: ElectronAnalyserController[ TAbstractAnalyserDriverIO, TAbstractBaseRegion ], name: str = "", ): - self._sequence_class = sequence_class + # Save on device so connect works and names it as child + self.driver = controller.driver super().__init__(controller, name) @AsyncStatus.wrap @@ -160,38 +160,24 @@ async def unstage(self) -> None: """Disarm the detector.""" await self._controller.disarm() - def load_sequence(self, filename: str) -> TAbstractBaseSequence: - """Load the sequence data from a provided json file into a sequence class. - - Args: - filename (str): Path to the sequence file containing the region data. - - Returns: - Pydantic model representing the sequence file. - """ - return load_json_file_to_class(self._sequence_class, filename) - def create_region_detector_list( - self, filename: str, enabled_only=True + self, regions: list[TAbstractBaseRegion] ) -> list[ ElectronAnalyserRegionDetector[TAbstractAnalyserDriverIO, TAbstractBaseRegion] ]: - """Create a list of detectors equal to the number of regions in a sequence file. - Each detector is responsible for setting up a specific region. + """This method can hopefully be dropped when this is merged and released. + https://github.com/bluesky/bluesky/pull/1978. + + Create a list of detectors equal to the number of regions. Each detector is + responsible for setting up a specific region. Args: - filename (str): Path to the sequence file containing the region data. - enabled_only (bool, optional): If true, only include the region if enabled - is True. + regions: The list of regions to give to each region detector. Returns: List of ElectronAnalyserRegionDetector, equal to the number of regions in - the sequence file. + the sequence file. """ - seq = self.load_sequence(filename) - regions: list[TAbstractBaseRegion] = ( - seq.get_enabled_regions() if enabled_only else seq.regions - ) return [ ElectronAnalyserRegionDetector[ TAbstractAnalyserDriverIO, TAbstractBaseRegion @@ -201,7 +187,7 @@ def create_region_detector_list( GenericElectronAnalyserDetector = ElectronAnalyserDetector[ - GenericSequence, GenericAnalyserDriverIO, GenericRegion + GenericAnalyserDriverIO, GenericRegion ] TElectronAnalyserDetector = TypeVar( "TElectronAnalyserDetector", diff --git a/src/dodal/devices/electron_analyser/specs/specs_detector.py b/src/dodal/devices/electron_analyser/specs/specs_detector.py index fb54f47c49f..6d8eacaf515 100644 --- a/src/dodal/devices/electron_analyser/specs/specs_detector.py +++ b/src/dodal/devices/electron_analyser/specs/specs_detector.py @@ -7,17 +7,13 @@ from dodal.devices.electron_analyser.base.base_region import TLensMode, TPsuMode from dodal.devices.electron_analyser.base.energy_sources import AbstractEnergySource from dodal.devices.electron_analyser.specs.specs_driver_io import SpecsAnalyserDriverIO -from dodal.devices.electron_analyser.specs.specs_region import ( - SpecsRegion, - SpecsSequence, -) +from dodal.devices.electron_analyser.specs.specs_region import SpecsRegion from dodal.devices.fast_shutter import FastShutter from dodal.devices.selectable_source import SourceSelector class SpecsDetector( ElectronAnalyserDetector[ - SpecsSequence[TLensMode, TPsuMode], SpecsAnalyserDriverIO[TLensMode, TPsuMode], SpecsRegion[TLensMode, TPsuMode], ], @@ -33,15 +29,11 @@ def __init__( source_selector: SourceSelector | None = None, name: str = "", ): - # Save to class so takes part with connect() - self.driver = SpecsAnalyserDriverIO[TLensMode, TPsuMode]( + driver = SpecsAnalyserDriverIO[TLensMode, TPsuMode]( prefix, lens_mode_type, psu_mode_type ) - controller = ElectronAnalyserController[ SpecsAnalyserDriverIO[TLensMode, TPsuMode], SpecsRegion[TLensMode, TPsuMode] - ](self.driver, energy_source, shutter, source_selector) - - sequence_class = SpecsSequence[lens_mode_type, psu_mode_type] + ](driver, energy_source, shutter, source_selector) - super().__init__(sequence_class, controller, name) + super().__init__(controller, name) diff --git a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py index 6e508fec5f0..29626b2d353 100644 --- a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +++ b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py @@ -12,7 +12,6 @@ from dodal.devices.electron_analyser.vgscienta.vgscienta_region import ( TPassEnergyEnum, VGScientaRegion, - VGScientaSequence, ) from dodal.devices.fast_shutter import FastShutter from dodal.devices.selectable_source import SourceSelector @@ -20,7 +19,6 @@ class VGScientaDetector( ElectronAnalyserDetector[ - VGScientaSequence[TLensMode, TPsuMode, TPassEnergyEnum], VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum], VGScientaRegion[TLensMode, TPassEnergyEnum], ], @@ -37,17 +35,12 @@ def __init__( source_selector: SourceSelector | None = None, name: str = "", ): - # Save to class so takes part with connect() - self.driver = VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum]( + driver = VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum]( prefix, lens_mode_type, psu_mode_type, pass_energy_type ) - controller = ElectronAnalyserController[ VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum], VGScientaRegion[TLensMode, TPassEnergyEnum], - ](self.driver, energy_source, shutter, source_selector) + ](driver, energy_source, shutter, source_selector) - sequence_class = VGScientaSequence[ - lens_mode_type, psu_mode_type, pass_energy_type - ] - super().__init__(sequence_class, controller, name) + super().__init__(controller, name) diff --git a/src/dodal/testing/electron_analyser/__init__.py b/src/dodal/testing/electron_analyser/__init__.py deleted file mode 100644 index dffbe8d4923..00000000000 --- a/src/dodal/testing/electron_analyser/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .device_factory import create_detector, create_driver - -__all__ = [ - "create_detector", - "create_driver", -] diff --git a/src/dodal/testing/electron_analyser/device_factory.py b/src/dodal/testing/electron_analyser/device_factory.py deleted file mode 100644 index 961be125b2a..00000000000 --- a/src/dodal/testing/electron_analyser/device_factory.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Any, get_args, get_origin - -from dodal.devices.electron_analyser.base.base_detector import TElectronAnalyserDetector -from dodal.devices.electron_analyser.base.base_driver_io import ( - TAbstractAnalyserDriverIO, -) -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, - VGScientaDetector, -) - - -def create_driver( - driver_class: type[TAbstractAnalyserDriverIO], - **kwargs: Any, -) -> TAbstractAnalyserDriverIO: - """Helper function that helps to reduce the code to setup an analyser driver. The - parameters used for the enum types are taken directly from the subscripts of the - class so the user only needs to provide it in one place. - - Args: - driver_class: The class for the driver which must include the enums in the - subscript, for example MyDriverClass[MyLensMode, ...]. - kwargs: Additional key worded arguments that the driver needs for initalisation. - """ - parameters = { - "lens_mode_type": get_args(driver_class)[0], - "psu_mode_type": get_args(driver_class)[1], - } - if get_origin(driver_class) is VGScientaAnalyserDriverIO: - parameters["pass_energy_type"] = get_args(driver_class)[2] - - return driver_class(**(parameters | kwargs)) - - -def create_detector( - detector_class: type[TElectronAnalyserDetector], - **kwargs: Any, -) -> TElectronAnalyserDetector: - """Helper function that helps to reduce the code to setup an analyser detector. The - parameters used for the enum types are taken directly from the subscripts of the - class so the user only needs to provide it in one place. - - Args: - detector_class: The class for the detector which must include the enums in the - subscript, for example MyDetectorClass[MyLensMode, ...]. - kwargs: Additional key worded arguments that the detector needs for - initalisation. - """ - parameters = { - "lens_mode_type": get_args(detector_class)[0], - "psu_mode_type": get_args(detector_class)[1], - } - if get_origin(detector_class) is VGScientaDetector: - parameters["pass_energy_type"] = get_args(detector_class)[2] - - return detector_class(**(parameters | kwargs)) diff --git a/tests/devices/electron_analyser/base/test_base_controller.py b/tests/devices/electron_analyser/base/test_base_controller.py index aa899c66fc1..62a353c5d56 100644 --- a/tests/devices/electron_analyser/base/test_base_controller.py +++ b/tests/devices/electron_analyser/base/test_base_controller.py @@ -1,121 +1,32 @@ from unittest.mock import AsyncMock, patch import pytest -from ophyd_async.core import InOut, TriggerInfo, get_mock_put, init_devices +from ophyd_async.core import TriggerInfo, get_mock_put -from dodal.beamlines import b07, i09 from dodal.devices.electron_analyser.base import ( AbstractAnalyserDriverIO, AbstractBaseRegion, - DualEnergySource, - EnergySource, -) -from dodal.devices.electron_analyser.base.base_controller import ( ElectronAnalyserController, + GenericElectronAnalyserController, + GenericElectronAnalyserDetector, + GenericSequence, ) -from dodal.devices.electron_analyser.specs import SpecsAnalyserDriverIO -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, -) -from dodal.devices.fast_shutter import DualFastShutter, GenericFastShutter -from dodal.devices.selectable_source import SourceSelector -from dodal.testing.electron_analyser import create_driver from tests.devices.electron_analyser.helper_util import ( TEST_SEQUENCE_REGION_NAMES, get_test_sequence, ) -@pytest.fixture( - params=[ - SpecsAnalyserDriverIO[b07.LensMode, b07.PsuMode], - VGScientaAnalyserDriverIO[i09.LensMode, i09.PsuMode, i09.PassEnergy], - ] -) -async def sim_driver( - request: pytest.FixtureRequest, -) -> AbstractAnalyserDriverIO: - async with init_devices(mock=True): - sim_detector = create_driver( - request.param, - prefix="TEST:", - ) - return sim_detector - - @pytest.fixture -def sequence_file_path( - sim_driver: AbstractAnalyserDriverIO, -) -> str: - return get_test_sequence(type(sim_driver)) - - -@pytest.fixture -def shutter1() -> GenericFastShutter[InOut]: - with init_devices(mock=True): - shutter1 = GenericFastShutter[InOut]( - pv="TEST:", - open_state=InOut.OUT, - close_state=InOut.IN, - ) - return shutter1 - - -@pytest.fixture -def shutter2() -> GenericFastShutter[InOut]: - with init_devices(mock=True): - shutter2 = GenericFastShutter[InOut]( - pv="TEST:", - open_state=InOut.OUT, - close_state=InOut.IN, - ) - return shutter2 - - -@pytest.fixture -def dual_fast_shutter( - shutter1: GenericFastShutter[InOut], - shutter2: GenericFastShutter[InOut], - source_selector: SourceSelector, -) -> DualFastShutter[InOut]: - with init_devices(mock=True): - dual_fast_shutter = DualFastShutter[InOut]( - shutter1, - shutter2, - source_selector.selected_source, - ) - return dual_fast_shutter +def sequence(sim_detector: GenericElectronAnalyserDetector) -> GenericSequence: + return get_test_sequence(type(sim_detector)) @pytest.fixture def analyser_controller( - sim_driver: AbstractAnalyserDriverIO, - single_energy_source: EnergySource, - dual_energy_source: DualEnergySource, - dual_fast_shutter: DualFastShutter, - source_selector: SourceSelector, -) -> ElectronAnalyserController[AbstractAnalyserDriverIO, AbstractBaseRegion]: - if isinstance(sim_driver, SpecsAnalyserDriverIO): - controller = ElectronAnalyserController[ - AbstractAnalyserDriverIO, AbstractBaseRegion - ]( - sim_driver, - single_energy_source, - source_selector=None, - ) - elif isinstance(sim_driver, VGScientaAnalyserDriverIO): - controller = ElectronAnalyserController[ - AbstractAnalyserDriverIO, AbstractBaseRegion - ]( - sim_driver, - dual_energy_source, - dual_fast_shutter, - source_selector, - ) - else: - raise ValueError(f"sim_driver is of unsupported type {type(sim_driver)}.") - - return controller + sim_detector: GenericElectronAnalyserDetector, +) -> GenericElectronAnalyserController: + return sim_detector._controller async def test_controller_prepare_sets_excitation_energy( diff --git a/tests/devices/electron_analyser/base/test_base_detector.py b/tests/devices/electron_analyser/base/test_base_detector.py index 11f76cfd621..2b0ffee8475 100644 --- a/tests/devices/electron_analyser/base/test_base_detector.py +++ b/tests/devices/electron_analyser/base/test_base_detector.py @@ -3,45 +3,23 @@ import pytest from bluesky import plan_stubs as bps from bluesky.run_engine import RunEngine -from ophyd_async.core import TriggerInfo, init_devices, set_mock_value +from ophyd_async.core import TriggerInfo from ophyd_async.testing import ( assert_configuration, assert_reading, ) -import dodal.devices.beamlines.b07 as b07 -import dodal.devices.beamlines.i09 as i09 from dodal.devices.electron_analyser.base import ( - EnergySource, GenericBaseElectronAnalyserDetector, GenericElectronAnalyserDetector, + GenericSequence, ) -from dodal.devices.electron_analyser.base.energy_sources import EnergySource -from dodal.devices.electron_analyser.specs import SpecsDetector -from dodal.devices.electron_analyser.vgscienta import VGScientaDetector -from dodal.testing.electron_analyser import create_detector -from tests.devices.electron_analyser.helper_util import get_test_sequence +from tests.devices.electron_analyser.helper_util.sequence import get_test_sequence -@pytest.fixture( - params=[ - VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy], - SpecsDetector[b07.LensMode, b07.PsuMode], - ] -) -async def sim_detector( - request: pytest.FixtureRequest, - single_energy_source: EnergySource, -) -> GenericElectronAnalyserDetector: - async with init_devices(mock=True): - sim_detector = create_detector( - request.param, - prefix="TEST:", - energy_source=single_energy_source, - ) - # Needed for specs so we don't get division by zero error. - set_mock_value(sim_detector.driver.slices, 1) - return sim_detector +@pytest.fixture +def sequence(sim_detector: GenericElectronAnalyserDetector) -> GenericSequence: + return get_test_sequence(type(sim_detector)) def test_base_analyser_detector_trigger( @@ -93,18 +71,11 @@ async def test_base_analyser_detector_describe_configuration( assert await sim_detector.describe_configuration() == driver_describe_config -@pytest.fixture -def sequence_file_path( - sim_detector: GenericElectronAnalyserDetector, -) -> str: - return get_test_sequence(type(sim_detector)) - - def test_analyser_detector_loads_sequence_correctly( sim_detector: GenericElectronAnalyserDetector, - sequence_file_path: str, + sequence: GenericSequence, ) -> None: - seq = sim_detector.load_sequence(sequence_file_path) + seq = sim_detector.create_region_detector_list(sequence.get_enabled_regions()) assert seq is not None @@ -130,12 +101,12 @@ async def test_analyser_detector_unstage( def test_analyser_detector_creates_region_detectors( sim_detector: GenericElectronAnalyserDetector, - sequence_file_path: str, + sequence: GenericSequence, ) -> None: - seq = sim_detector.load_sequence(sequence_file_path) - region_detectors = sim_detector.create_region_detector_list(sequence_file_path) - - assert len(region_detectors) == len(seq.get_enabled_regions()) + region_detectors = sim_detector.create_region_detector_list( + sequence.get_enabled_regions() + ) + assert len(region_detectors) == len(sequence.get_enabled_regions()) for det in region_detectors: assert det.region.enabled is True assert det.name == sim_detector.name + "_" + det.region.name @@ -143,7 +114,7 @@ def test_analyser_detector_creates_region_detectors( def test_analyser_detector_has_driver_as_child_and_region_detector_does_not( sim_detector: GenericElectronAnalyserDetector, - sequence_file_path: str, + sequence: GenericSequence, ) -> None: # Remove parent name from driver name so it can be checked it exists in # _child_devices dict @@ -154,8 +125,9 @@ def test_analyser_detector_has_driver_as_child_and_region_detector_does_not( assert sim_detector._controller.driver.parent == sim_detector assert sim_detector._child_devices.get(driver_name) is not None - region_detectors = sim_detector.create_region_detector_list(sequence_file_path) - + region_detectors = sim_detector.create_region_detector_list( + sequence.get_enabled_regions() + ) for det in region_detectors: assert det._child_devices.get(driver_name) is None assert det._controller.driver.parent == sim_detector @@ -178,11 +150,10 @@ def test_analyser_detector_trigger_called_controller_prepare( def test_analyser_detector_set_called_controller_setup_with_region( sim_detector: GenericElectronAnalyserDetector, - sequence_file_path: str, + sequence: GenericSequence, run_engine: RunEngine, ) -> None: - seq = sim_detector.load_sequence(sequence_file_path) - region = seq.get_enabled_regions()[0] + region = sequence.get_enabled_regions()[0] sim_detector._controller.setup_with_region = AsyncMock() run_engine(bps.mv(sim_detector, region), wait=True) sim_detector._controller.setup_with_region.assert_awaited_once_with(region) @@ -190,11 +161,11 @@ def test_analyser_detector_set_called_controller_setup_with_region( async def test_analyser_region_detector_trigger_sets_driver_with_region( sim_detector: GenericElectronAnalyserDetector, - sequence_file_path: str, + sequence: GenericSequence, run_engine: RunEngine, ) -> None: region_detectors = sim_detector.create_region_detector_list( - sequence_file_path, enabled_only=False + sequence.get_enabled_regions() ) trigger_info = TriggerInfo() diff --git a/tests/devices/electron_analyser/base/test_base_driver_io.py b/tests/devices/electron_analyser/base/test_base_driver_io.py index d4e56e0c138..3a61bccbfc5 100644 --- a/tests/devices/electron_analyser/base/test_base_driver_io.py +++ b/tests/devices/electron_analyser/base/test_base_driver_io.py @@ -2,31 +2,14 @@ from bluesky import plan_stubs as bps from bluesky.run_engine import RunEngine from bluesky.utils import FailedStatus -from ophyd_async.core import StrictEnum, init_devices +from ophyd_async.core import StrictEnum -from dodal.devices.beamlines import b07, i09 from dodal.devices.electron_analyser.base import GenericAnalyserDriverIO -from dodal.devices.electron_analyser.specs import ( - SpecsAnalyserDriverIO, -) -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, -) -from dodal.testing.electron_analyser import create_driver -@pytest.fixture( - params=[ - VGScientaAnalyserDriverIO[i09.LensMode, i09.PsuMode, i09.PassEnergy], - SpecsAnalyserDriverIO[b07.LensMode, b07.PsuMode], - ] -) -async def sim_driver( - request: pytest.FixtureRequest, -) -> GenericAnalyserDriverIO: - async with init_devices(mock=True): - sim_driver = create_driver(request.param, prefix="TEST:") - return sim_driver +@pytest.fixture +async def sim_driver(sim_detector) -> GenericAnalyserDriverIO: + return sim_detector.driver def test_driver_throws_error_with_wrong_lens_mode( diff --git a/tests/devices/electron_analyser/base/test_base_region.py b/tests/devices/electron_analyser/base/test_base_region.py index 50615fbba0b..f52151f2dcb 100644 --- a/tests/devices/electron_analyser/base/test_base_region.py +++ b/tests/devices/electron_analyser/base/test_base_region.py @@ -2,7 +2,6 @@ import pytest -from dodal.common.data_util import load_json_file_to_class from dodal.devices.beamlines import b07, i09 from dodal.devices.electron_analyser.base import ( AbstractBaseRegion, @@ -28,10 +27,10 @@ params=[ SpecsSequence[b07.LensMode, b07.PsuMode], VGScientaSequence[i09.LensMode, i09.PsuMode, i09.PassEnergy], - ] + ], ) def sequence(request: pytest.FixtureRequest) -> GenericSequence: - return load_json_file_to_class(request.param, get_test_sequence(request.param)) + return get_test_sequence(request.param) @pytest.fixture diff --git a/tests/devices/electron_analyser/conftest.py b/tests/devices/electron_analyser/conftest.py index 7007cf55264..9d0f9c5d502 100644 --- a/tests/devices/electron_analyser/conftest.py +++ b/tests/devices/electron_analyser/conftest.py @@ -1,8 +1,10 @@ from typing import Any import pytest -from ophyd_async.core import init_devices +from ophyd_async.core import InOut, init_devices, set_mock_value +import dodal.devices.beamlines.b07 as b07 +import dodal.devices.beamlines.i09 as i09 from dodal.devices.beamlines.i09 import Grating from dodal.devices.common_dcm import ( DoubleCrystalMonochromatorWithDSpacing, @@ -10,26 +12,17 @@ StationaryCrystal, ) from dodal.devices.electron_analyser.base import ( - AbstractAnalyserDriverIO, AbstractBaseRegion, AbstractBaseSequence, DualEnergySource, - ElectronAnalyserController, - ElectronAnalyserDetector, EnergySource, - TAbstractBaseSequence, -) -from dodal.devices.electron_analyser.specs import ( - SpecsAnalyserDriverIO, - SpecsSequence, -) -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, - VGScientaSequence, + GenericElectronAnalyserDetector, ) +from dodal.devices.electron_analyser.specs import SpecsDetector +from dodal.devices.electron_analyser.vgscienta import VGScientaDetector +from dodal.devices.fast_shutter import DualFastShutter, GenericFastShutter from dodal.devices.pgm import PlaneGratingMonochromator from dodal.devices.selectable_source import SourceSelector -from tests.devices.electron_analyser.helper_util import get_test_sequence @pytest.fixture @@ -71,34 +64,91 @@ async def dual_energy_source(source_selector: SourceSelector) -> DualEnergySourc @pytest.fixture -def sequence_class( - sim_driver: AbstractAnalyserDriverIO, -) -> type[AbstractBaseSequence]: - # We must include the pass energy, lens and psu mode types here, otherwise the - # sequence file can't be loaded as pydantic won't be able to resolve the enums. - if isinstance(sim_driver, VGScientaAnalyserDriverIO): - return VGScientaSequence[ - sim_driver.lens_mode_type, - sim_driver.psu_mode_type, - sim_driver.pass_energy_type, - ] - elif isinstance(sim_driver, SpecsAnalyserDriverIO): - return SpecsSequence[sim_driver.lens_mode_type, sim_driver.psu_mode_type] - raise ValueError("class " + str(sim_driver) + " not recognised") +def shutter1() -> GenericFastShutter[InOut]: + with init_devices(mock=True): + shutter1 = GenericFastShutter[InOut]( + pv="TEST:", + open_state=InOut.OUT, + close_state=InOut.IN, + ) + return shutter1 + + +@pytest.fixture +def shutter2() -> GenericFastShutter[InOut]: + with init_devices(mock=True): + shutter2 = GenericFastShutter[InOut]( + pv="TEST:", + open_state=InOut.OUT, + close_state=InOut.IN, + ) + return shutter2 @pytest.fixture -def sequence( - sim_driver: AbstractAnalyserDriverIO, - sequence_class: type[TAbstractBaseSequence], +def dual_fast_shutter( + shutter1: GenericFastShutter[InOut], + shutter2: GenericFastShutter[InOut], + source_selector: SourceSelector, +) -> DualFastShutter[InOut]: + with init_devices(mock=True): + dual_fast_shutter = DualFastShutter[InOut]( + shutter1, + shutter2, + source_selector.selected_source, + ) + return dual_fast_shutter + + +@pytest.fixture +async def b07b_specs150( single_energy_source: EnergySource, -) -> AbstractBaseSequence: - controller = ElectronAnalyserController(sim_driver, single_energy_source) - det = ElectronAnalyserDetector( - sequence_class=sequence_class, - controller=controller, - ) - return det.load_sequence(get_test_sequence(type(sim_driver))) + shutter1: GenericFastShutter, +) -> SpecsDetector[b07.LensMode, b07.PsuMode]: + with init_devices(mock=True): + b07b_specs150 = SpecsDetector[b07.LensMode, b07.PsuMode]( + prefix="TEST:", + lens_mode_type=b07.LensMode, + psu_mode_type=b07.PsuMode, + energy_source=single_energy_source, + shutter=shutter1, + ) + # Needed for specs so we don't get division by zero error. + set_mock_value(b07b_specs150.driver.slices, 1) + return b07b_specs150 + + +@pytest.fixture +async def ew4000( + dual_energy_source: DualEnergySource, + dual_fast_shutter: DualFastShutter, + source_selector: SourceSelector, +) -> VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy]: + with init_devices(mock=True): + ew4000 = VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy]( + prefix="TEST:", + lens_mode_type=i09.LensMode, + psu_mode_type=i09.PsuMode, + pass_energy_type=i09.PassEnergy, + energy_source=dual_energy_source, + shutter=dual_fast_shutter, + source_selector=source_selector, + ) + return ew4000 + + +@pytest.fixture(params=["ew4000", "b07b_specs150"]) +def sim_detector( + request: pytest.FixtureRequest, + ew4000: VGScientaDetector, + b07b_specs150: SpecsDetector, +) -> GenericElectronAnalyserDetector: + detectors = [ew4000, b07b_specs150] + for detector in detectors: + if detector.name == request.param: + return detector + + raise ValueError(f"Detector with name '{request.param}' not found") @pytest.fixture diff --git a/tests/devices/electron_analyser/helper_util/assert_func.py b/tests/devices/electron_analyser/helper_util/assert_func.py index 01433c8712f..4e20df0bcfe 100644 --- a/tests/devices/electron_analyser/helper_util/assert_func.py +++ b/tests/devices/electron_analyser/helper_util/assert_func.py @@ -11,6 +11,6 @@ def assert_region_has_expected_values( actual_values = r.__dict__ diff = DeepDiff(expected_region_values, actual_values) if diff: - raise AssertionError(f"Region does not match expected values:\n{diff}") + raise AssertionError(f"Region {r.name} does not match expected values:\n{diff}") for key in expected_region_values.keys(): assert actual_values.get(key) is not None diff --git a/tests/devices/electron_analyser/helper_util/sequence.py b/tests/devices/electron_analyser/helper_util/sequence.py index 85d47a8f56d..790b852cdc1 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -1,3 +1,6 @@ +from dodal.common.data_util import load_json_file_to_class +from dodal.devices.beamlines import b07, i09 +from dodal.devices.electron_analyser.base import GenericSequence from dodal.devices.electron_analyser.specs import ( SpecsAnalyserDriverIO, SpecsDetector, @@ -13,22 +16,38 @@ TEST_VGSCIENTA_SEQUENCE, ) +TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] + + +def b07_specs_test_sequence_loader() -> SpecsSequence[b07.LensMode, b07.PsuMode]: + return load_json_file_to_class( + SpecsSequence[b07.LensMode, b07.PsuMode], TEST_SPECS_SEQUENCE + ) + + +def i09_vgscienta_test_sequence_loader() -> VGScientaSequence[ + i09.LensMode, i09.PsuMode, i09.PassEnergy +]: + return load_json_file_to_class( + VGScientaSequence[i09.LensMode, i09.PsuMode, i09.PassEnergy], + TEST_VGSCIENTA_SEQUENCE, + ) + + +# Map to know what function to load in sequence an analyser driver should use. TEST_SEQUENCES = { - VGScientaSequence: TEST_VGSCIENTA_SEQUENCE, - VGScientaDetector: TEST_VGSCIENTA_SEQUENCE, - VGScientaAnalyserDriverIO: TEST_VGSCIENTA_SEQUENCE, - SpecsSequence: TEST_SPECS_SEQUENCE, - SpecsDetector: TEST_SPECS_SEQUENCE, - SpecsAnalyserDriverIO: TEST_SPECS_SEQUENCE, + SpecsDetector: b07_specs_test_sequence_loader, + SpecsAnalyserDriverIO: b07_specs_test_sequence_loader, + SpecsSequence: b07_specs_test_sequence_loader, + VGScientaDetector: i09_vgscienta_test_sequence_loader, + VGScientaAnalyserDriverIO: i09_vgscienta_test_sequence_loader, + VGScientaSequence: i09_vgscienta_test_sequence_loader, } -def get_test_sequence(key: type) -> str: +def get_test_sequence(key: type) -> GenericSequence: for cls in key.__mro__: # Check for unscripted class only if cls in TEST_SEQUENCES: - return TEST_SEQUENCES[cls] + return TEST_SEQUENCES[cls]() raise KeyError(f"Found no match with type {key}") - - -TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] diff --git a/tests/devices/electron_analyser/specs/test_specs_detector.py b/tests/devices/electron_analyser/specs/test_specs_detector.py index 9a1fd9c1434..ac91086a57a 100644 --- a/tests/devices/electron_analyser/specs/test_specs_detector.py +++ b/tests/devices/electron_analyser/specs/test_specs_detector.py @@ -1,27 +1,12 @@ -import pytest -from ophyd_async.core import init_devices, set_mock_value +from ophyd_async.core import set_mock_value -from dodal.devices.beamlines.b07 import LensMode, PsuMode -from dodal.devices.electron_analyser.base import EnergySource from dodal.devices.electron_analyser.specs import SpecsDetector -from dodal.testing.electron_analyser import create_detector -@pytest.fixture -async def sim_detector( - single_energy_source: EnergySource, -) -> SpecsDetector[LensMode, PsuMode]: - async with init_devices(mock=True): - sim_driver = create_detector( - SpecsDetector[LensMode, PsuMode], - prefix="TEST:", - energy_source=single_energy_source, - ) - return sim_driver - - -async def test_analyser_specs_detector_image_shape(sim_detector: SpecsDetector) -> None: - driver = sim_detector.driver +async def test_analyser_specs_detector_image_shape( + b07b_specs150: SpecsDetector, +) -> None: + driver = b07b_specs150.driver prefix = driver.name + "-" low_energy = 1 @@ -39,7 +24,7 @@ async def test_analyser_specs_detector_image_shape(sim_detector: SpecsDetector) angle_axis = await driver.angle_axis.get_value() energy_axis = await driver.energy_axis.get_value() - describe = await sim_detector.describe() + describe = await b07b_specs150.describe() assert describe[f"{prefix}image"]["shape"] == [ len(angle_axis), len(energy_axis), diff --git a/tests/devices/electron_analyser/specs/test_specs_driver_io.py b/tests/devices/electron_analyser/specs/test_specs_driver_io.py index 65e6234816b..842a2adfc65 100644 --- a/tests/devices/electron_analyser/specs/test_specs_driver_io.py +++ b/tests/devices/electron_analyser/specs/test_specs_driver_io.py @@ -4,7 +4,7 @@ import pytest from bluesky import plan_stubs as bps from bluesky.run_engine import RunEngine -from ophyd_async.core import get_mock_put, init_devices, set_mock_value +from ophyd_async.core import get_mock_put, set_mock_value from ophyd_async.testing import ( assert_configuration, assert_reading, @@ -20,20 +20,20 @@ SpecsAnalyserDriverIO, SpecsRegion, ) -from dodal.testing.electron_analyser import create_driver from tests.devices.electron_analyser.helper_util import ( TEST_SEQUENCE_REGION_NAMES, + get_test_sequence, ) @pytest.fixture -async def sim_driver() -> SpecsAnalyserDriverIO[LensMode, PsuMode]: - async with init_devices(mock=True): - sim_driver = create_driver( - SpecsAnalyserDriverIO[LensMode, PsuMode], - prefix="TEST:", - ) - return sim_driver +async def sim_driver(b07b_specs150) -> SpecsAnalyserDriverIO[LensMode, PsuMode]: + return b07b_specs150.driver + + +@pytest.fixture +def sequence(sim_driver: SpecsAnalyserDriverIO[LensMode, PsuMode]): + return get_test_sequence(type(sim_driver)) @pytest.mark.parametrize("region", TEST_SEQUENCE_REGION_NAMES, indirect=True) diff --git a/tests/devices/electron_analyser/specs/test_specs_region.py b/tests/devices/electron_analyser/specs/test_specs_region.py index 0ed8c133075..9371cc2162e 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -2,13 +2,9 @@ import pytest -from dodal.common.data_util import load_json_file_to_class from dodal.devices.beamlines.b07 import LensMode, PsuMode from dodal.devices.electron_analyser.base import EnergyMode -from dodal.devices.electron_analyser.specs import ( - AcquisitionMode, - SpecsSequence, -) +from dodal.devices.electron_analyser.specs import AcquisitionMode, SpecsSequence from dodal.devices.selectable_source import SelectedSource from tests.devices.electron_analyser.helper_util import ( assert_region_has_expected_values, @@ -17,9 +13,8 @@ @pytest.fixture -def sequence() -> SpecsSequence[LensMode, PsuMode]: - seq = SpecsSequence[LensMode, PsuMode] - return load_json_file_to_class(seq, get_test_sequence(seq)) +def sequence(): + return get_test_sequence(SpecsSequence[LensMode, PsuMode]) @pytest.fixture diff --git a/tests/devices/electron_analyser/vgscienta/test_vgscienta_detector.py b/tests/devices/electron_analyser/vgscienta/test_vgscienta_detector.py index 4d7dadc549d..48ac964f0f6 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgscienta_detector.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgscienta_detector.py @@ -1,32 +1,16 @@ import numpy as np -import pytest -from ophyd_async.core import init_devices, set_mock_value +from ophyd_async.core import set_mock_value from dodal.devices.beamlines.i09 import LensMode, PassEnergy, PsuMode -from dodal.devices.electron_analyser.base import DualEnergySource from dodal.devices.electron_analyser.vgscienta import ( VGScientaDetector, ) -from dodal.testing.electron_analyser import create_detector - - -@pytest.fixture -async def sim_detector( - dual_energy_source: DualEnergySource, -) -> VGScientaDetector[LensMode, PsuMode, PassEnergy]: - async with init_devices(mock=True): - sim_driver = create_detector( - VGScientaDetector[LensMode, PsuMode, PassEnergy], - prefix="TEST:", - energy_source=dual_energy_source, - ) - return sim_driver async def test_analyser_vgscienta_detector_image_shape( - sim_detector: VGScientaDetector, + ew4000: VGScientaDetector[LensMode, PsuMode, PassEnergy], ) -> None: - driver = sim_detector.driver + driver = ew4000.driver prefix = driver.name + "-" energy_axis = np.array([1, 2, 3, 4, 5]) @@ -34,7 +18,7 @@ async def test_analyser_vgscienta_detector_image_shape( set_mock_value(driver.energy_axis, energy_axis) set_mock_value(driver.angle_axis, angle_axis) - describe = await sim_detector.describe() + describe = await ew4000.describe() assert describe[f"{prefix}image"]["shape"] == [ len(angle_axis), len(energy_axis), diff --git a/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py b/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py index a31afd82a7d..f3284eda852 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py @@ -5,7 +5,7 @@ from bluesky import plan_stubs as bps from bluesky.run_engine import RunEngine from bluesky.utils import FailedStatus -from ophyd_async.core import StrictEnum, get_mock_put, init_devices, set_mock_value +from ophyd_async.core import StrictEnum, get_mock_put, set_mock_value from ophyd_async.testing import ( assert_configuration, assert_reading, @@ -19,19 +19,22 @@ VGScientaAnalyserDriverIO, VGScientaRegion, ) -from dodal.testing.electron_analyser import create_driver from tests.devices.electron_analyser.helper_util import ( TEST_SEQUENCE_REGION_NAMES, + get_test_sequence, ) @pytest.fixture -async def sim_driver() -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]: - async with init_devices(mock=True): - sim_driver = create_driver( - VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy], prefix="TEST:" - ) - return sim_driver +async def sim_driver( + ew4000, +) -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]: + return ew4000.driver + + +@pytest.fixture +def sequence(sim_driver: VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]): + return get_test_sequence(type(sim_driver)) @pytest.mark.parametrize("region", TEST_SEQUENCE_REGION_NAMES, indirect=True) diff --git a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py index 81ea9d03cee..7a8c5c1fc74 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py @@ -2,7 +2,6 @@ import pytest -from dodal.common.data_util import load_json_file_to_class from dodal.devices.beamlines.i09 import LensMode, PassEnergy, PsuMode from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.vgscienta import ( @@ -19,9 +18,8 @@ @pytest.fixture -def sequence() -> VGScientaSequence[LensMode, PsuMode, PassEnergy]: - seq = VGScientaSequence[LensMode, PsuMode, PassEnergy] - return load_json_file_to_class(seq, get_test_sequence(seq)) +def sequence(): + return get_test_sequence(VGScientaSequence[LensMode, PsuMode, PassEnergy]) @pytest.fixture From 30750790989f372ff931dba8b82cab049c975ea6 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 17 Feb 2026 14:31:16 +0000 Subject: [PATCH 2/4] Improve type checking --- .../electron_analyser/base/test_base_driver_io.py | 9 +++++++-- tests/devices/electron_analyser/helper_util/sequence.py | 3 +-- .../electron_analyser/specs/test_specs_driver_io.py | 5 ++++- .../devices/electron_analyser/specs/test_specs_region.py | 2 +- .../vgscienta/test_vgscienta_driver_io.py | 3 ++- .../electron_analyser/vgscienta/test_vgsicenta_region.py | 2 +- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/devices/electron_analyser/base/test_base_driver_io.py b/tests/devices/electron_analyser/base/test_base_driver_io.py index 3a61bccbfc5..cdbe6a717cc 100644 --- a/tests/devices/electron_analyser/base/test_base_driver_io.py +++ b/tests/devices/electron_analyser/base/test_base_driver_io.py @@ -4,11 +4,16 @@ from bluesky.utils import FailedStatus from ophyd_async.core import StrictEnum -from dodal.devices.electron_analyser.base import GenericAnalyserDriverIO +from dodal.devices.electron_analyser.base import ( + GenericAnalyserDriverIO, + GenericElectronAnalyserDetector, +) @pytest.fixture -async def sim_driver(sim_detector) -> GenericAnalyserDriverIO: +async def sim_driver( + sim_detector: GenericElectronAnalyserDetector, +) -> GenericAnalyserDriverIO: return sim_detector.driver diff --git a/tests/devices/electron_analyser/helper_util/sequence.py b/tests/devices/electron_analyser/helper_util/sequence.py index 790b852cdc1..38b98ff1a84 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -1,6 +1,5 @@ from dodal.common.data_util import load_json_file_to_class from dodal.devices.beamlines import b07, i09 -from dodal.devices.electron_analyser.base import GenericSequence from dodal.devices.electron_analyser.specs import ( SpecsAnalyserDriverIO, SpecsDetector, @@ -45,7 +44,7 @@ def i09_vgscienta_test_sequence_loader() -> VGScientaSequence[ } -def get_test_sequence(key: type) -> GenericSequence: +def get_test_sequence(key: type): for cls in key.__mro__: # Check for unscripted class only if cls in TEST_SEQUENCES: diff --git a/tests/devices/electron_analyser/specs/test_specs_driver_io.py b/tests/devices/electron_analyser/specs/test_specs_driver_io.py index 842a2adfc65..ded61535d66 100644 --- a/tests/devices/electron_analyser/specs/test_specs_driver_io.py +++ b/tests/devices/electron_analyser/specs/test_specs_driver_io.py @@ -18,6 +18,7 @@ from dodal.devices.electron_analyser.specs import ( AcquisitionMode, SpecsAnalyserDriverIO, + SpecsDetector, SpecsRegion, ) from tests.devices.electron_analyser.helper_util import ( @@ -27,7 +28,9 @@ @pytest.fixture -async def sim_driver(b07b_specs150) -> SpecsAnalyserDriverIO[LensMode, PsuMode]: +async def sim_driver( + b07b_specs150: SpecsDetector[LensMode, PsuMode], +) -> SpecsAnalyserDriverIO[LensMode, PsuMode]: return b07b_specs150.driver diff --git a/tests/devices/electron_analyser/specs/test_specs_region.py b/tests/devices/electron_analyser/specs/test_specs_region.py index 9371cc2162e..ebdbd075fce 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -13,7 +13,7 @@ @pytest.fixture -def sequence(): +def sequence() -> SpecsSequence[LensMode, PsuMode]: return get_test_sequence(SpecsSequence[LensMode, PsuMode]) diff --git a/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py b/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py index f3284eda852..17e8b4fbd11 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py @@ -17,6 +17,7 @@ from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.vgscienta import ( VGScientaAnalyserDriverIO, + VGScientaDetector, VGScientaRegion, ) from tests.devices.electron_analyser.helper_util import ( @@ -27,7 +28,7 @@ @pytest.fixture async def sim_driver( - ew4000, + ew4000: VGScientaDetector, ) -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]: return ew4000.driver diff --git a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py index 7a8c5c1fc74..0e4fe4f88bb 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py @@ -18,7 +18,7 @@ @pytest.fixture -def sequence(): +def sequence() -> VGScientaSequence[LensMode, PsuMode, PassEnergy]: return get_test_sequence(VGScientaSequence[LensMode, PsuMode, PassEnergy]) From 9c2887e811c0a10c56b6e340cea7c85d314a6248 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 17 Feb 2026 14:45:12 +0000 Subject: [PATCH 3/4] Move PsuMode to B07 shared, improve type checking --- src/dodal/beamlines/b07.py | 2 +- src/dodal/beamlines/b07_1.py | 2 +- src/dodal/devices/beamlines/b07/__init__.py | 4 ++-- src/dodal/devices/beamlines/b07/enums.py | 12 ------------ .../devices/beamlines/b07_shared/__init__.py | 3 +++ src/dodal/devices/beamlines/b07_shared/enums.py | 13 +++++++++++++ .../electron_analyser/base/test_base_region.py | 6 +++--- tests/devices/electron_analyser/conftest.py | 15 +++++++-------- .../electron_analyser/helper_util/sequence.py | 6 +++--- .../specs/test_specs_driver_io.py | 3 ++- .../electron_analyser/specs/test_specs_region.py | 3 ++- 11 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 src/dodal/devices/beamlines/b07_shared/__init__.py create mode 100644 src/dodal/devices/beamlines/b07_shared/enums.py diff --git a/src/dodal/beamlines/b07.py b/src/dodal/beamlines/b07.py index 87cdf54b7bd..3f712cdd500 100644 --- a/src/dodal/beamlines/b07.py +++ b/src/dodal/beamlines/b07.py @@ -5,8 +5,8 @@ B07SampleManipulator52B, Grating, LensMode, - PsuMode, ) +from dodal.devices.beamlines.b07_shared import PsuMode from dodal.devices.electron_analyser.base import EnergySource from dodal.devices.electron_analyser.specs import SpecsDetector from dodal.devices.motors import XYZPolarStage diff --git a/src/dodal/beamlines/b07_1.py b/src/dodal/beamlines/b07_1.py index b2c03102f41..a80bb89afde 100644 --- a/src/dodal/beamlines/b07_1.py +++ b/src/dodal/beamlines/b07_1.py @@ -1,12 +1,12 @@ from dodal.beamlines.b07_shared import devices as b07_shared_devices from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.device_manager import DeviceManager -from dodal.devices.beamlines.b07 import PsuMode from dodal.devices.beamlines.b07_1 import ( ChannelCutMonochromator, Grating, LensMode, ) +from dodal.devices.beamlines.b07_shared import PsuMode from dodal.devices.electron_analyser.base import EnergySource from dodal.devices.electron_analyser.specs import SpecsDetector from dodal.devices.motors import XYZPolarAzimuthStage diff --git a/src/dodal/devices/beamlines/b07/__init__.py b/src/dodal/devices/beamlines/b07/__init__.py index cf3fe955bb3..d1583c2a25e 100644 --- a/src/dodal/devices/beamlines/b07/__init__.py +++ b/src/dodal/devices/beamlines/b07/__init__.py @@ -1,4 +1,4 @@ from .b07_motors import B07SampleManipulator52B -from .enums import Grating, LensMode, PsuMode +from .enums import Grating, LensMode -__all__ = ["B07SampleManipulator52B", "Grating", "LensMode", "PsuMode"] +__all__ = ["B07SampleManipulator52B", "Grating", "LensMode"] diff --git a/src/dodal/devices/beamlines/b07/enums.py b/src/dodal/devices/beamlines/b07/enums.py index 58c826bb372..2e27db999d4 100644 --- a/src/dodal/devices/beamlines/b07/enums.py +++ b/src/dodal/devices/beamlines/b07/enums.py @@ -25,15 +25,3 @@ class LensMode(SupersetEnum): # option if disconnected. Once it is connected, "Not connected" is replaced with the # options above. This is also why this must be a SupersetEnum. NOT_CONNECTED = "Not connected" - - -class PsuMode(SupersetEnum): - V3500 = "3.5kV" - V1500 = "1.5kV" - V400 = "400V" - V100 = "100V" - V10 = "10V" - # This is connected to the device separately and will only have "Not connected" as - # option if disconnected. Once it is connected, "Not connected" is replaced with the - # options above. This is also why this must be a SupersetEnum. - NOT_CONNECTED = "Not connected" diff --git a/src/dodal/devices/beamlines/b07_shared/__init__.py b/src/dodal/devices/beamlines/b07_shared/__init__.py new file mode 100644 index 00000000000..9fa24dbb0f4 --- /dev/null +++ b/src/dodal/devices/beamlines/b07_shared/__init__.py @@ -0,0 +1,3 @@ +from .enums import PsuMode + +__all__ = ["PsuMode"] diff --git a/src/dodal/devices/beamlines/b07_shared/enums.py b/src/dodal/devices/beamlines/b07_shared/enums.py new file mode 100644 index 00000000000..0c6bbba3a8d --- /dev/null +++ b/src/dodal/devices/beamlines/b07_shared/enums.py @@ -0,0 +1,13 @@ +from ophyd_async.core import SupersetEnum + + +class PsuMode(SupersetEnum): + V3500 = "3.5kV" + V1500 = "1.5kV" + V400 = "400V" + V100 = "100V" + V10 = "10V" + # This is connected to the device separately and will only have "Not connected" as + # option if disconnected. Once it is connected, "Not connected" is replaced with the + # options above. This is also why this must be a SupersetEnum. + NOT_CONNECTED = "Not connected" diff --git a/tests/devices/electron_analyser/base/test_base_region.py b/tests/devices/electron_analyser/base/test_base_region.py index f52151f2dcb..9d53f4a5e59 100644 --- a/tests/devices/electron_analyser/base/test_base_region.py +++ b/tests/devices/electron_analyser/base/test_base_region.py @@ -2,7 +2,7 @@ import pytest -from dodal.devices.beamlines import b07, i09 +from dodal.devices.beamlines import b07, b07_shared, i09 from dodal.devices.electron_analyser.base import ( AbstractBaseRegion, EnergyMode, @@ -25,7 +25,7 @@ @pytest.fixture( params=[ - SpecsSequence[b07.LensMode, b07.PsuMode], + SpecsSequence[b07.LensMode, b07_shared.PsuMode], VGScientaSequence[i09.LensMode, i09.PsuMode, i09.PassEnergy], ], ) @@ -38,7 +38,7 @@ def expected_region_class( sequence: GenericSequence, ) -> type[AbstractBaseRegion]: if isinstance(sequence, SpecsSequence): - return SpecsRegion[b07.LensMode, b07.PsuMode] + return SpecsRegion[b07.LensMode, b07_shared.PsuMode] elif isinstance(sequence, VGScientaSequence): return VGScientaRegion[i09.LensMode, i09.PassEnergy] raise TypeError(f"Unknown sequence type {type(sequence)}") diff --git a/tests/devices/electron_analyser/conftest.py b/tests/devices/electron_analyser/conftest.py index 9d0f9c5d502..2cd1bb29e25 100644 --- a/tests/devices/electron_analyser/conftest.py +++ b/tests/devices/electron_analyser/conftest.py @@ -3,8 +3,7 @@ import pytest from ophyd_async.core import InOut, init_devices, set_mock_value -import dodal.devices.beamlines.b07 as b07 -import dodal.devices.beamlines.i09 as i09 +from dodal.devices.beamlines import b07, b07_shared, i09 from dodal.devices.beamlines.i09 import Grating from dodal.devices.common_dcm import ( DoubleCrystalMonochromatorWithDSpacing, @@ -104,12 +103,12 @@ def dual_fast_shutter( async def b07b_specs150( single_energy_source: EnergySource, shutter1: GenericFastShutter, -) -> SpecsDetector[b07.LensMode, b07.PsuMode]: +) -> SpecsDetector[b07.LensMode, b07_shared.PsuMode]: with init_devices(mock=True): - b07b_specs150 = SpecsDetector[b07.LensMode, b07.PsuMode]( + b07b_specs150 = SpecsDetector[b07.LensMode, b07_shared.PsuMode]( prefix="TEST:", lens_mode_type=b07.LensMode, - psu_mode_type=b07.PsuMode, + psu_mode_type=b07_shared.PsuMode, energy_source=single_energy_source, shutter=shutter1, ) @@ -140,8 +139,8 @@ async def ew4000( @pytest.fixture(params=["ew4000", "b07b_specs150"]) def sim_detector( request: pytest.FixtureRequest, - ew4000: VGScientaDetector, - b07b_specs150: SpecsDetector, + ew4000: VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy], + b07b_specs150: SpecsDetector[b07.LensMode, b07_shared.PsuMode], ) -> GenericElectronAnalyserDetector: detectors = [ew4000, b07b_specs150] for detector in detectors: @@ -154,7 +153,7 @@ def sim_detector( @pytest.fixture def region( request: pytest.FixtureRequest, - sequence: AbstractBaseSequence, + sequence: AbstractBaseSequence[AbstractBaseRegion], ) -> AbstractBaseRegion: region = sequence.get_region_by_name(request.param) if region is None: diff --git a/tests/devices/electron_analyser/helper_util/sequence.py b/tests/devices/electron_analyser/helper_util/sequence.py index 38b98ff1a84..67bf1496760 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -1,5 +1,5 @@ from dodal.common.data_util import load_json_file_to_class -from dodal.devices.beamlines import b07, i09 +from dodal.devices.beamlines import b07, b07_shared, i09 from dodal.devices.electron_analyser.specs import ( SpecsAnalyserDriverIO, SpecsDetector, @@ -18,9 +18,9 @@ TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] -def b07_specs_test_sequence_loader() -> SpecsSequence[b07.LensMode, b07.PsuMode]: +def b07_specs_test_sequence_loader() -> SpecsSequence[b07.LensMode, b07_shared.PsuMode]: return load_json_file_to_class( - SpecsSequence[b07.LensMode, b07.PsuMode], TEST_SPECS_SEQUENCE + SpecsSequence[b07.LensMode, b07_shared.PsuMode], TEST_SPECS_SEQUENCE ) diff --git a/tests/devices/electron_analyser/specs/test_specs_driver_io.py b/tests/devices/electron_analyser/specs/test_specs_driver_io.py index ded61535d66..69e3fc590dc 100644 --- a/tests/devices/electron_analyser/specs/test_specs_driver_io.py +++ b/tests/devices/electron_analyser/specs/test_specs_driver_io.py @@ -12,7 +12,8 @@ partial_reading, ) -from dodal.devices.beamlines.b07 import LensMode, PsuMode +from dodal.devices.beamlines.b07 import LensMode +from dodal.devices.beamlines.b07_shared import PsuMode from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.base.base_enums import EnergyMode from dodal.devices.electron_analyser.specs import ( diff --git a/tests/devices/electron_analyser/specs/test_specs_region.py b/tests/devices/electron_analyser/specs/test_specs_region.py index ebdbd075fce..c26a1f27dcd 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -2,7 +2,8 @@ import pytest -from dodal.devices.beamlines.b07 import LensMode, PsuMode +from dodal.devices.beamlines.b07 import LensMode +from dodal.devices.beamlines.b07_shared import PsuMode from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.specs import AcquisitionMode, SpecsSequence from dodal.devices.selectable_source import SelectedSource From 7a97a2353562e3145b42c1f31c1b151e414d93c3 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Thu, 19 Feb 2026 11:14:40 +0000 Subject: [PATCH 4/4] Moved some pytest fixtures to where they are used --- .../electron_analyser/base/conftest.py | 20 ++++++++++++++++ tests/devices/electron_analyser/conftest.py | 23 ------------------- 2 files changed, 20 insertions(+), 23 deletions(-) create mode 100644 tests/devices/electron_analyser/base/conftest.py diff --git a/tests/devices/electron_analyser/base/conftest.py b/tests/devices/electron_analyser/base/conftest.py new file mode 100644 index 00000000000..2e8677610a4 --- /dev/null +++ b/tests/devices/electron_analyser/base/conftest.py @@ -0,0 +1,20 @@ +import pytest + +from dodal.devices.beamlines import b07, b07_shared, i09 +from dodal.devices.electron_analyser.base import GenericElectronAnalyserDetector +from dodal.devices.electron_analyser.specs import SpecsDetector +from dodal.devices.electron_analyser.vgscienta import VGScientaDetector + + +@pytest.fixture(params=["ew4000", "b07b_specs150"]) +def sim_detector( + request: pytest.FixtureRequest, + ew4000: VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy], + b07b_specs150: SpecsDetector[b07.LensMode, b07_shared.PsuMode], +) -> GenericElectronAnalyserDetector: + detectors = [ew4000, b07b_specs150] + for detector in detectors: + if detector.name == request.param: + return detector + + raise ValueError(f"Detector with name '{request.param}' not found") diff --git a/tests/devices/electron_analyser/conftest.py b/tests/devices/electron_analyser/conftest.py index 2cd1bb29e25..07720f5ffb2 100644 --- a/tests/devices/electron_analyser/conftest.py +++ b/tests/devices/electron_analyser/conftest.py @@ -15,7 +15,6 @@ AbstractBaseSequence, DualEnergySource, EnergySource, - GenericElectronAnalyserDetector, ) from dodal.devices.electron_analyser.specs import SpecsDetector from dodal.devices.electron_analyser.vgscienta import VGScientaDetector @@ -136,20 +135,6 @@ async def ew4000( return ew4000 -@pytest.fixture(params=["ew4000", "b07b_specs150"]) -def sim_detector( - request: pytest.FixtureRequest, - ew4000: VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy], - b07b_specs150: SpecsDetector[b07.LensMode, b07_shared.PsuMode], -) -> GenericElectronAnalyserDetector: - detectors = [ew4000, b07b_specs150] - for detector in detectors: - if detector.name == request.param: - return detector - - raise ValueError(f"Detector with name '{request.param}' not found") - - @pytest.fixture def region( request: pytest.FixtureRequest, @@ -161,14 +146,6 @@ def region( return region -@pytest.fixture -def expected_region_names(expected_region_values: list[dict[str, Any]]) -> list[str]: - names = [] - for expected_region_value in expected_region_values: - names.append(expected_region_value["name"]) - return names - - @pytest.fixture def expected_enabled_region_names( expected_region_values: list[dict[str, Any]],