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/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/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/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..cdbe6a717cc 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,19 @@ 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.base import ( + GenericAnalyserDriverIO, + GenericElectronAnalyserDetector, ) -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], - ] -) +@pytest.fixture async def sim_driver( - request: pytest.FixtureRequest, + sim_detector: GenericElectronAnalyserDetector, ) -> GenericAnalyserDriverIO: - async with init_devices(mock=True): - sim_driver = create_driver(request.param, prefix="TEST:") - return sim_driver + 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..9d53f4a5e59 100644 --- a/tests/devices/electron_analyser/base/test_base_region.py +++ b/tests/devices/electron_analyser/base/test_base_region.py @@ -2,8 +2,7 @@ import pytest -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.base import ( AbstractBaseRegion, EnergyMode, @@ -26,12 +25,12 @@ @pytest.fixture( params=[ - SpecsSequence[b07.LensMode, b07.PsuMode], + SpecsSequence[b07.LensMode, b07_shared.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 @@ -39,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 7007cf55264..07720f5ffb2 100644 --- a/tests/devices/electron_analyser/conftest.py +++ b/tests/devices/electron_analyser/conftest.py @@ -1,8 +1,9 @@ from typing import Any import pytest -from ophyd_async.core import init_devices +from ophyd_async.core import InOut, init_devices, set_mock_value +from dodal.devices.beamlines import b07, b07_shared, i09 from dodal.devices.beamlines.i09 import Grating from dodal.devices.common_dcm import ( DoubleCrystalMonochromatorWithDSpacing, @@ -10,26 +11,16 @@ 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, ) +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,40 +62,83 @@ 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 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 -def sequence( - sim_driver: AbstractAnalyserDriverIO, - sequence_class: type[TAbstractBaseSequence], +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_shared.PsuMode]: + with init_devices(mock=True): + b07b_specs150 = SpecsDetector[b07.LensMode, b07_shared.PsuMode]( + prefix="TEST:", + lens_mode_type=b07.LensMode, + psu_mode_type=b07_shared.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 def region( request: pytest.FixtureRequest, - sequence: AbstractBaseSequence, + sequence: AbstractBaseSequence[AbstractBaseRegion], ) -> AbstractBaseRegion: region = sequence.get_region_by_name(request.param) if region is None: @@ -112,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]], 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..67bf1496760 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -1,3 +1,5 @@ +from dodal.common.data_util import load_json_file_to_class +from dodal.devices.beamlines import b07, b07_shared, i09 from dodal.devices.electron_analyser.specs import ( SpecsAnalyserDriverIO, SpecsDetector, @@ -13,22 +15,38 @@ TEST_VGSCIENTA_SEQUENCE, ) +TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] + + +def b07_specs_test_sequence_loader() -> SpecsSequence[b07.LensMode, b07_shared.PsuMode]: + return load_json_file_to_class( + SpecsSequence[b07.LensMode, b07_shared.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): 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..69e3fc590dc 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, @@ -12,28 +12,32 @@ 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 ( AcquisitionMode, SpecsAnalyserDriverIO, + SpecsDetector, 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: SpecsDetector[LensMode, PsuMode], +) -> 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..c26a1f27dcd 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -2,13 +2,10 @@ import pytest -from dodal.common.data_util import load_json_file_to_class -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.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, @@ -18,8 +15,7 @@ @pytest.fixture def sequence() -> SpecsSequence[LensMode, PsuMode]: - seq = SpecsSequence[LensMode, PsuMode] - return load_json_file_to_class(seq, get_test_sequence(seq)) + 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..17e8b4fbd11 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, @@ -17,21 +17,25 @@ from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.vgscienta import ( VGScientaAnalyserDriverIO, + VGScientaDetector, 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: VGScientaDetector, +) -> 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..0e4fe4f88bb 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 ( @@ -20,8 +19,7 @@ @pytest.fixture def sequence() -> VGScientaSequence[LensMode, PsuMode, PassEnergy]: - seq = VGScientaSequence[LensMode, PsuMode, PassEnergy] - return load_json_file_to_class(seq, get_test_sequence(seq)) + return get_test_sequence(VGScientaSequence[LensMode, PsuMode, PassEnergy]) @pytest.fixture