From 4ed70eccac39ac5ab396522205890d3410cf852d Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 16 Jan 2026 15:21:03 +0000 Subject: [PATCH 01/22] Implement concrete analyser classes for beamlines to remove generics --- src/dodal/beamlines/b07.py | 16 +++--------- src/dodal/beamlines/b07_1.py | 21 ++++----------- src/dodal/beamlines/i09.py | 14 +++------- src/dodal/beamlines/p60.py | 19 +++----------- src/dodal/devices/b07/__init__.py | 5 ++-- src/dodal/devices/b07/analyser.py | 16 ++++++++++++ src/dodal/devices/b07/enums.py | 12 --------- src/dodal/devices/b07_1/__init__.py | 9 +++---- src/dodal/devices/b07_1/analyser.py | 16 ++++++++++++ src/dodal/devices/b07_shared/__init__.py | 3 +++ src/dodal/devices/b07_shared/enums.py | 13 ++++++++++ src/dodal/devices/i09/__init__.py | 3 ++- src/dodal/devices/i09/analyser.py | 26 +++++++++++++++++++ src/dodal/devices/p60/__init__.py | 2 ++ src/dodal/devices/p60/analyser.py | 22 ++++++++++++++++ .../base/test_base_controller.py | 4 +-- .../base/test_base_detector.py | 11 ++++---- .../base/test_base_driver_io.py | 4 +-- .../base/test_base_region.py | 6 ++--- .../specs/test_specs_detector.py | 3 ++- .../specs/test_specs_driver_io.py | 3 ++- .../specs/test_specs_region.py | 3 ++- 22 files changed, 141 insertions(+), 90 deletions(-) create mode 100644 src/dodal/devices/b07/analyser.py create mode 100644 src/dodal/devices/b07_1/analyser.py create mode 100644 src/dodal/devices/b07_shared/__init__.py create mode 100644 src/dodal/devices/b07_shared/enums.py create mode 100644 src/dodal/devices/i09/analyser.py create mode 100644 src/dodal/devices/p60/analyser.py diff --git a/src/dodal/beamlines/b07.py b/src/dodal/beamlines/b07.py index 8fdba8cc47..d0e44814dd 100644 --- a/src/dodal/beamlines/b07.py +++ b/src/dodal/beamlines/b07.py @@ -1,10 +1,7 @@ -from dodal.common.beamlines.beamline_utils import ( - device_factory, -) +from dodal.common.beamlines.beamline_utils import device_factory from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline -from dodal.devices.b07 import Grating, LensMode, PsuMode +from dodal.devices.b07 import Grating, Specs2DCMOS from dodal.devices.electron_analyser.base import EnergySource -from dodal.devices.electron_analyser.specs import SpecsDetector from dodal.devices.pgm import PlaneGratingMonochromator from dodal.devices.synchrotron import Synchrotron from dodal.log import set_beamline as set_log_beamline @@ -37,10 +34,5 @@ def energy_source() -> EnergySource: # Connect will work again after this work completed # https://jira.diamond.ac.uk/browse/B07-1104 @device_factory() -def analyser() -> SpecsDetector[LensMode, PsuMode]: - return SpecsDetector[LensMode, PsuMode]( - prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:", - lens_mode_type=LensMode, - psu_mode_type=PsuMode, - energy_source=energy_source(), - ) +def analyser() -> Specs2DCMOS: + return Specs2DCMOS(f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:", energy_source()) diff --git a/src/dodal/beamlines/b07_1.py b/src/dodal/beamlines/b07_1.py index fffc534785..2c582a4d6f 100644 --- a/src/dodal/beamlines/b07_1.py +++ b/src/dodal/beamlines/b07_1.py @@ -1,13 +1,7 @@ from dodal.common.beamlines.beamline_utils import device_factory from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline -from dodal.devices.b07 import PsuMode -from dodal.devices.b07_1 import ( - ChannelCutMonochromator, - Grating, - LensMode, -) +from dodal.devices.b07_1 import ChannelCutMonochromator, Grating, SpecsPhoibos from dodal.devices.electron_analyser.base import EnergySource -from dodal.devices.electron_analyser.specs import SpecsDetector from dodal.devices.pgm import PlaneGratingMonochromator from dodal.devices.synchrotron import Synchrotron from dodal.log import set_beamline as set_log_beamline @@ -32,8 +26,6 @@ def pgm() -> PlaneGratingMonochromator: ) -# Connect will work again after this work completed -# https://jira.diamond.ac.uk/browse/B07-1104 @device_factory() def ccmc() -> ChannelCutMonochromator: return ChannelCutMonochromator(prefix=f"{PREFIX.beamline_prefix}-OP-CCM-01:") @@ -44,11 +36,8 @@ def energy_source() -> EnergySource: return EnergySource(pgm().energy.user_readback) +# Connect will work again after this work completed +# https://jira.diamond.ac.uk/browse/B07-1104 @device_factory() -def analyser() -> SpecsDetector[LensMode, PsuMode]: - return SpecsDetector[LensMode, PsuMode]( - prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:", - lens_mode_type=LensMode, - psu_mode_type=PsuMode, - energy_source=energy_source(), - ) +def analyser() -> SpecsPhoibos: + return SpecsPhoibos(f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:", energy_source()) diff --git a/src/dodal/beamlines/i09.py b/src/dodal/beamlines/i09.py index 880be61677..b4251492e3 100644 --- a/src/dodal/beamlines/i09.py +++ b/src/dodal/beamlines/i09.py @@ -12,15 +12,12 @@ from dodal.devices.electron_analyser.base import ( DualEnergySource, ) -from dodal.devices.electron_analyser.vgscienta import VGScientaDetector from dodal.devices.fast_shutter import DualFastShutter, GenericFastShutter -from dodal.devices.i09 import Grating, LensMode, PassEnergy, PsuMode +from dodal.devices.i09 import EW4000, Grating from dodal.devices.pgm import PlaneGratingMonochromator from dodal.devices.selectable_source import SourceSelector from dodal.devices.synchrotron import Synchrotron -from dodal.devices.temperture_controller import ( - Lakeshore336, -) +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 @@ -86,12 +83,9 @@ def dual_fast_shutter() -> DualFastShutter[InOut]: # Connect will work again after this work completed # https://jira.diamond.ac.uk/browse/I09-651 @device_factory() -def ew4000() -> VGScientaDetector[LensMode, PsuMode, PassEnergy]: - return VGScientaDetector[LensMode, PsuMode, PassEnergy]( +def ew4000() -> EW4000: + return EW4000( prefix=f"{I_PREFIX.beamline_prefix}-EA-DET-01:CAM:", - lens_mode_type=LensMode, - psu_mode_type=PsuMode, - pass_energy_type=PassEnergy, energy_source=dual_energy_source(), shutter=dual_fast_shutter(), source_selector=source_selector(), diff --git a/src/dodal/beamlines/p60.py b/src/dodal/beamlines/p60.py index 3cf0dc9304..2b218d8328 100644 --- a/src/dodal/beamlines/p60.py +++ b/src/dodal/beamlines/p60.py @@ -3,14 +3,7 @@ ) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.devices.electron_analyser.base import DualEnergySource -from dodal.devices.electron_analyser.vgscienta import VGScientaDetector -from dodal.devices.p60 import ( - LabXraySource, - LabXraySourceReadable, - LensMode, - PassEnergy, - PsuMode, -) +from dodal.devices.p60 import R4000, LabXraySource, LabXraySourceReadable from dodal.devices.selectable_source import SourceSelector from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name @@ -48,11 +41,5 @@ def energy_source() -> DualEnergySource: # Connect will work again after this work completed # https://jira.diamond.ac.uk/browse/P60-13 @device_factory() -def r4000() -> VGScientaDetector[LensMode, PsuMode, PassEnergy]: - return VGScientaDetector[LensMode, PsuMode, PassEnergy]( - prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:", - lens_mode_type=LensMode, - psu_mode_type=PsuMode, - pass_energy_type=PassEnergy, - energy_source=energy_source(), - ) +def r4000() -> R4000: + return R4000(f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:", energy_source()) diff --git a/src/dodal/devices/b07/__init__.py b/src/dodal/devices/b07/__init__.py index 15895a410d..a294200bba 100644 --- a/src/dodal/devices/b07/__init__.py +++ b/src/dodal/devices/b07/__init__.py @@ -1,3 +1,4 @@ -from dodal.devices.b07.enums import Grating, LensMode, PsuMode +from .analyser import Specs2DCMOS +from .enums import Grating, LensMode -__all__ = ["Grating", "LensMode", "PsuMode"] +__all__ = ["Specs2DCMOS", "Grating", "LensMode"] diff --git a/src/dodal/devices/b07/analyser.py b/src/dodal/devices/b07/analyser.py new file mode 100644 index 0000000000..9ab654d60c --- /dev/null +++ b/src/dodal/devices/b07/analyser.py @@ -0,0 +1,16 @@ +from dodal.devices.b07.enums import LensMode +from dodal.devices.b07_shared.enums import PsuMode +from dodal.devices.electron_analyser.base.energy_sources import EnergySource +from dodal.devices.electron_analyser.specs import SpecsDetector +from dodal.devices.fast_shutter import FastShutter + + +class Specs2DCMOS(SpecsDetector[LensMode, PsuMode]): + def __init__( + self, + prefix: str, + energy_source: EnergySource, + shutter: FastShutter | None = None, + name: str = "", + ): + super().__init__(prefix, LensMode, PsuMode, energy_source, shutter, None, name) diff --git a/src/dodal/devices/b07/enums.py b/src/dodal/devices/b07/enums.py index 58c826bb37..2e27db999d 100644 --- a/src/dodal/devices/b07/enums.py +++ b/src/dodal/devices/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/b07_1/__init__.py b/src/dodal/devices/b07_1/__init__.py index 4573de4ab9..a9578cd56e 100644 --- a/src/dodal/devices/b07_1/__init__.py +++ b/src/dodal/devices/b07_1/__init__.py @@ -1,10 +1,9 @@ -from dodal.devices.b07_1.ccmc import ( - ChannelCutMonochromator, - ChannelCutMonochromatorPositions, -) -from dodal.devices.b07_1.enums import Grating, LensMode +from .analyser import SpecsPhoibos +from .ccmc import ChannelCutMonochromator, ChannelCutMonochromatorPositions +from .enums import Grating, LensMode __all__ = [ + "SpecsPhoibos", "Grating", "LensMode", "ChannelCutMonochromator", diff --git a/src/dodal/devices/b07_1/analyser.py b/src/dodal/devices/b07_1/analyser.py new file mode 100644 index 0000000000..be59d031c2 --- /dev/null +++ b/src/dodal/devices/b07_1/analyser.py @@ -0,0 +1,16 @@ +from dodal.devices.b07_1.enums import LensMode +from dodal.devices.b07_shared.enums import PsuMode +from dodal.devices.electron_analyser.base.energy_sources import EnergySource +from dodal.devices.electron_analyser.specs import SpecsDetector +from dodal.devices.fast_shutter import FastShutter + + +class SpecsPhoibos(SpecsDetector[LensMode, PsuMode]): + def __init__( + self, + prefix: str, + energy_source: EnergySource, + shutter: FastShutter | None = None, + name: str = "", + ): + super().__init__(prefix, LensMode, PsuMode, energy_source, shutter, None, name) diff --git a/src/dodal/devices/b07_shared/__init__.py b/src/dodal/devices/b07_shared/__init__.py new file mode 100644 index 0000000000..9fa24dbb0f --- /dev/null +++ b/src/dodal/devices/b07_shared/__init__.py @@ -0,0 +1,3 @@ +from .enums import PsuMode + +__all__ = ["PsuMode"] diff --git a/src/dodal/devices/b07_shared/enums.py b/src/dodal/devices/b07_shared/enums.py new file mode 100644 index 0000000000..0c6bbba3a8 --- /dev/null +++ b/src/dodal/devices/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/i09/__init__.py b/src/dodal/devices/i09/__init__.py index 012f6c5325..a4c08c18da 100644 --- a/src/dodal/devices/i09/__init__.py +++ b/src/dodal/devices/i09/__init__.py @@ -1,3 +1,4 @@ +from dodal.devices.i09.analyser import EW4000 from dodal.devices.i09.enums import Grating, LensMode, PassEnergy, PsuMode -__all__ = ["Grating", "LensMode", "PsuMode", "PassEnergy"] +__all__ = ["EW4000", "Grating", "LensMode", "PsuMode", "PassEnergy"] diff --git a/src/dodal/devices/i09/analyser.py b/src/dodal/devices/i09/analyser.py new file mode 100644 index 0000000000..533ce08825 --- /dev/null +++ b/src/dodal/devices/i09/analyser.py @@ -0,0 +1,26 @@ +from dodal.devices.electron_analyser.base import AbstractEnergySource +from dodal.devices.electron_analyser.vgscienta import VGScientaDetector +from dodal.devices.fast_shutter import FastShutter +from dodal.devices.i09.enums import LensMode, PassEnergy, PsuMode +from dodal.devices.selectable_source import SourceSelector + + +class EW4000(VGScientaDetector[LensMode, PsuMode, PassEnergy]): + def __init__( + self, + prefix: str, + energy_source: AbstractEnergySource, + shutter: FastShutter | None = None, + source_selector: SourceSelector | None = None, + name: str = "", + ): + super().__init__( + prefix, + LensMode, + PsuMode, + PassEnergy, + energy_source, + shutter, + source_selector, + name, + ) diff --git a/src/dodal/devices/p60/__init__.py b/src/dodal/devices/p60/__init__.py index 6a25e7d0ba..42d52a2d03 100644 --- a/src/dodal/devices/p60/__init__.py +++ b/src/dodal/devices/p60/__init__.py @@ -1,7 +1,9 @@ +from .analyser import R4000 from .enums import LensMode, PassEnergy, PsuMode from .lab_xray_source import LabXraySource, LabXraySourceReadable __all__ = [ + "R4000", "LensMode", "PsuMode", "PassEnergy", diff --git a/src/dodal/devices/p60/analyser.py b/src/dodal/devices/p60/analyser.py new file mode 100644 index 0000000000..125f8a0566 --- /dev/null +++ b/src/dodal/devices/p60/analyser.py @@ -0,0 +1,22 @@ +from dodal.devices.electron_analyser.base import AbstractEnergySource +from dodal.devices.electron_analyser.vgscienta import VGScientaDetector +from dodal.devices.p60.enums import LensMode, PassEnergy, PsuMode + + +class R4000(VGScientaDetector[LensMode, PsuMode, PassEnergy]): + def __init__( + self, + prefix: str, + energy_source: AbstractEnergySource, + name: str = "", + ): + super().__init__( + prefix, + LensMode, + PsuMode, + PassEnergy, + energy_source, + None, + None, + name, + ) diff --git a/tests/devices/electron_analyser/base/test_base_controller.py b/tests/devices/electron_analyser/base/test_base_controller.py index aa899c66fc..d9c8a9f260 100644 --- a/tests/devices/electron_analyser/base/test_base_controller.py +++ b/tests/devices/electron_analyser/base/test_base_controller.py @@ -3,7 +3,7 @@ import pytest from ophyd_async.core import InOut, TriggerInfo, get_mock_put, init_devices -from dodal.beamlines import b07, i09 +from dodal.devices import b07, b07_shared, i09 from dodal.devices.electron_analyser.base import ( AbstractAnalyserDriverIO, AbstractBaseRegion, @@ -28,7 +28,7 @@ @pytest.fixture( params=[ - SpecsAnalyserDriverIO[b07.LensMode, b07.PsuMode], + SpecsAnalyserDriverIO[b07.LensMode, b07_shared.PsuMode], VGScientaAnalyserDriverIO[i09.LensMode, i09.PsuMode, i09.PassEnergy], ] ) diff --git a/tests/devices/electron_analyser/base/test_base_detector.py b/tests/devices/electron_analyser/base/test_base_detector.py index 68191af0c7..cc8e85d0cd 100644 --- a/tests/devices/electron_analyser/base/test_base_detector.py +++ b/tests/devices/electron_analyser/base/test_base_detector.py @@ -10,6 +10,7 @@ ) import dodal.devices.b07 as b07 +import dodal.devices.b07_shared as b07_shared import dodal.devices.i09 as i09 from dodal.devices.electron_analyser.base import ( EnergySource, @@ -22,13 +23,11 @@ from dodal.testing.electron_analyser import create_detector from tests.devices.electron_analyser.helper_util import get_test_sequence +VGScientaDetector = VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy] +SpecsDetector = SpecsDetector[b07.LensMode, b07_shared.PsuMode] -@pytest.fixture( - params=[ - VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy], - SpecsDetector[b07.LensMode, b07.PsuMode], - ] -) + +@pytest.fixture(params=[VGScientaDetector, SpecsDetector]) async def sim_detector( request: pytest.FixtureRequest, single_energy_source: EnergySource, 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 36dd31a343..c991d64164 100644 --- a/tests/devices/electron_analyser/base/test_base_driver_io.py +++ b/tests/devices/electron_analyser/base/test_base_driver_io.py @@ -4,7 +4,7 @@ from bluesky.utils import FailedStatus from ophyd_async.core import StrictEnum, init_devices -from dodal.devices import b07, i09 +from dodal.devices import b07, b07_shared, i09 from dodal.devices.electron_analyser.base import GenericAnalyserDriverIO from dodal.devices.electron_analyser.specs import ( SpecsAnalyserDriverIO, @@ -18,7 +18,7 @@ @pytest.fixture( params=[ VGScientaAnalyserDriverIO[i09.LensMode, i09.PsuMode, i09.PassEnergy], - SpecsAnalyserDriverIO[b07.LensMode, b07.PsuMode], + SpecsAnalyserDriverIO[b07.LensMode, b07_shared.PsuMode], ] ) async def sim_driver( diff --git a/tests/devices/electron_analyser/base/test_base_region.py b/tests/devices/electron_analyser/base/test_base_region.py index faea3fdbcf..323d854182 100644 --- a/tests/devices/electron_analyser/base/test_base_region.py +++ b/tests/devices/electron_analyser/base/test_base_region.py @@ -3,7 +3,7 @@ import pytest from dodal.common.data_util import load_json_file_to_class -from dodal.devices import b07, i09 +from dodal.devices import b07, b07_shared, i09 from dodal.devices.electron_analyser.base import ( AbstractBaseRegion, EnergyMode, @@ -26,7 +26,7 @@ @pytest.fixture( params=[ - SpecsSequence[b07.LensMode, b07.PsuMode], + SpecsSequence[b07.LensMode, b07_shared.PsuMode], VGScientaSequence[i09.LensMode, i09.PsuMode, i09.PassEnergy], ] ) @@ -39,7 +39,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/specs/test_specs_detector.py b/tests/devices/electron_analyser/specs/test_specs_detector.py index 2efb409ed3..043d8b4ee6 100644 --- a/tests/devices/electron_analyser/specs/test_specs_detector.py +++ b/tests/devices/electron_analyser/specs/test_specs_detector.py @@ -1,7 +1,8 @@ import pytest from ophyd_async.core import init_devices, set_mock_value -from dodal.devices.b07 import LensMode, PsuMode +from dodal.devices.b07 import LensMode +from dodal.devices.b07_shared import 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 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 898132bb6e..455e9a712e 100644 --- a/tests/devices/electron_analyser/specs/test_specs_driver_io.py +++ b/tests/devices/electron_analyser/specs/test_specs_driver_io.py @@ -12,7 +12,8 @@ partial_reading, ) -from dodal.devices.b07 import LensMode, PsuMode +from dodal.devices.b07 import LensMode +from dodal.devices.b07_shared import PsuMode from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.base.base_enums import EnergyMode from dodal.devices.electron_analyser.specs import ( diff --git a/tests/devices/electron_analyser/specs/test_specs_region.py b/tests/devices/electron_analyser/specs/test_specs_region.py index f06fdb0084..992b3eb9da 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -3,7 +3,8 @@ import pytest from dodal.common.data_util import load_json_file_to_class -from dodal.devices.b07 import LensMode, PsuMode +from dodal.devices.b07 import LensMode +from dodal.devices.b07_shared import PsuMode from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.specs import ( AcquisitionMode, From 7b8b8115a25b8b59e650605f6c930da0d78eccc1 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Mon, 19 Jan 2026 11:13:39 +0000 Subject: [PATCH 02/22] Add i09_1 specs model --- src/dodal/beamlines/i09_1.py | 12 +++--------- src/dodal/devices/i09_1/__init__.py | 3 ++- src/dodal/devices/i09_1/analyser.py | 15 +++++++++++++++ src/dodal/devices/p60/analyser.py | 4 ++++ 4 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 src/dodal/devices/i09_1/analyser.py diff --git a/src/dodal/beamlines/i09_1.py b/src/dodal/beamlines/i09_1.py index c5321ba3bb..d1c63a5b2b 100644 --- a/src/dodal/beamlines/i09_1.py +++ b/src/dodal/beamlines/i09_1.py @@ -8,8 +8,7 @@ StationaryCrystal, ) from dodal.devices.electron_analyser.base import EnergySource -from dodal.devices.electron_analyser.specs import SpecsDetector -from dodal.devices.i09_1 import LensMode, PsuMode +from dodal.devices.i09_1 import SpecsPhoibos225 from dodal.devices.i09_1_shared.hard_energy import HardEnergy, HardInsertionDeviceEnergy from dodal.devices.i09_1_shared.hard_undulator_functions import ( calculate_energy_i09_hu, @@ -49,13 +48,8 @@ def energy_source() -> EnergySource: # Connect will work again after this work completed # https://jira.diamond.ac.uk/browse/I09-651 @device_factory(skip=True) -def analyser() -> 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(), - ) +def analyser() -> SpecsPhoibos225: + return SpecsPhoibos225(f"{PREFIX.beamline_prefix}-EA-DET-02:CAM:", energy_source()) @device_factory() diff --git a/src/dodal/devices/i09_1/__init__.py b/src/dodal/devices/i09_1/__init__.py index b54345b114..3577fd23d9 100644 --- a/src/dodal/devices/i09_1/__init__.py +++ b/src/dodal/devices/i09_1/__init__.py @@ -1,3 +1,4 @@ +from .analyser import SpecsPhoibos225 from .enums import LensMode, PsuMode -__all__ = ["LensMode", "PsuMode"] +__all__ = ["SpecsPhoibos225", "LensMode", "PsuMode"] diff --git a/src/dodal/devices/i09_1/analyser.py b/src/dodal/devices/i09_1/analyser.py new file mode 100644 index 0000000000..cb1a15529d --- /dev/null +++ b/src/dodal/devices/i09_1/analyser.py @@ -0,0 +1,15 @@ +from dodal.devices.electron_analyser.base.energy_sources import EnergySource +from dodal.devices.electron_analyser.specs import SpecsDetector +from dodal.devices.fast_shutter import FastShutter +from dodal.devices.i09_1.enums import LensMode, PsuMode + + +class SpecsPhoibos225(SpecsDetector[LensMode, PsuMode]): + def __init__( + self, + prefix: str, + energy_source: EnergySource, + shutter: FastShutter | None = None, + name: str = "", + ): + super().__init__(prefix, LensMode, PsuMode, energy_source, shutter, None, name) diff --git a/src/dodal/devices/p60/analyser.py b/src/dodal/devices/p60/analyser.py index 125f8a0566..3ad46b0d78 100644 --- a/src/dodal/devices/p60/analyser.py +++ b/src/dodal/devices/p60/analyser.py @@ -4,6 +4,10 @@ class R4000(VGScientaDetector[LensMode, PsuMode, PassEnergy]): + """Lab specific analyser for P60 lab. It does not have any shutters connected so + will be None for this implementation. The selected_source also cannot be dynamically + changed between regions, so will also be None so regions cannot select.""" + def __init__( self, prefix: str, From fd65ea620c820ce8d09d9a7225eac0c6513a69de Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Mon, 19 Jan 2026 11:20:18 +0000 Subject: [PATCH 03/22] Add device args --- src/dodal/devices/i09/analyser.py | 16 ++++++++-------- src/dodal/devices/p60/analyser.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/dodal/devices/i09/analyser.py b/src/dodal/devices/i09/analyser.py index 533ce08825..e3dbe59737 100644 --- a/src/dodal/devices/i09/analyser.py +++ b/src/dodal/devices/i09/analyser.py @@ -15,12 +15,12 @@ def __init__( name: str = "", ): super().__init__( - prefix, - LensMode, - PsuMode, - PassEnergy, - energy_source, - shutter, - source_selector, - name, + prefix=prefix, + lens_mode_type=LensMode, + psu_mode_type=PsuMode, + pass_energy_type=PassEnergy, + energy_source=energy_source, + shutter=shutter, + source_selector=source_selector, + name=name, ) diff --git a/src/dodal/devices/p60/analyser.py b/src/dodal/devices/p60/analyser.py index 3ad46b0d78..2fd306af5b 100644 --- a/src/dodal/devices/p60/analyser.py +++ b/src/dodal/devices/p60/analyser.py @@ -15,12 +15,12 @@ def __init__( name: str = "", ): super().__init__( - prefix, - LensMode, - PsuMode, - PassEnergy, - energy_source, - None, - None, - name, + prefix=prefix, + lens_mode_type=LensMode, + psu_mode_type=PsuMode, + pass_energy_type=PassEnergy, + energy_source=energy_source, + shutter=None, + source_selector=None, + name=name, ) From 6a9465d225abc1ab2a9b340e0fd76bc1c09c1e67 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Thu, 22 Jan 2026 16:29:59 +0000 Subject: [PATCH 04/22] Fix lint --- src/dodal/beamlines/p60.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/dodal/beamlines/p60.py b/src/dodal/beamlines/p60.py index 89612420b1..32e67d124e 100644 --- a/src/dodal/beamlines/p60.py +++ b/src/dodal/beamlines/p60.py @@ -30,7 +30,7 @@ def mg_kalpha_source() -> LabXraySourceReadable: @devices.factory() -def energy_source( +def dual_energy_source( al_kalpha_source: LabXraySourceReadable, mg_kalpha_source: LabXraySourceReadable, source_selector: SourceSelector, @@ -45,7 +45,5 @@ def energy_source( # Connect will work again after this work completed # https://jira.diamond.ac.uk/browse/P60-13 @devices.factory() -def r4000(energy_source: DualEnergySource) -> R4000: - return R4000(f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:", energy_source) - - +def r4000(dual_energy_source: DualEnergySource) -> R4000: + return R4000(f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:", dual_energy_source) From 146163cc7cf519c09a740715f61295c84fe9932e Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Thu, 22 Jan 2026 16:33:14 +0000 Subject: [PATCH 05/22] Reorder DeviceManager placement for P60 --- src/dodal/beamlines/p60.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dodal/beamlines/p60.py b/src/dodal/beamlines/p60.py index 32e67d124e..a897bd3b83 100644 --- a/src/dodal/beamlines/p60.py +++ b/src/dodal/beamlines/p60.py @@ -6,13 +6,13 @@ from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name -devices = DeviceManager() - BL = get_beamline_name("p60") PREFIX = BeamlinePrefix(BL) set_log_beamline(BL) set_utils_beamline(BL) +devices = DeviceManager() + @devices.factory() def source_selector() -> SourceSelector: From 04a03710bf16d0882d9877f043b32f882b0b821b Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Mon, 26 Jan 2026 17:25:57 +0000 Subject: [PATCH 06/22] Removed VGScienta and Specs Detector. Moved this to beamline implementation instead. --- src/dodal/beamlines/i09.py | 14 +- src/dodal/devices/b07/__init__.py | 18 ++- src/dodal/devices/b07/analyser.py | 29 ++++- src/dodal/devices/b07_1/__init__.py | 12 +- src/dodal/devices/b07_1/analyser.py | 31 ++++- .../electron_analyser/base/base_detector.py | 39 ++---- .../electron_analyser/specs/__init__.py | 2 - .../electron_analyser/specs/specs_detector.py | 47 ------- .../electron_analyser/vgscienta/__init__.py | 2 - .../vgscienta/vgscienta_detector.py | 53 -------- src/dodal/devices/i09/__init__.py | 18 ++- src/dodal/devices/i09/analyser.py | 61 ++++++--- src/dodal/devices/i09_1/analyser.py | 31 ++++- src/dodal/devices/p60/analyser.py | 46 +++++-- .../testing/electron_analyser/__init__.py | 6 - .../electron_analyser/device_factory.py | 59 --------- .../base/test_base_controller.py | 122 +++++------------- .../base/test_base_detector.py | 94 +++++++------- .../base/test_base_region.py | 24 ++-- tests/devices/electron_analyser/conftest.py | 73 ++++++----- .../electron_analyser/helper_util/__init__.py | 11 +- .../electron_analyser/helper_util/sequence.py | 42 +++--- .../specs/test_specs_detector.py | 47 ------- .../specs/test_specs_driver_io.py | 9 +- .../specs/test_specs_region.py | 18 +-- .../vgscienta/test_vgscienta_detector.py | 41 ------ .../vgscienta/test_vgscienta_driver_io.py | 34 +++-- .../vgscienta/test_vgsicenta_region.py | 21 ++- 28 files changed, 414 insertions(+), 590 deletions(-) delete mode 100644 src/dodal/devices/electron_analyser/specs/specs_detector.py delete mode 100644 src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py delete mode 100644 src/dodal/testing/electron_analyser/__init__.py delete mode 100644 src/dodal/testing/electron_analyser/device_factory.py delete mode 100644 tests/devices/electron_analyser/specs/test_specs_detector.py delete mode 100644 tests/devices/electron_analyser/vgscienta/test_vgscienta_detector.py diff --git a/src/dodal/beamlines/i09.py b/src/dodal/beamlines/i09.py index 944e71c1a4..2ab46ec1cf 100644 --- a/src/dodal/beamlines/i09.py +++ b/src/dodal/beamlines/i09.py @@ -5,9 +5,7 @@ from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.device_manager import DeviceManager from dodal.devices.common_dcm import DoubleCrystalMonochromatorWithDSpacing -from dodal.devices.electron_analyser.base import ( - DualEnergySource, -) +from dodal.devices.electron_analyser.base import DualEnergySource from dodal.devices.fast_shutter import DualFastShutter, GenericFastShutter from dodal.devices.i09 import EW4000 from dodal.devices.pgm import PlaneGratingMonochromator @@ -80,15 +78,15 @@ def dual_fast_shutter( # see https://github.com/DiamondLightSource/dodal/issues/1852 @devices.factory() def ew4000( - dual_fast_shutter: DualFastShutter, dual_energy_source: DualEnergySource, + dual_fast_shutter: DualFastShutter, source_selector: SourceSelector, ) -> EW4000: return EW4000( - prefix=f"{I_PREFIX.beamline_prefix}-EA-DET-01:CAM:", - energy_source=dual_energy_source, - shutter=dual_fast_shutter, - source_selector=source_selector, + f"{I_PREFIX.beamline_prefix}-EA-DET-01:CAM:", + dual_energy_source, + dual_fast_shutter, + source_selector, ) diff --git a/src/dodal/devices/b07/__init__.py b/src/dodal/devices/b07/__init__.py index a294200bba..8c7f6c984a 100644 --- a/src/dodal/devices/b07/__init__.py +++ b/src/dodal/devices/b07/__init__.py @@ -1,4 +1,18 @@ -from .analyser import Specs2DCMOS +from .analyser import ( + B07ElectronAnalyserController, + B07SpecsAnalyserDriverIO, + B07SpecsRegion, + B07SpecsSequence, + Specs2DCMOS, +) from .enums import Grating, LensMode -__all__ = ["Specs2DCMOS", "Grating", "LensMode"] +__all__ = [ + "B07ElectronAnalyserController", + "B07SpecsAnalyserDriverIO", + "B07SpecsRegion", + "B07SpecsSequence", + "Specs2DCMOS", + "Grating", + "LensMode", +] diff --git a/src/dodal/devices/b07/analyser.py b/src/dodal/devices/b07/analyser.py index 9ab654d60c..945a610a5c 100644 --- a/src/dodal/devices/b07/analyser.py +++ b/src/dodal/devices/b07/analyser.py @@ -1,11 +1,32 @@ from dodal.devices.b07.enums import LensMode from dodal.devices.b07_shared.enums import PsuMode +from dodal.devices.electron_analyser.base.base_controller import ( + ElectronAnalyserController, +) +from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector from dodal.devices.electron_analyser.base.energy_sources import EnergySource -from dodal.devices.electron_analyser.specs import SpecsDetector +from dodal.devices.electron_analyser.specs import ( + SpecsAnalyserDriverIO, + SpecsRegion, + SpecsSequence, +) from dodal.devices.fast_shutter import FastShutter +B07SpecsRegion = SpecsRegion[LensMode, PsuMode] +B07SpecsSequence = SpecsSequence[LensMode, PsuMode] -class Specs2DCMOS(SpecsDetector[LensMode, PsuMode]): + +class B07SpecsAnalyserDriverIO(SpecsAnalyserDriverIO): + def __init__(self, prefix: str, name: str = ""): + super().__init__(prefix, LensMode, PsuMode, name) + + +B07ElectronAnalyserController = ElectronAnalyserController[ + B07SpecsAnalyserDriverIO, B07SpecsRegion +] + + +class Specs2DCMOS(ElectronAnalyserDetector[B07SpecsAnalyserDriverIO, B07SpecsRegion]): def __init__( self, prefix: str, @@ -13,4 +34,6 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - super().__init__(prefix, LensMode, PsuMode, energy_source, shutter, None, name) + driver = B07SpecsAnalyserDriverIO(prefix) + controller = B07ElectronAnalyserController(driver, energy_source, shutter) + super().__init__(controller, name) diff --git a/src/dodal/devices/b07_1/__init__.py b/src/dodal/devices/b07_1/__init__.py index a9578cd56e..8158354ada 100644 --- a/src/dodal/devices/b07_1/__init__.py +++ b/src/dodal/devices/b07_1/__init__.py @@ -1,8 +1,18 @@ -from .analyser import SpecsPhoibos +from .analyser import ( + B071ElectronAnalyserController, + B071SpecsAnalyserDriverIO, + B071SpecsRegion, + B071SpecsSequence, + SpecsPhoibos, +) from .ccmc import ChannelCutMonochromator, ChannelCutMonochromatorPositions from .enums import Grating, LensMode __all__ = [ + "B071ElectronAnalyserController", + "B071SpecsAnalyserDriverIO", + "B071SpecsRegion", + "B071SpecsSequence", "SpecsPhoibos", "Grating", "LensMode", diff --git a/src/dodal/devices/b07_1/analyser.py b/src/dodal/devices/b07_1/analyser.py index be59d031c2..ce92b07689 100644 --- a/src/dodal/devices/b07_1/analyser.py +++ b/src/dodal/devices/b07_1/analyser.py @@ -1,11 +1,34 @@ from dodal.devices.b07_1.enums import LensMode from dodal.devices.b07_shared.enums import PsuMode +from dodal.devices.electron_analyser.base.base_controller import ( + ElectronAnalyserController, +) +from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector from dodal.devices.electron_analyser.base.energy_sources import EnergySource -from dodal.devices.electron_analyser.specs import SpecsDetector +from dodal.devices.electron_analyser.specs import ( + SpecsAnalyserDriverIO, + SpecsRegion, + SpecsSequence, +) from dodal.devices.fast_shutter import FastShutter +B071SpecsRegion = SpecsRegion[LensMode, PsuMode] +B071SpecsSequence = SpecsSequence[LensMode, PsuMode] -class SpecsPhoibos(SpecsDetector[LensMode, PsuMode]): + +class B071SpecsAnalyserDriverIO(SpecsAnalyserDriverIO): + def __init__(self, prefix: str, name: str = ""): + super().__init__(prefix, LensMode, PsuMode, name) + + +B071ElectronAnalyserController = ElectronAnalyserController[ + B071SpecsAnalyserDriverIO, B071SpecsRegion +] + + +class SpecsPhoibos( + ElectronAnalyserDetector[B071SpecsAnalyserDriverIO, B071SpecsRegion] +): def __init__( self, prefix: str, @@ -13,4 +36,6 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - super().__init__(prefix, LensMode, PsuMode, energy_source, shutter, None, name) + driver = B071SpecsAnalyserDriverIO(prefix) + controller = B071ElectronAnalyserController(driver, energy_source, shutter) + super().__init__(controller, name) diff --git a/src/dodal/devices/electron_analyser/base/base_detector.py b/src/dodal/devices/electron_analyser/base/base_detector.py index bcd68e4fc9..24de42b445 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, ) @@ -125,7 +122,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 @@ -135,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 @@ -164,39 +161,25 @@ 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: 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: Path to the sequence file containing the region data. - enabled_only: 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. """ - seq = self.load_sequence(filename) - regions: list[TAbstractBaseRegion] = ( - seq.get_enabled_regions() if enabled_only else seq.regions - ) return [ ElectronAnalyserRegionDetector[ TAbstractAnalyserDriverIO, TAbstractBaseRegion @@ -206,7 +189,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/__init__.py b/src/dodal/devices/electron_analyser/specs/__init__.py index 64819341c0..ca5ce2686c 100644 --- a/src/dodal/devices/electron_analyser/specs/__init__.py +++ b/src/dodal/devices/electron_analyser/specs/__init__.py @@ -1,10 +1,8 @@ -from .specs_detector import SpecsDetector from .specs_driver_io import SpecsAnalyserDriverIO from .specs_enums import AcquisitionMode from .specs_region import SpecsRegion, SpecsSequence __all__ = [ - "SpecsDetector", "SpecsAnalyserDriverIO", "AcquisitionMode", "SpecsRegion", diff --git a/src/dodal/devices/electron_analyser/specs/specs_detector.py b/src/dodal/devices/electron_analyser/specs/specs_detector.py deleted file mode 100644 index fb54f47c49..0000000000 --- a/src/dodal/devices/electron_analyser/specs/specs_detector.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Generic - -from dodal.devices.electron_analyser.base.base_controller import ( - ElectronAnalyserController, -) -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.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.fast_shutter import FastShutter -from dodal.devices.selectable_source import SourceSelector - - -class SpecsDetector( - ElectronAnalyserDetector[ - SpecsSequence[TLensMode, TPsuMode], - SpecsAnalyserDriverIO[TLensMode, TPsuMode], - SpecsRegion[TLensMode, TPsuMode], - ], - Generic[TLensMode, TPsuMode], -): - def __init__( - self, - prefix: str, - lens_mode_type: type[TLensMode], - psu_mode_type: type[TPsuMode], - energy_source: AbstractEnergySource, - shutter: FastShutter | None = None, - source_selector: SourceSelector | None = None, - name: str = "", - ): - # Save to class so takes part with connect() - self.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] - - super().__init__(sequence_class, controller, name) diff --git a/src/dodal/devices/electron_analyser/vgscienta/__init__.py b/src/dodal/devices/electron_analyser/vgscienta/__init__.py index c344269cc1..3b746193df 100644 --- a/src/dodal/devices/electron_analyser/vgscienta/__init__.py +++ b/src/dodal/devices/electron_analyser/vgscienta/__init__.py @@ -1,10 +1,8 @@ -from .vgscienta_detector import VGScientaDetector from .vgscienta_driver_io import VGScientaAnalyserDriverIO from .vgscienta_enums import AcquisitionMode, DetectorMode from .vgscienta_region import VGScientaRegion, VGScientaSequence __all__ = [ - "VGScientaDetector", "VGScientaAnalyserDriverIO", "AcquisitionMode", "DetectorMode", diff --git a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py deleted file mode 100644 index 6e508fec5f..0000000000 --- a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Generic - -from dodal.devices.electron_analyser.base.base_controller import ( - ElectronAnalyserController, -) -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.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, - VGScientaSequence, -) -from dodal.devices.fast_shutter import FastShutter -from dodal.devices.selectable_source import SourceSelector - - -class VGScientaDetector( - ElectronAnalyserDetector[ - VGScientaSequence[TLensMode, TPsuMode, TPassEnergyEnum], - VGScientaAnalyserDriverIO[TLensMode, TPsuMode, TPassEnergyEnum], - VGScientaRegion[TLensMode, TPassEnergyEnum], - ], - Generic[TLensMode, TPsuMode, TPassEnergyEnum], -): - def __init__( - self, - prefix: str, - lens_mode_type: type[TLensMode], - psu_mode_type: type[TPsuMode], - pass_energy_type: type[TPassEnergyEnum], - energy_source: AbstractEnergySource, - shutter: FastShutter | None = None, - source_selector: SourceSelector | None = None, - name: str = "", - ): - # Save to class so takes part with connect() - self.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) - - sequence_class = VGScientaSequence[ - lens_mode_type, psu_mode_type, pass_energy_type - ] - super().__init__(sequence_class, controller, name) diff --git a/src/dodal/devices/i09/__init__.py b/src/dodal/devices/i09/__init__.py index a4c08c18da..85bbe45d09 100644 --- a/src/dodal/devices/i09/__init__.py +++ b/src/dodal/devices/i09/__init__.py @@ -1,4 +1,18 @@ -from dodal.devices.i09.analyser import EW4000 +from dodal.devices.i09.analyser import ( + EW4000, + I09VGScientaAnalyserDriverIO, + I09VGScientaRegion, + I09VGScientaSequence, +) from dodal.devices.i09.enums import Grating, LensMode, PassEnergy, PsuMode -__all__ = ["EW4000", "Grating", "LensMode", "PsuMode", "PassEnergy"] +__all__ = [ + "EW4000", + "I09VGScientaAnalyserDriverIO", + "I09VGScientaRegion", + "I09VGScientaSequence", + "Grating", + "LensMode", + "PsuMode", + "PassEnergy", +] diff --git a/src/dodal/devices/i09/analyser.py b/src/dodal/devices/i09/analyser.py index e3dbe59737..33d75caa78 100644 --- a/src/dodal/devices/i09/analyser.py +++ b/src/dodal/devices/i09/analyser.py @@ -1,26 +1,55 @@ -from dodal.devices.electron_analyser.base import AbstractEnergySource -from dodal.devices.electron_analyser.vgscienta import VGScientaDetector -from dodal.devices.fast_shutter import FastShutter +from dodal.devices.electron_analyser.base import DualEnergySource +from dodal.devices.electron_analyser.base.base_controller import ( + ElectronAnalyserController, +) +from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector +from dodal.devices.electron_analyser.vgscienta import ( + VGScientaAnalyserDriverIO, + VGScientaRegion, + VGScientaSequence, +) +from dodal.devices.electron_analyser.vgscienta.vgscienta_driver_io import ( + VGScientaAnalyserDriverIO, +) +from dodal.devices.electron_analyser.vgscienta.vgscienta_region import ( + VGScientaRegion, + VGScientaSequence, +) +from dodal.devices.fast_shutter import DualFastShutter from dodal.devices.i09.enums import LensMode, PassEnergy, PsuMode from dodal.devices.selectable_source import SourceSelector +I09VGScientaRegion = VGScientaRegion[LensMode, PassEnergy] +I09VGScientaSequence = VGScientaSequence[LensMode, PsuMode, PassEnergy] + + +class I09VGScientaAnalyserDriverIO( + VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy] +): + def __init__(self, prefix: str, name: str = ""): + super().__init__(prefix, LensMode, PsuMode, PassEnergy, name) + + +I09Controller = ElectronAnalyserController[ + I09VGScientaAnalyserDriverIO, I09VGScientaRegion +] + + +class EW4000( + ElectronAnalyserDetector[I09VGScientaAnalyserDriverIO, I09VGScientaRegion] +): + """ """ -class EW4000(VGScientaDetector[LensMode, PsuMode, PassEnergy]): def __init__( self, prefix: str, - energy_source: AbstractEnergySource, - shutter: FastShutter | None = None, - source_selector: SourceSelector | None = None, + dual_energy_source: DualEnergySource, + dual_fast_shutter: DualFastShutter, + source_selector: SourceSelector, name: str = "", ): - super().__init__( - prefix=prefix, - lens_mode_type=LensMode, - psu_mode_type=PsuMode, - pass_energy_type=PassEnergy, - energy_source=energy_source, - shutter=shutter, - source_selector=source_selector, - name=name, + driver = I09VGScientaAnalyserDriverIO(prefix) + controller = I09Controller( + driver, dual_energy_source, dual_fast_shutter, source_selector ) + super().__init__(controller, name) diff --git a/src/dodal/devices/i09_1/analyser.py b/src/dodal/devices/i09_1/analyser.py index cb1a15529d..c432235122 100644 --- a/src/dodal/devices/i09_1/analyser.py +++ b/src/dodal/devices/i09_1/analyser.py @@ -1,10 +1,33 @@ +from dodal.devices.electron_analyser.base.base_controller import ( + ElectronAnalyserController, +) +from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector from dodal.devices.electron_analyser.base.energy_sources import EnergySource -from dodal.devices.electron_analyser.specs import SpecsDetector +from dodal.devices.electron_analyser.specs import ( + SpecsAnalyserDriverIO, + SpecsRegion, + SpecsSequence, +) from dodal.devices.fast_shutter import FastShutter from dodal.devices.i09_1.enums import LensMode, PsuMode +I091SpecsRegion = SpecsRegion[LensMode, PsuMode] +I091SpecsSequence = SpecsSequence[LensMode, PsuMode] -class SpecsPhoibos225(SpecsDetector[LensMode, PsuMode]): + +class I091SpecsAnalyserDriverIO(SpecsAnalyserDriverIO): + def __init__(self, prefix: str, name: str = ""): + super().__init__(prefix, LensMode, PsuMode, name) + + +I091ElectronAnalyserController = ElectronAnalyserController[ + I091SpecsAnalyserDriverIO, I091SpecsRegion +] + + +class SpecsPhoibos225( + ElectronAnalyserDetector[I091SpecsAnalyserDriverIO, I091SpecsRegion] +): def __init__( self, prefix: str, @@ -12,4 +35,6 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - super().__init__(prefix, LensMode, PsuMode, energy_source, shutter, None, name) + driver = I091SpecsAnalyserDriverIO(prefix) + controller = I091ElectronAnalyserController(driver, energy_source, shutter) + super().__init__(controller, name) diff --git a/src/dodal/devices/p60/analyser.py b/src/dodal/devices/p60/analyser.py index 2fd306af5b..be45e4bada 100644 --- a/src/dodal/devices/p60/analyser.py +++ b/src/dodal/devices/p60/analyser.py @@ -1,9 +1,41 @@ from dodal.devices.electron_analyser.base import AbstractEnergySource -from dodal.devices.electron_analyser.vgscienta import VGScientaDetector +from dodal.devices.electron_analyser.base.base_controller import ( + ElectronAnalyserController, +) +from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector +from dodal.devices.electron_analyser.vgscienta import ( + VGScientaAnalyserDriverIO, + VGScientaRegion, + VGScientaSequence, +) +from dodal.devices.electron_analyser.vgscienta.vgscienta_driver_io import ( + VGScientaAnalyserDriverIO, +) +from dodal.devices.electron_analyser.vgscienta.vgscienta_region import ( + VGScientaRegion, + VGScientaSequence, +) from dodal.devices.p60.enums import LensMode, PassEnergy, PsuMode +P60VGScientnaRegion = VGScientaRegion[LensMode, PassEnergy] +P60VGScientaSequence = VGScientaSequence[LensMode, PsuMode, PassEnergy] -class R4000(VGScientaDetector[LensMode, PsuMode, PassEnergy]): + +class P60VGScientaAnalyserDriverIO( + VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy] +): + def __init__(self, prefix: str, name: str = ""): + super().__init__(prefix, LensMode, PsuMode, PassEnergy, name) + + +P60ElectronAnalyserController = ElectronAnalyserController[ + P60VGScientaAnalyserDriverIO, P60VGScientnaRegion +] + + +class R4000( + ElectronAnalyserDetector[P60VGScientaAnalyserDriverIO, P60VGScientnaRegion] +): """Lab specific analyser for P60 lab. It does not have any shutters connected so will be None for this implementation. The selected_source also cannot be dynamically changed between regions, so will also be None so regions cannot select.""" @@ -14,13 +46,11 @@ def __init__( energy_source: AbstractEnergySource, name: str = "", ): - super().__init__( - prefix=prefix, - lens_mode_type=LensMode, - psu_mode_type=PsuMode, - pass_energy_type=PassEnergy, + driver = P60VGScientaAnalyserDriverIO(prefix) + controller = P60ElectronAnalyserController( + driver, energy_source=energy_source, shutter=None, source_selector=None, - name=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 dffbe8d492..0000000000 --- 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 2d219fcc42..0000000000 --- a/src/dodal/testing/electron_analyser/device_factory.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Any, get_args, get_origin - -from dodal.devices.electron_analyser.base.base_detector import TElectronAnalyserDetector -from dodal.devices.electron_analyser.base.base_driver_io import ( - TAbstractAnalyserDriverIO, -) -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, - VGScientaDetector, -) - - -def create_driver( - driver_class: type[TAbstractAnalyserDriverIO], - **kwargs: Any, -) -> TAbstractAnalyserDriverIO: - """ - Helper function that helps to reduce the code to setup an analyser driver. The - parameters used for the enum types are taken directly from the subscripts of the - class so the user only needs to provide it in one place. - - Args: - driver_class: The class for the driver which must include the enums in the - subscript, for example MyDriverClass[MyLensMode, ...] - kwargs: Additional key worded arguments that the driver needs for initalisation. - """ - parameters = { - "lens_mode_type": get_args(driver_class)[0], - "psu_mode_type": get_args(driver_class)[1], - } - if get_origin(driver_class) is VGScientaAnalyserDriverIO: - parameters["pass_energy_type"] = get_args(driver_class)[2] - - return driver_class(**(parameters | kwargs)) - - -def create_detector( - detector_class: type[TElectronAnalyserDetector], - **kwargs: Any, -) -> TElectronAnalyserDetector: - """ - Helper function that helps to reduce the code to setup an analyser detector. The - parameters used for the enum types are taken directly from the subscripts of the - class so the user only needs to provide it in one place. - - Args: - detector_class: The class for the detector which must include the enums in the - subscript, for example MyDetectorClass[MyLensMode, ...] - kwargs: Additional key worded arguments that the detector needs for - initalisation. - """ - parameters = { - "lens_mode_type": get_args(detector_class)[0], - "psu_mode_type": get_args(detector_class)[1], - } - if get_origin(detector_class) is VGScientaDetector: - parameters["pass_energy_type"] = get_args(detector_class)[2] - - return detector_class(**(parameters | kwargs)) diff --git a/tests/devices/electron_analyser/base/test_base_controller.py b/tests/devices/electron_analyser/base/test_base_controller.py index d9c8a9f260..2cc24a5154 100644 --- a/tests/devices/electron_analyser/base/test_base_controller.py +++ b/tests/devices/electron_analyser/base/test_base_controller.py @@ -1,9 +1,9 @@ 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, init_devices -from dodal.devices import b07, b07_shared, i09 +from dodal.devices.b07.analyser import Specs2DCMOS from dodal.devices.electron_analyser.base import ( AbstractAnalyserDriverIO, AbstractBaseRegion, @@ -13,109 +13,49 @@ from dodal.devices.electron_analyser.base.base_controller import ( ElectronAnalyserController, ) -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.fast_shutter import DualFastShutter +from dodal.devices.i09.analyser import EW4000 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_shared.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)) +from tests.devices.electron_analyser.helper_util import TEST_SEQUENCE_REGION_NAMES @pytest.fixture -def shutter1() -> GenericFastShutter[InOut]: +def ew4000( + dual_energy_source: DualEnergySource, + dual_fast_shutter: DualFastShutter, + source_selector: SourceSelector, +) -> EW4000: with init_devices(mock=True): - shutter1 = GenericFastShutter[InOut]( - pv="TEST:", - open_state=InOut.OUT, - close_state=InOut.IN, - ) - return shutter1 + ew4000 = EW4000("TEST:", dual_energy_source, dual_fast_shutter, source_selector) + return ew4000 @pytest.fixture -def shutter2() -> GenericFastShutter[InOut]: +def specs_2dcmos(single_energy_source: EnergySource) -> Specs2DCMOS: with init_devices(mock=True): - shutter2 = GenericFastShutter[InOut]( - pv="TEST:", - open_state=InOut.OUT, - close_state=InOut.IN, - ) - return shutter2 + specs_2dcmos = Specs2DCMOS("TEST:", single_energy_source) + return specs_2dcmos -@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(params=["ew4000", "specs_2dcmos"]) +def analyser_controller( + request: pytest.FixtureRequest, ew4000: EW4000, specs_2dcmos: Specs2DCMOS +): + detectors = [ew4000, specs_2dcmos] + for detector in detectors: + if detector.name == request.param: + return detector._controller + raise ValueError(f"Detector with name '{request.param}' not found") -@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 +@pytest.fixture +def sim_driver( + analyser_controller: ElectronAnalyserController[ + AbstractAnalyserDriverIO, AbstractBaseRegion + ], +) -> AbstractAnalyserDriverIO: + return analyser_controller.driver 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 cc8e85d0cd..755f584d02 100644 --- a/tests/devices/electron_analyser/base/test_base_detector.py +++ b/tests/devices/electron_analyser/base/test_base_detector.py @@ -9,38 +9,51 @@ assert_reading, ) -import dodal.devices.b07 as b07 -import dodal.devices.b07_shared as b07_shared -import dodal.devices.i09 as i09 +from dodal.devices.b07.analyser import Specs2DCMOS from dodal.devices.electron_analyser.base import ( + DualEnergySource, EnergySource, + GenericAnalyserDriverIO, 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 dodal.devices.fast_shutter import DualFastShutter +from dodal.devices.i09.analyser import EW4000 +from dodal.devices.selectable_source import SourceSelector -VGScientaDetector = VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy] -SpecsDetector = SpecsDetector[b07.LensMode, b07_shared.PsuMode] +@pytest.fixture +def ew4000( + dual_energy_source: DualEnergySource, + dual_fast_shutter: DualFastShutter, + source_selector: SourceSelector, +) -> EW4000: + with init_devices(mock=True): + ew4000 = EW4000("TEST:", dual_energy_source, dual_fast_shutter, source_selector) + return ew4000 -@pytest.fixture(params=[VGScientaDetector, SpecsDetector]) -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, - ) + +@pytest.fixture +def specs_2dcmos(single_energy_source: EnergySource) -> Specs2DCMOS: + with init_devices(mock=True): + specs_2dcmos = Specs2DCMOS("TEST:", 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 + set_mock_value(specs_2dcmos.driver.slices, 1) + return specs_2dcmos + + +@pytest.fixture(params=["ew4000", "specs_2dcmos"]) +def sim_detector( + request: pytest.FixtureRequest, ew4000: EW4000, specs_2dcmos: Specs2DCMOS +) -> GenericElectronAnalyserDetector: + detectors = [ew4000, specs_2dcmos] + for detector in detectors: + if detector.name == request.param: + return detector + + raise ValueError(f"Detector with name '{request.param}' not found") def test_base_analyser_detector_trigger( @@ -93,18 +106,10 @@ async def test_base_analyser_detector_describe_configuration( @pytest.fixture -def sequence_file_path( - sim_detector: GenericElectronAnalyserDetector, -) -> str: - return get_test_sequence(type(sim_detector)) - - -def test_analyser_detector_loads_sequence_correctly( +def sim_driver( sim_detector: GenericElectronAnalyserDetector, - sequence_file_path: str, -) -> None: - seq = sim_detector.load_sequence(sequence_file_path) - assert seq is not None +) -> GenericAnalyserDriverIO: + return sim_detector.driver async def test_analyser_detector_stage( @@ -129,20 +134,18 @@ 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) + region_detectors = sim_detector.create_region_detector_list(sequence.regions) - assert len(region_detectors) == len(seq.get_enabled_regions()) + assert len(region_detectors) == len(sequence.regions) for det in region_detectors: - assert det.region.enabled is True assert det.name == sim_detector.name + "_" + det.region.name 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 @@ -153,7 +156,7 @@ 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.regions) for det in region_detectors: assert det._child_devices.get(driver_name) is None @@ -177,11 +180,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) @@ -189,12 +191,10 @@ 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 - ) + region_detectors = sim_detector.create_region_detector_list(sequence.regions) trigger_info = TriggerInfo() for reg_det in region_detectors: diff --git a/tests/devices/electron_analyser/base/test_base_region.py b/tests/devices/electron_analyser/base/test_base_region.py index 323d854182..8455f7709f 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 import b07, b07_shared, i09 +from dodal.devices.b07 import B07SpecsRegion from dodal.devices.electron_analyser.base import ( AbstractBaseRegion, EnergyMode, @@ -13,25 +12,24 @@ to_binding_energy, to_kinetic_energy, ) -from dodal.devices.electron_analyser.specs import ( - SpecsRegion, - SpecsSequence, -) -from dodal.devices.electron_analyser.vgscienta import VGScientaRegion, VGScientaSequence +from dodal.devices.electron_analyser.specs import SpecsSequence +from dodal.devices.electron_analyser.vgscienta import VGScientaSequence +from dodal.devices.i09 import I09VGScientaRegion from tests.devices.electron_analyser.helper_util import ( TEST_SEQUENCE_REGION_NAMES, - get_test_sequence, + b07_specs_sequence_loader, + i09_vgscienta_sequence_loader, ) @pytest.fixture( params=[ - SpecsSequence[b07.LensMode, b07_shared.PsuMode], - VGScientaSequence[i09.LensMode, i09.PsuMode, i09.PassEnergy], + b07_specs_sequence_loader(), + i09_vgscienta_sequence_loader(), ] ) def sequence(request: pytest.FixtureRequest) -> GenericSequence: - return load_json_file_to_class(request.param, get_test_sequence(request.param)) + return request.param @pytest.fixture @@ -39,9 +37,9 @@ def expected_region_class( sequence: GenericSequence, ) -> type[AbstractBaseRegion]: if isinstance(sequence, SpecsSequence): - return SpecsRegion[b07.LensMode, b07_shared.PsuMode] + return B07SpecsRegion elif isinstance(sequence, VGScientaSequence): - return VGScientaRegion[i09.LensMode, i09.PassEnergy] + return I09VGScientaRegion 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 5ba46737e2..3885984ef4 100644 --- a/tests/devices/electron_analyser/conftest.py +++ b/tests/devices/electron_analyser/conftest.py @@ -1,7 +1,7 @@ from typing import Any import pytest -from ophyd_async.core import init_devices +from ophyd_async.core import InOut, init_devices from dodal.devices.common_dcm import ( DoubleCrystalMonochromatorWithDSpacing, @@ -13,23 +13,13 @@ 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.fast_shutter import DualFastShutter, GenericFastShutter from dodal.devices.i09 import Grating from dodal.devices.pgm import PlaneGratingMonochromator from dodal.devices.selectable_source import SourceSelector -from tests.devices.electron_analyser.helper_util import get_test_sequence +from tests.devices.electron_analyser.helper_util import DRIVER_TO_TEST_SEQUENCE @pytest.fixture @@ -70,35 +60,48 @@ async def dual_energy_source(source_selector: SourceSelector) -> DualEnergySourc return dual_energy_source +def fast_shutter() -> GenericFastShutter: + with init_devices(mock=True): + fast_shutter = GenericFastShutter[InOut]( + pv="TEST:", + open_state=InOut.OUT, + close_state=InOut.IN, + ) + return fast_shutter + + @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 dual_fast_shutter( + source_selector: SourceSelector, +) -> DualFastShutter[InOut]: + with init_devices(mock=True): + shutter1 = GenericFastShutter[InOut]( + pv="TEST:", + open_state=InOut.OUT, + close_state=InOut.IN, + ) + + with init_devices(mock=True): + shutter2 = GenericFastShutter[InOut]( + pv="TEST:", + open_state=InOut.OUT, + close_state=InOut.IN, + ) + + 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], - 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))) + return DRIVER_TO_TEST_SEQUENCE[type(sim_driver)] @pytest.fixture diff --git a/tests/devices/electron_analyser/helper_util/__init__.py b/tests/devices/electron_analyser/helper_util/__init__.py index c3026b1e71..ec5e212561 100644 --- a/tests/devices/electron_analyser/helper_util/__init__.py +++ b/tests/devices/electron_analyser/helper_util/__init__.py @@ -1,8 +1,15 @@ from .assert_func import assert_region_has_expected_values -from .sequence import TEST_SEQUENCE_REGION_NAMES, get_test_sequence +from .sequence import ( + DRIVER_TO_TEST_SEQUENCE, + TEST_SEQUENCE_REGION_NAMES, + b07_specs_sequence_loader, + i09_vgscienta_sequence_loader, +) __all__ = [ "assert_region_has_expected_values", - "get_test_sequence", + "DRIVER_TO_TEST_SEQUENCE", "TEST_SEQUENCE_REGION_NAMES", + "b07_specs_sequence_loader", + "i09_vgscienta_sequence_loader", ] diff --git a/tests/devices/electron_analyser/helper_util/sequence.py b/tests/devices/electron_analyser/helper_util/sequence.py index 85d47a8f56..83d20760c9 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -1,34 +1,30 @@ -from dodal.devices.electron_analyser.specs import ( - SpecsAnalyserDriverIO, - SpecsDetector, - SpecsSequence, +from dodal.common.data_util import load_json_file_to_class +from dodal.devices.b07.analyser import ( + B07SpecsAnalyserDriverIO, + B07SpecsSequence, ) -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, - VGScientaDetector, - VGScientaSequence, +from dodal.devices.i09.analyser import ( + I09VGScientaAnalyserDriverIO, + I09VGScientaSequence, ) from tests.devices.electron_analyser.test_data import ( TEST_SPECS_SEQUENCE, TEST_VGSCIENTA_SEQUENCE, ) -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, -} +TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] -def get_test_sequence(key: type) -> str: - for cls in key.__mro__: - # Check for unscripted class only - if cls in TEST_SEQUENCES: - return TEST_SEQUENCES[cls] - raise KeyError(f"Found no match with type {key}") +def b07_specs_sequence_loader() -> B07SpecsSequence: + return load_json_file_to_class(B07SpecsSequence, TEST_SPECS_SEQUENCE) -TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] +def i09_vgscienta_sequence_loader() -> I09VGScientaSequence: + return load_json_file_to_class(I09VGScientaSequence, TEST_VGSCIENTA_SEQUENCE) + + +# Map to know what function to load in sequence an analyser driver should use. +DRIVER_TO_TEST_SEQUENCE = { + B07SpecsAnalyserDriverIO: b07_specs_sequence_loader(), + I09VGScientaAnalyserDriverIO: i09_vgscienta_sequence_loader(), +} diff --git a/tests/devices/electron_analyser/specs/test_specs_detector.py b/tests/devices/electron_analyser/specs/test_specs_detector.py deleted file mode 100644 index 043d8b4ee6..0000000000 --- a/tests/devices/electron_analyser/specs/test_specs_detector.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest -from ophyd_async.core import init_devices, set_mock_value - -from dodal.devices.b07 import LensMode -from dodal.devices.b07_shared import 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 - prefix = driver.name + "-" - - low_energy = 1 - high_energy = 10 - slices = 4 - set_mock_value(driver.low_energy, low_energy) - set_mock_value(driver.high_energy, high_energy) - set_mock_value(driver.slices, slices) - - min_angle = 1 - max_angle = 10 - set_mock_value(driver.min_angle_axis, min_angle) - set_mock_value(driver.max_angle_axis, max_angle) - - angle_axis = await driver.angle_axis.get_value() - energy_axis = await driver.energy_axis.get_value() - - describe = await sim_detector.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 455e9a712e..2623e0fe29 100644 --- a/tests/devices/electron_analyser/specs/test_specs_driver_io.py +++ b/tests/devices/electron_analyser/specs/test_specs_driver_io.py @@ -13,6 +13,7 @@ ) from dodal.devices.b07 import LensMode +from dodal.devices.b07.analyser import B07SpecsAnalyserDriverIO from dodal.devices.b07_shared import PsuMode from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.base.base_enums import EnergyMode @@ -21,19 +22,15 @@ SpecsAnalyserDriverIO, SpecsRegion, ) -from dodal.testing.electron_analyser import create_driver from tests.devices.electron_analyser.helper_util import ( TEST_SEQUENCE_REGION_NAMES, ) @pytest.fixture -async def sim_driver() -> SpecsAnalyserDriverIO[LensMode, PsuMode]: +async def sim_driver() -> B07SpecsAnalyserDriverIO: async with init_devices(mock=True): - sim_driver = create_driver( - SpecsAnalyserDriverIO[LensMode, PsuMode], - prefix="TEST:", - ) + sim_driver = B07SpecsAnalyserDriverIO(prefix="TEST:") return sim_driver diff --git a/tests/devices/electron_analyser/specs/test_specs_region.py b/tests/devices/electron_analyser/specs/test_specs_region.py index 992b3eb9da..c5ef29290a 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -2,25 +2,21 @@ import pytest -from dodal.common.data_util import load_json_file_to_class from dodal.devices.b07 import LensMode +from dodal.devices.b07.analyser import B07SpecsSequence from dodal.devices.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 from dodal.devices.selectable_source import SelectedSource from tests.devices.electron_analyser.helper_util import ( assert_region_has_expected_values, - get_test_sequence, + b07_specs_sequence_loader, ) @pytest.fixture -def sequence() -> SpecsSequence[LensMode, PsuMode]: - seq = SpecsSequence[LensMode, PsuMode] - return load_json_file_to_class(seq, get_test_sequence(seq)) +def sequence() -> B07SpecsSequence: + return b07_specs_sequence_loader() @pytest.fixture @@ -87,7 +83,7 @@ def expected_region_values() -> list[dict[str, Any]]: def test_sequence_get_expected_enabled_region_names( - sequence: SpecsSequence[LensMode, PsuMode], + sequence: B07SpecsSequence, expected_enabled_region_names: list[str], ) -> None: assert sequence.get_enabled_region_names() == expected_enabled_region_names @@ -96,7 +92,7 @@ def test_sequence_get_expected_enabled_region_names( def test_file_loads_into_class_with_expected_values( - sequence: SpecsSequence[LensMode, PsuMode], + sequence: B07SpecsSequence, expected_region_values: list[dict[str, Any]], ) -> None: assert len(sequence.regions) == len(expected_region_values) diff --git a/tests/devices/electron_analyser/vgscienta/test_vgscienta_detector.py b/tests/devices/electron_analyser/vgscienta/test_vgscienta_detector.py deleted file mode 100644 index b7d771bca5..0000000000 --- a/tests/devices/electron_analyser/vgscienta/test_vgscienta_detector.py +++ /dev/null @@ -1,41 +0,0 @@ -import numpy as np -import pytest -from ophyd_async.core import init_devices, set_mock_value - -from dodal.devices.electron_analyser.base import DualEnergySource -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaDetector, -) -from dodal.devices.i09 import LensMode, PassEnergy, PsuMode -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, -) -> None: - driver = sim_detector.driver - prefix = driver.name + "-" - - energy_axis = np.array([1, 2, 3, 4, 5]) - angle_axis = np.array([1, 2]) - set_mock_value(driver.energy_axis, energy_axis) - set_mock_value(driver.angle_axis, angle_axis) - - describe = await sim_detector.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 61f1c381db..c63939b6fb 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py @@ -14,30 +14,26 @@ ) from dodal.devices.electron_analyser.base import EnergyMode -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, - VGScientaRegion, +from dodal.devices.i09.analyser import ( + I09VGScientaAnalyserDriverIO, + I09VGScientaRegion, ) -from dodal.devices.i09 import LensMode, PassEnergy, PsuMode -from dodal.testing.electron_analyser import create_driver from tests.devices.electron_analyser.helper_util import ( TEST_SEQUENCE_REGION_NAMES, ) @pytest.fixture -async def sim_driver() -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]: +async def sim_driver() -> I09VGScientaAnalyserDriverIO: async with init_devices(mock=True): - sim_driver = create_driver( - VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy], prefix="TEST:" - ) + sim_driver = I09VGScientaAnalyserDriverIO(prefix="TEST:") return sim_driver @pytest.mark.parametrize("region", TEST_SEQUENCE_REGION_NAMES, indirect=True) async def test_analyser_sets_region_correctly( - sim_driver: VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy], - region: VGScientaRegion[LensMode, PassEnergy], + sim_driver: I09VGScientaAnalyserDriverIO, + region: I09VGScientaRegion, run_engine: RunEngine, ) -> None: run_engine(bps.mv(sim_driver, region), wait=True) @@ -94,8 +90,8 @@ async def test_analyser_sets_region_correctly( @pytest.mark.parametrize("region", TEST_SEQUENCE_REGION_NAMES, indirect=True) async def test_analyser_sets_region_and_read_configuration_is_correct( - sim_driver: VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy], - region: VGScientaRegion[LensMode, PassEnergy], + sim_driver: I09VGScientaAnalyserDriverIO, + region: I09VGScientaRegion, run_engine: RunEngine, ) -> None: run_engine(bps.mv(sim_driver, region), wait=True) @@ -136,8 +132,8 @@ async def test_analyser_sets_region_and_read_configuration_is_correct( @pytest.mark.parametrize("region", TEST_SEQUENCE_REGION_NAMES, indirect=True) async def test_analyser_sets_region_and_read_is_correct( - sim_driver: VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy], - region: VGScientaRegion[LensMode, PassEnergy], + sim_driver: I09VGScientaAnalyserDriverIO, + region: I09VGScientaRegion, run_engine: RunEngine, ) -> None: run_engine(bps.mv(sim_driver, region), wait=True) @@ -160,8 +156,8 @@ async def test_analyser_sets_region_and_read_is_correct( @pytest.mark.parametrize("region", TEST_SEQUENCE_REGION_NAMES, indirect=True) async def test_analayser_binding_energy_is_correct( - sim_driver: VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy], - region: VGScientaRegion[LensMode, PassEnergy], + sim_driver: I09VGScientaAnalyserDriverIO, + region: I09VGScientaRegion, run_engine: RunEngine, ) -> None: run_engine(bps.mv(sim_driver, region), wait=True) @@ -183,7 +179,7 @@ async def test_analayser_binding_energy_is_correct( def test_driver_throws_error_with_wrong_pass_energy( - sim_driver: VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy], + sim_driver: I09VGScientaAnalyserDriverIO, run_engine: RunEngine, ) -> None: class PassEnergyTestEnum(StrictEnum): @@ -200,7 +196,7 @@ class PassEnergyTestEnum(StrictEnum): def test_driver_throws_error_with_wrong_detector_mode( - sim_driver: VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy], + sim_driver: I09VGScientaAnalyserDriverIO, run_engine: RunEngine, ) -> None: class DetectorModeTestEnum(StrictEnum): diff --git a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py index 55df78dfaf..a2f0904dde 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py @@ -2,31 +2,28 @@ import pytest -from dodal.common.data_util import load_json_file_to_class from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.vgscienta import ( AcquisitionMode, DetectorMode, - VGScientaRegion, - VGScientaSequence, ) -from dodal.devices.i09 import LensMode, PassEnergy, PsuMode +from dodal.devices.i09 import LensMode, PassEnergy +from dodal.devices.i09.analyser import I09VGScientaRegion, I09VGScientaSequence from dodal.devices.selectable_source import SelectedSource from tests.devices.electron_analyser.helper_util import ( assert_region_has_expected_values, - get_test_sequence, + i09_vgscienta_sequence_loader, ) @pytest.fixture -def sequence() -> VGScientaSequence[LensMode, PsuMode, PassEnergy]: - seq = VGScientaSequence[LensMode, PsuMode, PassEnergy] - return load_json_file_to_class(seq, get_test_sequence(seq)) +def sequence() -> I09VGScientaSequence: + return i09_vgscienta_sequence_loader() @pytest.fixture -def expected_region_class() -> type[VGScientaRegion[LensMode, PassEnergy]]: - return VGScientaRegion[LensMode, PassEnergy] +def expected_region_class() -> type[I09VGScientaRegion]: + return I09VGScientaRegion @pytest.fixture @@ -108,7 +105,7 @@ def expected_region_values() -> list[dict[str, Any]]: def test_sequence_get_expected_enabled_region_names( - sequence: VGScientaSequence[LensMode, PsuMode, PassEnergy], + sequence: I09VGScientaSequence, expected_enabled_region_names: list[str], ) -> None: assert sequence.get_enabled_region_names() == expected_enabled_region_names @@ -117,7 +114,7 @@ def test_sequence_get_expected_enabled_region_names( def test_file_loads_into_class_with_expected_values( - sequence: VGScientaSequence[LensMode, PsuMode, PassEnergy], + sequence: I09VGScientaSequence, expected_region_values: list[dict[str, Any]], ) -> None: assert len(sequence.regions) == len(expected_region_values) From e31edfe9bd25113821e2de99269be0d9396db4b7 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 27 Jan 2026 09:42:03 +0000 Subject: [PATCH 07/22] Fix tests --- .../base/test_base_driver_io.py | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/tests/devices/electron_analyser/base/test_base_driver_io.py b/tests/devices/electron_analyser/base/test_base_driver_io.py index c991d64164..da7a1ab2ff 100644 --- a/tests/devices/electron_analyser/base/test_base_driver_io.py +++ b/tests/devices/electron_analyser/base/test_base_driver_io.py @@ -4,28 +4,15 @@ from bluesky.utils import FailedStatus from ophyd_async.core import StrictEnum, init_devices -from dodal.devices import b07, b07_shared, i09 +from dodal.devices.b07.analyser import B07SpecsAnalyserDriverIO from dodal.devices.electron_analyser.base import GenericAnalyserDriverIO -from dodal.devices.electron_analyser.specs import ( - SpecsAnalyserDriverIO, -) -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, -) -from dodal.testing.electron_analyser import create_driver +from dodal.devices.i09.analyser import I09VGScientaAnalyserDriverIO -@pytest.fixture( - params=[ - VGScientaAnalyserDriverIO[i09.LensMode, i09.PsuMode, i09.PassEnergy], - SpecsAnalyserDriverIO[b07.LensMode, b07_shared.PsuMode], - ] -) -async def sim_driver( - request: pytest.FixtureRequest, -) -> GenericAnalyserDriverIO: +@pytest.fixture(params=[I09VGScientaAnalyserDriverIO, B07SpecsAnalyserDriverIO]) +async def sim_driver(request: pytest.FixtureRequest) -> GenericAnalyserDriverIO: async with init_devices(mock=True): - sim_driver = create_driver(request.param, prefix="TEST:") + sim_driver = request.param(prefix="TEST:") return sim_driver From c5063d0771f97adeddc94158727eadba9c19a8ef Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 27 Jan 2026 10:30:24 +0000 Subject: [PATCH 08/22] Add doc string to EW4000 --- src/dodal/devices/i09/analyser.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/dodal/devices/i09/analyser.py b/src/dodal/devices/i09/analyser.py index 33d75caa78..ca880d6c2c 100644 --- a/src/dodal/devices/i09/analyser.py +++ b/src/dodal/devices/i09/analyser.py @@ -30,7 +30,7 @@ def __init__(self, prefix: str, name: str = ""): super().__init__(prefix, LensMode, PsuMode, PassEnergy, name) -I09Controller = ElectronAnalyserController[ +I09ElectronAnalyserController = ElectronAnalyserController[ I09VGScientaAnalyserDriverIO, I09VGScientaRegion ] @@ -38,7 +38,9 @@ def __init__(self, prefix: str, name: str = ""): class EW4000( ElectronAnalyserDetector[I09VGScientaAnalyserDriverIO, I09VGScientaRegion] ): - """ """ + """Implementation of VGScienta Electron Analyser. This model is unique for i09 + beamline because it has access to multiple energy sources and shutters. The selected + source is deterimined by the source_selector device.""" def __init__( self, @@ -49,7 +51,7 @@ def __init__( name: str = "", ): driver = I09VGScientaAnalyserDriverIO(prefix) - controller = I09Controller( + controller = I09ElectronAnalyserController( driver, dual_energy_source, dual_fast_shutter, source_selector ) super().__init__(controller, name) From 66ce50b31e91505a21be77bd509d6dbc7abb7cf3 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 27 Jan 2026 10:30:37 +0000 Subject: [PATCH 09/22] Add missing classes from __init__ --- src/dodal/devices/i09/__init__.py | 2 ++ src/dodal/devices/i09_1/__init__.py | 18 ++++++++++++++++-- src/dodal/devices/p60/__init__.py | 12 +++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/dodal/devices/i09/__init__.py b/src/dodal/devices/i09/__init__.py index 85bbe45d09..8516e467d4 100644 --- a/src/dodal/devices/i09/__init__.py +++ b/src/dodal/devices/i09/__init__.py @@ -1,5 +1,6 @@ from dodal.devices.i09.analyser import ( EW4000, + I09ElectronAnalyserController, I09VGScientaAnalyserDriverIO, I09VGScientaRegion, I09VGScientaSequence, @@ -8,6 +9,7 @@ __all__ = [ "EW4000", + "I09ElectronAnalyserController", "I09VGScientaAnalyserDriverIO", "I09VGScientaRegion", "I09VGScientaSequence", diff --git a/src/dodal/devices/i09_1/__init__.py b/src/dodal/devices/i09_1/__init__.py index 3577fd23d9..7e509f537b 100644 --- a/src/dodal/devices/i09_1/__init__.py +++ b/src/dodal/devices/i09_1/__init__.py @@ -1,4 +1,18 @@ -from .analyser import SpecsPhoibos225 +from .analyser import ( + I091ElectronAnalyserController, + I091SpecsAnalyserDriverIO, + I091SpecsRegion, + I091SpecsSequence, + SpecsPhoibos225, +) from .enums import LensMode, PsuMode -__all__ = ["SpecsPhoibos225", "LensMode", "PsuMode"] +__all__ = [ + "I091ElectronAnalyserController", + "I091SpecsAnalyserDriverIO", + "I091SpecsRegion", + "I091SpecsSequence", + "SpecsPhoibos225", + "LensMode", + "PsuMode", +] diff --git a/src/dodal/devices/p60/__init__.py b/src/dodal/devices/p60/__init__.py index 42d52a2d03..136ecb09ba 100644 --- a/src/dodal/devices/p60/__init__.py +++ b/src/dodal/devices/p60/__init__.py @@ -1,9 +1,19 @@ -from .analyser import R4000 +from .analyser import ( + R4000, + P60ElectronAnalyserController, + P60VGScientaAnalyserDriverIO, + P60VGScientaSequence, + P60VGScientnaRegion, +) from .enums import LensMode, PassEnergy, PsuMode from .lab_xray_source import LabXraySource, LabXraySourceReadable __all__ = [ "R4000", + "P60ElectronAnalyserController", + "P60VGScientaAnalyserDriverIO", + "P60VGScientaSequence", + "P60VGScientnaRegion", "LensMode", "PsuMode", "PassEnergy", From ec6d8178ab7d8f36ae1521053216e09f51276acd Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 27 Jan 2026 10:58:26 +0000 Subject: [PATCH 10/22] Add psu_mode_suffix option to driver to fix i09 and p60 connection fail --- src/dodal/devices/electron_analyser/base/base_driver_io.py | 3 ++- .../devices/electron_analyser/vgscienta/vgscienta_driver_io.py | 2 ++ src/dodal/devices/i09/analyser.py | 2 +- src/dodal/devices/p60/analyser.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/dodal/devices/electron_analyser/base/base_driver_io.py b/src/dodal/devices/electron_analyser/base/base_driver_io.py index 817949fb0e..615c8124fb 100644 --- a/src/dodal/devices/electron_analyser/base/base_driver_io.py +++ b/src/dodal/devices/electron_analyser/base/base_driver_io.py @@ -54,6 +54,7 @@ def __init__( lens_mode_type: type[TLensMode], psu_mode_type: type[TPsuMode], pass_energy_type: type[TPassEnergy], + psu_mode_suffix: str = "PSU_MODE", name: str = "", ) -> None: """ @@ -112,7 +113,7 @@ def __init__( ) # This is used by each electron analyser, however it depends on the electron # analyser type to know if is moved with region settings. - self.psu_mode = epics_signal_rw(psu_mode_type, prefix + "PSU_MODE") + self.psu_mode = epics_signal_rw(psu_mode_type, prefix + psu_mode_suffix) # This is defined in the parent class, add it as readable configuration. self.add_readables([self.acquire_time], StandardReadableFormat.CONFIG_SIGNAL) 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 3d7db84aa9..2563f0a7e3 100644 --- a/src/dodal/devices/electron_analyser/vgscienta/vgscienta_driver_io.py +++ b/src/dodal/devices/electron_analyser/vgscienta/vgscienta_driver_io.py @@ -40,6 +40,7 @@ def __init__( lens_mode_type: type[TLensMode], psu_mode_type: type[TPsuMode], pass_energy_type: type[TPassEnergyEnum], + psu_mode_suffix: str = "PSU_MODE", name: str = "", ) -> None: with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): @@ -60,6 +61,7 @@ def __init__( lens_mode_type, psu_mode_type, pass_energy_type, + psu_mode_suffix, name, ) diff --git a/src/dodal/devices/i09/analyser.py b/src/dodal/devices/i09/analyser.py index ca880d6c2c..ee5cb43d7d 100644 --- a/src/dodal/devices/i09/analyser.py +++ b/src/dodal/devices/i09/analyser.py @@ -27,7 +27,7 @@ class I09VGScientaAnalyserDriverIO( VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy] ): def __init__(self, prefix: str, name: str = ""): - super().__init__(prefix, LensMode, PsuMode, PassEnergy, name) + super().__init__(prefix, LensMode, PsuMode, PassEnergy, "ELEMENT_SET", name) I09ElectronAnalyserController = ElectronAnalyserController[ diff --git a/src/dodal/devices/p60/analyser.py b/src/dodal/devices/p60/analyser.py index be45e4bada..0c530bacf9 100644 --- a/src/dodal/devices/p60/analyser.py +++ b/src/dodal/devices/p60/analyser.py @@ -25,7 +25,7 @@ class P60VGScientaAnalyserDriverIO( VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy] ): def __init__(self, prefix: str, name: str = ""): - super().__init__(prefix, LensMode, PsuMode, PassEnergy, name) + super().__init__(prefix, LensMode, PsuMode, PassEnergy, "ELEMENT_SET", name) P60ElectronAnalyserController = ElectronAnalyserController[ From 60804b98a6cef0b7ed003de2bfadb43fc9b04792 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 27 Jan 2026 17:35:59 +0000 Subject: [PATCH 11/22] Renamed sequence functions --- tests/devices/electron_analyser/base/test_base_region.py | 8 ++++---- tests/devices/electron_analyser/helper_util/__init__.py | 8 ++++---- tests/devices/electron_analyser/helper_util/sequence.py | 8 ++++---- .../devices/electron_analyser/specs/test_specs_region.py | 4 ++-- .../electron_analyser/vgscienta/test_vgsicenta_region.py | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/devices/electron_analyser/base/test_base_region.py b/tests/devices/electron_analyser/base/test_base_region.py index 8455f7709f..3dba078910 100644 --- a/tests/devices/electron_analyser/base/test_base_region.py +++ b/tests/devices/electron_analyser/base/test_base_region.py @@ -17,15 +17,15 @@ from dodal.devices.i09 import I09VGScientaRegion from tests.devices.electron_analyser.helper_util import ( TEST_SEQUENCE_REGION_NAMES, - b07_specs_sequence_loader, - i09_vgscienta_sequence_loader, + b07_specs_test_sequence_loader, + i09_vgscienta_test_sequence_loader, ) @pytest.fixture( params=[ - b07_specs_sequence_loader(), - i09_vgscienta_sequence_loader(), + b07_specs_test_sequence_loader(), + i09_vgscienta_test_sequence_loader(), ] ) def sequence(request: pytest.FixtureRequest) -> GenericSequence: diff --git a/tests/devices/electron_analyser/helper_util/__init__.py b/tests/devices/electron_analyser/helper_util/__init__.py index ec5e212561..ed73731525 100644 --- a/tests/devices/electron_analyser/helper_util/__init__.py +++ b/tests/devices/electron_analyser/helper_util/__init__.py @@ -2,14 +2,14 @@ from .sequence import ( DRIVER_TO_TEST_SEQUENCE, TEST_SEQUENCE_REGION_NAMES, - b07_specs_sequence_loader, - i09_vgscienta_sequence_loader, + b07_specs_test_sequence_loader, + i09_vgscienta_test_sequence_loader, ) __all__ = [ "assert_region_has_expected_values", "DRIVER_TO_TEST_SEQUENCE", "TEST_SEQUENCE_REGION_NAMES", - "b07_specs_sequence_loader", - "i09_vgscienta_sequence_loader", + "b07_specs_test_sequence_loader", + "i09_vgscienta_test_sequence_loader", ] diff --git a/tests/devices/electron_analyser/helper_util/sequence.py b/tests/devices/electron_analyser/helper_util/sequence.py index 83d20760c9..1169228d5b 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -15,16 +15,16 @@ TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] -def b07_specs_sequence_loader() -> B07SpecsSequence: +def b07_specs_test_sequence_loader() -> B07SpecsSequence: return load_json_file_to_class(B07SpecsSequence, TEST_SPECS_SEQUENCE) -def i09_vgscienta_sequence_loader() -> I09VGScientaSequence: +def i09_vgscienta_test_sequence_loader() -> I09VGScientaSequence: return load_json_file_to_class(I09VGScientaSequence, TEST_VGSCIENTA_SEQUENCE) # Map to know what function to load in sequence an analyser driver should use. DRIVER_TO_TEST_SEQUENCE = { - B07SpecsAnalyserDriverIO: b07_specs_sequence_loader(), - I09VGScientaAnalyserDriverIO: i09_vgscienta_sequence_loader(), + B07SpecsAnalyserDriverIO: b07_specs_test_sequence_loader(), + I09VGScientaAnalyserDriverIO: i09_vgscienta_test_sequence_loader(), } diff --git a/tests/devices/electron_analyser/specs/test_specs_region.py b/tests/devices/electron_analyser/specs/test_specs_region.py index c5ef29290a..ad52bd6ba5 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -10,13 +10,13 @@ from dodal.devices.selectable_source import SelectedSource from tests.devices.electron_analyser.helper_util import ( assert_region_has_expected_values, - b07_specs_sequence_loader, + b07_specs_test_sequence_loader, ) @pytest.fixture def sequence() -> B07SpecsSequence: - return b07_specs_sequence_loader() + return b07_specs_test_sequence_loader() @pytest.fixture diff --git a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py index a2f0904dde..d9e97b8129 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py @@ -12,13 +12,13 @@ from dodal.devices.selectable_source import SelectedSource from tests.devices.electron_analyser.helper_util import ( assert_region_has_expected_values, - i09_vgscienta_sequence_loader, + i09_vgscienta_test_sequence_loader, ) @pytest.fixture def sequence() -> I09VGScientaSequence: - return i09_vgscienta_sequence_loader() + return i09_vgscienta_test_sequence_loader() @pytest.fixture From 1264e9ac23a6ba8716dcc930991061223e72b70a Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Wed, 28 Jan 2026 16:37:08 +0000 Subject: [PATCH 12/22] Add comment linking bluesky issue --- src/dodal/devices/electron_analyser/base/base_detector.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dodal/devices/electron_analyser/base/base_detector.py b/src/dodal/devices/electron_analyser/base/base_detector.py index 24de42b445..9d6f732ac7 100644 --- a/src/dodal/devices/electron_analyser/base/base_detector.py +++ b/src/dodal/devices/electron_analyser/base/base_detector.py @@ -110,6 +110,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 ] From 7db944a8a7472f5ac38d3452e9e1b9667a049e6d Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 30 Jan 2026 13:42:57 +0000 Subject: [PATCH 13/22] Update outdated doc string --- src/dodal/devices/electron_analyser/base/base_driver_io.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/dodal/devices/electron_analyser/base/base_driver_io.py b/src/dodal/devices/electron_analyser/base/base_driver_io.py index cf5ece6482..25a3704d07 100644 --- a/src/dodal/devices/electron_analyser/base/base_driver_io.py +++ b/src/dodal/devices/electron_analyser/base/base_driver_io.py @@ -56,8 +56,6 @@ class AbstractAnalyserDriverIO( pass_energy_type (type[TPassEnergy]): Can be enum or float, depending on electron analyser model. If enum, it determines the available pass energies for this device. - energy_source: Device that can give us the correct excitation energy (in eV) - and switch sources if applicable. name (str, optional): Name of the device. """ From 9e1108e270e2d68df690eba78e6e24847419f736 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Wed, 4 Feb 2026 13:57:04 +0000 Subject: [PATCH 14/22] Merge into main --- src/dodal/devices/beamlines/b07/__init__.py | 19 +++++++++++++++-- .../{ => beamlines}/b07_shared/__init__.py | 0 .../{ => beamlines}/b07_shared/enums.py | 0 src/dodal/devices/beamlines/i09/__init__.py | 21 +++++++++++++++++-- .../base/test_base_controller.py | 4 ++-- .../electron_analyser/helper_util/sequence.py | 4 ++-- 6 files changed, 40 insertions(+), 8 deletions(-) rename src/dodal/devices/{ => beamlines}/b07_shared/__init__.py (100%) rename src/dodal/devices/{ => beamlines}/b07_shared/enums.py (100%) diff --git a/src/dodal/devices/beamlines/b07/__init__.py b/src/dodal/devices/beamlines/b07/__init__.py index dd31225628..8c7f6c984a 100644 --- a/src/dodal/devices/beamlines/b07/__init__.py +++ b/src/dodal/devices/beamlines/b07/__init__.py @@ -1,3 +1,18 @@ -from dodal.devices.beamlines.b07.enums import Grating, LensMode, PsuMode +from .analyser import ( + B07ElectronAnalyserController, + B07SpecsAnalyserDriverIO, + B07SpecsRegion, + B07SpecsSequence, + Specs2DCMOS, +) +from .enums import Grating, LensMode -__all__ = ["Grating", "LensMode", "PsuMode"] +__all__ = [ + "B07ElectronAnalyserController", + "B07SpecsAnalyserDriverIO", + "B07SpecsRegion", + "B07SpecsSequence", + "Specs2DCMOS", + "Grating", + "LensMode", +] diff --git a/src/dodal/devices/b07_shared/__init__.py b/src/dodal/devices/beamlines/b07_shared/__init__.py similarity index 100% rename from src/dodal/devices/b07_shared/__init__.py rename to src/dodal/devices/beamlines/b07_shared/__init__.py diff --git a/src/dodal/devices/b07_shared/enums.py b/src/dodal/devices/beamlines/b07_shared/enums.py similarity index 100% rename from src/dodal/devices/b07_shared/enums.py rename to src/dodal/devices/beamlines/b07_shared/enums.py diff --git a/src/dodal/devices/beamlines/i09/__init__.py b/src/dodal/devices/beamlines/i09/__init__.py index cfb2a43d07..ea53440d01 100644 --- a/src/dodal/devices/beamlines/i09/__init__.py +++ b/src/dodal/devices/beamlines/i09/__init__.py @@ -1,3 +1,20 @@ -from dodal.devices.beamlines.i09.enums import Grating, LensMode, PassEnergy, PsuMode +from .analyser import ( + EW4000, + I09ElectronAnalyserController, + I09VGScientaAnalyserDriverIO, + I09VGScientaRegion, + I09VGScientaSequence, +) +from .enums import Grating, LensMode, PassEnergy, PsuMode -__all__ = ["Grating", "LensMode", "PsuMode", "PassEnergy"] +__all__ = [ + "EW4000", + "I09ElectronAnalyserController", + "I09VGScientaAnalyserDriverIO", + "I09VGScientaRegion", + "I09VGScientaSequence", + "Grating", + "LensMode", + "PsuMode", + "PassEnergy", +] diff --git a/tests/devices/electron_analyser/base/test_base_controller.py b/tests/devices/electron_analyser/base/test_base_controller.py index 2cc24a5154..94588b949d 100644 --- a/tests/devices/electron_analyser/base/test_base_controller.py +++ b/tests/devices/electron_analyser/base/test_base_controller.py @@ -3,7 +3,8 @@ import pytest from ophyd_async.core import TriggerInfo, get_mock_put, init_devices -from dodal.devices.b07.analyser import Specs2DCMOS +from dodal.devices.beamlines.b07.analyser import Specs2DCMOS +from dodal.devices.beamlines.i09.analyser import EW4000 from dodal.devices.electron_analyser.base import ( AbstractAnalyserDriverIO, AbstractBaseRegion, @@ -14,7 +15,6 @@ ElectronAnalyserController, ) from dodal.devices.fast_shutter import DualFastShutter -from dodal.devices.i09.analyser import EW4000 from dodal.devices.selectable_source import SourceSelector from tests.devices.electron_analyser.helper_util import TEST_SEQUENCE_REGION_NAMES diff --git a/tests/devices/electron_analyser/helper_util/sequence.py b/tests/devices/electron_analyser/helper_util/sequence.py index 1169228d5b..0245c0435b 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -1,9 +1,9 @@ from dodal.common.data_util import load_json_file_to_class -from dodal.devices.b07.analyser import ( +from dodal.devices.beamlines.b07.analyser import ( B07SpecsAnalyserDriverIO, B07SpecsSequence, ) -from dodal.devices.i09.analyser import ( +from dodal.devices.beamlines.i09.analyser import ( I09VGScientaAnalyserDriverIO, I09VGScientaSequence, ) From e470d24734959bb4a065e029c3206cd151c2dfef Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 10 Feb 2026 12:08:39 +0000 Subject: [PATCH 15/22] Rename driver to drv --- src/dodal/devices/beamlines/b07/analyser.py | 4 ++-- src/dodal/devices/beamlines/b07_1/analyser.py | 4 ++-- src/dodal/devices/beamlines/i09/analyser.py | 4 ++-- src/dodal/devices/beamlines/i09_1/analyser.py | 4 ++-- src/dodal/devices/beamlines/p60/analyser.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/dodal/devices/beamlines/b07/analyser.py b/src/dodal/devices/beamlines/b07/analyser.py index 46997bc583..80fe3b491a 100644 --- a/src/dodal/devices/beamlines/b07/analyser.py +++ b/src/dodal/devices/beamlines/b07/analyser.py @@ -34,6 +34,6 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - driver = B07SpecsAnalyserDriverIO(prefix) - controller = B07ElectronAnalyserController(driver, energy_source, shutter) + drv = B07SpecsAnalyserDriverIO(prefix) + controller = B07ElectronAnalyserController(drv, energy_source, shutter) super().__init__(controller, name) diff --git a/src/dodal/devices/beamlines/b07_1/analyser.py b/src/dodal/devices/beamlines/b07_1/analyser.py index 42e95bc4fd..dcb03873a8 100644 --- a/src/dodal/devices/beamlines/b07_1/analyser.py +++ b/src/dodal/devices/beamlines/b07_1/analyser.py @@ -36,6 +36,6 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - driver = B071SpecsAnalyserDriverIO(prefix) - controller = B071ElectronAnalyserController(driver, energy_source, shutter) + drv = B071SpecsAnalyserDriverIO(prefix) + controller = B071ElectronAnalyserController(drv, energy_source, shutter) super().__init__(controller, name) diff --git a/src/dodal/devices/beamlines/i09/analyser.py b/src/dodal/devices/beamlines/i09/analyser.py index 7a7359d46c..e09064f71e 100644 --- a/src/dodal/devices/beamlines/i09/analyser.py +++ b/src/dodal/devices/beamlines/i09/analyser.py @@ -51,8 +51,8 @@ def __init__( source_selector: SourceSelector, name: str = "", ): - driver = I09VGScientaAnalyserDriverIO(prefix) + drv = I09VGScientaAnalyserDriverIO(prefix) controller = I09ElectronAnalyserController( - driver, dual_energy_source, dual_fast_shutter, source_selector + drv, dual_energy_source, dual_fast_shutter, source_selector ) super().__init__(controller, name) diff --git a/src/dodal/devices/beamlines/i09_1/analyser.py b/src/dodal/devices/beamlines/i09_1/analyser.py index d57016d61a..3f6bd55904 100644 --- a/src/dodal/devices/beamlines/i09_1/analyser.py +++ b/src/dodal/devices/beamlines/i09_1/analyser.py @@ -35,6 +35,6 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - driver = I091SpecsAnalyserDriverIO(prefix) - controller = I091ElectronAnalyserController(driver, energy_source, shutter) + drv = I091SpecsAnalyserDriverIO(prefix) + controller = I091ElectronAnalyserController(drv, energy_source, shutter) super().__init__(controller, name) diff --git a/src/dodal/devices/beamlines/p60/analyser.py b/src/dodal/devices/beamlines/p60/analyser.py index 41c610c23b..e9ce7d69e0 100644 --- a/src/dodal/devices/beamlines/p60/analyser.py +++ b/src/dodal/devices/beamlines/p60/analyser.py @@ -47,9 +47,9 @@ def __init__( energy_source: AbstractEnergySource, name: str = "", ): - driver = P60VGScientaAnalyserDriverIO(prefix) + drv = P60VGScientaAnalyserDriverIO(prefix) controller = P60ElectronAnalyserController( - driver, + drv, energy_source=energy_source, shutter=None, source_selector=None, From 95aae829ebceb888ba9601909a2de066249435ec Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 13 Feb 2026 15:55:06 +0000 Subject: [PATCH 16/22] Update the device names --- src/dodal/beamlines/b07.py | 6 +++--- src/dodal/beamlines/b07_1.py | 6 +++--- src/dodal/beamlines/i09.py | 6 +++--- src/dodal/beamlines/i09_1.py | 8 +++++--- src/dodal/beamlines/p60.py | 12 ++++++++--- src/dodal/devices/beamlines/b07/__init__.py | 20 +++++++++---------- src/dodal/devices/beamlines/b07/analyser.py | 18 +++++++++-------- src/dodal/devices/beamlines/b07_1/__init__.py | 20 +++++++++---------- src/dodal/devices/beamlines/b07_1/analyser.py | 18 ++++++++--------- src/dodal/devices/beamlines/i09/__init__.py | 4 ++-- src/dodal/devices/beamlines/i09/analyser.py | 2 +- src/dodal/devices/beamlines/i09_1/__init__.py | 4 ++-- src/dodal/devices/beamlines/i09_1/analyser.py | 2 +- src/dodal/devices/beamlines/p60/__init__.py | 4 ++-- src/dodal/devices/beamlines/p60/analyser.py | 2 +- 15 files changed, 71 insertions(+), 61 deletions(-) diff --git a/src/dodal/beamlines/b07.py b/src/dodal/beamlines/b07.py index ce350b4b73..cc5937c452 100644 --- a/src/dodal/beamlines/b07.py +++ b/src/dodal/beamlines/b07.py @@ -1,7 +1,7 @@ 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 B07SampleManipulator52B, Grating, Specs2DCMOS +from dodal.devices.beamlines.b07 import B07BSpecs150, B07SampleManipulator52B, Grating from dodal.devices.electron_analyser.base import EnergySource from dodal.devices.motors import XYZPolarStage from dodal.devices.pgm import PlaneGratingMonochromator @@ -33,8 +33,8 @@ def energy_source(pgm: PlaneGratingMonochromator) -> EnergySource: # 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) -> Specs2DCMOS: - return Specs2DCMOS(f"{B_PREFIX.beamline_prefix}-EA-DET-01:CAM:", energy_source) +def analyser(energy_source: EnergySource) -> B07BSpecs150: + return B07BSpecs150(f"{B_PREFIX.beamline_prefix}-EA-DET-01:CAM:", energy_source) @devices.factory() diff --git a/src/dodal/beamlines/b07_1.py b/src/dodal/beamlines/b07_1.py index 5ba4c85290..a5c76ae93e 100644 --- a/src/dodal/beamlines/b07_1.py +++ b/src/dodal/beamlines/b07_1.py @@ -2,9 +2,9 @@ from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.device_manager import DeviceManager from dodal.devices.beamlines.b07_1 import ( + B07CSpecs150, ChannelCutMonochromator, Grating, - SpecsPhoibos, ) from dodal.devices.electron_analyser.base import EnergySource from dodal.devices.motors import XYZPolarAzimuthStage @@ -42,8 +42,8 @@ def energy_source(pgm: PlaneGratingMonochromator) -> EnergySource: # 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) -> SpecsPhoibos: - return SpecsPhoibos( +def analyser(energy_source: EnergySource) -> B07CSpecs150: + return B07CSpecs150( prefix=f"{C_PREFIX.beamline_prefix}-EA-DET-01:CAM:", energy_source=energy_source, ) diff --git a/src/dodal/beamlines/i09.py b/src/dodal/beamlines/i09.py index 2f39974b08..792317ede9 100644 --- a/src/dodal/beamlines/i09.py +++ b/src/dodal/beamlines/i09.py @@ -4,7 +4,7 @@ from dodal.beamlines.i09_2_shared import devices as i09_2_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.i09 import EW4000 +from dodal.devices.beamlines.i09 import I09VGScientaEW4000 from dodal.devices.common_dcm import DoubleCrystalMonochromatorWithDSpacing from dodal.devices.electron_analyser.base import DualEnergySource from dodal.devices.fast_shutter import DualFastShutter, GenericFastShutter @@ -82,8 +82,8 @@ def ew4000( dual_energy_source: DualEnergySource, dual_fast_shutter: DualFastShutter, source_selector: SourceSelector, -) -> EW4000: - return EW4000( +) -> I09VGScientaEW4000: + return I09VGScientaEW4000( f"{I_PREFIX.beamline_prefix}-EA-DET-01:CAM:", dual_energy_source, dual_fast_shutter, diff --git a/src/dodal/beamlines/i09_1.py b/src/dodal/beamlines/i09_1.py index ba6de32e3c..d42cc7fddd 100644 --- a/src/dodal/beamlines/i09_1.py +++ b/src/dodal/beamlines/i09_1.py @@ -1,7 +1,7 @@ from dodal.beamlines.i09_1_shared import devices as i09_1_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.i09_1 import SpecsPhoibos225 +from dodal.devices.beamlines.i09_1 import I091SpecsPhoibos225 from dodal.devices.common_dcm import DoubleCrystalMonochromatorWithDSpacing from dodal.devices.electron_analyser.base import EnergySource from dodal.devices.motors import XYZPolarAzimuthTiltStage @@ -32,8 +32,10 @@ def energy_source(dcm: DoubleCrystalMonochromatorWithDSpacing) -> EnergySource: # 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) -> SpecsPhoibos225: - return SpecsPhoibos225(f"{PREFIX.beamline_prefix}-EA-DET-02:CAM:", energy_source) +def analyser(energy_source: EnergySource) -> I091SpecsPhoibos225: + return I091SpecsPhoibos225( + f"{PREFIX.beamline_prefix}-EA-DET-02:CAM:", energy_source + ) @devices.factory() diff --git a/src/dodal/beamlines/p60.py b/src/dodal/beamlines/p60.py index f034eebe32..76b0318bb7 100644 --- a/src/dodal/beamlines/p60.py +++ b/src/dodal/beamlines/p60.py @@ -1,6 +1,10 @@ from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.device_manager import DeviceManager -from dodal.devices.beamlines.p60 import R4000, LabXraySource, LabXraySourceReadable +from dodal.devices.beamlines.p60 import ( + LabXraySource, + LabXraySourceReadable, + P60VGScientaR4000, +) from dodal.devices.electron_analyser.base import DualEnergySource from dodal.devices.selectable_source import SourceSelector from dodal.log import set_beamline as set_log_beamline @@ -50,5 +54,7 @@ def dual_energy_source( # Connect will work again after this work completed # https://jira.diamond.ac.uk/browse/P60-13 @devices.factory() -def r4000(dual_energy_source: DualEnergySource) -> R4000: - return R4000(f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:", dual_energy_source) +def r4000(dual_energy_source: DualEnergySource) -> P60VGScientaR4000: + return P60VGScientaR4000( + f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:", dual_energy_source + ) diff --git a/src/dodal/devices/beamlines/b07/__init__.py b/src/dodal/devices/beamlines/b07/__init__.py index e3a59cd2ec..3f4f53514c 100644 --- a/src/dodal/devices/beamlines/b07/__init__.py +++ b/src/dodal/devices/beamlines/b07/__init__.py @@ -1,19 +1,19 @@ from .analyser import ( - B07ElectronAnalyserController, - B07SpecsAnalyserDriverIO, - B07SpecsRegion, - B07SpecsSequence, - Specs2DCMOS, + B07BElectronAnalyserController, + B07BSpecs150, + B07BSpecsAnalyserDriverIO, + B07BSpecsRegion, + B07BSpecsSequence, ) from .b07_motors import B07SampleManipulator52B from .enums import Grating, LensMode __all__ = [ - "B07ElectronAnalyserController", - "B07SpecsAnalyserDriverIO", - "B07SpecsRegion", - "B07SpecsSequence", - "Specs2DCMOS", + "B07BElectronAnalyserController", + "B07BSpecsAnalyserDriverIO", + "B07BSpecsRegion", + "B07BSpecsSequence", + "B07BSpecs150", "B07SampleManipulator52B", "Grating", "LensMode", diff --git a/src/dodal/devices/beamlines/b07/analyser.py b/src/dodal/devices/beamlines/b07/analyser.py index 80fe3b491a..6b5ba860e4 100644 --- a/src/dodal/devices/beamlines/b07/analyser.py +++ b/src/dodal/devices/beamlines/b07/analyser.py @@ -12,21 +12,23 @@ ) from dodal.devices.fast_shutter import FastShutter -B07SpecsRegion = SpecsRegion[LensMode, PsuMode] -B07SpecsSequence = SpecsSequence[LensMode, PsuMode] +B07BSpecsRegion = SpecsRegion[LensMode, PsuMode] +B07BSpecsSequence = SpecsSequence[LensMode, PsuMode] -class B07SpecsAnalyserDriverIO(SpecsAnalyserDriverIO): +class B07BSpecsAnalyserDriverIO(SpecsAnalyserDriverIO): def __init__(self, prefix: str, name: str = ""): super().__init__(prefix, LensMode, PsuMode, name) -B07ElectronAnalyserController = ElectronAnalyserController[ - B07SpecsAnalyserDriverIO, B07SpecsRegion +B07BElectronAnalyserController = ElectronAnalyserController[ + B07BSpecsAnalyserDriverIO, B07BSpecsRegion ] -class Specs2DCMOS(ElectronAnalyserDetector[B07SpecsAnalyserDriverIO, B07SpecsRegion]): +class B07BSpecs150( + ElectronAnalyserDetector[B07BSpecsAnalyserDriverIO, B07BSpecsRegion] +): def __init__( self, prefix: str, @@ -34,6 +36,6 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - drv = B07SpecsAnalyserDriverIO(prefix) - controller = B07ElectronAnalyserController(drv, energy_source, shutter) + drv = B07BSpecsAnalyserDriverIO(prefix) + controller = B07BElectronAnalyserController(drv, energy_source, shutter) super().__init__(controller, name) diff --git a/src/dodal/devices/beamlines/b07_1/__init__.py b/src/dodal/devices/beamlines/b07_1/__init__.py index 3bb90c94c4..72fac821b2 100644 --- a/src/dodal/devices/beamlines/b07_1/__init__.py +++ b/src/dodal/devices/beamlines/b07_1/__init__.py @@ -1,9 +1,9 @@ from .analyser import ( - B071ElectronAnalyserController, - B071SpecsAnalyserDriverIO, - B071SpecsRegion, - B071SpecsSequence, - SpecsPhoibos, + B07CElectronAnalyserController, + B07CSpecs150, + B07CSpecsAnalyserDriverIO, + B07CSpecsRegion, + B07CSpecsSequence, ) from .ccmc import ( ChannelCutMonochromator, @@ -12,11 +12,11 @@ from .enums import Grating, LensMode __all__ = [ - "B071ElectronAnalyserController", - "B071SpecsAnalyserDriverIO", - "B071SpecsRegion", - "B071SpecsSequence", - "SpecsPhoibos", + "B07CElectronAnalyserController", + "B07CSpecsAnalyserDriverIO", + "B07CSpecsRegion", + "B07CSpecsSequence", + "B07CSpecs150", "Grating", "LensMode", "ChannelCutMonochromator", diff --git a/src/dodal/devices/beamlines/b07_1/analyser.py b/src/dodal/devices/beamlines/b07_1/analyser.py index dcb03873a8..e0436404c4 100644 --- a/src/dodal/devices/beamlines/b07_1/analyser.py +++ b/src/dodal/devices/beamlines/b07_1/analyser.py @@ -12,22 +12,22 @@ ) from dodal.devices.fast_shutter import FastShutter -B071SpecsRegion = SpecsRegion[LensMode, PsuMode] -B071SpecsSequence = SpecsSequence[LensMode, PsuMode] +B07CSpecsRegion = SpecsRegion[LensMode, PsuMode] +B07CSpecsSequence = SpecsSequence[LensMode, PsuMode] -class B071SpecsAnalyserDriverIO(SpecsAnalyserDriverIO): +class B07CSpecsAnalyserDriverIO(SpecsAnalyserDriverIO): def __init__(self, prefix: str, name: str = ""): super().__init__(prefix, LensMode, PsuMode, name) -B071ElectronAnalyserController = ElectronAnalyserController[ - B071SpecsAnalyserDriverIO, B071SpecsRegion +B07CElectronAnalyserController = ElectronAnalyserController[ + B07CSpecsAnalyserDriverIO, B07CSpecsRegion ] -class SpecsPhoibos( - ElectronAnalyserDetector[B071SpecsAnalyserDriverIO, B071SpecsRegion] +class B07CSpecs150( + ElectronAnalyserDetector[B07CSpecsAnalyserDriverIO, B07CSpecsRegion] ): def __init__( self, @@ -36,6 +36,6 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - drv = B071SpecsAnalyserDriverIO(prefix) - controller = B071ElectronAnalyserController(drv, energy_source, shutter) + drv = B07CSpecsAnalyserDriverIO(prefix) + controller = B07CElectronAnalyserController(drv, energy_source, shutter) super().__init__(controller, name) diff --git a/src/dodal/devices/beamlines/i09/__init__.py b/src/dodal/devices/beamlines/i09/__init__.py index ea53440d01..458ffdd079 100644 --- a/src/dodal/devices/beamlines/i09/__init__.py +++ b/src/dodal/devices/beamlines/i09/__init__.py @@ -1,16 +1,16 @@ from .analyser import ( - EW4000, I09ElectronAnalyserController, I09VGScientaAnalyserDriverIO, + I09VGScientaEW4000, I09VGScientaRegion, I09VGScientaSequence, ) from .enums import Grating, LensMode, PassEnergy, PsuMode __all__ = [ - "EW4000", "I09ElectronAnalyserController", "I09VGScientaAnalyserDriverIO", + "I09VGScientaEW4000", "I09VGScientaRegion", "I09VGScientaSequence", "Grating", diff --git a/src/dodal/devices/beamlines/i09/analyser.py b/src/dodal/devices/beamlines/i09/analyser.py index e09064f71e..689fe01728 100644 --- a/src/dodal/devices/beamlines/i09/analyser.py +++ b/src/dodal/devices/beamlines/i09/analyser.py @@ -35,7 +35,7 @@ def __init__(self, prefix: str, name: str = ""): ] -class EW4000( +class I09VGScientaEW4000( ElectronAnalyserDetector[I09VGScientaAnalyserDriverIO, I09VGScientaRegion] ): """Implementation of VGScienta Electron Analyser. This model is unique for i09 diff --git a/src/dodal/devices/beamlines/i09_1/__init__.py b/src/dodal/devices/beamlines/i09_1/__init__.py index 7e509f537b..fc3d25d176 100644 --- a/src/dodal/devices/beamlines/i09_1/__init__.py +++ b/src/dodal/devices/beamlines/i09_1/__init__.py @@ -1,18 +1,18 @@ from .analyser import ( I091ElectronAnalyserController, I091SpecsAnalyserDriverIO, + I091SpecsPhoibos225, I091SpecsRegion, I091SpecsSequence, - SpecsPhoibos225, ) from .enums import LensMode, PsuMode __all__ = [ "I091ElectronAnalyserController", "I091SpecsAnalyserDriverIO", + "I091SpecsPhoibos225", "I091SpecsRegion", "I091SpecsSequence", - "SpecsPhoibos225", "LensMode", "PsuMode", ] diff --git a/src/dodal/devices/beamlines/i09_1/analyser.py b/src/dodal/devices/beamlines/i09_1/analyser.py index 3f6bd55904..bbbafb4c22 100644 --- a/src/dodal/devices/beamlines/i09_1/analyser.py +++ b/src/dodal/devices/beamlines/i09_1/analyser.py @@ -25,7 +25,7 @@ def __init__(self, prefix: str, name: str = ""): ] -class SpecsPhoibos225( +class I091SpecsPhoibos225( ElectronAnalyserDetector[I091SpecsAnalyserDriverIO, I091SpecsRegion] ): def __init__( diff --git a/src/dodal/devices/beamlines/p60/__init__.py b/src/dodal/devices/beamlines/p60/__init__.py index 136ecb09ba..0ccdeeaa15 100644 --- a/src/dodal/devices/beamlines/p60/__init__.py +++ b/src/dodal/devices/beamlines/p60/__init__.py @@ -1,7 +1,7 @@ from .analyser import ( - R4000, P60ElectronAnalyserController, P60VGScientaAnalyserDriverIO, + P60VGScientaR4000, P60VGScientaSequence, P60VGScientnaRegion, ) @@ -9,9 +9,9 @@ from .lab_xray_source import LabXraySource, LabXraySourceReadable __all__ = [ - "R4000", "P60ElectronAnalyserController", "P60VGScientaAnalyserDriverIO", + "P60VGScientaR4000", "P60VGScientaSequence", "P60VGScientnaRegion", "LensMode", diff --git a/src/dodal/devices/beamlines/p60/analyser.py b/src/dodal/devices/beamlines/p60/analyser.py index e9ce7d69e0..e656becf99 100644 --- a/src/dodal/devices/beamlines/p60/analyser.py +++ b/src/dodal/devices/beamlines/p60/analyser.py @@ -33,7 +33,7 @@ def __init__(self, prefix: str, name: str = ""): ] -class R4000( +class P60VGScientaR4000( ElectronAnalyserDetector[P60VGScientaAnalyserDriverIO, P60VGScientnaRegion] ): """Lab specific analyser for P60 lab. It does not have any shutters connected so From 360419cf2418fc7c6c4af8ce17604834d1101583 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 13 Feb 2026 16:06:12 +0000 Subject: [PATCH 17/22] Fix tests --- .../base/test_base_controller.py | 24 +++++++++++-------- .../base/test_base_detector.py | 24 ++++++++++--------- .../base/test_base_driver_io.py | 4 ++-- .../base/test_base_region.py | 4 ++-- .../electron_analyser/helper_util/sequence.py | 10 ++++---- .../specs/test_specs_driver_io.py | 6 ++--- .../specs/test_specs_region.py | 8 +++---- 7 files changed, 43 insertions(+), 37 deletions(-) diff --git a/tests/devices/electron_analyser/base/test_base_controller.py b/tests/devices/electron_analyser/base/test_base_controller.py index 94588b949d..839efedfa6 100644 --- a/tests/devices/electron_analyser/base/test_base_controller.py +++ b/tests/devices/electron_analyser/base/test_base_controller.py @@ -3,8 +3,8 @@ import pytest from ophyd_async.core import TriggerInfo, get_mock_put, init_devices -from dodal.devices.beamlines.b07.analyser import Specs2DCMOS -from dodal.devices.beamlines.i09.analyser import EW4000 +from dodal.devices.beamlines.b07.analyser import B07BSpecs150 +from dodal.devices.beamlines.i09.analyser import I09VGScientaEW4000 from dodal.devices.electron_analyser.base import ( AbstractAnalyserDriverIO, AbstractBaseRegion, @@ -24,24 +24,28 @@ def ew4000( dual_energy_source: DualEnergySource, dual_fast_shutter: DualFastShutter, source_selector: SourceSelector, -) -> EW4000: +) -> I09VGScientaEW4000: with init_devices(mock=True): - ew4000 = EW4000("TEST:", dual_energy_source, dual_fast_shutter, source_selector) + ew4000 = I09VGScientaEW4000( + "TEST:", dual_energy_source, dual_fast_shutter, source_selector + ) return ew4000 @pytest.fixture -def specs_2dcmos(single_energy_source: EnergySource) -> Specs2DCMOS: +def specs150(single_energy_source: EnergySource) -> B07BSpecs150: with init_devices(mock=True): - specs_2dcmos = Specs2DCMOS("TEST:", single_energy_source) - return specs_2dcmos + specs150 = B07BSpecs150("TEST:", single_energy_source) + return specs150 -@pytest.fixture(params=["ew4000", "specs_2dcmos"]) +@pytest.fixture(params=["ew4000", "specs150"]) def analyser_controller( - request: pytest.FixtureRequest, ew4000: EW4000, specs_2dcmos: Specs2DCMOS + request: pytest.FixtureRequest, + ew4000: I09VGScientaEW4000, + specs150: B07BSpecs150, ): - detectors = [ew4000, specs_2dcmos] + detectors = [ew4000, specs150] for detector in detectors: if detector.name == request.param: return detector._controller diff --git a/tests/devices/electron_analyser/base/test_base_detector.py b/tests/devices/electron_analyser/base/test_base_detector.py index bc802c026d..bcd9912057 100644 --- a/tests/devices/electron_analyser/base/test_base_detector.py +++ b/tests/devices/electron_analyser/base/test_base_detector.py @@ -9,8 +9,8 @@ assert_reading, ) -from dodal.devices.beamlines.b07.analyser import Specs2DCMOS -from dodal.devices.beamlines.i09.analyser import EW4000 +from dodal.devices.beamlines.b07.analyser import B07BSpecs150 +from dodal.devices.beamlines.i09.analyser import I09VGScientaEW4000 from dodal.devices.electron_analyser.base import ( DualEnergySource, EnergySource, @@ -29,26 +29,28 @@ def ew4000( dual_energy_source: DualEnergySource, dual_fast_shutter: DualFastShutter, source_selector: SourceSelector, -) -> EW4000: +) -> I09VGScientaEW4000: with init_devices(mock=True): - ew4000 = EW4000("TEST:", dual_energy_source, dual_fast_shutter, source_selector) + ew4000 = I09VGScientaEW4000( + "TEST:", dual_energy_source, dual_fast_shutter, source_selector + ) return ew4000 @pytest.fixture -def specs_2dcmos(single_energy_source: EnergySource) -> Specs2DCMOS: +def specs150(single_energy_source: EnergySource) -> B07BSpecs150: with init_devices(mock=True): - specs_2dcmos = Specs2DCMOS("TEST:", single_energy_source) + specs150 = B07BSpecs150("TEST:", single_energy_source) # Needed for specs so we don't get division by zero error. - set_mock_value(specs_2dcmos.driver.slices, 1) - return specs_2dcmos + set_mock_value(specs150.driver.slices, 1) + return specs150 -@pytest.fixture(params=["ew4000", "specs_2dcmos"]) +@pytest.fixture(params=["ew4000", "specs150"]) def sim_detector( - request: pytest.FixtureRequest, ew4000: EW4000, specs_2dcmos: Specs2DCMOS + request: pytest.FixtureRequest, ew4000: I09VGScientaEW4000, specs150: B07BSpecs150 ) -> GenericElectronAnalyserDetector: - detectors = [ew4000, specs_2dcmos] + detectors = [ew4000, specs150] for detector in detectors: if detector.name == request.param: return detector 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 4a27bbfbf7..aff192b972 100644 --- a/tests/devices/electron_analyser/base/test_base_driver_io.py +++ b/tests/devices/electron_analyser/base/test_base_driver_io.py @@ -4,12 +4,12 @@ from bluesky.utils import FailedStatus from ophyd_async.core import StrictEnum, init_devices -from dodal.devices.beamlines.b07.analyser import B07SpecsAnalyserDriverIO +from dodal.devices.beamlines.b07.analyser import B07BSpecsAnalyserDriverIO from dodal.devices.beamlines.i09.analyser import I09VGScientaAnalyserDriverIO from dodal.devices.electron_analyser.base import GenericAnalyserDriverIO -@pytest.fixture(params=[I09VGScientaAnalyserDriverIO, B07SpecsAnalyserDriverIO]) +@pytest.fixture(params=[I09VGScientaAnalyserDriverIO, B07BSpecsAnalyserDriverIO]) async def sim_driver(request: pytest.FixtureRequest) -> GenericAnalyserDriverIO: async with init_devices(mock=True): sim_driver = request.param(prefix="TEST:") diff --git a/tests/devices/electron_analyser/base/test_base_region.py b/tests/devices/electron_analyser/base/test_base_region.py index 198d6974d8..e6467e1e24 100644 --- a/tests/devices/electron_analyser/base/test_base_region.py +++ b/tests/devices/electron_analyser/base/test_base_region.py @@ -2,7 +2,7 @@ import pytest -from dodal.devices.beamlines.b07 import B07SpecsRegion +from dodal.devices.beamlines.b07 import B07BSpecsRegion from dodal.devices.beamlines.i09 import I09VGScientaRegion from dodal.devices.electron_analyser.base import ( AbstractBaseRegion, @@ -37,7 +37,7 @@ def expected_region_class( sequence: GenericSequence, ) -> type[AbstractBaseRegion]: if isinstance(sequence, SpecsSequence): - return B07SpecsRegion + return B07BSpecsRegion elif isinstance(sequence, VGScientaSequence): return I09VGScientaRegion raise TypeError(f"Unknown sequence type {type(sequence)}") diff --git a/tests/devices/electron_analyser/helper_util/sequence.py b/tests/devices/electron_analyser/helper_util/sequence.py index 0245c0435b..73a5e2aab8 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -1,7 +1,7 @@ from dodal.common.data_util import load_json_file_to_class from dodal.devices.beamlines.b07.analyser import ( - B07SpecsAnalyserDriverIO, - B07SpecsSequence, + B07BSpecsAnalyserDriverIO, + B07BSpecsSequence, ) from dodal.devices.beamlines.i09.analyser import ( I09VGScientaAnalyserDriverIO, @@ -15,8 +15,8 @@ TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] -def b07_specs_test_sequence_loader() -> B07SpecsSequence: - return load_json_file_to_class(B07SpecsSequence, TEST_SPECS_SEQUENCE) +def b07_specs_test_sequence_loader() -> B07BSpecsSequence: + return load_json_file_to_class(B07BSpecsSequence, TEST_SPECS_SEQUENCE) def i09_vgscienta_test_sequence_loader() -> I09VGScientaSequence: @@ -25,6 +25,6 @@ def i09_vgscienta_test_sequence_loader() -> I09VGScientaSequence: # Map to know what function to load in sequence an analyser driver should use. DRIVER_TO_TEST_SEQUENCE = { - B07SpecsAnalyserDriverIO: b07_specs_test_sequence_loader(), + B07BSpecsAnalyserDriverIO: b07_specs_test_sequence_loader(), I09VGScientaAnalyserDriverIO: i09_vgscienta_test_sequence_loader(), } 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 f4e6cba04a..223cf2434f 100644 --- a/tests/devices/electron_analyser/specs/test_specs_driver_io.py +++ b/tests/devices/electron_analyser/specs/test_specs_driver_io.py @@ -13,7 +13,7 @@ ) from dodal.devices.beamlines.b07 import LensMode -from dodal.devices.beamlines.b07.analyser import B07SpecsAnalyserDriverIO +from dodal.devices.beamlines.b07.analyser import B07BSpecsAnalyserDriverIO 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 @@ -28,9 +28,9 @@ @pytest.fixture -async def sim_driver() -> B07SpecsAnalyserDriverIO: +async def sim_driver() -> B07BSpecsAnalyserDriverIO: async with init_devices(mock=True): - sim_driver = B07SpecsAnalyserDriverIO(prefix="TEST:") + sim_driver = B07BSpecsAnalyserDriverIO(prefix="TEST:") return sim_driver diff --git a/tests/devices/electron_analyser/specs/test_specs_region.py b/tests/devices/electron_analyser/specs/test_specs_region.py index 34e6656f7e..bb3617e661 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -2,7 +2,7 @@ import pytest -from dodal.devices.beamlines.b07 import B07SpecsSequence, LensMode +from dodal.devices.beamlines.b07 import B07BSpecsSequence, 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 @@ -14,7 +14,7 @@ @pytest.fixture -def sequence() -> B07SpecsSequence: +def sequence() -> B07BSpecsSequence: return b07_specs_test_sequence_loader() @@ -82,7 +82,7 @@ def expected_region_values() -> list[dict[str, Any]]: def test_sequence_get_expected_enabled_region_names( - sequence: B07SpecsSequence, + sequence: B07BSpecsSequence, expected_enabled_region_names: list[str], ) -> None: assert sequence.get_enabled_region_names() == expected_enabled_region_names @@ -91,7 +91,7 @@ def test_sequence_get_expected_enabled_region_names( def test_file_loads_into_class_with_expected_values( - sequence: B07SpecsSequence, + sequence: B07BSpecsSequence, expected_region_values: list[dict[str, Any]], ) -> None: assert len(sequence.regions) == len(expected_region_values) From b3582661bfb3dd9f1a5d7411f5c701e7cb9e3408 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Fri, 13 Feb 2026 16:14:11 +0000 Subject: [PATCH 18/22] Remove temp drv variable from analyser --- src/dodal/devices/beamlines/b07/analyser.py | 5 +++-- src/dodal/devices/beamlines/b07_1/analyser.py | 5 +++-- src/dodal/devices/beamlines/i09/analyser.py | 6 ++++-- src/dodal/devices/beamlines/i09_1/analyser.py | 5 +++-- src/dodal/devices/beamlines/p60/analyser.py | 3 +-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/dodal/devices/beamlines/b07/analyser.py b/src/dodal/devices/beamlines/b07/analyser.py index 6b5ba860e4..93dbc5457f 100644 --- a/src/dodal/devices/beamlines/b07/analyser.py +++ b/src/dodal/devices/beamlines/b07/analyser.py @@ -36,6 +36,7 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - drv = B07BSpecsAnalyserDriverIO(prefix) - controller = B07BElectronAnalyserController(drv, energy_source, shutter) + controller = B07BElectronAnalyserController( + B07BSpecsAnalyserDriverIO(prefix), energy_source, shutter + ) super().__init__(controller, name) diff --git a/src/dodal/devices/beamlines/b07_1/analyser.py b/src/dodal/devices/beamlines/b07_1/analyser.py index e0436404c4..bf36c0b9e8 100644 --- a/src/dodal/devices/beamlines/b07_1/analyser.py +++ b/src/dodal/devices/beamlines/b07_1/analyser.py @@ -36,6 +36,7 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - drv = B07CSpecsAnalyserDriverIO(prefix) - controller = B07CElectronAnalyserController(drv, energy_source, shutter) + controller = B07CElectronAnalyserController( + B07CSpecsAnalyserDriverIO(prefix), energy_source, shutter + ) super().__init__(controller, name) diff --git a/src/dodal/devices/beamlines/i09/analyser.py b/src/dodal/devices/beamlines/i09/analyser.py index 689fe01728..748933c24d 100644 --- a/src/dodal/devices/beamlines/i09/analyser.py +++ b/src/dodal/devices/beamlines/i09/analyser.py @@ -51,8 +51,10 @@ def __init__( source_selector: SourceSelector, name: str = "", ): - drv = I09VGScientaAnalyserDriverIO(prefix) controller = I09ElectronAnalyserController( - drv, dual_energy_source, dual_fast_shutter, source_selector + I09VGScientaAnalyserDriverIO(prefix), + dual_energy_source, + dual_fast_shutter, + source_selector, ) super().__init__(controller, name) diff --git a/src/dodal/devices/beamlines/i09_1/analyser.py b/src/dodal/devices/beamlines/i09_1/analyser.py index bbbafb4c22..bc7af96ae8 100644 --- a/src/dodal/devices/beamlines/i09_1/analyser.py +++ b/src/dodal/devices/beamlines/i09_1/analyser.py @@ -35,6 +35,7 @@ def __init__( shutter: FastShutter | None = None, name: str = "", ): - drv = I091SpecsAnalyserDriverIO(prefix) - controller = I091ElectronAnalyserController(drv, energy_source, shutter) + controller = I091ElectronAnalyserController( + I091SpecsAnalyserDriverIO(prefix), energy_source, shutter + ) super().__init__(controller, name) diff --git a/src/dodal/devices/beamlines/p60/analyser.py b/src/dodal/devices/beamlines/p60/analyser.py index e656becf99..a459a76c98 100644 --- a/src/dodal/devices/beamlines/p60/analyser.py +++ b/src/dodal/devices/beamlines/p60/analyser.py @@ -47,9 +47,8 @@ def __init__( energy_source: AbstractEnergySource, name: str = "", ): - drv = P60VGScientaAnalyserDriverIO(prefix) controller = P60ElectronAnalyserController( - drv, + P60VGScientaAnalyserDriverIO(prefix), energy_source=energy_source, shutter=None, source_selector=None, From 59d30c8d3a978abfe7c8048d910de859702c6943 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 17 Feb 2026 11:42:38 +0000 Subject: [PATCH 19/22] ElectronAnalyser no longer depends directly on sequence file path to work with BlueAPI --- .../electron_analyser/base/base_detector.py | 44 +++--- .../electron_analyser/specs/specs_detector.py | 16 +-- .../vgscienta/vgscienta_detector.py | 13 +- .../testing/electron_analyser/__init__.py | 6 - .../electron_analyser/device_factory.py | 57 -------- .../base/test_base_controller.py | 107 ++------------- .../base/test_base_detector.py | 71 +++------- .../base/test_base_driver_io.py | 25 +--- .../base/test_base_region.py | 5 +- tests/devices/electron_analyser/conftest.py | 126 ++++++++++++------ .../helper_util/assert_func.py | 2 +- .../electron_analyser/helper_util/sequence.py | 41 ++++-- .../specs/test_specs_detector.py | 27 +--- .../specs/test_specs_driver_io.py | 18 +-- .../specs/test_specs_region.py | 11 +- .../vgscienta/test_vgscienta_detector.py | 24 +--- .../vgscienta/test_vgscienta_driver_io.py | 19 +-- .../vgscienta/test_vgsicenta_region.py | 6 +- 18 files changed, 212 insertions(+), 406 deletions(-) delete mode 100644 src/dodal/testing/electron_analyser/__init__.py delete mode 100644 src/dodal/testing/electron_analyser/device_factory.py diff --git a/src/dodal/devices/electron_analyser/base/base_detector.py b/src/dodal/devices/electron_analyser/base/base_detector.py index 320acd9735..8fcab34fb6 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 fb54f47c49..6d8eacaf51 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 6e508fec5f..29626b2d35 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 dffbe8d492..0000000000 --- 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 961be125b2..0000000000 --- a/src/dodal/testing/electron_analyser/device_factory.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Any, get_args, get_origin - -from dodal.devices.electron_analyser.base.base_detector import TElectronAnalyserDetector -from dodal.devices.electron_analyser.base.base_driver_io import ( - TAbstractAnalyserDriverIO, -) -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, - VGScientaDetector, -) - - -def create_driver( - driver_class: type[TAbstractAnalyserDriverIO], - **kwargs: Any, -) -> TAbstractAnalyserDriverIO: - """Helper function that helps to reduce the code to setup an analyser driver. The - parameters used for the enum types are taken directly from the subscripts of the - class so the user only needs to provide it in one place. - - Args: - driver_class: The class for the driver which must include the enums in the - subscript, for example MyDriverClass[MyLensMode, ...]. - kwargs: Additional key worded arguments that the driver needs for initalisation. - """ - parameters = { - "lens_mode_type": get_args(driver_class)[0], - "psu_mode_type": get_args(driver_class)[1], - } - if get_origin(driver_class) is VGScientaAnalyserDriverIO: - parameters["pass_energy_type"] = get_args(driver_class)[2] - - return driver_class(**(parameters | kwargs)) - - -def create_detector( - detector_class: type[TElectronAnalyserDetector], - **kwargs: Any, -) -> TElectronAnalyserDetector: - """Helper function that helps to reduce the code to setup an analyser detector. The - parameters used for the enum types are taken directly from the subscripts of the - class so the user only needs to provide it in one place. - - Args: - detector_class: The class for the detector which must include the enums in the - subscript, for example MyDetectorClass[MyLensMode, ...]. - kwargs: Additional key worded arguments that the detector needs for - initalisation. - """ - parameters = { - "lens_mode_type": get_args(detector_class)[0], - "psu_mode_type": get_args(detector_class)[1], - } - if get_origin(detector_class) is VGScientaDetector: - parameters["pass_energy_type"] = get_args(detector_class)[2] - - return detector_class(**(parameters | kwargs)) diff --git a/tests/devices/electron_analyser/base/test_base_controller.py b/tests/devices/electron_analyser/base/test_base_controller.py index aa899c66fc..62a353c5d5 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 11f76cfd62..2b0ffee847 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 d4e56e0c13..3a61bccbfc 100644 --- a/tests/devices/electron_analyser/base/test_base_driver_io.py +++ b/tests/devices/electron_analyser/base/test_base_driver_io.py @@ -2,31 +2,14 @@ from bluesky import plan_stubs as bps from bluesky.run_engine import RunEngine from bluesky.utils import FailedStatus -from ophyd_async.core import StrictEnum, init_devices +from ophyd_async.core import StrictEnum -from dodal.devices.beamlines import b07, i09 from dodal.devices.electron_analyser.base import GenericAnalyserDriverIO -from dodal.devices.electron_analyser.specs import ( - SpecsAnalyserDriverIO, -) -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, -) -from dodal.testing.electron_analyser import create_driver -@pytest.fixture( - params=[ - VGScientaAnalyserDriverIO[i09.LensMode, i09.PsuMode, i09.PassEnergy], - SpecsAnalyserDriverIO[b07.LensMode, b07.PsuMode], - ] -) -async def sim_driver( - request: pytest.FixtureRequest, -) -> GenericAnalyserDriverIO: - async with init_devices(mock=True): - sim_driver = create_driver(request.param, prefix="TEST:") - return sim_driver +@pytest.fixture +async def sim_driver(sim_detector) -> GenericAnalyserDriverIO: + return sim_detector.driver def test_driver_throws_error_with_wrong_lens_mode( diff --git a/tests/devices/electron_analyser/base/test_base_region.py b/tests/devices/electron_analyser/base/test_base_region.py index 50615fbba0..f52151f2dc 100644 --- a/tests/devices/electron_analyser/base/test_base_region.py +++ b/tests/devices/electron_analyser/base/test_base_region.py @@ -2,7 +2,6 @@ import pytest -from dodal.common.data_util import load_json_file_to_class from dodal.devices.beamlines import b07, i09 from dodal.devices.electron_analyser.base import ( AbstractBaseRegion, @@ -28,10 +27,10 @@ params=[ SpecsSequence[b07.LensMode, b07.PsuMode], VGScientaSequence[i09.LensMode, i09.PsuMode, i09.PassEnergy], - ] + ], ) def sequence(request: pytest.FixtureRequest) -> GenericSequence: - return load_json_file_to_class(request.param, get_test_sequence(request.param)) + return get_test_sequence(request.param) @pytest.fixture diff --git a/tests/devices/electron_analyser/conftest.py b/tests/devices/electron_analyser/conftest.py index 7007cf5526..9d0f9c5d50 100644 --- a/tests/devices/electron_analyser/conftest.py +++ b/tests/devices/electron_analyser/conftest.py @@ -1,8 +1,10 @@ from typing import Any import pytest -from ophyd_async.core import init_devices +from ophyd_async.core import InOut, init_devices, set_mock_value +import dodal.devices.beamlines.b07 as b07 +import dodal.devices.beamlines.i09 as i09 from dodal.devices.beamlines.i09 import Grating from dodal.devices.common_dcm import ( DoubleCrystalMonochromatorWithDSpacing, @@ -10,26 +12,17 @@ StationaryCrystal, ) from dodal.devices.electron_analyser.base import ( - AbstractAnalyserDriverIO, AbstractBaseRegion, AbstractBaseSequence, DualEnergySource, - ElectronAnalyserController, - ElectronAnalyserDetector, EnergySource, - TAbstractBaseSequence, -) -from dodal.devices.electron_analyser.specs import ( - SpecsAnalyserDriverIO, - SpecsSequence, -) -from dodal.devices.electron_analyser.vgscienta import ( - VGScientaAnalyserDriverIO, - VGScientaSequence, + GenericElectronAnalyserDetector, ) +from dodal.devices.electron_analyser.specs import SpecsDetector +from dodal.devices.electron_analyser.vgscienta import VGScientaDetector +from dodal.devices.fast_shutter import DualFastShutter, GenericFastShutter from dodal.devices.pgm import PlaneGratingMonochromator from dodal.devices.selectable_source import SourceSelector -from tests.devices.electron_analyser.helper_util import get_test_sequence @pytest.fixture @@ -71,34 +64,91 @@ async def dual_energy_source(source_selector: SourceSelector) -> DualEnergySourc @pytest.fixture -def sequence_class( - sim_driver: AbstractAnalyserDriverIO, -) -> type[AbstractBaseSequence]: - # We must include the pass energy, lens and psu mode types here, otherwise the - # sequence file can't be loaded as pydantic won't be able to resolve the enums. - if isinstance(sim_driver, VGScientaAnalyserDriverIO): - return VGScientaSequence[ - sim_driver.lens_mode_type, - sim_driver.psu_mode_type, - sim_driver.pass_energy_type, - ] - elif isinstance(sim_driver, SpecsAnalyserDriverIO): - return SpecsSequence[sim_driver.lens_mode_type, sim_driver.psu_mode_type] - raise ValueError("class " + str(sim_driver) + " not recognised") +def shutter1() -> GenericFastShutter[InOut]: + with init_devices(mock=True): + shutter1 = GenericFastShutter[InOut]( + pv="TEST:", + open_state=InOut.OUT, + close_state=InOut.IN, + ) + return shutter1 + + +@pytest.fixture +def shutter2() -> GenericFastShutter[InOut]: + with init_devices(mock=True): + shutter2 = GenericFastShutter[InOut]( + pv="TEST:", + open_state=InOut.OUT, + close_state=InOut.IN, + ) + return shutter2 @pytest.fixture -def sequence( - sim_driver: AbstractAnalyserDriverIO, - sequence_class: type[TAbstractBaseSequence], +def dual_fast_shutter( + shutter1: GenericFastShutter[InOut], + shutter2: GenericFastShutter[InOut], + source_selector: SourceSelector, +) -> DualFastShutter[InOut]: + with init_devices(mock=True): + dual_fast_shutter = DualFastShutter[InOut]( + shutter1, + shutter2, + source_selector.selected_source, + ) + return dual_fast_shutter + + +@pytest.fixture +async def b07b_specs150( single_energy_source: EnergySource, -) -> AbstractBaseSequence: - controller = ElectronAnalyserController(sim_driver, single_energy_source) - det = ElectronAnalyserDetector( - sequence_class=sequence_class, - controller=controller, - ) - return det.load_sequence(get_test_sequence(type(sim_driver))) + shutter1: GenericFastShutter, +) -> SpecsDetector[b07.LensMode, b07.PsuMode]: + with init_devices(mock=True): + b07b_specs150 = SpecsDetector[b07.LensMode, b07.PsuMode]( + prefix="TEST:", + lens_mode_type=b07.LensMode, + psu_mode_type=b07.PsuMode, + energy_source=single_energy_source, + shutter=shutter1, + ) + # Needed for specs so we don't get division by zero error. + set_mock_value(b07b_specs150.driver.slices, 1) + return b07b_specs150 + + +@pytest.fixture +async def ew4000( + dual_energy_source: DualEnergySource, + dual_fast_shutter: DualFastShutter, + source_selector: SourceSelector, +) -> VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy]: + with init_devices(mock=True): + ew4000 = VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy]( + prefix="TEST:", + lens_mode_type=i09.LensMode, + psu_mode_type=i09.PsuMode, + pass_energy_type=i09.PassEnergy, + energy_source=dual_energy_source, + shutter=dual_fast_shutter, + source_selector=source_selector, + ) + return ew4000 + + +@pytest.fixture(params=["ew4000", "b07b_specs150"]) +def sim_detector( + request: pytest.FixtureRequest, + ew4000: VGScientaDetector, + b07b_specs150: SpecsDetector, +) -> GenericElectronAnalyserDetector: + detectors = [ew4000, b07b_specs150] + for detector in detectors: + if detector.name == request.param: + return detector + + raise ValueError(f"Detector with name '{request.param}' not found") @pytest.fixture diff --git a/tests/devices/electron_analyser/helper_util/assert_func.py b/tests/devices/electron_analyser/helper_util/assert_func.py index 01433c8712..4e20df0bcf 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 85d47a8f56..790b852cdc 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -1,3 +1,6 @@ +from dodal.common.data_util import load_json_file_to_class +from dodal.devices.beamlines import b07, i09 +from dodal.devices.electron_analyser.base import GenericSequence from dodal.devices.electron_analyser.specs import ( SpecsAnalyserDriverIO, SpecsDetector, @@ -13,22 +16,38 @@ TEST_VGSCIENTA_SEQUENCE, ) +TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] + + +def b07_specs_test_sequence_loader() -> SpecsSequence[b07.LensMode, b07.PsuMode]: + return load_json_file_to_class( + SpecsSequence[b07.LensMode, b07.PsuMode], TEST_SPECS_SEQUENCE + ) + + +def i09_vgscienta_test_sequence_loader() -> VGScientaSequence[ + i09.LensMode, i09.PsuMode, i09.PassEnergy +]: + return load_json_file_to_class( + VGScientaSequence[i09.LensMode, i09.PsuMode, i09.PassEnergy], + TEST_VGSCIENTA_SEQUENCE, + ) + + +# Map to know what function to load in sequence an analyser driver should use. TEST_SEQUENCES = { - VGScientaSequence: TEST_VGSCIENTA_SEQUENCE, - VGScientaDetector: TEST_VGSCIENTA_SEQUENCE, - VGScientaAnalyserDriverIO: TEST_VGSCIENTA_SEQUENCE, - SpecsSequence: TEST_SPECS_SEQUENCE, - SpecsDetector: TEST_SPECS_SEQUENCE, - SpecsAnalyserDriverIO: TEST_SPECS_SEQUENCE, + SpecsDetector: b07_specs_test_sequence_loader, + SpecsAnalyserDriverIO: b07_specs_test_sequence_loader, + SpecsSequence: b07_specs_test_sequence_loader, + VGScientaDetector: i09_vgscienta_test_sequence_loader, + VGScientaAnalyserDriverIO: i09_vgscienta_test_sequence_loader, + VGScientaSequence: i09_vgscienta_test_sequence_loader, } -def get_test_sequence(key: type) -> str: +def get_test_sequence(key: type) -> GenericSequence: for cls in key.__mro__: # Check for unscripted class only if cls in TEST_SEQUENCES: - return TEST_SEQUENCES[cls] + return TEST_SEQUENCES[cls]() raise KeyError(f"Found no match with type {key}") - - -TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] diff --git a/tests/devices/electron_analyser/specs/test_specs_detector.py b/tests/devices/electron_analyser/specs/test_specs_detector.py index 9a1fd9c143..ac91086a57 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 65e6234816..842a2adfc6 100644 --- a/tests/devices/electron_analyser/specs/test_specs_driver_io.py +++ b/tests/devices/electron_analyser/specs/test_specs_driver_io.py @@ -4,7 +4,7 @@ import pytest from bluesky import plan_stubs as bps from bluesky.run_engine import RunEngine -from ophyd_async.core import get_mock_put, init_devices, set_mock_value +from ophyd_async.core import get_mock_put, set_mock_value from ophyd_async.testing import ( assert_configuration, assert_reading, @@ -20,20 +20,20 @@ SpecsAnalyserDriverIO, SpecsRegion, ) -from dodal.testing.electron_analyser import create_driver from tests.devices.electron_analyser.helper_util import ( TEST_SEQUENCE_REGION_NAMES, + get_test_sequence, ) @pytest.fixture -async def sim_driver() -> SpecsAnalyserDriverIO[LensMode, PsuMode]: - async with init_devices(mock=True): - sim_driver = create_driver( - SpecsAnalyserDriverIO[LensMode, PsuMode], - prefix="TEST:", - ) - return sim_driver +async def sim_driver(b07b_specs150) -> SpecsAnalyserDriverIO[LensMode, PsuMode]: + return b07b_specs150.driver + + +@pytest.fixture +def sequence(sim_driver: SpecsAnalyserDriverIO[LensMode, PsuMode]): + return get_test_sequence(type(sim_driver)) @pytest.mark.parametrize("region", TEST_SEQUENCE_REGION_NAMES, indirect=True) diff --git a/tests/devices/electron_analyser/specs/test_specs_region.py b/tests/devices/electron_analyser/specs/test_specs_region.py index 0ed8c13307..9371cc2162 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -2,13 +2,9 @@ import pytest -from dodal.common.data_util import load_json_file_to_class from dodal.devices.beamlines.b07 import LensMode, PsuMode from dodal.devices.electron_analyser.base import EnergyMode -from dodal.devices.electron_analyser.specs import ( - AcquisitionMode, - SpecsSequence, -) +from dodal.devices.electron_analyser.specs import AcquisitionMode, SpecsSequence from dodal.devices.selectable_source import SelectedSource from tests.devices.electron_analyser.helper_util import ( assert_region_has_expected_values, @@ -17,9 +13,8 @@ @pytest.fixture -def sequence() -> SpecsSequence[LensMode, PsuMode]: - seq = SpecsSequence[LensMode, PsuMode] - return load_json_file_to_class(seq, get_test_sequence(seq)) +def sequence(): + return get_test_sequence(SpecsSequence[LensMode, PsuMode]) @pytest.fixture diff --git a/tests/devices/electron_analyser/vgscienta/test_vgscienta_detector.py b/tests/devices/electron_analyser/vgscienta/test_vgscienta_detector.py index 4d7dadc549..48ac964f0f 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 a31afd82a7..f3284eda85 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py @@ -5,7 +5,7 @@ from bluesky import plan_stubs as bps from bluesky.run_engine import RunEngine from bluesky.utils import FailedStatus -from ophyd_async.core import StrictEnum, get_mock_put, init_devices, set_mock_value +from ophyd_async.core import StrictEnum, get_mock_put, set_mock_value from ophyd_async.testing import ( assert_configuration, assert_reading, @@ -19,19 +19,22 @@ VGScientaAnalyserDriverIO, VGScientaRegion, ) -from dodal.testing.electron_analyser import create_driver from tests.devices.electron_analyser.helper_util import ( TEST_SEQUENCE_REGION_NAMES, + get_test_sequence, ) @pytest.fixture -async def sim_driver() -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]: - async with init_devices(mock=True): - sim_driver = create_driver( - VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy], prefix="TEST:" - ) - return sim_driver +async def sim_driver( + ew4000, +) -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]: + return ew4000.driver + + +@pytest.fixture +def sequence(sim_driver: VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]): + return get_test_sequence(type(sim_driver)) @pytest.mark.parametrize("region", TEST_SEQUENCE_REGION_NAMES, indirect=True) diff --git a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py index 81ea9d03ce..7a8c5c1fc7 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py @@ -2,7 +2,6 @@ import pytest -from dodal.common.data_util import load_json_file_to_class from dodal.devices.beamlines.i09 import LensMode, PassEnergy, PsuMode from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.vgscienta import ( @@ -19,9 +18,8 @@ @pytest.fixture -def sequence() -> VGScientaSequence[LensMode, PsuMode, PassEnergy]: - seq = VGScientaSequence[LensMode, PsuMode, PassEnergy] - return load_json_file_to_class(seq, get_test_sequence(seq)) +def sequence(): + return get_test_sequence(VGScientaSequence[LensMode, PsuMode, PassEnergy]) @pytest.fixture From ef6b7d342f467c63418e5d54688f4caa96f8786a Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 17 Feb 2026 14:21:40 +0000 Subject: [PATCH 20/22] Fix tests --- tests/devices/electron_analyser/base/test_base_region.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/devices/electron_analyser/base/test_base_region.py b/tests/devices/electron_analyser/base/test_base_region.py index eb1388488e..a3c1a70705 100644 --- a/tests/devices/electron_analyser/base/test_base_region.py +++ b/tests/devices/electron_analyser/base/test_base_region.py @@ -2,8 +2,8 @@ import pytest -from dodal.devices.beamlines.b07 import B07BSpecsRegion -from dodal.devices.beamlines.i09 import I09VGScientaRegion +from dodal.devices.beamlines.b07 import B07BSpecsRegion, B07BSpecsSequence +from dodal.devices.beamlines.i09 import I09VGScientaRegion, I09VGScientaSequence from dodal.devices.electron_analyser.base import ( AbstractBaseRegion, EnergyMode, @@ -21,7 +21,7 @@ ) -@pytest.fixture(params=[B07BSpecsRegion, I09VGScientaRegion]) +@pytest.fixture(params=[B07BSpecsSequence, I09VGScientaSequence]) def sequence(request: pytest.FixtureRequest) -> GenericSequence: return TEST_SEQUENCES[request.param]() From 30750790989f372ff931dba8b82cab049c975ea6 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 17 Feb 2026 14:31:16 +0000 Subject: [PATCH 21/22] Improve type checking --- .../electron_analyser/base/test_base_driver_io.py | 9 +++++++-- tests/devices/electron_analyser/helper_util/sequence.py | 3 +-- .../electron_analyser/specs/test_specs_driver_io.py | 5 ++++- .../devices/electron_analyser/specs/test_specs_region.py | 2 +- .../vgscienta/test_vgscienta_driver_io.py | 3 ++- .../electron_analyser/vgscienta/test_vgsicenta_region.py | 2 +- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/devices/electron_analyser/base/test_base_driver_io.py b/tests/devices/electron_analyser/base/test_base_driver_io.py index 3a61bccbfc..cdbe6a717c 100644 --- a/tests/devices/electron_analyser/base/test_base_driver_io.py +++ b/tests/devices/electron_analyser/base/test_base_driver_io.py @@ -4,11 +4,16 @@ from bluesky.utils import FailedStatus from ophyd_async.core import StrictEnum -from dodal.devices.electron_analyser.base import GenericAnalyserDriverIO +from dodal.devices.electron_analyser.base import ( + GenericAnalyserDriverIO, + GenericElectronAnalyserDetector, +) @pytest.fixture -async def sim_driver(sim_detector) -> GenericAnalyserDriverIO: +async def sim_driver( + sim_detector: GenericElectronAnalyserDetector, +) -> GenericAnalyserDriverIO: return sim_detector.driver diff --git a/tests/devices/electron_analyser/helper_util/sequence.py b/tests/devices/electron_analyser/helper_util/sequence.py index 790b852cdc..38b98ff1a8 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -1,6 +1,5 @@ from dodal.common.data_util import load_json_file_to_class from dodal.devices.beamlines import b07, i09 -from dodal.devices.electron_analyser.base import GenericSequence from dodal.devices.electron_analyser.specs import ( SpecsAnalyserDriverIO, SpecsDetector, @@ -45,7 +44,7 @@ def i09_vgscienta_test_sequence_loader() -> VGScientaSequence[ } -def get_test_sequence(key: type) -> GenericSequence: +def get_test_sequence(key: type): for cls in key.__mro__: # Check for unscripted class only if cls in TEST_SEQUENCES: diff --git a/tests/devices/electron_analyser/specs/test_specs_driver_io.py b/tests/devices/electron_analyser/specs/test_specs_driver_io.py index 842a2adfc6..ded61535d6 100644 --- a/tests/devices/electron_analyser/specs/test_specs_driver_io.py +++ b/tests/devices/electron_analyser/specs/test_specs_driver_io.py @@ -18,6 +18,7 @@ from dodal.devices.electron_analyser.specs import ( AcquisitionMode, SpecsAnalyserDriverIO, + SpecsDetector, SpecsRegion, ) from tests.devices.electron_analyser.helper_util import ( @@ -27,7 +28,9 @@ @pytest.fixture -async def sim_driver(b07b_specs150) -> SpecsAnalyserDriverIO[LensMode, PsuMode]: +async def sim_driver( + b07b_specs150: SpecsDetector[LensMode, PsuMode], +) -> SpecsAnalyserDriverIO[LensMode, PsuMode]: return b07b_specs150.driver diff --git a/tests/devices/electron_analyser/specs/test_specs_region.py b/tests/devices/electron_analyser/specs/test_specs_region.py index 9371cc2162..ebdbd075fc 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -13,7 +13,7 @@ @pytest.fixture -def sequence(): +def sequence() -> SpecsSequence[LensMode, PsuMode]: return get_test_sequence(SpecsSequence[LensMode, PsuMode]) diff --git a/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py b/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py index f3284eda85..17e8b4fbd1 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgscienta_driver_io.py @@ -17,6 +17,7 @@ from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.vgscienta import ( VGScientaAnalyserDriverIO, + VGScientaDetector, VGScientaRegion, ) from tests.devices.electron_analyser.helper_util import ( @@ -27,7 +28,7 @@ @pytest.fixture async def sim_driver( - ew4000, + ew4000: VGScientaDetector, ) -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]: return ew4000.driver diff --git a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py index 7a8c5c1fc7..0e4fe4f88b 100644 --- a/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py +++ b/tests/devices/electron_analyser/vgscienta/test_vgsicenta_region.py @@ -18,7 +18,7 @@ @pytest.fixture -def sequence(): +def sequence() -> VGScientaSequence[LensMode, PsuMode, PassEnergy]: return get_test_sequence(VGScientaSequence[LensMode, PsuMode, PassEnergy]) From 9c2887e811c0a10c56b6e340cea7c85d314a6248 Mon Sep 17 00:00:00 2001 From: Oli Wenman Date: Tue, 17 Feb 2026 14:45:12 +0000 Subject: [PATCH 22/22] Move PsuMode to B07 shared, improve type checking --- src/dodal/beamlines/b07.py | 2 +- src/dodal/beamlines/b07_1.py | 2 +- src/dodal/devices/beamlines/b07/__init__.py | 4 ++-- src/dodal/devices/beamlines/b07/enums.py | 12 ------------ .../devices/beamlines/b07_shared/__init__.py | 3 +++ src/dodal/devices/beamlines/b07_shared/enums.py | 13 +++++++++++++ .../electron_analyser/base/test_base_region.py | 6 +++--- tests/devices/electron_analyser/conftest.py | 15 +++++++-------- .../electron_analyser/helper_util/sequence.py | 6 +++--- .../specs/test_specs_driver_io.py | 3 ++- .../electron_analyser/specs/test_specs_region.py | 3 ++- 11 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 src/dodal/devices/beamlines/b07_shared/__init__.py create mode 100644 src/dodal/devices/beamlines/b07_shared/enums.py diff --git a/src/dodal/beamlines/b07.py b/src/dodal/beamlines/b07.py index 87cdf54b7b..3f712cdd50 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 b2c03102f4..a80bb89afd 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 cf3fe955bb..d1583c2a25 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 58c826bb37..2e27db999d 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 0000000000..9fa24dbb0f --- /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 0000000000..0c6bbba3a8 --- /dev/null +++ b/src/dodal/devices/beamlines/b07_shared/enums.py @@ -0,0 +1,13 @@ +from ophyd_async.core import SupersetEnum + + +class PsuMode(SupersetEnum): + V3500 = "3.5kV" + V1500 = "1.5kV" + V400 = "400V" + V100 = "100V" + V10 = "10V" + # This is connected to the device separately and will only have "Not connected" as + # option if disconnected. Once it is connected, "Not connected" is replaced with the + # options above. This is also why this must be a SupersetEnum. + NOT_CONNECTED = "Not connected" diff --git a/tests/devices/electron_analyser/base/test_base_region.py b/tests/devices/electron_analyser/base/test_base_region.py index f52151f2dc..9d53f4a5e5 100644 --- a/tests/devices/electron_analyser/base/test_base_region.py +++ b/tests/devices/electron_analyser/base/test_base_region.py @@ -2,7 +2,7 @@ import pytest -from dodal.devices.beamlines import b07, i09 +from dodal.devices.beamlines import b07, b07_shared, i09 from dodal.devices.electron_analyser.base import ( AbstractBaseRegion, EnergyMode, @@ -25,7 +25,7 @@ @pytest.fixture( params=[ - SpecsSequence[b07.LensMode, b07.PsuMode], + SpecsSequence[b07.LensMode, b07_shared.PsuMode], VGScientaSequence[i09.LensMode, i09.PsuMode, i09.PassEnergy], ], ) @@ -38,7 +38,7 @@ def expected_region_class( sequence: GenericSequence, ) -> type[AbstractBaseRegion]: if isinstance(sequence, SpecsSequence): - return SpecsRegion[b07.LensMode, b07.PsuMode] + return SpecsRegion[b07.LensMode, b07_shared.PsuMode] elif isinstance(sequence, VGScientaSequence): return VGScientaRegion[i09.LensMode, i09.PassEnergy] raise TypeError(f"Unknown sequence type {type(sequence)}") diff --git a/tests/devices/electron_analyser/conftest.py b/tests/devices/electron_analyser/conftest.py index 9d0f9c5d50..2cd1bb29e2 100644 --- a/tests/devices/electron_analyser/conftest.py +++ b/tests/devices/electron_analyser/conftest.py @@ -3,8 +3,7 @@ import pytest from ophyd_async.core import InOut, init_devices, set_mock_value -import dodal.devices.beamlines.b07 as b07 -import dodal.devices.beamlines.i09 as i09 +from dodal.devices.beamlines import b07, b07_shared, i09 from dodal.devices.beamlines.i09 import Grating from dodal.devices.common_dcm import ( DoubleCrystalMonochromatorWithDSpacing, @@ -104,12 +103,12 @@ def dual_fast_shutter( async def b07b_specs150( single_energy_source: EnergySource, shutter1: GenericFastShutter, -) -> SpecsDetector[b07.LensMode, b07.PsuMode]: +) -> SpecsDetector[b07.LensMode, b07_shared.PsuMode]: with init_devices(mock=True): - b07b_specs150 = SpecsDetector[b07.LensMode, b07.PsuMode]( + b07b_specs150 = SpecsDetector[b07.LensMode, b07_shared.PsuMode]( prefix="TEST:", lens_mode_type=b07.LensMode, - psu_mode_type=b07.PsuMode, + psu_mode_type=b07_shared.PsuMode, energy_source=single_energy_source, shutter=shutter1, ) @@ -140,8 +139,8 @@ async def ew4000( @pytest.fixture(params=["ew4000", "b07b_specs150"]) def sim_detector( request: pytest.FixtureRequest, - ew4000: VGScientaDetector, - b07b_specs150: SpecsDetector, + ew4000: VGScientaDetector[i09.LensMode, i09.PsuMode, i09.PassEnergy], + b07b_specs150: SpecsDetector[b07.LensMode, b07_shared.PsuMode], ) -> GenericElectronAnalyserDetector: detectors = [ew4000, b07b_specs150] for detector in detectors: @@ -154,7 +153,7 @@ def sim_detector( @pytest.fixture def region( request: pytest.FixtureRequest, - sequence: AbstractBaseSequence, + sequence: AbstractBaseSequence[AbstractBaseRegion], ) -> AbstractBaseRegion: region = sequence.get_region_by_name(request.param) if region is None: diff --git a/tests/devices/electron_analyser/helper_util/sequence.py b/tests/devices/electron_analyser/helper_util/sequence.py index 38b98ff1a8..67bf149676 100644 --- a/tests/devices/electron_analyser/helper_util/sequence.py +++ b/tests/devices/electron_analyser/helper_util/sequence.py @@ -1,5 +1,5 @@ from dodal.common.data_util import load_json_file_to_class -from dodal.devices.beamlines import b07, i09 +from dodal.devices.beamlines import b07, b07_shared, i09 from dodal.devices.electron_analyser.specs import ( SpecsAnalyserDriverIO, SpecsDetector, @@ -18,9 +18,9 @@ TEST_SEQUENCE_REGION_NAMES = ["New_Region", "New_Region1", "New_Region2"] -def b07_specs_test_sequence_loader() -> SpecsSequence[b07.LensMode, b07.PsuMode]: +def b07_specs_test_sequence_loader() -> SpecsSequence[b07.LensMode, b07_shared.PsuMode]: return load_json_file_to_class( - SpecsSequence[b07.LensMode, b07.PsuMode], TEST_SPECS_SEQUENCE + SpecsSequence[b07.LensMode, b07_shared.PsuMode], TEST_SPECS_SEQUENCE ) diff --git a/tests/devices/electron_analyser/specs/test_specs_driver_io.py b/tests/devices/electron_analyser/specs/test_specs_driver_io.py index ded61535d6..69e3fc590d 100644 --- a/tests/devices/electron_analyser/specs/test_specs_driver_io.py +++ b/tests/devices/electron_analyser/specs/test_specs_driver_io.py @@ -12,7 +12,8 @@ partial_reading, ) -from dodal.devices.beamlines.b07 import LensMode, PsuMode +from dodal.devices.beamlines.b07 import LensMode +from dodal.devices.beamlines.b07_shared import PsuMode from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.base.base_enums import EnergyMode from dodal.devices.electron_analyser.specs import ( diff --git a/tests/devices/electron_analyser/specs/test_specs_region.py b/tests/devices/electron_analyser/specs/test_specs_region.py index ebdbd075fc..c26a1f27dc 100644 --- a/tests/devices/electron_analyser/specs/test_specs_region.py +++ b/tests/devices/electron_analyser/specs/test_specs_region.py @@ -2,7 +2,8 @@ import pytest -from dodal.devices.beamlines.b07 import LensMode, PsuMode +from dodal.devices.beamlines.b07 import LensMode +from dodal.devices.beamlines.b07_shared import PsuMode from dodal.devices.electron_analyser.base import EnergyMode from dodal.devices.electron_analyser.specs import AcquisitionMode, SpecsSequence from dodal.devices.selectable_source import SelectedSource