diff --git a/src/dodal/common/data_util.py b/src/dodal/common/data_util.py index 1c2f2c1939..316a0ac46f 100644 --- a/src/dodal/common/data_util.py +++ b/src/dodal/common/data_util.py @@ -1,3 +1,4 @@ +import os from typing import TypeVar from pydantic import BaseModel @@ -9,6 +10,9 @@ def load_json_file_to_class( t: type[TBaseModel], file: str, ) -> TBaseModel: + if not os.path.isfile(file): + raise FileNotFoundError(f"Cannot find file {file}") + with open(file) as f: json_obj = f.read() cls = t.model_validate_json(json_obj) diff --git a/src/dodal/devices/electron_analyser/__init__.py b/src/dodal/devices/electron_analyser/__init__.py index 2ee1bd10f3..bb69802f0a 100644 --- a/src/dodal/devices/electron_analyser/__init__.py +++ b/src/dodal/devices/electron_analyser/__init__.py @@ -1,4 +1,5 @@ from .abstract_region import EnergyMode +from .detector import ElectronAnalyserDetector, ElectronAnalyserRegionDetector from .specs_analyser_io import SpecsAnalyserDriverIO from .specs_region import SpecsRegion, SpecsSequence from .util import to_binding_energy, to_kinetic_energy @@ -11,6 +12,8 @@ __all__ = [ "EnergyMode", + "ElectronAnalyserDetector", + "ElectronAnalyserRegionDetector", "SpecsAnalyserDriverIO", "SpecsRegion", "SpecsSequence", diff --git a/src/dodal/devices/electron_analyser/abstract_analyser_io.py b/src/dodal/devices/electron_analyser/abstract_analyser_io.py index b92fd8d03b..18032e492a 100644 --- a/src/dodal/devices/electron_analyser/abstract_analyser_io.py +++ b/src/dodal/devices/electron_analyser/abstract_analyser_io.py @@ -1,9 +1,10 @@ from abc import ABC, abstractmethod -from typing import TypeVar +from typing import Generic, TypeVar import numpy as np from ophyd_async.core import ( Array1D, + Reference, SignalR, StandardReadable, StandardReadableFormat, @@ -13,17 +14,32 @@ from ophyd_async.epics.adcore import ADBaseIO from ophyd_async.epics.core import epics_signal_r, epics_signal_rw -from dodal.devices.electron_analyser.abstract_region import EnergyMode +from dodal.devices.electron_analyser.abstract_region import ( + EnergyMode, + TAbstractBaseSequence, +) from dodal.devices.electron_analyser.util import to_binding_energy -class AbstractAnalyserDriverIO(ABC, StandardReadable, ADBaseIO): +class AbstractAnalyserDriverIO( + ABC, StandardReadable, ADBaseIO, Generic[TAbstractBaseSequence] +): """ Generic device to configure electron analyser with new region settings. Electron analysers should inherit from this class for further specialisation. """ def __init__(self, prefix: str, name: str = "") -> None: + with self.add_children_as_readables(): + self.image = Reference( + epics_signal_r(Array1D[np.float64], prefix + "IMAGE") + ) + self.spectrum = epics_signal_r(Array1D[np.float64], prefix + "INT_SPECTRUM") + self.total_intensity = derived_signal_r( + self._calculate_total_intensity, spectrum=self.spectrum + ) + self.excitation_energy = soft_signal_rw(float, initial_value=0, units="eV") + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): # Used for setting up region data acquisition. self.region_name = soft_signal_rw(str, initial_value="null") @@ -40,28 +56,8 @@ def __init__(self, prefix: str, name: str = "") -> None: self.energy_step = epics_signal_rw(float, prefix + "STEP_SIZE") self.iterations = epics_signal_rw(int, prefix + "NumExposures") self.acquisition_mode = epics_signal_rw(str, prefix + "ACQ_MODE") - self.step_time = epics_signal_r(float, prefix + "AcquireTime") - self.total_steps = self._create_total_steps_signal(prefix) - self.total_time = derived_signal_r( - self._calculate_total_time, - "s", - total_steps=self.total_steps, - step_time=self.step_time, - iterations=self.iterations, - ) - - with self.add_children_as_readables(): - self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE") - self.spectrum = epics_signal_r(Array1D[np.float64], prefix + "INT_SPECTRUM") - self.total_intensity = derived_signal_r( - self._calculate_total_intensity, spectrum=self.spectrum - ) - # ToDo - Ideally the below are only collected once per region. However, they - # need to be read after the first point of a region (stream). Bluesky / - # ophyd currently doesn't support this and therefore must be read per point - # as a workaround, otherwise they return the previous region values. - self.excitation_energy = soft_signal_rw(float, initial_value=0, units="eV") + # Read once per scan after data acquired self.energy_axis = self._create_energy_axis_signal(prefix) self.binding_energy_axis = derived_signal_r( self._calculate_binding_energy_axis, @@ -71,6 +67,16 @@ def __init__(self, prefix: str, name: str = "") -> None: energy_mode=self.energy_mode, ) self.angle_axis = self._create_angle_axis_signal(prefix) + self.step_time = epics_signal_r(float, prefix + "AcquireTime") + self.total_steps = epics_signal_r(int, prefix + "TOTAL_POINTS_RBV") + self.total_time = derived_signal_r( + self._calculate_total_time, + "s", + total_steps=self.total_steps, + step_time=self.step_time, + iterations=self.iterations, + ) + super().__init__(prefix=prefix, name=name) @abstractmethod @@ -101,20 +107,13 @@ def _calculate_binding_energy_axis( ] ) - @abstractmethod - def _create_total_steps_signal(self, prefix: str) -> SignalR[int]: - """ - The signal that defines the total steps. Depends if analyser knows this - information before the first point. - """ - def _calculate_total_time( self, total_steps: int, step_time: float, iterations: int ) -> float: return total_steps * step_time * iterations def _calculate_total_intensity(self, spectrum: Array1D[np.float64]) -> float: - return float(np.sum(spectrum)) + return float(np.sum(spectrum, dtype=np.float64)) @property @abstractmethod @@ -124,6 +123,10 @@ def pass_energy_type(self) -> type: for the underlying analyser software and cannot be changed on epics side. """ + @abstractmethod + def sequence_type(self) -> type[TAbstractBaseSequence]: + pass + TAbstractAnalyserDriverIO = TypeVar( "TAbstractAnalyserDriverIO", bound=AbstractAnalyserDriverIO diff --git a/src/dodal/devices/electron_analyser/detector.py b/src/dodal/devices/electron_analyser/detector.py new file mode 100644 index 0000000000..0d445d9565 --- /dev/null +++ b/src/dodal/devices/electron_analyser/detector.py @@ -0,0 +1,185 @@ +import asyncio +from typing import Generic + +from bluesky.protocols import ( + Reading, + Stageable, + Triggerable, +) +from event_model import DataKey +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + AsyncStatus, + Device, + Reference, + set_and_wait_for_value, +) +from ophyd_async.core._protocol import AsyncConfigurable, AsyncReadable +from ophyd_async.epics.adcore import ( + DEFAULT_GOOD_STATES, + ADState, + stop_busy_record, +) + +from dodal.common.data_util import load_json_file_to_class +from dodal.devices.electron_analyser.abstract_analyser_io import ( + AbstractAnalyserDriverIO, +) +from dodal.devices.electron_analyser.abstract_region import ( + TAbstractBaseRegion, + TAbstractBaseSequence, +) + + +class AnalyserController: + def __init__( + self, + driver: AbstractAnalyserDriverIO, + good_states: frozenset[ADState] = DEFAULT_GOOD_STATES, + ) -> None: + self.driver = driver + self.good_states = good_states + self.frame_timeout = DEFAULT_TIMEOUT + self._arm_status: AsyncStatus | None = None + + async def arm(self): + self._arm_status = await self.start_acquiring_driver_and_ensure_status() + + async def disarm(self): + # We can't use caput callback as we already used it in arm() and we can't have + # 2 or they will deadlock + await stop_busy_record(self.driver.acquire, False, timeout=1) + + async def start_acquiring_driver_and_ensure_status(self) -> AsyncStatus: + """Start acquiring driver, raising ValueError if the detector is in a bad state. + This sets driver.acquire to True, and waits for it to be True up to a timeout. + Then, it checks that the DetectorState PV is in DEFAULT_GOOD_STATES, + and otherwise raises a ValueError. + :returns AsyncStatus: + An AsyncStatus that can be awaited to set driver.acquire to True and perform + subsequent raising (if applicable) due to detector state. + """ + status = await set_and_wait_for_value( + self.driver.acquire, + True, + timeout=DEFAULT_TIMEOUT, + wait_for_set_completion=False, + ) + + async def complete_acquisition() -> None: + # NOTE: possible race condition here between the callback from + # set_and_wait_for_value and the detector state updating. + await status + state = await self.driver.detector_state.get_value() + if state not in self.good_states: + raise ValueError( + f"Final detector state {state.value} not " + "in valid end states: {self.good_states}" + ) + + return AsyncStatus(complete_acquisition()) + + async def wait_for_idle(self): + if self._arm_status and not self._arm_status.done: + await self._arm_status + self._arm_status = None + + +class BaseElectronAnalyserDetector( + Device, + Stageable, + Triggerable, + AsyncReadable, + AsyncConfigurable, +): + """ + Detector for data acquisition of electron analyser. Can only acquire using settings + already configured for the device. + """ + + def __init__( + self, + name: str, + driver: AbstractAnalyserDriverIO, + ): + self.driver_ref: Reference[AbstractAnalyserDriverIO] = Reference(driver) + self.controller: AnalyserController = AnalyserController(driver=driver) + super().__init__(name) + + @AsyncStatus.wrap + async def trigger(self) -> None: + await self.controller.arm() + await self.controller.wait_for_idle() + + @AsyncStatus.wrap + async def stage(self) -> None: + """Make sure the detector is idle and ready to be used.""" + await asyncio.gather(self.controller.disarm()) + + @AsyncStatus.wrap + async def unstage(self) -> None: + """Disarm the detector.""" + await asyncio.gather(self.controller.disarm()) + + async def read(self) -> dict[str, Reading]: + return await self.driver_ref().read() + + async def describe(self) -> dict[str, DataKey]: + data = await self.driver_ref().describe() + # Correct the shape for image + prefix = self.driver_ref().name + "-" + energy_size = len(await self.driver_ref().energy_axis.get_value()) + angle_size = len(await self.driver_ref().angle_axis.get_value()) + data[prefix + "image"]["shape"] = [angle_size, energy_size] + return data + + async def read_configuration(self) -> dict[str, Reading]: + return await self.driver_ref().read_configuration() + + async def describe_configuration(self) -> dict[str, DataKey]: + return await self.driver_ref().describe_configuration() + + +class ElectronAnalyserRegionDetector( + BaseElectronAnalyserDetector, + Stageable, + Generic[TAbstractBaseRegion], +): + """ + Extends electron analyser detector to configure specific region settings before data + acqusition. + """ + + def __init__( + self, name: str, driver: AbstractAnalyserDriverIO, region: TAbstractBaseRegion + ): + super().__init__(name, driver) + self.region = region + + @AsyncStatus.wrap + async def stage(self) -> None: + super().stage() + # configure region here + + +class ElectronAnalyserDetector( + BaseElectronAnalyserDetector, + Generic[TAbstractBaseSequence, TAbstractBaseRegion], +): + """ + Electron analyser detector with the additional functionality to load a sequence file + and create a list of ElectronAnalyserRegionDetector objects. These will setup + configured region settings before data acquisition. + """ + + def load_sequence(self, filename: str) -> TAbstractBaseSequence: + return load_json_file_to_class(self.driver_ref().sequence_type(), filename) + + def create_region_detectors( + self, filename: str + ) -> list[ElectronAnalyserRegionDetector[TAbstractBaseRegion]]: + seq = self.load_sequence(filename) + return [ + ElectronAnalyserRegionDetector(self.name, self.driver_ref(), region) + for region in seq.get_enabled_regions() + ] diff --git a/src/dodal/devices/electron_analyser/specs_analyser_io.py b/src/dodal/devices/electron_analyser/specs_analyser_io.py index 169f731dca..c156e5ea6f 100644 --- a/src/dodal/devices/electron_analyser/specs_analyser_io.py +++ b/src/dodal/devices/electron_analyser/specs_analyser_io.py @@ -5,9 +5,10 @@ from dodal.devices.electron_analyser.abstract_analyser_io import ( AbstractAnalyserDriverIO, ) +from dodal.devices.electron_analyser.specs_region import SpecsSequence -class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO): +class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO[SpecsSequence]): def __init__(self, prefix: str, name: str = "") -> None: with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): # Used for setting up region data acquisition. @@ -59,9 +60,9 @@ def _calculate_energy_axis( axis = np.array([min_energy + i * step for i in range(total_points_iterations)]) return axis - def _create_total_steps_signal(self, prefix: str) -> SignalR[int]: - return epics_signal_r(int, prefix + "TOTAL_POINTS_RBV") - @property def pass_energy_type(self) -> type: return float + + def sequence_type(self) -> type[SpecsSequence]: + return SpecsSequence diff --git a/src/dodal/devices/electron_analyser/vgscienta_analyser_io.py b/src/dodal/devices/electron_analyser/vgscienta_analyser_io.py index 24f0e527ca..c3d6dc4c63 100644 --- a/src/dodal/devices/electron_analyser/vgscienta_analyser_io.py +++ b/src/dodal/devices/electron_analyser/vgscienta_analyser_io.py @@ -1,10 +1,5 @@ import numpy as np -from ophyd_async.core import ( - Array1D, - SignalR, - StandardReadableFormat, - soft_signal_rw, -) +from ophyd_async.core import Array1D, SignalR, StandardReadableFormat, soft_signal_rw from ophyd_async.epics.core import epics_signal_r, epics_signal_rw from dodal.devices.electron_analyser.abstract_analyser_io import ( @@ -12,12 +7,14 @@ ) from dodal.devices.electron_analyser.vgscienta_region import ( DetectorMode, + VGScientaSequence, ) -class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO): +class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO[VGScientaSequence]): def __init__(self, prefix: str, name: str = "") -> None: with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): + self.excitation_energy_source = soft_signal_rw(str, initial_value=None) # Used for setting up region data acquisition. self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY") self.first_x_channel = epics_signal_rw(int, prefix + "MinX") @@ -38,14 +35,9 @@ def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64] def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]: return epics_signal_r(Array1D[np.float64], prefix + "Y_SCALE_RBV") - def _create_total_steps_signal(self, prefix: str) -> SignalR[int]: - # ToDo - This ideally needs to be read from EPICS using "TOTAL_POINTS_RBV". - # However, this is only known after the first point and blueksky / ophyd - # currently doesn't support config signals read after first point. Instead - # use estimated value from region instead. This also might be fixed with - # implementing PEAKS for analyser software. - return soft_signal_rw(int, initial_value=1) - @property def pass_energy_type(self) -> type: return str + + def sequence_type(self) -> type[VGScientaSequence]: + return VGScientaSequence diff --git a/src/dodal/plan_stubs/electron_analyser/configure_driver.py b/src/dodal/plan_stubs/electron_analyser/configure_driver.py index 281a289c90..2e77238aa9 100644 --- a/src/dodal/plan_stubs/electron_analyser/configure_driver.py +++ b/src/dodal/plan_stubs/electron_analyser/configure_driver.py @@ -9,7 +9,9 @@ from dodal.devices.electron_analyser.abstract_region import ( AbstractBaseRegion, ) -from dodal.devices.electron_analyser.specs_analyser_io import SpecsAnalyserDriverIO +from dodal.devices.electron_analyser.specs_analyser_io import ( + SpecsAnalyserDriverIO, +) from dodal.devices.electron_analyser.specs_region import SpecsRegion from dodal.devices.electron_analyser.util import to_kinetic_energy from dodal.devices.electron_analyser.vgscienta_analyser_io import ( @@ -89,9 +91,7 @@ def configure_vgscienta( analyser.x_channel_size, region.x_channel_size(), analyser.y_channel_size, region.y_channel_size(), analyser.detector_mode, region.detector_mode, + analyser.excitation_energy_source, region.excitation_energy_source, analyser.image_mode, ADImageMode.SINGLE, - # The below should ideally be done read from epics. - # See issue https://github.com/bluesky/ophyd-async/issues/877 - analyser.total_steps, region.total_steps ) # fmt: on diff --git a/tests/devices/unit_tests/electron_analyser/conftest.py b/tests/devices/unit_tests/electron_analyser/conftest.py index 45ff2d2ebf..71d5066b6a 100644 --- a/tests/devices/unit_tests/electron_analyser/conftest.py +++ b/tests/devices/unit_tests/electron_analyser/conftest.py @@ -4,19 +4,25 @@ import pytest from ophyd_async.core import init_devices -from dodal.common.data_util import load_json_file_to_class from dodal.devices.electron_analyser import ( + ElectronAnalyserDetector, + SpecsAnalyserDriverIO, + VGScientaAnalyserDriverIO, VGScientaRegion, VGScientaSequence, ) from dodal.devices.electron_analyser.abstract_analyser_io import ( + AbstractAnalyserDriverIO, TAbstractAnalyserDriverIO, ) from dodal.devices.electron_analyser.abstract_region import ( AbstractBaseRegion, AbstractBaseSequence, TAbstractBaseRegion, - TAbstractBaseSequence, +) +from tests.devices.unit_tests.electron_analyser.test_util import ( + TEST_SPECS_SEQUENCE, + TEST_VGSCIENTA_SEQUENCE, ) TEST_DATA_PATH = "tests/test_data/electron_analyser/" @@ -27,11 +33,43 @@ def sequence_file_path(sequence_file: str) -> str: return os.path.join(TEST_DATA_PATH, sequence_file) +@pytest.fixture +def sequence_file(analyser_class: type[AbstractAnalyserDriverIO]) -> str: + if analyser_class == VGScientaAnalyserDriverIO: + return TEST_VGSCIENTA_SEQUENCE + elif analyser_class == SpecsAnalyserDriverIO: + return TEST_SPECS_SEQUENCE + raise Exception + + +@pytest.fixture +async def sim_analyser_driver( + analyser_class: type[TAbstractAnalyserDriverIO], +) -> TAbstractAnalyserDriverIO: + async with init_devices(mock=True, connect=True): + sim_analyser_driver = analyser_class( + prefix="sim_analyser_driver:", + name="analyser_driver", + ) + return sim_analyser_driver + + +@pytest.fixture +async def sim_analyser( + sim_analyser_driver: AbstractAnalyserDriverIO, +) -> ElectronAnalyserDetector[AbstractBaseSequence, AbstractBaseRegion]: + async with init_devices(mock=True, connect=True): + sim_analyser = ElectronAnalyserDetector( + name="analyser", driver=sim_analyser_driver + ) + return sim_analyser + + @pytest.fixture def sequence( - sequence_class: type[TAbstractBaseSequence], sequence_file_path: str -) -> TAbstractBaseSequence: - return load_json_file_to_class(sequence_class, sequence_file_path) + sim_analyser: ElectronAnalyserDetector, sequence_file_path: str +) -> AbstractBaseSequence[AbstractBaseRegion]: + return sim_analyser.load_sequence(sequence_file_path) @pytest.fixture @@ -70,15 +108,3 @@ def expected_enabled_region_names( if expected_region_value["enabled"]: names.append(expected_region_value["name"]) return names - - -@pytest.fixture -async def sim_analyser_driver( - analyser_type: type[TAbstractAnalyserDriverIO], -) -> TAbstractAnalyserDriverIO: - async with init_devices(mock=True, connect=True): - sim_analyser_driver = analyser_type( - prefix="sim_analyser_driver:", - name="analyser_driver", - ) - return sim_analyser_driver diff --git a/tests/devices/unit_tests/electron_analyser/test_base_analyser_io.py b/tests/devices/unit_tests/electron_analyser/test_base_analyser_io.py index e2356285c0..374eb86866 100644 --- a/tests/devices/unit_tests/electron_analyser/test_base_analyser_io.py +++ b/tests/devices/unit_tests/electron_analyser/test_base_analyser_io.py @@ -8,9 +8,7 @@ from dodal.devices.electron_analyser import ( SpecsAnalyserDriverIO, - SpecsSequence, VGScientaAnalyserDriverIO, - VGScientaSequence, to_kinetic_energy, ) from dodal.devices.electron_analyser.abstract_analyser_io import ( @@ -18,43 +16,20 @@ ) from dodal.devices.electron_analyser.abstract_region import ( AbstractBaseRegion, - AbstractBaseSequence, ) from dodal.plan_stubs.electron_analyser import configure_analyser from tests.devices.unit_tests.electron_analyser.test_util import ( TEST_SEQUENCE_REGION_NAMES, - TEST_SPECS_SEQUENCE, - TEST_VGSCIENTA_SEQUENCE, assert_read_configuration_has_expected_value, assert_read_has_expected_value, ) -@pytest.fixture(params=[SpecsSequence, VGScientaSequence]) -def sequence_class(request: pytest.FixtureRequest) -> type[AbstractBaseSequence]: +@pytest.fixture(params=[SpecsAnalyserDriverIO, VGScientaAnalyserDriverIO]) +def analyser_class(request: pytest.FixtureRequest) -> type[AbstractAnalyserDriverIO]: return request.param -@pytest.fixture -def sequence_file(sequence_class: type[AbstractBaseSequence]) -> str: - if sequence_class == VGScientaSequence: - return TEST_VGSCIENTA_SEQUENCE - elif sequence_class == SpecsSequence: - return TEST_SPECS_SEQUENCE - raise Exception - - -@pytest.fixture -def analyser_type( - sequence_class: type[AbstractBaseSequence], -) -> type[AbstractAnalyserDriverIO]: - if sequence_class == VGScientaSequence: - return VGScientaAnalyserDriverIO - elif sequence_class == SpecsSequence: - return SpecsAnalyserDriverIO - raise Exception - - @pytest.mark.parametrize("region", TEST_SEQUENCE_REGION_NAMES, indirect=["region"]) def test_analyser_to_kinetic_energy( sim_analyser_driver: AbstractAnalyserDriverIO, diff --git a/tests/devices/unit_tests/electron_analyser/test_detector.py b/tests/devices/unit_tests/electron_analyser/test_detector.py new file mode 100644 index 0000000000..01e7d7299b --- /dev/null +++ b/tests/devices/unit_tests/electron_analyser/test_detector.py @@ -0,0 +1,52 @@ +import pytest + +from dodal.devices.electron_analyser import ( + ElectronAnalyserDetector, + SpecsAnalyserDriverIO, + VGScientaAnalyserDriverIO, +) +from dodal.devices.electron_analyser.abstract_analyser_io import ( + AbstractAnalyserDriverIO, +) +from dodal.devices.electron_analyser.abstract_region import ( + AbstractBaseRegion, + AbstractBaseSequence, +) +from tests.devices.unit_tests.electron_analyser.test_util import ( + TEST_SPECS_SEQUENCE, + TEST_VGSCIENTA_SEQUENCE, +) + + +@pytest.fixture(params=[SpecsAnalyserDriverIO, VGScientaAnalyserDriverIO]) +def analyser_class(request: pytest.FixtureRequest) -> type[AbstractAnalyserDriverIO]: + return request.param + + +@pytest.fixture +def sequence_file(analyser_class: type[AbstractAnalyserDriverIO]) -> str: + if analyser_class == VGScientaAnalyserDriverIO: + return TEST_VGSCIENTA_SEQUENCE + elif analyser_class == SpecsAnalyserDriverIO: + return TEST_SPECS_SEQUENCE + raise Exception + + +def test_analyser_detector_loads_sequence_correctly( + sim_analyser: ElectronAnalyserDetector[AbstractBaseSequence, AbstractBaseRegion], + sequence_file_path: str, +): + seq = sim_analyser.load_sequence(sequence_file_path) + assert isinstance(seq, sim_analyser.driver_ref().sequence_type()) + + +def test_analyser_detector_creates_region_detectors( + sim_analyser: ElectronAnalyserDetector[AbstractBaseSequence, AbstractBaseRegion], + sequence_file_path: str, +): + seq = sim_analyser.load_sequence(sequence_file_path) + region_detectors = sim_analyser.create_region_detectors(sequence_file_path) + + assert len(region_detectors) == len(seq.get_enabled_regions()) + for det in region_detectors: + assert det.region.enabled is True diff --git a/tests/devices/unit_tests/electron_analyser/test_specs_analyser_io.py b/tests/devices/unit_tests/electron_analyser/test_specs_analyser_io.py index 5b4e8754ba..e6eafa5625 100644 --- a/tests/devices/unit_tests/electron_analyser/test_specs_analyser_io.py +++ b/tests/devices/unit_tests/electron_analyser/test_specs_analyser_io.py @@ -11,7 +11,6 @@ EnergyMode, SpecsAnalyserDriverIO, SpecsRegion, - SpecsSequence, ) from dodal.plan_stubs.electron_analyser import configure_specs from tests.devices.unit_tests.electron_analyser.test_util import ( @@ -22,7 +21,7 @@ @pytest.fixture -def analyser_type() -> type[SpecsAnalyserDriverIO]: +def analyser_class() -> type[SpecsAnalyserDriverIO]: return SpecsAnalyserDriverIO @@ -31,11 +30,6 @@ def sequence_file() -> str: return TEST_SPECS_SEQUENCE -@pytest.fixture -def sequence_class() -> type[SpecsSequence]: - return SpecsSequence - - @pytest.mark.parametrize("region", TEST_SEQUENCE_REGION_NAMES, indirect=["region"]) async def test_given_region_that_analyser_sets_energy_values_correctly( sim_analyser_driver: SpecsAnalyserDriverIO, diff --git a/tests/devices/unit_tests/electron_analyser/test_specs_region.py b/tests/devices/unit_tests/electron_analyser/test_specs_region.py index 10168efd9c..a95ff01e42 100644 --- a/tests/devices/unit_tests/electron_analyser/test_specs_region.py +++ b/tests/devices/unit_tests/electron_analyser/test_specs_region.py @@ -2,6 +2,7 @@ import pytest +from dodal.common.data_util import load_json_file_to_class from dodal.devices.electron_analyser import EnergyMode, SpecsRegion, SpecsSequence from dodal.devices.electron_analyser.abstract_region import TAbstractBaseRegion from tests.devices.unit_tests.electron_analyser.test_util import ( @@ -17,8 +18,8 @@ def sequence_file() -> str: @pytest.fixture -def sequence_class() -> type[SpecsSequence]: - return SpecsSequence +def sequence(sequence_file_path: str) -> SpecsSequence: + return load_json_file_to_class(SpecsSequence, sequence_file_path) @pytest.fixture diff --git a/tests/devices/unit_tests/electron_analyser/test_vgscienta_analyser_io.py b/tests/devices/unit_tests/electron_analyser/test_vgscienta_analyser_io.py index 2d601d3824..5fd2a30f81 100644 --- a/tests/devices/unit_tests/electron_analyser/test_vgscienta_analyser_io.py +++ b/tests/devices/unit_tests/electron_analyser/test_vgscienta_analyser_io.py @@ -11,7 +11,6 @@ EnergyMode, VGScientaAnalyserDriverIO, VGScientaRegion, - VGScientaSequence, to_kinetic_energy, ) from dodal.plan_stubs.electron_analyser import configure_vgscienta @@ -28,12 +27,7 @@ def sequence_file() -> str: @pytest.fixture -def sequence_class() -> type[VGScientaSequence]: - return VGScientaSequence - - -@pytest.fixture -def analyser_type() -> type[VGScientaAnalyserDriverIO]: +def analyser_class() -> type[VGScientaAnalyserDriverIO]: return VGScientaAnalyserDriverIO @@ -143,11 +137,3 @@ async def test_that_data_to_read_is_correct( await sim_analyser_driver.binding_energy_axis.get_value(), expected_binding_energy_axis, ) - - expected_total_steps = region.total_steps - get_mock_put(sim_analyser_driver.total_steps).assert_called_once_with( - expected_total_steps, wait=True - ) - await assert_read_configuration_has_expected_value( - sim_analyser_driver, "total_steps", expected_total_steps - ) diff --git a/tests/devices/unit_tests/electron_analyser/test_vgscienta_region.py b/tests/devices/unit_tests/electron_analyser/test_vgscienta_region.py index 221cac61ae..fc7776312b 100644 --- a/tests/devices/unit_tests/electron_analyser/test_vgscienta_region.py +++ b/tests/devices/unit_tests/electron_analyser/test_vgscienta_region.py @@ -2,6 +2,7 @@ import pytest +from dodal.common.data_util import load_json_file_to_class from dodal.devices.electron_analyser import ( EnergyMode, VGScientaRegion, @@ -28,8 +29,8 @@ def sequence_file() -> str: @pytest.fixture -def sequence_class() -> type[VGScientaSequence]: - return VGScientaSequence +def sequence(sequence_file_path: str) -> VGScientaSequence: + return load_json_file_to_class(VGScientaSequence, sequence_file_path) @pytest.fixture