Converting an existing beamline to use a DeviceManager#

This guide assumes you have a beamline module file (eg dodal/beamlines/ixx.py) with a number of functions defined and decorated with @device_factory (dodal.common.beamlines.beamline_utils).

Create a DeviceManager#

By convention this should be named devices but can be custom if there is good reason.

from dodal.device_manager import DeviceManager

devices = DeviceManager()

This device manager keeps track of all device factories for a beamline making it easier to determine which are present and the order in which to build them to ensure dependencies are available before their dependents.

Replace the @device_factory decorators#

Each factory function should be decorated with @devices.factory (where devices is the name of the DeviceManager created above) instead of @device_factory. If no arguments are required, the parentheses can be omitted, eg

@devices.factory()
def device() -> Device:
    ...

is equivalent to

@devices.factory
def device() -> Device:
    ...

If arguments (eg skip or mock) were passed to device_factory, these can continue to be passed to devices.factory in the same way.

Replace get_path_provider with fixture#

Previously, the path_provider used by detectors was set and accessed as a global.

Where device factory functions used this global (via get_path_provider) they should be updated to accept path_provider as a parameter. This can then be passed in when creating the devices.

# Previous approach using `get_path_provider`...
@device_factory()
def panda() -> HDFPanda:
    return HDFPanda(
        f"{PREFIX.beamline_prefix}-EA-PANDA-01:",
        path_provider=get_path_provider(),
    )

# ... should be replaced with
@devices.factory()
def panda(path_provider: PathProvider) -> HDFPanda:
    return HDFPanda(
        f"{PREFIX.beamline_prefix}-EA-PANDA-01:",
        path_provider=path_provider,
    )

Where a beamline was setting the path provider via set_path_provider, a fixture should be created which will provide a fallback instance to be used if one is not passed in.

# The previous approach...
set_path_provider(PathProviderImplementation(...))

# ...should be removed and replaced with
@devices.fixture
def path_provider() -> PathProvider:
    return PathProviderImplementation(...)

Replace factory calls with parameters#

Where one function previously called another to access another device, this should be replaced with a parameter of the same name. This is to prevent multiple instances of devices being created as functions no longer cache the devices they create.

# Previous example using a device factory function
@device_factory()
def beamsize() -> Beamsize:
    return Beamsize(aperture_scatterguard())

# New version with `aperture_scatterguard` passed as a parameter
@devices.factory
def beamsize(aperture_scatterguard: ApertureScatterguard) -> Beamsize:
    return Beamsize(aperture_scatterguard)

Merge multiple device managers#

If devices need to be shared between multiple beamlines, eg branch beamlines sharing optics components, an external device manager can be imported and merged into another.

# in ixx_shared.py
devices = DeviceManager()

@devices.factory
def common_device() -> CommonDevice:
   ...

# in ixx.py
from ixx_shared import devices as shared_devices

devices = DeviceManager()
devices.include(shared_devices)

# devices defined in the common beamline module can then be used as dependencies
# in the beamline module
@devices.factory
def beamline_device(common_device: CommonDevice) -> BeamlineDevice:
    ...