From 2e59d796d124832ee87cddbee79e1393b6daf670 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 22 May 2026 13:38:08 +0000 Subject: [PATCH 1/4] Add MbsDetector, remove unneeded EnergySource logic --- src/dodal/beamlines/b07.py | 10 +-- src/dodal/beamlines/b07_1.py | 10 +-- src/dodal/beamlines/i05.py | 8 +- src/dodal/beamlines/i05_1.py | 8 +- src/dodal/beamlines/i09.py | 2 +- src/dodal/beamlines/i09_1.py | 12 +-- src/dodal/beamlines/p60.py | 2 +- .../electron_analyser/base/__init__.py | 4 +- .../electron_analyser/base/base_detector.py | 14 ++-- .../electron_analyser/base/detector_logic.py | 5 +- .../electron_analyser/base/energy_sources.py | 64 ++++---------- .../devices/electron_analyser/mbs/__init__.py | 9 +- .../electron_analyser/mbs/mbs_detector.py | 84 +++++++++++++++++++ .../electron_analyser/specs/specs_detector.py | 49 +++++------ .../vgscienta/vgscienta_detector.py | 82 +++++++++--------- .../vgscienta/vgscienta_driver_io.py | 21 ++--- .../vgscienta/vgscienta_region.py | 16 ++-- .../base/test_base_detector.py | 4 +- .../base/test_detector_logic.py | 17 ++-- .../base/test_energy_sources.py | 60 +++---------- tests/devices/electron_analyser/conftest.py | 24 +++--- 21 files changed, 254 insertions(+), 251 deletions(-) create mode 100644 src/dodal/devices/electron_analyser/mbs/mbs_detector.py diff --git a/src/dodal/beamlines/b07.py b/src/dodal/beamlines/b07.py index 578b4ac6a44..a30c3343c5a 100644 --- a/src/dodal/beamlines/b07.py +++ b/src/dodal/beamlines/b07.py @@ -7,7 +7,6 @@ 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.hutch_shutter import ( EXP_SHUTTER_2_INFIX, @@ -48,20 +47,15 @@ def pgm() -> PlaneGratingMonochromator: ) -@devices.factory() -def energy_source(pgm: PlaneGratingMonochromator) -> EnergySource: - return EnergySource(pgm.energy.user_readback) - - # CAM:IMAGE will fail to connect outside the beamline network, # see https://github.com/DiamondLightSource/dodal/issues/1852 @devices.factory() -def analyser(energy_source: EnergySource) -> SpecsDetector[LensMode, PsuMode]: +def analyser(pgm: PlaneGratingMonochromator) -> SpecsDetector[LensMode, PsuMode]: return SpecsDetector[LensMode, PsuMode]( prefix=f"{B_PREFIX.beamline_prefix}-EA-DET-01:CAM:", lens_mode_type=LensMode, psu_mode_type=PsuMode, - energy_source=energy_source, + energy_source=pgm.energy.user_readback, ) diff --git a/src/dodal/beamlines/b07_1.py b/src/dodal/beamlines/b07_1.py index 90391c3e7af..0e21d76eccf 100644 --- a/src/dodal/beamlines/b07_1.py +++ b/src/dodal/beamlines/b07_1.py @@ -7,7 +7,6 @@ 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.hutch_shutter import HutchShutter from dodal.devices.motors import XYZAzimuthPolarStage @@ -42,20 +41,15 @@ def ccmc() -> ChannelCutMonochromator: return ChannelCutMonochromator(prefix=f"{C_PREFIX.beamline_prefix}-OP-CCM-01:") -@devices.factory() -def energy_source(pgm: PlaneGratingMonochromator) -> EnergySource: - return EnergySource(pgm.energy.user_readback) - - # CAM:IMAGE will fail to connect outside the beamline network, # see https://github.com/DiamondLightSource/dodal/issues/1852 @devices.factory() -def analyser(energy_source: EnergySource) -> SpecsDetector[LensMode, PsuMode]: +def analyser(pgm: PlaneGratingMonochromator) -> SpecsDetector[LensMode, PsuMode]: return SpecsDetector[LensMode, PsuMode]( prefix=f"{C_PREFIX.beamline_prefix}-EA-DET-01:CAM:", lens_mode_type=LensMode, psu_mode_type=PsuMode, - energy_source=energy_source, + energy_source=pgm.energy.user_readback, ) diff --git a/src/dodal/beamlines/i05.py b/src/dodal/beamlines/i05.py index 1ad7d939e36..c0a3d525923 100644 --- a/src/dodal/beamlines/i05.py +++ b/src/dodal/beamlines/i05.py @@ -4,8 +4,9 @@ from dodal.devices.beamlines.i05 import I05Goniometer from dodal.devices.beamlines.i05_shared import LensMode, M4M5Mirror, PassEnergy from dodal.devices.common_mirror import XYZSwitchingMirror -from dodal.devices.electron_analyser.mbs import MbsAnalyserDriverIO +from dodal.devices.electron_analyser.mbs import MbsDetector from dodal.devices.hutch_shutter import HutchShutter +from dodal.devices.pgm import PlaneGratingMonochromator from dodal.devices.temperture_controller import Lakeshore336 from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name @@ -50,9 +51,10 @@ def sa() -> I05Goniometer: @devices.factory -def analyser_driver() -> MbsAnalyserDriverIO: - return MbsAnalyserDriverIO[LensMode, PassEnergy]( +def analyser(pgm: PlaneGratingMonochromator) -> MbsDetector[LensMode, PassEnergy]: + return MbsDetector[LensMode, PassEnergy]( prefix=f"{PREFIX.beamline_prefix}-EA-DET-02:CAM:", lens_mode_type=LensMode, pass_energy_type=PassEnergy, + energy_source=pgm.energy.user_readback, ) diff --git a/src/dodal/beamlines/i05_1.py b/src/dodal/beamlines/i05_1.py index 0623dc15c26..62e62d68c56 100644 --- a/src/dodal/beamlines/i05_1.py +++ b/src/dodal/beamlines/i05_1.py @@ -4,8 +4,9 @@ from dodal.devices.beamlines.i05_1 import XYZAzimuthPolarDefocusStage from dodal.devices.beamlines.i05_shared import LensMode, Mj7j8Mirror, PassEnergy from dodal.devices.common_mirror import XYZPiezoSwitchingMirror -from dodal.devices.electron_analyser.mbs import MbsAnalyserDriverIO +from dodal.devices.electron_analyser.mbs import MbsDetector from dodal.devices.hutch_shutter import HutchShutter +from dodal.devices.pgm import PlaneGratingMonochromator from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name @@ -39,9 +40,10 @@ def sm() -> XYZAzimuthPolarDefocusStage: @devices.factory -def analyser_driver() -> MbsAnalyserDriverIO[LensMode, PassEnergy]: - return MbsAnalyserDriverIO[LensMode, PassEnergy]( +def analyser(pgm: PlaneGratingMonochromator) -> MbsDetector[LensMode, PassEnergy]: + return MbsDetector[LensMode, PassEnergy]( prefix=f"{PREFIX.beamline_prefix}-EA-DET-04:CAM:", lens_mode_type=LensMode, pass_energy_type=PassEnergy, + energy_source=pgm.energy.user_readback, ) diff --git a/src/dodal/beamlines/i09.py b/src/dodal/beamlines/i09.py index b7c989c4a18..a005371723f 100644 --- a/src/dodal/beamlines/i09.py +++ b/src/dodal/beamlines/i09.py @@ -117,7 +117,7 @@ def ew4000( lens_mode_type=LensMode, psu_mode_type=PsuMode, pass_energy_type=PassEnergy, - energy_source=dual_energy_source, + energy_source=dual_energy_source.energy, shutter=dual_fast_shutter, source_selector=source_selector, ) diff --git a/src/dodal/beamlines/i09_1.py b/src/dodal/beamlines/i09_1.py index bb92effeece..8e504ad0d97 100644 --- a/src/dodal/beamlines/i09_1.py +++ b/src/dodal/beamlines/i09_1.py @@ -3,7 +3,6 @@ from dodal.device_manager import DeviceManager from dodal.devices.beamlines.i09_1 import LensMode, PsuMode from dodal.devices.common_dcm import DoubleCrystalMonochromatorWithDSpacing -from dodal.devices.electron_analyser.base import EnergySource from dodal.devices.electron_analyser.specs import SpecsDetector from dodal.devices.motors import XYZAzimuthTiltPolarStage from dodal.devices.synchrotron import Synchrotron @@ -25,20 +24,17 @@ def synchrotron() -> Synchrotron: return Synchrotron() -@devices.factory() -def energy_source(dcm: DoubleCrystalMonochromatorWithDSpacing) -> EnergySource: - return EnergySource(dcm.energy_in_eV) - - # CAM:IMAGE will fail to connect outside the beamline network, # see https://github.com/DiamondLightSource/dodal/issues/1852 @devices.factory() -def analyser(energy_source: EnergySource) -> SpecsDetector[LensMode, PsuMode]: +def analyser( + dcm: DoubleCrystalMonochromatorWithDSpacing, +) -> SpecsDetector[LensMode, PsuMode]: return SpecsDetector[LensMode, PsuMode]( prefix=f"{PREFIX.beamline_prefix}-EA-DET-02:CAM:", lens_mode_type=LensMode, psu_mode_type=PsuMode, - energy_source=energy_source, + energy_source=dcm.energy_in_eV, ) diff --git a/src/dodal/beamlines/p60.py b/src/dodal/beamlines/p60.py index d0a35b881f9..cfe2176488f 100644 --- a/src/dodal/beamlines/p60.py +++ b/src/dodal/beamlines/p60.py @@ -65,5 +65,5 @@ def r4000( lens_mode_type=LensMode, psu_mode_type=PsuMode, pass_energy_type=PassEnergy, - energy_source=energy_source, + energy_source=energy_source.energy, ) diff --git a/src/dodal/devices/electron_analyser/base/__init__.py b/src/dodal/devices/electron_analyser/base/__init__.py index 6feb7e04556..a088b6fd8dd 100644 --- a/src/dodal/devices/electron_analyser/base/__init__.py +++ b/src/dodal/devices/electron_analyser/base/__init__.py @@ -16,7 +16,7 @@ TLensMode, ) from .base_util import to_binding_energy, to_kinetic_energy -from .energy_sources import AbstractEnergySource, DualEnergySource, EnergySource +from .energy_sources import DualEnergySource __all__ = [ "ElectronAnalyserDetector", @@ -35,7 +35,5 @@ "TLensMode", "to_binding_energy", "to_kinetic_energy", - "AbstractEnergySource", "DualEnergySource", - "EnergySource", ] diff --git a/src/dodal/devices/electron_analyser/base/base_detector.py b/src/dodal/devices/electron_analyser/base/base_detector.py index 9cd56067c58..3c1acd6dec8 100644 --- a/src/dodal/devices/electron_analyser/base/base_detector.py +++ b/src/dodal/devices/electron_analyser/base/base_detector.py @@ -1,4 +1,5 @@ import asyncio +from collections.abc import Sequence from typing import Generic import numpy as np @@ -8,6 +9,7 @@ AsyncStatus, DetectorArmLogic, DetectorTriggerLogic, + SignalR, StandardDetector, derived_signal_r, ) @@ -63,19 +65,21 @@ def __init__( arm_logic: DetectorArmLogic, trigger_logic: DetectorTriggerLogic, region_logic: RegionLogic, + config_sigs: Sequence[SignalR] = (), name: str = "", ): + self._region_logic = region_logic + # ToDo - Add data logic + self.add_detector_logics(arm_logic, trigger_logic) + self.binding_energy_axis = derived_signal_r( self._calculate_binding_energy_axis, "eV", energy_axis=region_logic.driver.energy_axis, - excitation_energy=region_logic.energy_source.energy, + excitation_energy=region_logic.energy_source, energy_mode=region_logic.driver.energy_mode, ) - self._region_logic = region_logic - # ToDo - Add data logic - self.add_detector_logics(arm_logic, trigger_logic) - self.add_config_signals(self.binding_energy_axis) + self.add_config_signals(self.binding_energy_axis, *config_sigs) self.sequence = SequenceHolder() super().__init__(name) diff --git a/src/dodal/devices/electron_analyser/base/detector_logic.py b/src/dodal/devices/electron_analyser/base/detector_logic.py index f4d60ffa816..17f8dbd1981 100644 --- a/src/dodal/devices/electron_analyser/base/detector_logic.py +++ b/src/dodal/devices/electron_analyser/base/detector_logic.py @@ -12,7 +12,6 @@ TAbstractAnalyserDriverIO, ) from dodal.devices.electron_analyser.base.base_region import BaseRegion -from dodal.devices.electron_analyser.base.energy_sources import AbstractEnergySource from dodal.devices.fast_shutter import GenericFastShutter from dodal.devices.selectable_source import SourceSelector @@ -76,7 +75,7 @@ class RegionLogic: def __init__( self, driver: AbstractAnalyserDriverIO, - energy_source: AbstractEnergySource, + energy_source: SignalR[float], source_selector: SourceSelector | None = None, ): self.driver = driver @@ -88,6 +87,6 @@ async def setup_with_region(self, region: BaseRegion) -> None: if self.source_selector is not None: await self.source_selector.set(region.excitation_energy_source) - excitation_energy = await self.energy_source.energy.get_value() + excitation_energy = await self.energy_source.get_value() epics_region = region.prepare_for_epics(excitation_energy) await self.driver.set(epics_region) diff --git a/src/dodal/devices/electron_analyser/base/energy_sources.py b/src/dodal/devices/electron_analyser/base/energy_sources.py index 6404ac5dce3..14c81496f06 100644 --- a/src/dodal/devices/electron_analyser/base/energy_sources.py +++ b/src/dodal/devices/electron_analyser/base/energy_sources.py @@ -1,5 +1,3 @@ -from abc import abstractmethod - from ophyd_async.core import ( Reference, SignalR, @@ -13,37 +11,6 @@ from dodal.devices.selectable_source import SelectedSource, get_obj_from_selected_source -class AbstractEnergySource(StandardReadable): - """Abstract device that wraps an energy source signal and provides common interface - via a energy signal. - """ - - @property - @abstractmethod - def energy(self) -> SignalR[float]: - """Signal to provide the excitation energy value in eV.""" - - -class EnergySource(AbstractEnergySource): - """Wraps a signal that relates to energy and provides common interface via energy - signal. It provides the name of the wrapped signal as a child signal in the - read_configuration via wrapped_device_name and adds the signal as a readable. - """ - - def __init__(self, source: SignalR[float], name: str = "") -> None: - self.add_readables([source]) - with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): - self.wrapped_device_name, _ = soft_signal_r_and_setter( - str, initial_value=source.name - ) - self._source_ref = Reference(source) - super().__init__(name) - - @property - def energy(self) -> SignalR[float]: - return self._source_ref() - - def get_float_from_selected_source( selected: SelectedSource, s1: float, s2: float ) -> float: @@ -51,8 +18,8 @@ def get_float_from_selected_source( return get_obj_from_selected_source(selected, s1, s2) -class DualEnergySource(AbstractEnergySource): - """Holds two EnergySource devices and provides a signal to read energy depending on +class DualEnergySource(StandardReadable): + """Provides a signal to read energy depending on which source is selected. The energy is the one that corrosponds to the selected_source signal. For example, selected_source is source1 if selected_source is at SelectedSource.SOURCE1 and vise versa for source2 and @@ -73,21 +40,20 @@ def __init__( name: str = "", ): self.selected_source_ref = Reference(selected_source) + self.source1_ref = Reference(source1) + self.source2_ref = Reference(source2) with self.add_children_as_readables(): - self.source1 = EnergySource(source1) - self.source2 = EnergySource(source2) + self.energy = derived_signal_r( + get_float_from_selected_source, + "eV", + selected_source=selected_source, + s1=source1, + s2=source2, + ) - self._selected_energy = derived_signal_r( - get_float_from_selected_source, - "eV", - selected=self.selected_source_ref(), - s1=self.source1.energy, - s2=self.source2.energy, - ) - self.add_readables([selected_source]) + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): + self.source1, _ = soft_signal_r_and_setter(str, initial_value=source1.name) + self.source2, _ = soft_signal_r_and_setter(str, initial_value=source2.name) + self.add_readables([selected_source, source1, source2]) super().__init__(name) - - @property - def energy(self) -> SignalR[float]: - return self._selected_energy diff --git a/src/dodal/devices/electron_analyser/mbs/__init__.py b/src/dodal/devices/electron_analyser/mbs/__init__.py index 51bf30be1db..befe299bcca 100644 --- a/src/dodal/devices/electron_analyser/mbs/__init__.py +++ b/src/dodal/devices/electron_analyser/mbs/__init__.py @@ -1,5 +1,12 @@ +from .mbs_detector import MbsDetector from .mbs_driver_io import MbsAnalyserDriverIO from .mbs_enums import AcquisitionMode from .mbs_region import MbsRegion, MbsSequence -__all__ = ["MbsAnalyserDriverIO", "AcquisitionMode", "MbsRegion", "MbsSequence"] +__all__ = [ + "MbsDetector", + "MbsAnalyserDriverIO", + "AcquisitionMode", + "MbsRegion", + "MbsSequence", +] diff --git a/src/dodal/devices/electron_analyser/mbs/mbs_detector.py b/src/dodal/devices/electron_analyser/mbs/mbs_detector.py new file mode 100644 index 00000000000..1ac189d36f5 --- /dev/null +++ b/src/dodal/devices/electron_analyser/mbs/mbs_detector.py @@ -0,0 +1,84 @@ +from typing import Generic + +from ophyd_async.core import SignalR, soft_signal_rw +from ophyd_async.epics.adcore import ADArmLogic + +from dodal.devices.electron_analyser.base import ElectronAnalyserDetector +from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector +from dodal.devices.electron_analyser.base.base_region import TLensMode, TPassEnergy +from dodal.devices.electron_analyser.base.detector_logic import ( + ADArmLogic, + ElectronAnalayserTriggerLogic, + RegionLogic, + ShutterCoordinatorADArmLogic, +) +from dodal.devices.electron_analyser.mbs.mbs_driver_io import MbsAnalyserDriverIO +from dodal.devices.electron_analyser.mbs.mbs_region import MbsRegion +from dodal.devices.fast_shutter import GenericFastShutter +from dodal.devices.selectable_source import SourceSelector + + +class MbsDetector( + ElectronAnalyserDetector[ + MbsAnalyserDriverIO[TLensMode, TPassEnergy], MbsRegion[TLensMode, TPassEnergy] + ], + Generic[TLensMode, TPassEnergy], +): + def __init__( + self, + prefix: str, + lens_mode_type: type[TLensMode], + pass_energy_type: type[TPassEnergy], + energy_source: SignalR[float], + shutter: GenericFastShutter | None = None, + source_selector: SourceSelector | None = None, + name: str = "", + ): + # Make attribute of class so connect applies to driver and populates parent. + self.driver = MbsAnalyserDriverIO[TLensMode, TPassEnergy]( + prefix, lens_mode_type, pass_energy_type + ) + region_logic = RegionLogic(self.driver, energy_source, source_selector) + self.close_shutter_idle = soft_signal_rw(bool, initial_value=True) + arm_logic = ( + ShutterCoordinatorADArmLogic(self.driver, shutter, self.close_shutter_idle) + if shutter is not None + else ADArmLogic(self.driver) + ) + trigger_logic = ElectronAnalayserTriggerLogic(self.driver, set()) + config_sigs = ( + self.driver.region_name, + self.driver.energy_mode, + self.driver.acquisition_mode, + self.driver.lens_mode, + self.driver.low_energy, + self.driver.centre_energy, + self.driver.high_energy, + self.driver.deflector_x, + self.driver.energy_step, + self.driver.pass_energy, + self.driver.slices, + self.driver.iterations, + self.driver.total_steps, + self.driver.acquire_time, + self.driver.acquire_period, + self.driver.total_time, + self.driver.energy_axis, + self.driver.angle_axis, + self.driver.psu_mode, + self.driver.dither_steps, + self.driver.spin_offset, + self.driver.array_size_x, + self.driver.array_size_y, + self.driver.min_x, + self.driver.min_y, + self.driver.max_x, + self.driver.max_y, + ) + super().__init__( + region_logic=region_logic, + arm_logic=arm_logic, + trigger_logic=trigger_logic, + config_sigs=config_sigs, + name=name, + ) diff --git a/src/dodal/devices/electron_analyser/specs/specs_detector.py b/src/dodal/devices/electron_analyser/specs/specs_detector.py index 21ec603c91d..0b382ec32b3 100644 --- a/src/dodal/devices/electron_analyser/specs/specs_detector.py +++ b/src/dodal/devices/electron_analyser/specs/specs_detector.py @@ -1,6 +1,6 @@ from typing import Generic -from ophyd_async.core import soft_signal_rw +from ophyd_async.core import SignalR, soft_signal_rw from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector from dodal.devices.electron_analyser.base.base_region import TLensMode, TPsuMode @@ -10,7 +10,6 @@ RegionLogic, ShutterCoordinatorADArmLogic, ) -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 from dodal.devices.fast_shutter import GenericFastShutter @@ -29,7 +28,7 @@ def __init__( prefix: str, lens_mode_type: type[TLensMode], psu_mode_type: type[TPsuMode], - energy_source: AbstractEnergySource, + energy_source: SignalR[float], shutter: GenericFastShutter | None = None, source_selector: SourceSelector | None = None, name: str = "", @@ -45,33 +44,31 @@ def __init__( if shutter is not None else ADArmLogic(self.driver) ) - trigger_logic = ElectronAnalayserTriggerLogic( - self.driver, - { - self.driver.region_name, - self.driver.energy_mode, - self.driver.acquisition_mode, - self.driver.lens_mode, - self.driver.low_energy, - self.driver.centre_energy, - self.driver.high_energy, - self.driver.energy_step, - self.driver.pass_energy, - self.driver.slices, - self.driver.acquire_time, - self.driver.iterations, - self.driver.total_steps, - self.driver.total_time, - self.driver.energy_axis, - self.driver.angle_axis, - self.driver.snapshot_values, - self.driver.psu_mode, - }, + trigger_logic = ElectronAnalayserTriggerLogic(self.driver, set()) + config_sigs = ( + self.driver.region_name, + self.driver.energy_mode, + self.driver.acquisition_mode, + self.driver.lens_mode, + self.driver.low_energy, + self.driver.centre_energy, + self.driver.high_energy, + self.driver.energy_step, + self.driver.pass_energy, + self.driver.slices, + self.driver.acquire_time, + self.driver.iterations, + self.driver.total_steps, + self.driver.total_time, + self.driver.energy_axis, + self.driver.angle_axis, + self.driver.snapshot_values, + self.driver.psu_mode, ) - super().__init__( region_logic=region_logic, arm_logic=arm_logic, trigger_logic=trigger_logic, + config_sigs=config_sigs, name=name, ) diff --git a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py index 1850588cf17..b5a2f692ef3 100644 --- a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +++ b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py @@ -1,48 +1,48 @@ from typing import Generic -from ophyd_async.core import soft_signal_rw +from ophyd_async.core import SignalR, soft_signal_rw from ophyd_async.epics.adcore import ADArmLogic from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector -from dodal.devices.electron_analyser.base.base_region import TLensMode, TPsuMode +from dodal.devices.electron_analyser.base.base_region import ( + TLensMode, + TPassEnergy, + TPsuMode, +) from dodal.devices.electron_analyser.base.detector_logic import ( ADArmLogic, ElectronAnalayserTriggerLogic, RegionLogic, ShutterCoordinatorADArmLogic, ) -from dodal.devices.electron_analyser.base.energy_sources import AbstractEnergySource from dodal.devices.electron_analyser.vgscienta.vgscienta_driver_io import ( VGScientaAnalyserDriverIO, ) -from dodal.devices.electron_analyser.vgscienta.vgscienta_region import ( - TPassEnergyEnum, - VGScientaRegion, -) +from dodal.devices.electron_analyser.vgscienta.vgscienta_region import VGScientaRegion from dodal.devices.fast_shutter import GenericFastShutter from dodal.devices.selectable_source import SourceSelector class VGScientaDetector( ElectronAnalyserDetector[ - VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum], - VGScientaRegion[TLensMode, TPassEnergyEnum], + VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergy], + VGScientaRegion[TLensMode, TPassEnergy], ], - Generic[TLensMode, TPsuMode, TPassEnergyEnum], + Generic[TLensMode, TPsuMode, TPassEnergy], ): def __init__( self, prefix: str, lens_mode_type: type[TLensMode], psu_mode_type: type[TPsuMode], - pass_energy_type: type[TPassEnergyEnum], - energy_source: AbstractEnergySource, + pass_energy_type: type[TPassEnergy], + energy_source: SignalR[float], shutter: GenericFastShutter | None = None, source_selector: SourceSelector | None = None, name: str = "", ): # Make attribute of class so connect applies to driver and populates parent. - self.driver = VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum]( + self.driver = VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergy]( prefix, lens_mode_type, psu_mode_type, pass_energy_type ) region_logic = RegionLogic(self.driver, energy_source, source_selector) @@ -52,39 +52,37 @@ def __init__( if shutter is not None else ADArmLogic(self.driver) ) - trigger_logic = ElectronAnalayserTriggerLogic( - self.driver, - { - self.driver.region_name, - self.driver.energy_mode, - self.driver.acquisition_mode, - self.driver.lens_mode, - self.driver.low_energy, - self.driver.centre_energy, - self.driver.high_energy, - self.driver.energy_step, - self.driver.pass_energy, - self.driver.slices, - self.driver.iterations, - self.driver.total_steps, - self.driver.acquire_time, - self.driver.total_time, - self.driver.energy_axis, - self.driver.angle_axis, - self.driver.detector_mode, - self.driver.region_min_x, - self.driver.region_size_x, - self.driver.sensor_max_size_x, - self.driver.region_min_y, - self.driver.region_size_y, - self.driver.sensor_max_size_y, - self.driver.psu_mode, - }, + trigger_logic = ElectronAnalayserTriggerLogic(self.driver, set()) + config_sigs = ( + self.driver.region_name, + self.driver.energy_mode, + self.driver.acquisition_mode, + self.driver.lens_mode, + self.driver.low_energy, + self.driver.centre_energy, + self.driver.high_energy, + self.driver.energy_step, + self.driver.pass_energy, + self.driver.slices, + self.driver.iterations, + self.driver.total_steps, + self.driver.acquire_time, + self.driver.total_time, + self.driver.energy_axis, + self.driver.angle_axis, + self.driver.detector_mode, + self.driver.region_min_x, + self.driver.region_size_x, + self.driver.sensor_max_size_x, + self.driver.region_min_y, + self.driver.region_size_y, + self.driver.sensor_max_size_y, + self.driver.psu_mode, ) - super().__init__( region_logic=region_logic, arm_logic=arm_logic, trigger_logic=trigger_logic, + config_sigs=config_sigs, name=name, ) diff --git a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_driver_io.py b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_driver_io.py index cce7f6023c8..f4e2b0f9f2a 100644 --- a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_driver_io.py +++ b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_driver_io.py @@ -14,26 +14,27 @@ AbstractAnalyserDriverIO, ElectronAnalyserPVConfig, ) -from dodal.devices.electron_analyser.base.base_region import TLensMode, TPsuMode +from dodal.devices.electron_analyser.base.base_region import ( + TLensMode, + TPassEnergy, + TPsuMode, +) from dodal.devices.electron_analyser.vgscienta.vgscienta_enums import ( AcquisitionMode, DetectorMode, ) -from dodal.devices.electron_analyser.vgscienta.vgscienta_region import ( - TPassEnergyEnum, - VGScientaRegion, -) +from dodal.devices.electron_analyser.vgscienta.vgscienta_region import VGScientaRegion class VGScientaAnalyserDriverIO( AbstractAnalyserDriverIO[ - VGScientaRegion[TLensMode, TPassEnergyEnum], + VGScientaRegion[TLensMode, TPassEnergy], AcquisitionMode, TLensMode, TPsuMode, - TPassEnergyEnum, + TPassEnergy, ], - Generic[TLensMode, TPsuMode, TPassEnergyEnum], + Generic[TLensMode, TPsuMode, TPassEnergy], ): PV_CFG = ElectronAnalyserPVConfig(psu_mode="ELEMENT_SET") @@ -42,7 +43,7 @@ def __init__( prefix: str, lens_mode_type: type[TLensMode], psu_mode_type: type[TPsuMode], - pass_energy_type: type[TPassEnergyEnum], + pass_energy_type: type[TPassEnergy], name: str = "", ) -> None: with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): @@ -67,7 +68,7 @@ def __init__( ) @AsyncStatus.wrap - async def set(self, epics_region: VGScientaRegion[TLensMode, TPassEnergyEnum]): + async def set(self, epics_region: VGScientaRegion[TLensMode, TPassEnergy]): await asyncio.gather( self.region_name.set(epics_region.name), self.low_energy.set(epics_region.low_energy), diff --git a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_region.py b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_region.py index c080c51716b..a6481acbf47 100644 --- a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +++ b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_region.py @@ -1,28 +1,26 @@ -from typing import Generic, TypeVar +from typing import Generic -from ophyd_async.core import StrictEnum from pydantic import Field, field_validator from dodal.devices.electron_analyser.base.base_region import ( BaseRegion, BaseSequence, TLensMode, + TPassEnergy, ) from dodal.devices.electron_analyser.vgscienta.vgscienta_enums import ( AcquisitionMode, DetectorMode, ) -TPassEnergyEnum = TypeVar("TPassEnergyEnum", bound=StrictEnum) - class VGScientaRegion( - BaseRegion[AcquisitionMode, TLensMode, TPassEnergyEnum], - Generic[TLensMode, TPassEnergyEnum], + BaseRegion[AcquisitionMode, TLensMode, TPassEnergy], + Generic[TLensMode, TPassEnergy], ): # Override defaults of base region class lens_mode: TLensMode - pass_energy: TPassEnergyEnum + pass_energy: TPassEnergy acquisition_mode: AcquisitionMode = AcquisitionMode.SWEPT low_energy: float = 8.0 centre_energy: float = Field(alias="fix_energy", default=9) @@ -55,7 +53,7 @@ def validate_pass_energy(cls, val): class VGScientaSequence( - BaseSequence[VGScientaRegion[TLensMode, TPassEnergyEnum]], - Generic[TLensMode, TPassEnergyEnum], + BaseSequence[VGScientaRegion[TLensMode, TPassEnergy]], + Generic[TLensMode, TPassEnergy], ): pass diff --git a/tests/devices/electron_analyser/base/test_base_detector.py b/tests/devices/electron_analyser/base/test_base_detector.py index 66a877e0f35..fc4ff5e78b8 100644 --- a/tests/devices/electron_analyser/base/test_base_detector.py +++ b/tests/devices/electron_analyser/base/test_base_detector.py @@ -40,9 +40,7 @@ async def test_base_analyser_detector_describe_configuration( # Catch that driver correctly reflects what region energy mode is. assert is_region_binding == is_driver_binding energy_axis = await driver.energy_axis.get_value() - excitation_energy = ( - await sim_detector._region_logic.energy_source.energy.get_value() - ) + excitation_energy = await sim_detector._region_logic.energy_source.get_value() expected_binding_energy_axis = np.array( [excitation_energy - e if is_driver_binding else e for e in energy_axis] ) diff --git a/tests/devices/electron_analyser/base/test_detector_logic.py b/tests/devices/electron_analyser/base/test_detector_logic.py index 1118cf630d6..55080849570 100644 --- a/tests/devices/electron_analyser/base/test_detector_logic.py +++ b/tests/devices/electron_analyser/base/test_detector_logic.py @@ -5,6 +5,7 @@ import pytest from ophyd_async.core import ( SignalDict, + SignalR, StandardDetector, get_mock_put, init_devices, @@ -14,11 +15,7 @@ from ophyd_async.testing import assert_configuration, partial_reading from dodal.devices.beamlines import b07, b07_shared, i05_shared, i09 -from dodal.devices.electron_analyser.base import ( - AbstractAnalyserDriverIO, - AbstractEnergySource, - BaseRegion, -) +from dodal.devices.electron_analyser.base import AbstractAnalyserDriverIO, BaseRegion from dodal.devices.electron_analyser.base.detector_logic import ( ElectronAnalayserTriggerLogic, RegionLogic, @@ -113,15 +110,15 @@ async def test_shutter_arm_logic_opens_shutters( ) -@pytest.fixture(params=["single_energy_source", "dual_energy_source"]) -def energy_source(request: pytest.FixtureRequest) -> AbstractEnergySource: +@pytest.fixture(params=["source_energy", "dual_source_energy"]) +def energy_source(request: pytest.FixtureRequest) -> SignalR[float]: return request.getfixturevalue(request.param) @pytest.fixture def region_logic( driver: AbstractAnalyserDriverIO, - energy_source: AbstractEnergySource, + energy_source: SignalR[float], source_selector: SourceSelector, ) -> RegionLogic: return RegionLogic(driver, energy_source, source_selector) @@ -154,7 +151,7 @@ async def test_region_logic_setup_with_region_sets_region_for_epics_and_sets_dri mock_prepare_for_epics.assert_called_once_with( region, - await region_logic.energy_source.energy.get_value(), + await region_logic.energy_source.get_value(), ) if region_logic.source_selector is not None: @@ -163,7 +160,7 @@ async def test_region_logic_setup_with_region_sets_region_for_epics_and_sets_dri ).assert_called_once_with(region.excitation_energy_source) # Check set was called with epics_region epics_region = mock_prepare_for_epics.call_args[0][0].prepare_for_epics( - await region_logic.energy_source.energy.get_value(), + await region_logic.energy_source.get_value(), ) region_logic.driver.set.assert_called_once_with(epics_region) diff --git a/tests/devices/electron_analyser/base/test_energy_sources.py b/tests/devices/electron_analyser/base/test_energy_sources.py index 940400ac79a..c7eebebc120 100644 --- a/tests/devices/electron_analyser/base/test_energy_sources.py +++ b/tests/devices/electron_analyser/base/test_energy_sources.py @@ -5,44 +5,15 @@ partial_reading, ) -from dodal.devices.electron_analyser.base import ( - DualEnergySource, - EnergySource, -) +from dodal.devices.electron_analyser.base import DualEnergySource from dodal.devices.selectable_source import SelectedSource -async def test_single_energy_source_read( - single_energy_source: EnergySource, -) -> None: - await assert_reading( - single_energy_source, - { - f"{single_energy_source._source_ref().name}": partial_reading( - await single_energy_source._source_ref().get_value() - ), - }, - ) - - -async def test_single_energy_souce_read_configuration( - single_energy_source: EnergySource, -) -> None: - await assert_configuration( - single_energy_source, - { - f"{single_energy_source.name}-wrapped_device_name": partial_reading( - single_energy_source._source_ref().name - ), - }, - ) - - async def test_dual_energy_source_energy_is_correct_when_switching_between_sources( dual_energy_source: DualEnergySource, ) -> None: - dcm_energy_val = await dual_energy_source.source1.energy.get_value() - pgm_energy_val = await dual_energy_source.source2.energy.get_value() + dcm_energy_val = await dual_energy_source.source1_ref().get_value() + pgm_energy_val = await dual_energy_source.source2_ref().get_value() # Make sure energy sources values are different for this test so we can tell them a # part when switching @@ -54,16 +25,14 @@ async def test_dual_energy_source_energy_is_correct_when_switching_between_sourc await assert_value(dual_energy_source.energy, pgm_energy_val) -async def test_dual_energy_souce_read( - dual_energy_source: DualEnergySource, -) -> None: +async def test_dual_energy_souce_read(dual_energy_source: DualEnergySource) -> None: await dual_energy_source.selected_source_ref().set(SelectedSource.SOURCE1) - source1_name = await dual_energy_source.source1.wrapped_device_name.get_value() - source1_energy_value = await dual_energy_source.source1.energy.get_value() + source1_name = await dual_energy_source.source1.get_value() + source1_energy_value = await dual_energy_source.source1_ref().get_value() - source2_name = await dual_energy_source.source2.wrapped_device_name.get_value() - source2_energy_value = await dual_energy_source.source2.energy.get_value() + source2_name = await dual_energy_source.source2.get_value() + source2_energy_value = await dual_energy_source.source2_ref().get_value() await assert_reading( dual_energy_source, @@ -71,8 +40,9 @@ async def test_dual_energy_souce_read( dual_energy_source.selected_source_ref().name: partial_reading( SelectedSource.SOURCE1 ), - f"{source1_name}": partial_reading(source1_energy_value), - f"{source2_name}": partial_reading(source2_energy_value), + dual_energy_source.energy.name: partial_reading(source1_energy_value), + source1_name: partial_reading(source1_energy_value), + source2_name: partial_reading(source2_energy_value), }, ) @@ -84,11 +54,7 @@ async def test_dual_energy_souce_read_configuration( await assert_configuration( dual_energy_source, { - f"{prefix}-source1-wrapped_device_name": partial_reading( - dual_energy_source.source1._source_ref().name - ), - f"{prefix}-source2-wrapped_device_name": partial_reading( - dual_energy_source.source2._source_ref().name - ), + f"{prefix}-source1": partial_reading(dual_energy_source.source1_ref().name), + f"{prefix}-source2": partial_reading(dual_energy_source.source2_ref().name), }, ) diff --git a/tests/devices/electron_analyser/conftest.py b/tests/devices/electron_analyser/conftest.py index 42f2adb92ba..fc5f4a875b8 100644 --- a/tests/devices/electron_analyser/conftest.py +++ b/tests/devices/electron_analyser/conftest.py @@ -2,7 +2,7 @@ import numpy as np import pytest -from ophyd_async.core import InOut, init_devices, set_mock_value +from ophyd_async.core import InOut, SignalR, init_devices, set_mock_value from dodal.devices.beamlines import b07, b07_shared, i09 from dodal.devices.beamlines.i09 import Grating @@ -11,7 +11,7 @@ PitchAndRollCrystal, StationaryCrystal, ) -from dodal.devices.electron_analyser.base import DualEnergySource, EnergySource +from dodal.devices.electron_analyser.base import DualEnergySource from dodal.devices.electron_analyser.specs import SpecsDetector from dodal.devices.electron_analyser.vgscienta import VGScientaDetector from dodal.devices.fast_shutter import DualFastShutter, FastShutter @@ -27,16 +27,13 @@ async def source_selector() -> SourceSelector: @pytest.fixture -async def single_energy_source() -> EnergySource: +async def source_energy() -> SignalR[float]: async with init_devices(mock=True): dcm = DoubleCrystalMonochromatorWithDSpacing( "DCM:", PitchAndRollCrystal, StationaryCrystal ) await dcm.energy_in_keV.set(2.2) - async with init_devices(mock=True): - dcm_energy_source = EnergySource(dcm.energy_in_eV) - - return dcm_energy_source + return dcm.energy_in_eV @pytest.fixture @@ -57,6 +54,11 @@ async def dual_energy_source(source_selector: SourceSelector) -> DualEnergySourc return dual_energy_source +@pytest.fixture +async def dual_source_energy(dual_energy_source: DualEnergySource) -> SignalR[float]: + return dual_energy_source.energy + + @pytest.fixture def shutter1() -> FastShutter[InOut]: with init_devices(mock=True): @@ -96,7 +98,7 @@ def dual_fast_shutter( @pytest.fixture async def b07b_specs150( - single_energy_source: EnergySource, + source_energy: SignalR[float], shutter1: FastShutter, ) -> SpecsDetector[b07.LensMode, b07_shared.PsuMode]: with init_devices(mock=True): @@ -104,7 +106,7 @@ async def b07b_specs150( prefix="TEST:", lens_mode_type=b07.LensMode, psu_mode_type=b07_shared.PsuMode, - energy_source=single_energy_source, + energy_source=source_energy, shutter=shutter1, ) # Needed for specs so we don't get division by zero error. @@ -114,7 +116,7 @@ async def b07b_specs150( @pytest.fixture async def ew4000( - dual_energy_source: DualEnergySource, + dual_source_energy: SignalR[float], dual_fast_shutter: DualFastShutter, source_selector: SourceSelector, ) -> VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy]: @@ -124,7 +126,7 @@ async def ew4000( lens_mode_type=i09.LensMode, psu_mode_type=i09.PsuMode, pass_energy_type=i09.PassEnergy, - energy_source=dual_energy_source, + energy_source=dual_source_energy, shutter=dual_fast_shutter, source_selector=source_selector, ) From 2c3832b554479ac42c4256d40d65fd7c3a190f97 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 22 May 2026 13:52:16 +0000 Subject: [PATCH 2/4] Fix tests --- src/dodal/devices/electron_analyser/base/energy_sources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dodal/devices/electron_analyser/base/energy_sources.py b/src/dodal/devices/electron_analyser/base/energy_sources.py index 14c81496f06..45e1714196f 100644 --- a/src/dodal/devices/electron_analyser/base/energy_sources.py +++ b/src/dodal/devices/electron_analyser/base/energy_sources.py @@ -46,7 +46,7 @@ def __init__( self.energy = derived_signal_r( get_float_from_selected_source, "eV", - selected_source=selected_source, + selected=selected_source, s1=source1, s2=source2, ) From 14faa968ce1214a0089d513d7d8ba42609d9cb53 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 22 May 2026 15:15:03 +0000 Subject: [PATCH 3/4] Add Mbs detector test --- .../electron_analyser/base/base_region.py | 1 - .../electron_analyser/specs/specs_region.py | 1 + .../vgscienta/vgscienta_region.py | 1 + tests/devices/electron_analyser/conftest.py | 23 ++++++++- .../mbs/test_mbs_detector.py | 47 +++++++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tests/devices/electron_analyser/mbs/test_mbs_detector.py diff --git a/src/dodal/devices/electron_analyser/base/base_region.py b/src/dodal/devices/electron_analyser/base/base_region.py index 210993130bf..29563bd0ec4 100644 --- a/src/dodal/devices/electron_analyser/base/base_region.py +++ b/src/dodal/devices/electron_analyser/base/base_region.py @@ -74,7 +74,6 @@ class BaseRegion( name: str = "New_region" enabled: bool = False - slices: int = 1 iterations: int = 1 excitation_energy_source: SelectedSource = SelectedSource.SOURCE1 # These ones we need subclasses to provide sensible default values diff --git a/src/dodal/devices/electron_analyser/specs/specs_region.py b/src/dodal/devices/electron_analyser/specs/specs_region.py index 302ce82adf0..5204997fb18 100644 --- a/src/dodal/devices/electron_analyser/specs/specs_region.py +++ b/src/dodal/devices/electron_analyser/specs/specs_region.py @@ -26,6 +26,7 @@ class SpecsRegion( energy_step: float = Field(default=0.1, alias="step_energy") # Specific to this class + slices: int = 1 values: int = 1 psu_mode: TPsuMode estimated_time_in_ms: float = 0 diff --git a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_region.py b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_region.py index a6481acbf47..4ab1768568d 100644 --- a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +++ b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_region.py @@ -28,6 +28,7 @@ class VGScientaRegion( acquire_time: float = Field(default=1.0, alias="step_time") energy_step: float = Field(default=200.0) # Specific to this class + slices: int = 1 total_steps: float = 13.0 total_time: float = 13.0 min_x: int = Field(alias="first_x_channel", default=1) diff --git a/tests/devices/electron_analyser/conftest.py b/tests/devices/electron_analyser/conftest.py index fc5f4a875b8..d8befa77ec5 100644 --- a/tests/devices/electron_analyser/conftest.py +++ b/tests/devices/electron_analyser/conftest.py @@ -4,7 +4,7 @@ import pytest from ophyd_async.core import InOut, SignalR, init_devices, set_mock_value -from dodal.devices.beamlines import b07, b07_shared, i09 +from dodal.devices.beamlines import b07, b07_shared, i05_shared, i09 from dodal.devices.beamlines.i09 import Grating from dodal.devices.common_dcm import ( DoubleCrystalMonochromatorWithDSpacing, @@ -12,6 +12,7 @@ StationaryCrystal, ) from dodal.devices.electron_analyser.base import DualEnergySource +from dodal.devices.electron_analyser.mbs import MbsDetector from dodal.devices.electron_analyser.specs import SpecsDetector from dodal.devices.electron_analyser.vgscienta import VGScientaDetector from dodal.devices.fast_shutter import DualFastShutter, FastShutter @@ -135,6 +136,26 @@ async def ew4000( return ew4000 +@pytest.fixture +async def i05_mbs_analyser( + source_energy: SignalR[float], + shutter1: FastShutter, +) -> MbsDetector[i05_shared.LensMode, i05_shared.PassEnergy]: + with init_devices(mock=True): + i05_mbs_analyser = MbsDetector[i05_shared.LensMode, i05_shared.PassEnergy]( + prefix="TEST:", + lens_mode_type=i05_shared.LensMode, + pass_energy_type=i05_shared.PassEnergy, + energy_source=source_energy, + shutter=shutter1, + ) + energy_axis = [1, 2, 3, 4, 5] + set_mock_value( + i05_mbs_analyser.driver.energy_axis, np.array(energy_axis, dtype=float) + ) + return i05_mbs_analyser + + @pytest.fixture def expected_enabled_region_names( expected_region_values: list[dict[str, Any]], diff --git a/tests/devices/electron_analyser/mbs/test_mbs_detector.py b/tests/devices/electron_analyser/mbs/test_mbs_detector.py new file mode 100644 index 00000000000..f24bfe13082 --- /dev/null +++ b/tests/devices/electron_analyser/mbs/test_mbs_detector.py @@ -0,0 +1,47 @@ +from unittest.mock import ANY + +from ophyd_async.testing import assert_configuration, partial_reading + +from dodal.devices.beamlines.i05_shared import LensMode, PassEnergy +from dodal.devices.electron_analyser.mbs import MbsDetector, MbsRegion + + +async def test_mbs_detector_read_configuration(i05_mbs_analyser: MbsDetector) -> None: + prefix = i05_mbs_analyser.driver.name + "-" + region = MbsRegion[LensMode, PassEnergy]( + lens_mode=LensMode.L4_ANG0_D8, pass_energy=PassEnergy.PE001 + ) + await i05_mbs_analyser.set(region) + await assert_configuration( + i05_mbs_analyser, + { + f"{prefix}region_name": partial_reading(region.name), + f"{prefix}energy_mode": partial_reading(region.energy_mode), + f"{prefix}acquisition_mode": partial_reading(region.acquisition_mode), + f"{prefix}lens_mode": partial_reading(region.lens_mode), + f"{prefix}low_energy": partial_reading(region.low_energy), + f"{prefix}centre_energy": partial_reading(region.centre_energy), + f"{prefix}high_energy": partial_reading(region.high_energy), + f"{prefix}deflector_x": partial_reading(region.deflector_x), + f"{prefix}energy_step": partial_reading(ANY), + f"{prefix}pass_energy": partial_reading(region.pass_energy), + f"{prefix}slices": partial_reading(ANY), + f"{prefix}iterations": partial_reading(region.iterations), + f"{prefix}total_steps": partial_reading(ANY), + f"{prefix}acquire_time": partial_reading(region.acquire_time), + f"{prefix}acquire_period": partial_reading(ANY), + f"{prefix}total_time": partial_reading(ANY), + f"{prefix}energy_axis": partial_reading(ANY), + f"{prefix}angle_axis": partial_reading(ANY), + f"{prefix}psu_mode": partial_reading(ANY), + f"{prefix}dither_steps": partial_reading(ANY), + f"{prefix}spin_offset": partial_reading(ANY), + f"{prefix}array_size_x": partial_reading(ANY), + f"{prefix}array_size_y": partial_reading(ANY), + f"{prefix}min_x": partial_reading(ANY), + f"{prefix}min_y": partial_reading(ANY), + f"{prefix}max_x": partial_reading(ANY), + f"{prefix}max_y": partial_reading(ANY), + f"{i05_mbs_analyser.name}-binding_energy_axis": partial_reading(ANY), + }, + ) From c784668f0d10d7dbcf66609293e5b886b9e57b3c Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 22 May 2026 15:29:27 +0000 Subject: [PATCH 4/4] Fix tests --- tests/devices/electron_analyser/mbs/test_mbs_region.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/devices/electron_analyser/mbs/test_mbs_region.py b/tests/devices/electron_analyser/mbs/test_mbs_region.py index 09405f93967..955a414f29a 100644 --- a/tests/devices/electron_analyser/mbs/test_mbs_region.py +++ b/tests/devices/electron_analyser/mbs/test_mbs_region.py @@ -22,7 +22,6 @@ def expected_xml_region_values() -> list[dict[str, Any]]: "enabled": True, "lens_mode": LensMode.L4_ANG0_D8, "pass_energy": PassEnergy.PE005, - "slices": 1, "iterations": 3, "acquisition_mode": AcquisitionMode.SWEPT, "excitation_energy_source": SelectedSource.SOURCE1,