From 8bb9b9b77a1a95022fac5836d9f7d3e7bbb5da07 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 10:52:00 +0000 Subject: [PATCH 01/11] Convert b01_1 to new device manager --- src/dodal/beamlines/b01_1.py | 44 +++++++++++++++++------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/dodal/beamlines/b01_1.py b/src/dodal/beamlines/b01_1.py index 2194c27b1f..b4fd0b7d3c 100644 --- a/src/dodal/beamlines/b01_1.py +++ b/src/dodal/beamlines/b01_1.py @@ -1,17 +1,14 @@ +from functools import cache from pathlib import Path -from ophyd_async.core import StaticPathProvider, UUIDFilenameProvider +from ophyd_async.core import PathProvider, StaticPathProvider, UUIDFilenameProvider from ophyd_async.epics.adaravis import AravisDetector from ophyd_async.epics.adcore import NDROIStatIO from ophyd_async.fastcs.panda import HDFPanda -from dodal.common.beamlines.beamline_utils import ( - device_factory, - get_path_provider, - set_path_provider, -) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.common.beamlines.device_helpers import CAM_SUFFIX, HDF5_SUFFIX +from dodal.device_manager import DeviceManager from dodal.devices.motors import XYZStage from dodal.devices.synchrotron import Synchrotron from dodal.log import set_beamline as set_log_beamline @@ -32,16 +29,17 @@ https://argocd.diamond.ac.uk/applications?showFavorites=false&proj=&sync=&autoSync=&health=&namespace=&cluster=&labels= """ -# This should be removed when the DeviceManager is adopted -try: - get_path_provider() -except NameError: - # If one hasn't already been set, use a default to stop things crashing - set_path_provider(StaticPathProvider(UUIDFilenameProvider(), Path("/tmp"))) +devices = DeviceManager() -@device_factory() -def panda() -> HDFPanda: +@devices.fixture +@cache +def path_provider() -> PathProvider: + return StaticPathProvider(UUIDFilenameProvider(), Path("/tmp")) + + +@devices.factory() +def panda(path_provider: PathProvider) -> HDFPanda: """Provides triggering of the detectors. Returns: @@ -49,17 +47,17 @@ def panda() -> HDFPanda: """ return HDFPanda( f"{PREFIX.beamline_prefix}-MO-PPANDA-01:", - path_provider=get_path_provider(), + path_provider=path_provider, ) -@device_factory() +@devices.factory() def synchrotron() -> Synchrotron: return Synchrotron() -@device_factory() -def spectroscopy_detector() -> AravisDetector: +@devices.factory() +def spectroscopy_detector(path_provider: PathProvider) -> AravisDetector: """The Manta camera for the spectroscopy experiment. Looks at the spectroscopy screen and visualises light @@ -72,7 +70,7 @@ def spectroscopy_detector() -> AravisDetector: pv_prefix = f"{PREFIX.beamline_prefix}-DI-DCAM-02:" return AravisDetector( pv_prefix, - path_provider=get_path_provider(), + path_provider=path_provider, drv_suffix=CAM_SUFFIX, fileio_suffix=HDF5_SUFFIX, plugins={ @@ -81,8 +79,8 @@ def spectroscopy_detector() -> AravisDetector: ) -@device_factory() -def imaging_detector() -> AravisDetector: +@devices.factory() +def imaging_detector(path_provider: PathProvider) -> AravisDetector: """The Mako camera for the imaging experiment. Looks at the on-axis viewing screen. @@ -92,13 +90,13 @@ def imaging_detector() -> AravisDetector: """ return AravisDetector( f"{PREFIX.beamline_prefix}-DI-DCAM-01:", - path_provider=get_path_provider(), + path_provider=path_provider, drv_suffix=CAM_SUFFIX, fileio_suffix=HDF5_SUFFIX, ) -@device_factory() +@devices.factory() def sample_stage() -> XYZStage: """An XYZ stage holding the sample. From f5710b92e76f50488e79a2abc016e26e3fa3d4f5 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 11:26:04 +0000 Subject: [PATCH 02/11] Move b16 to new device factory --- src/dodal/beamlines/b16.py | 48 +++++++++++++-------- src/dodal/devices/beamlines/b16/__init__.py | 0 src/dodal/devices/beamlines/b16/detector.py | 24 ----------- tests/beamlines/test_b16.py | 34 +++++---------- 4 files changed, 40 insertions(+), 66 deletions(-) delete mode 100644 src/dodal/devices/beamlines/b16/__init__.py delete mode 100644 src/dodal/devices/beamlines/b16/detector.py diff --git a/src/dodal/beamlines/b16.py b/src/dodal/beamlines/b16.py index a5857fb2b0..65bb8790f6 100644 --- a/src/dodal/beamlines/b16.py +++ b/src/dodal/beamlines/b16.py @@ -1,19 +1,22 @@ +from functools import cache from pathlib import Path +from ophyd_async.core import PathProvider from ophyd_async.epics.adcore import ( + ADBaseIO, + ADTIFFWriter, AreaDetector, ) from ophyd_async.epics.motor import Motor -from dodal.common.beamlines.beamline_utils import ( - device_factory, - set_path_provider, -) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline -from dodal.common.visit import RemoteDirectoryServiceClient, StaticVisitPathProvider -from dodal.devices.beamlines.b16.detector import ( - software_triggered_tiff_area_detector, +from dodal.common.beamlines.device_helpers import CAM_SUFFIX, TIFF_SUFFIX +from dodal.common.visit import ( + RemoteDirectoryServiceClient, + StaticVisitPathProvider, ) +from dodal.device_manager import DeviceManager +from dodal.devices.controllers import ConstantDeadTimeController from dodal.devices.motors import XYZStage from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name @@ -23,42 +26,51 @@ set_log_beamline(BL) set_utils_beamline(BL) -set_path_provider( - StaticVisitPathProvider( +devices = DeviceManager() + + +@devices.fixture +@cache +def path_provider() -> PathProvider: + return StaticVisitPathProvider( BL, Path("/dls/b16/data/2025/cm40635-3/bluesky"), client=RemoteDirectoryServiceClient("http://b16-control:8088/api"), ) -) -@device_factory() +@devices.factory() def attol1() -> Motor: return Motor(f"{PREFIX.beamline_prefix}-EA-ECC-03:ACT0") -@device_factory() +@devices.factory() def attol2() -> Motor: return Motor(f"{PREFIX.beamline_prefix}-EA-ECC-03:ACT1") -@device_factory() +@devices.factory() def attol3() -> Motor: return Motor(f"{PREFIX.beamline_prefix}-EA-ECC-03:ACT2") -@device_factory() +@devices.factory() def attorot1() -> Motor: return Motor(f"{PREFIX.beamline_prefix}-EA-ECC-02:ACT2") -@device_factory() -def fds2() -> AreaDetector: +@devices.factory() +def fds2(path_provider: PathProvider) -> AreaDetector: prefix = f"{PREFIX.beamline_prefix}-EA-FDS-02:" - return software_triggered_tiff_area_detector(prefix) + return AreaDetector( + writer=ADTIFFWriter.with_io(prefix, path_provider, fileio_suffix=TIFF_SUFFIX), + controller=ConstantDeadTimeController( + driver=ADBaseIO(prefix + CAM_SUFFIX), deadtime=0.0 + ), + ) -@device_factory() +@devices.factory() def sim_stage() -> XYZStage: return XYZStage( f"{PREFIX.beamline_prefix}-MO-SIM-01:", diff --git a/src/dodal/devices/beamlines/b16/__init__.py b/src/dodal/devices/beamlines/b16/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/dodal/devices/beamlines/b16/detector.py b/src/dodal/devices/beamlines/b16/detector.py deleted file mode 100644 index 2c00beae06..0000000000 --- a/src/dodal/devices/beamlines/b16/detector.py +++ /dev/null @@ -1,24 +0,0 @@ -from ophyd_async.epics.adcore import ( - ADBaseIO, - ADTIFFWriter, - AreaDetector, -) - -from dodal.common.beamlines.beamline_utils import get_path_provider -from dodal.common.beamlines.device_helpers import CAM_SUFFIX, TIFF_SUFFIX -from dodal.devices.controllers import ConstantDeadTimeController - - -def software_triggered_tiff_area_detector(prefix: str, deadtime: float = 0.0): - """Wrapper for AreaDetector with fixed dead time (defaulted to 0) - and a TIFF file writer. - Most detectors in B16 could be configured like this. - """ - return AreaDetector( - writer=ADTIFFWriter.with_io( - prefix=prefix, path_provider=get_path_provider(), fileio_suffix=TIFF_SUFFIX - ), - controller=ConstantDeadTimeController( - driver=ADBaseIO(prefix + CAM_SUFFIX), deadtime=deadtime - ), - ) diff --git a/tests/beamlines/test_b16.py b/tests/beamlines/test_b16.py index 3b768de46b..5535ddb0fc 100644 --- a/tests/beamlines/test_b16.py +++ b/tests/beamlines/test_b16.py @@ -1,34 +1,22 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import ANY, MagicMock, patch +from dodal.beamlines import b16 from dodal.common.beamlines.device_helpers import CAM_SUFFIX, TIFF_SUFFIX -from dodal.devices.beamlines.b16.detector import software_triggered_tiff_area_detector -def test_software_triggered_tiff_area_detector_calls_with_io_correctly(): - prefix = "PV-PREFIX:" +def test_detector_calls_with_io_correctly(): + prefix = "BL16B-EA-FDS-02:" default_deadtime = 0.0 with ( - patch( - "dodal.devices.beamlines.b16.detector.ADTIFFWriter.with_io" - ) as mock_writer_with_io, - patch( - "dodal.devices.beamlines.b16.detector.AreaDetector" - ) as mock_area_detector, - patch( - "dodal.devices.beamlines.b16.detector.ConstantDeadTimeController" - ) as mock_controller, - patch( - "dodal.devices.beamlines.b16.detector.get_path_provider" - ) as mock_get_path_provider, - patch("dodal.devices.beamlines.b16.detector.ADBaseIO") as mock_adbase_io, + patch("dodal.beamlines.b16.ADTIFFWriter.with_io") as mock_writer_with_io, + patch("dodal.beamlines.b16.AreaDetector") as mock_area_detector, + patch("dodal.beamlines.b16.ConstantDeadTimeController") as mock_controller, + patch("dodal.beamlines.b16.ADBaseIO") as mock_adbase_io, ): mock_writer = MagicMock(name="Writer") mock_writer_with_io.return_value = mock_writer - mock_path_provider = MagicMock(name="PathProvider") - mock_get_path_provider.return_value = mock_path_provider - mock_controller_instance = MagicMock(name="Controller") mock_controller.return_value = mock_controller_instance @@ -38,13 +26,11 @@ def test_software_triggered_tiff_area_detector_calls_with_io_correctly(): mock_area_detector_instance = MagicMock(name="AreaDetectorInstance") mock_area_detector.return_value = mock_area_detector_instance - result = software_triggered_tiff_area_detector(prefix) # default deadtime + result = b16.fds2.build(mock=True) # Assert with_io called with correct arguments mock_writer_with_io.assert_called_once_with( - prefix=prefix, - path_provider=mock_path_provider, - fileio_suffix=TIFF_SUFFIX, + prefix, ANY, fileio_suffix=TIFF_SUFFIX ) # Assert ADBaseIO called with correct prefix + suffix From 1053642f64fe10e316f9d92daaa8019ab8c963ff Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 11:35:00 +0000 Subject: [PATCH 03/11] Move b18 to new device factory --- src/dodal/beamlines/b18.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/dodal/beamlines/b18.py b/src/dodal/beamlines/b18.py index a9b13257ed..4a146bae7d 100644 --- a/src/dodal/beamlines/b18.py +++ b/src/dodal/beamlines/b18.py @@ -1,14 +1,14 @@ +from functools import cache from pathlib import Path -from dodal.common.beamlines.beamline_utils import ( - device_factory, - set_path_provider, -) +from ophyd_async.core import PathProvider + from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.common.visit import ( LocalDirectoryServiceClient, StaticVisitPathProvider, ) +from dodal.device_manager import DeviceManager from dodal.devices.synchrotron import Synchrotron from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name @@ -18,21 +18,19 @@ set_log_beamline(BL) set_utils_beamline(BL) +devices = DeviceManager() -# Currently we must hard-code the visit, determining the visit at runtime requires -# infrastructure that is still WIP. -# Communication with GDA is also WIP so for now we determine an arbitrary scan number -# locally and write the commissioning directory. The scan number is not guaranteed to -# be unique and the data is at risk - this configuration is for testing only. -set_path_provider( - StaticVisitPathProvider( + +@devices.fixture +@cache +def path_provider() -> PathProvider: + return StaticVisitPathProvider( BL, Path("/dls/b18/data/2025/cm40637-3/bluesky"), client=LocalDirectoryServiceClient(), ) -) -@device_factory() +@devices.factory() def synchrotron() -> Synchrotron: return Synchrotron() From 2c48826e1b797bd1262bee16dcedbaf3f0378cf0 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 11:38:06 +0000 Subject: [PATCH 04/11] Move i13-1 to new device manager --- src/dodal/beamlines/i13_1.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/dodal/beamlines/i13_1.py b/src/dodal/beamlines/i13_1.py index 4ce43ccc8f..2a6f140ba7 100644 --- a/src/dodal/beamlines/i13_1.py +++ b/src/dodal/beamlines/i13_1.py @@ -1,14 +1,12 @@ +from functools import cache from pathlib import Path +from ophyd_async.core import PathProvider from ophyd_async.epics.adaravis import AravisDetector -from dodal.common.beamlines.beamline_utils import ( - device_factory, - get_path_provider, - set_path_provider, -) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider +from dodal.device_manager import DeviceManager from dodal.devices.beamlines.i13_1.merlin import Merlin from dodal.devices.motors import XYZStage from dodal.log import set_beamline as set_log_beamline @@ -19,40 +17,45 @@ PREFIX = "BL13J" set_log_beamline(BL) set_utils_beamline(BL) -set_path_provider( - StaticVisitPathProvider( + +devices = DeviceManager() + + +@devices.fixture +@cache +def path_provider() -> PathProvider: + return StaticVisitPathProvider( BL, Path("/dls/i13-1/data/2024/cm37257-5/tmp/"), # latest commissioning visit client=LocalDirectoryServiceClient(), ) -) -@device_factory() +@devices.factory() def sample_xyz_stage() -> XYZStage: return XYZStage(prefix=f"{PREFIX}-MO-PI-02:") -@device_factory() +@devices.factory() def sample_xyz_lab_fa_stage() -> XYZStage: return XYZStage(prefix=f"{PREFIX}-MO-PI-02:FIXANG:") -@device_factory() -def side_camera() -> AravisDetector: +@devices.factory() +def side_camera(path_provider: PathProvider) -> AravisDetector: return AravisDetector( prefix=f"{PREFIX}-OP-FLOAT-03:", drv_suffix="CAM:", fileio_suffix="HDF5:", - path_provider=get_path_provider(), + path_provider=path_provider, ) -@device_factory() -def merlin() -> Merlin: +@devices.factory() +def merlin(path_provider: PathProvider) -> Merlin: return Merlin( prefix=f"{PREFIX}-EA-DET-04:", drv_suffix="CAM:", fileio_suffix="HDF5:", - path_provider=get_path_provider(), + path_provider=path_provider, ) From 62bff5e619b345c24468a6013c7fcb5a0671e9c2 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 11:46:05 +0000 Subject: [PATCH 05/11] Update i18 to new device factory --- src/dodal/beamlines/i18.py | 63 +++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/dodal/beamlines/i18.py b/src/dodal/beamlines/i18.py index f52772220d..9dd617acc9 100644 --- a/src/dodal/beamlines/i18.py +++ b/src/dodal/beamlines/i18.py @@ -1,17 +1,15 @@ +from functools import cache from pathlib import Path +from ophyd_async.core import PathProvider from ophyd_async.fastcs.panda import HDFPanda -from dodal.common.beamlines.beamline_utils import ( - device_factory, - get_path_provider, - set_path_provider, -) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.common.visit import ( LocalDirectoryServiceClient, StaticVisitPathProvider, ) +from dodal.device_manager import DeviceManager from dodal.devices.beamlines.i18.diode import Diode from dodal.devices.beamlines.i18.kb_mirror import KBMirror from dodal.devices.common_dcm import ( @@ -32,33 +30,36 @@ set_log_beamline(BL) set_utils_beamline(BL) +devices = DeviceManager() + -# Currently we must hard-code the visit, determining the visit at runtime requires -# infrastructure that is still WIP. -# Communication with GDA is also WIP so for now we determine an arbitrary scan number -# locally and write the commissioning directory. The scan number is not guaranteed to -# be unique and the data is at risk - this configuration is for testing only. -set_path_provider( - StaticVisitPathProvider( +@devices.fixture +@cache +def path_provider() -> PathProvider: + # Currently we must hard-code the visit, determining the visit at runtime requires + # infrastructure that is still WIP. + # Communication with GDA is also WIP so for now we determine an arbitrary scan number + # locally and write the commissioning directory. The scan number is not guaranteed to + # be unique and the data is at risk - this configuration is for testing only. + return StaticVisitPathProvider( BL, Path("/dls/i18/data/2024/cm37264-2/bluesky"), client=LocalDirectoryServiceClient(), ) -) -@device_factory() +@devices.factory() def synchrotron() -> Synchrotron: return Synchrotron() -@device_factory() +@devices.factory() def undulator() -> UndulatorInKeV: return UndulatorInKeV(f"{PREFIX.insertion_prefix}-MO-SERVC-01:") # See https://github.com/DiamondLightSource/dodal/issues/1180 -@device_factory(skip=True) +@devices.factory(skip=True) def dcm() -> DoubleCrystalMonochromatorWithDSpacing: """A double crystal monocromator device, used to select the beam energy. @@ -70,7 +71,7 @@ def dcm() -> DoubleCrystalMonochromatorWithDSpacing: ) -@device_factory() +@devices.factory() def slits_1() -> Slits: return Slits( f"{PREFIX.beamline_prefix}-AL-SLITS-01:", @@ -80,52 +81,52 @@ def slits_1() -> Slits: # PandA IOC needs to be updated to support PVI -@device_factory(skip=True) -def panda1() -> HDFPanda: +@devices.factory(skip=True) +def panda1(path_provider: PathProvider) -> HDFPanda: return HDFPanda( f"{PREFIX.beamline_prefix}-MO-PANDA-01:", - path_provider=get_path_provider(), + path_provider=path_provider, ) -@device_factory() -def i0() -> TetrammDetector: +@devices.factory() +def i0(path_provider: PathProvider) -> TetrammDetector: return TetrammDetector( f"{PREFIX.beamline_prefix}-DI-XBPM-02:", - path_provider=get_path_provider(), + path_provider=path_provider, type="Cividec Diamond XBPM", ) -@device_factory() -def it() -> TetrammDetector: +@devices.factory() +def it(path_provider: PathProvider) -> TetrammDetector: return TetrammDetector( f"{PREFIX.beamline_prefix}-DI-XBPM-01:", - path_provider=get_path_provider(), + path_provider=path_provider, ) -@device_factory(skip=True) +@devices.factory(skip=True) # VFM uses different IOC than HFM https://github.com/DiamondLightSource/dodal/issues/1009 def vfm() -> KBMirror: return KBMirror(f"{PREFIX.beamline_prefix}-OP-VFM-01:") -@device_factory() +@devices.factory() def hfm() -> KBMirror: return KBMirror(f"{PREFIX.beamline_prefix}-OP-HFM-01:") -@device_factory() +@devices.factory() def d7_diode() -> Diode: return Diode(f"{PREFIX.beamline_prefix}-DI-PHDGN-07:") -@device_factory() +@devices.factory() def main_table() -> XYZThetaStage: return XYZThetaStage(f"{PREFIX.beamline_prefix}-MO-TABLE-01:") -@device_factory() +@devices.factory() def thor_labs_stage() -> XYStage: return XYStage(f"{PREFIX.beamline_prefix}-MO-TABLE-02:") From 923fd5aa0d8806239c8e4b07a4f6049bef159970 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 11:47:59 +0000 Subject: [PATCH 06/11] Update k11 to new device factory --- src/dodal/beamlines/k11.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/dodal/beamlines/k11.py b/src/dodal/beamlines/k11.py index 302e3b85dd..b364fa4fdc 100644 --- a/src/dodal/beamlines/k11.py +++ b/src/dodal/beamlines/k11.py @@ -1,13 +1,12 @@ +from functools import cache from pathlib import Path +from ophyd_async.core import PathProvider from ophyd_async.epics.motor import Motor -from dodal.common.beamlines.beamline_utils import ( - device_factory, - set_path_provider, -) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.common.visit import RemoteDirectoryServiceClient, StaticVisitPathProvider +from dodal.device_manager import DeviceManager from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name @@ -16,20 +15,24 @@ set_log_beamline(beamline) set_utils_beamline(beamline) -set_path_provider( - StaticVisitPathProvider( +devices = DeviceManager() + + +@devices.fixture +@cache +def path_provider() -> PathProvider: + return StaticVisitPathProvider( beamline, Path("/dls/k11/data/2025/cm40627-3"), client=RemoteDirectoryServiceClient("https://k11-control:8088/api"), ) -) -@device_factory() +@devices.factory() def kb_x() -> Motor: return Motor(f"{PREFIX.beamline_prefix}-OP-KBM-01:CS:X") -@device_factory() +@devices.factory() def kb_y() -> Motor: return Motor(f"{PREFIX.beamline_prefix}-OP-KBM-01:CS:Y") From bdb5740e8951c040d6c6adb430c83e0b20951ea9 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 12:15:31 +0000 Subject: [PATCH 07/11] Move p38 to new device factory --- src/dodal/beamlines/p38.py | 98 +++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/src/dodal/beamlines/p38.py b/src/dodal/beamlines/p38.py index c4fc10f0a3..0987b28608 100644 --- a/src/dodal/beamlines/p38.py +++ b/src/dodal/beamlines/p38.py @@ -1,13 +1,10 @@ +from functools import cache from pathlib import Path +from ophyd_async.core import PathProvider from ophyd_async.epics.adaravis import AravisDetector from ophyd_async.fastcs.panda import HDFPanda -from dodal.common.beamlines.beamline_utils import ( - device_factory, - get_path_provider, - set_path_provider, -) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.common.beamlines.device_helpers import HDF5_SUFFIX from dodal.common.crystal_metadata import ( @@ -15,6 +12,7 @@ make_crystal_metadata_from_material, ) from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider +from dodal.device_manager import DeviceManager from dodal.devices.beamlines.i22.dcm import DCM from dodal.devices.beamlines.i22.fswitch import FSwitch from dodal.devices.focusing_mirror import FocusingMirror @@ -32,56 +30,60 @@ set_log_beamline(BL) set_utils_beamline(BL) -# Currently we must hard-code the visit, determining the visit at runtime requires -# infrastructure that is still WIP. -# Communication with GDA is also WIP so for now we determine an arbitrary scan number -# locally and write the commissioning directory. The scan number is not guaranteed to -# be unique and the data is at risk - this configuration is for testing only. -set_path_provider( - StaticVisitPathProvider( +devices = DeviceManager() + + +@devices.fixture +@cache +def path_provider() -> PathProvider: + # Currently we must hard-code the visit, determining the visit at runtime requires + # infrastructure that is still WIP. + # Communication with GDA is also WIP so for now we determine an arbitrary scan number + # locally and write the commissioning directory. The scan number is not guaranteed to + # be unique and the data is at risk - this configuration is for testing only. + return StaticVisitPathProvider( BL, Path("/dls/p38/data/2025/cm40650-2/bluesky"), client=LocalDirectoryServiceClient(), ) -) -@device_factory() -def d3() -> AravisDetector: +@devices.factory() +def d3(path_provider: PathProvider) -> AravisDetector: return AravisDetector( f"{PREFIX.beamline_prefix}-DI-DCAM-01:", - path_provider=get_path_provider(), + path_provider=path_provider, drv_suffix="DET:", fileio_suffix=HDF5_SUFFIX, ) # Disconnected -@device_factory(skip=True) -def d11() -> AravisDetector: +@devices.factory(skip=True) +def d11(path_provider: PathProvider) -> AravisDetector: return AravisDetector( f"{PREFIX.beamline_prefix}-DI-DCAM-03:", - path_provider=get_path_provider(), + path_provider=path_provider, drv_suffix="DET:", fileio_suffix=HDF5_SUFFIX, ) -@device_factory() -def d12() -> AravisDetector: +@devices.factory() +def d12(path_provider: PathProvider) -> AravisDetector: return AravisDetector( f"{PREFIX.beamline_prefix}-DI-DCAM-04:", - path_provider=get_path_provider(), + path_provider=path_provider, drv_suffix="DET:", fileio_suffix=HDF5_SUFFIX, ) -@device_factory() -def i0() -> TetrammDetector: +@devices.factory() +def i0(path_provider: PathProvider) -> TetrammDetector: return TetrammDetector( f"{PREFIX.beamline_prefix}-EA-XBPM-01:", - path_provider=get_path_provider(), + path_provider=path_provider, ) @@ -92,37 +94,37 @@ def i0() -> TetrammDetector: # -@device_factory(mock=True) +@devices.factory(mock=True) def slits_1() -> Slits: return Slits(f"{PREFIX.beamline_prefix}-AL-SLITS-01:") -@device_factory(mock=True) +@devices.factory(mock=True) def slits_2() -> Slits: return Slits(f"{PREFIX.beamline_prefix}-AL-SLITS-02:") -@device_factory(mock=True) +@devices.factory(mock=True) def slits_3() -> Slits: return Slits(f"{PREFIX.beamline_prefix}-AL-SLITS-03:") -@device_factory(mock=True) +@devices.factory(mock=True) def slits_4() -> Slits: return Slits(f"{PREFIX.beamline_prefix}-AL-SLITS-04:") -@device_factory(mock=True) +@devices.factory(mock=True) def slits_5() -> Slits: return Slits(f"{PREFIX.beamline_prefix}-AL-SLITS-05:") -@device_factory(mock=True) +@devices.factory(mock=True) def slits_6() -> Slits: return Slits(f"{PREFIX.beamline_prefix}-AL-SLITS-06:") -@device_factory(mock=True) +@devices.factory(mock=True) def fswitch() -> FSwitch: return FSwitch( f"{PREFIX.beamline_prefix}-MO-FSWT-01:", @@ -132,17 +134,17 @@ def fswitch() -> FSwitch: ) -@device_factory(mock=True) +@devices.factory(mock=True) def vfm() -> FocusingMirror: return FocusingMirror(f"{PREFIX.beamline_prefix}-OP-KBM-01:VFM:") -@device_factory(mock=True) +@devices.factory(mock=True) def hfm() -> FocusingMirror: return FocusingMirror(f"{PREFIX.beamline_prefix}-OP-KBM-01:HFM:") -@device_factory(mock=True) +@devices.factory(mock=True) def dcm() -> DCM: return DCM( prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:", @@ -156,7 +158,7 @@ def dcm() -> DCM: ) -@device_factory(mock=True) +@devices.factory(mock=True) def undulator() -> UndulatorInKeV: return UndulatorInKeV( f"{PREFIX.insertion_prefix}-MO-SERVC-01:", @@ -167,42 +169,42 @@ def undulator() -> UndulatorInKeV: # Must document what PandAs are physically connected to # See: https://github.com/bluesky/ophyd-async/issues/284 -@device_factory(skip=True) -def panda1() -> HDFPanda: +@devices.factory(skip=True) +def panda1(path_provider: PathProvider) -> HDFPanda: return HDFPanda( f"{PREFIX.beamline_prefix}-EA-PANDA-01:", - path_provider=get_path_provider(), + path_provider=path_provider, ) -@device_factory(skip=True) -def panda2() -> HDFPanda: +@devices.factory(skip=True) +def panda2(path_provider: PathProvider) -> HDFPanda: return HDFPanda( f"{PREFIX.beamline_prefix}-EA-PANDA-02:", - path_provider=get_path_provider(), + path_provider=path_provider, ) -@device_factory(skip=True) -def panda3() -> HDFPanda: +@devices.factory(skip=True) +def panda3(path_provider: PathProvider) -> HDFPanda: return HDFPanda( f"{PREFIX.beamline_prefix}-EA-PANDA-03:", - path_provider=get_path_provider(), + path_provider=path_provider, ) -@device_factory(skip=True) +@devices.factory(skip=True) def linkam() -> Linkam3: return Linkam3(f"{PREFIX.beamline_prefix}-EA-LINKM-02:") -@device_factory() +@devices.factory() def ppump() -> WatsonMarlow323Pump: """Peristaltic Pump.""" return WatsonMarlow323Pump(f"{PREFIX.beamline_prefix}-EA-PUMP-01:") -@device_factory() +@devices.factory() def high_pressure_xray_cell() -> PressureJumpCell: return PressureJumpCell( f"{PREFIX.beamline_prefix}-EA", From 166ece02fb0f462b4a8290637323503920cbdcd3 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 12:22:02 +0000 Subject: [PATCH 08/11] Move p45 to new device factory --- src/dodal/beamlines/p45.py | 46 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/dodal/beamlines/p45.py b/src/dodal/beamlines/p45.py index ddbb51d4a0..61c99b6cd0 100644 --- a/src/dodal/beamlines/p45.py +++ b/src/dodal/beamlines/p45.py @@ -1,16 +1,14 @@ +from functools import cache from pathlib import Path +from ophyd_async.core import PathProvider from ophyd_async.epics.adaravis import AravisDetector from ophyd_async.fastcs.panda import HDFPanda -from dodal.common.beamlines.beamline_utils import ( - device_factory, - get_path_provider, - set_path_provider, -) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.common.beamlines.device_helpers import DET_SUFFIX, HDF5_SUFFIX from dodal.common.visit import StaticVisitPathProvider +from dodal.device_manager import DeviceManager from dodal.devices.beamlines.p45 import TomoStageWithStretchAndSkew from dodal.devices.motors import XYStage from dodal.log import set_beamline as set_log_beamline @@ -21,41 +19,45 @@ set_log_beamline(BL) set_utils_beamline(BL) -set_path_provider( - StaticVisitPathProvider( +devices = DeviceManager() + + +@devices.fixture +@cache +def path_provider() -> PathProvider: + return StaticVisitPathProvider( BL, Path("/data/2024/cm37283-2/"), # latest commissioning visit ) -) -@device_factory() +@devices.factory() def sample() -> TomoStageWithStretchAndSkew: return TomoStageWithStretchAndSkew(f"{PREFIX.beamline_prefix}-MO-STAGE-01:") -@device_factory() +@devices.factory() def choppers() -> XYStage: return XYStage(f"{PREFIX.beamline_prefix}-MO-CHOP-01:") # Disconnected -@device_factory(skip=True) -def det() -> AravisDetector: +@devices.factory(skip=True) +def det(path_provider: PathProvider) -> AravisDetector: return AravisDetector( f"{PREFIX.beamline_prefix}-EA-MAP-01:", - path_provider=get_path_provider(), + path_provider=path_provider, drv_suffix=DET_SUFFIX, fileio_suffix=HDF5_SUFFIX, ) # Disconnected -@device_factory(skip=True) -def diff() -> AravisDetector: +@devices.factory(skip=True) +def diff(path_provider: PathProvider) -> AravisDetector: return AravisDetector( f"{PREFIX.beamline_prefix}-EA-DIFF-01:", - path_provider=get_path_provider(), + path_provider=path_provider, drv_suffix=DET_SUFFIX, fileio_suffix=HDF5_SUFFIX, ) @@ -63,17 +65,17 @@ def diff() -> AravisDetector: # Must document what PandAs are physically connected to # See: https://github.com/bluesky/ophyd-async/issues/284 -@device_factory(skip=True) -def panda1() -> HDFPanda: +@devices.factory(skip=True) +def panda1(path_provider: PathProvider) -> HDFPanda: return HDFPanda( f"{PREFIX.beamline_prefix}-MO-PANDA-01:", - path_provider=get_path_provider(), + path_provider=path_provider, ) -@device_factory(skip=True) -def panda2() -> HDFPanda: +@devices.factory(skip=True) +def panda2(path_provider: PathProvider) -> HDFPanda: return HDFPanda( f"{PREFIX.beamline_prefix}-MO-PANDA-02:", - path_provider=get_path_provider(), + path_provider=path_provider, ) From 4f9a8b9cbf20853ed51be2618aec212a09094256 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 12:24:34 +0000 Subject: [PATCH 09/11] Update p51 to new device manager --- src/dodal/beamlines/p51.py | 46 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/dodal/beamlines/p51.py b/src/dodal/beamlines/p51.py index 0ea32369ef..8512a4fee0 100644 --- a/src/dodal/beamlines/p51.py +++ b/src/dodal/beamlines/p51.py @@ -1,16 +1,14 @@ +from functools import cache from pathlib import Path +from ophyd_async.core import PathProvider from ophyd_async.epics.motor import Motor from ophyd_async.epics.pmac import PmacIO from ophyd_async.fastcs.panda import HDFPanda -from dodal.common.beamlines.beamline_utils import ( - device_factory, - get_path_provider, - set_path_provider, -) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.common.visit import RemoteDirectoryServiceClient, StaticVisitPathProvider +from dodal.device_manager import DeviceManager from dodal.devices.turbo_slit import TurboSlit from dodal.devices.xspress3.xspress3 import Xspress3 from dodal.log import set_beamline as set_log_beamline @@ -21,19 +19,23 @@ set_log_beamline(BL) set_utils_beamline(BL) +devices = DeviceManager() -# Currently we must hard-code the visit, determining the visit at runtime requires -# infrastructure that is still WIP. -# Communication with GDA is also WIP so for now we determine an arbitrary scan number -# locally and write the commissioning directory. The scan number is not guaranteed to -# be unique and the data is at risk - this configuration is for testing only. -set_path_provider( - StaticVisitPathProvider( + +@devices.fixture +@cache +def path_provider() -> PathProvider: + # Currently we must hard-code the visit, determining the visit at runtime requires + # infrastructure that is still WIP. + # Communication with GDA is also WIP so for now we determine an arbitrary scan number + # locally and write the commissioning directory. The scan number is not guaranteed to + # be unique and the data is at risk - this configuration is for testing only. + return StaticVisitPathProvider( BL, Path("/dls/p51/data/2026/cm44254-1/tmp"), client=RemoteDirectoryServiceClient("http://i20-1-control:8088/api"), ) -) + """ NOTE: Due to the CA gateway machine being switched off, PVs are not available remotely @@ -43,19 +45,19 @@ """ -@device_factory() +@devices.factory() def turbo_slit() -> TurboSlit: """Turboslit for selecting energy from the polychromator.""" return TurboSlit(f"{PREFIX.beamline_prefix}-OP-PCHRO-01:TS:") -@device_factory() +@devices.factory() def turbo_slit_x() -> Motor: """Turbo slit x motor.""" return Motor(f"{PREFIX.beamline_prefix}-OP-PCHRO-01:TS:XFINE") -@device_factory() +@devices.factory() def turbo_slit_pmac() -> PmacIO: """PMac controller using running fly scans with trajectory.""" motor = turbo_slit_x() @@ -66,26 +68,26 @@ def turbo_slit_pmac() -> PmacIO: ) -@device_factory() -def panda() -> HDFPanda: +@devices.factory() +def panda(path_provider: PathProvider) -> HDFPanda: return HDFPanda( - f"{PREFIX.beamline_prefix}-EA-PANDA-02:", path_provider=get_path_provider() + f"{PREFIX.beamline_prefix}-EA-PANDA-02:", path_provider=path_provider ) # Use mock device until motors are reconnected on the beamline -@device_factory(mock=True) +@devices.factory(mock=True) def alignment_x() -> Motor: return Motor(f"{PREFIX.beamline_prefix}-MO-STAGE-01:X") # Use mock device until motors are reconnected on the beamline -@device_factory(mock=True) +@devices.factory(mock=True) def alignment_y() -> Motor: return Motor(f"{PREFIX.beamline_prefix}-MO-STAGE-01:Y") -@device_factory(skip=True) +@devices.factory(skip=True) def xspress3() -> Xspress3: """16 channels Xspress3 detector.""" return Xspress3( From 2a95a236b7e205713785c925b82e13fbde7e0388 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 13:26:13 +0000 Subject: [PATCH 10/11] Update p99 to new device factory --- src/dodal/beamlines/p99.py | 37 +++++++++++----------- tests/devices/beamlines/i11/test_mythen.py | 25 ++------------- 2 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/dodal/beamlines/p99.py b/src/dodal/beamlines/p99.py index dc2d0d686b..f17bca2943 100644 --- a/src/dodal/beamlines/p99.py +++ b/src/dodal/beamlines/p99.py @@ -1,13 +1,11 @@ +from functools import cache from pathlib import Path +from ophyd_async.core import PathProvider from ophyd_async.epics.adandor import Andor2Detector from ophyd_async.fastcs.panda import HDFPanda -from dodal.common.beamlines.beamline_utils import ( - get_path_provider, - set_beamline, - set_path_provider, -) +from dodal.common.beamlines.beamline_utils import set_beamline from dodal.common.beamlines.device_helpers import CAM_SUFFIX, HDF5_SUFFIX from dodal.common.visit import ( LocalDirectoryServiceClient, @@ -26,9 +24,21 @@ PREFIX = BeamlinePrefix(BL) set_log_beamline(BL) set_beamline(BL) + + devices = DeviceManager() +@devices.fixture +@cache +def path_provider() -> PathProvider: + return StaticVisitPathProvider( + BL, + Path("/dls/p99/data/2024/cm37284-2/processing/writenData"), + client=LocalDirectoryServiceClient(), # RemoteDirectoryServiceClient("http://p99-control:8088/api"), + ) + + @devices.factory() def angle_stage() -> SampleAngleStage: return SampleAngleStage(f"{PREFIX.beamline_prefix}-MO-STAGE-01:") @@ -49,21 +59,12 @@ def lab_stage() -> XYZStage: return XYZStage(f"{PREFIX.beamline_prefix}-MO-STAGE-02:LAB:") -set_path_provider( - StaticVisitPathProvider( - BL, - Path("/dls/p99/data/2024/cm37284-2/processing/writenData"), - client=LocalDirectoryServiceClient(), # RemoteDirectoryServiceClient("http://p99-control:8088/api"), - ) -) - - @devices.factory() -def andor2_det() -> Andor2Detector: +def andor2_det(path_provider: PathProvider) -> Andor2Detector: """Andor model:DU897_BV.""" return Andor2Detector( prefix=f"{PREFIX.beamline_prefix}-EA-DET-03:", - path_provider=get_path_provider(), + path_provider=path_provider, drv_suffix=CAM_SUFFIX, fileio_suffix=HDF5_SUFFIX, ) @@ -82,12 +83,12 @@ def andor2_point() -> Andor2Point: @devices.factory() -def panda() -> HDFPanda: +def panda(path_provider: PathProvider) -> HDFPanda: """The Panda device is connected to two PMAC motors for position comparison under the pcomp[1] and pcomp[2] blocks, which handle positive and negative directions. This setup is used for triggering detectors during a flyscan. """ return HDFPanda( f"{PREFIX.beamline_prefix}-MO-PANDA-01:", - path_provider=get_path_provider(), + path_provider=path_provider, ) diff --git a/tests/devices/beamlines/i11/test_mythen.py b/tests/devices/beamlines/i11/test_mythen.py index ed6a1877d3..0fa0c2981b 100644 --- a/tests/devices/beamlines/i11/test_mythen.py +++ b/tests/devices/beamlines/i11/test_mythen.py @@ -1,11 +1,8 @@ -from pathlib import Path - import pytest -from ophyd_async.core import DetectorTrigger, TriggerInfo, init_devices +from ophyd_async.core import DetectorTrigger, TriggerInfo from ophyd_async.epics.adcore import ADImageMode -from dodal.common.beamlines.beamline_utils import get_path_provider, set_path_provider -from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider +from dodal.beamlines import i11 from dodal.devices.beamlines.i11.mythen import ( _BIT_DEPTH, _DEADTIMES, @@ -16,23 +13,7 @@ @pytest.fixture async def i11_mythen() -> Mythen3: - set_path_provider( - StaticVisitPathProvider( - "i11", - Path("/dls/i11/data/2025/cm12356-1/"), - client=LocalDirectoryServiceClient(), - ) - ) - - async with init_devices(mock=True): - i11_mythen = Mythen3( - prefix="BL11I-EA-DET-07:", - path_provider=get_path_provider(), - drv_suffix="DET", - fileio_suffix="HDF:", - ) - - return i11_mythen + return i11.mythen3.build(mock=True) def test_mythen_deadtime(i11_mythen: Mythen3) -> None: From ad483b41d473f57987e903c4fec289ee6e1a19d9 Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Fri, 6 Feb 2026 13:39:17 +0000 Subject: [PATCH 11/11] Connect the mock mythen immediately --- tests/devices/beamlines/i11/test_mythen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/devices/beamlines/i11/test_mythen.py b/tests/devices/beamlines/i11/test_mythen.py index 0fa0c2981b..8da03f4d35 100644 --- a/tests/devices/beamlines/i11/test_mythen.py +++ b/tests/devices/beamlines/i11/test_mythen.py @@ -13,7 +13,7 @@ @pytest.fixture async def i11_mythen() -> Mythen3: - return i11.mythen3.build(mock=True) + return i11.mythen3.build(connect_immediately=True, mock=True) def test_mythen_deadtime(i11_mythen: Mythen3) -> None: