Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
2a03b7d
Proof of concept device manager
tpoliaw Sep 17, 2025
ee86c74
Use fixtures parameter instead of **kwargs
tpoliaw Sep 18, 2025
658a45f
Expand dependencies
tpoliaw Sep 23, 2025
318593b
Pre-optional handling
tpoliaw Sep 25, 2025
d81502b
'Working' optional dependencies
tpoliaw Sep 25, 2025
123e96f
Docs, comments and warnings
tpoliaw Sep 25, 2025
15d4dc4
Check for positional only arguments
tpoliaw Sep 26, 2025
f3832d6
Handle connections and support new loader in cli
tpoliaw Sep 26, 2025
5fe05c7
Convert adsim to new loading
tpoliaw Sep 26, 2025
cbfa2f6
Add auto_connect to device manager
tpoliaw Sep 29, 2025
784856b
Remove references to connecting from device manager
tpoliaw Sep 30, 2025
5d8bfa5
DeviceResult wrappers
tpoliaw Sep 30, 2025
05ed69b
Demo adsim
tpoliaw Sep 30, 2025
62f41f7
build_and_connect method
tpoliaw Sep 30, 2025
37cdb9e
Multiple manager CLI
tpoliaw Sep 30, 2025
7f15f34
FIXTUEES
tpoliaw Sep 30, 2025
db69729
Use device decorator for timeouts
tpoliaw Oct 1, 2025
636ba06
Type build_and_connect and rely on fixtures for path provider
tpoliaw Oct 1, 2025
23d5c1e
mostly reformatting
tpoliaw Oct 1, 2025
ab2346b
Make fixture generators lazy
tpoliaw Oct 1, 2025
7635976
Apologise for build_order method
tpoliaw Oct 1, 2025
c7fcad1
Return function from fixture decorator
tpoliaw Oct 1, 2025
5e88698
Add timeout parameter to build_and_connect
tpoliaw Oct 1, 2025
7f0b927
Remove dead comment
tpoliaw Oct 1, 2025
922f0f5
Use set in expand_dependencies to prevent repetition
tpoliaw Oct 1, 2025
71d72cd
Check for duplicate factory names
tpoliaw Oct 2, 2025
13f0380
Add Ophyd v1 support
tpoliaw Oct 21, 2025
cbe6012
Connect Ophyd v1 devices
tpoliaw Oct 23, 2025
f7679eb
Move device_manager to new module
tpoliaw Oct 23, 2025
794067b
Remove test code from device_manager module
tpoliaw Oct 23, 2025
7a8afda
Remove debugging and commented code
tpoliaw Oct 23, 2025
10ee736
Merge connectionspec and connectionparameters
tpoliaw Oct 23, 2025
1e71b35
Add or_raise method to DeviceBuildResult
tpoliaw Oct 23, 2025
5cd9eef
Add docstrings
tpoliaw Oct 23, 2025
22d40fb
Use or_raise to handle build errors
tpoliaw Oct 23, 2025
04ff9cf
Only set device name if required
tpoliaw Oct 23, 2025
622cca5
Add TODOs to remove v1 support
tpoliaw Oct 23, 2025
75263da
Make v1 device timeout configurable
tpoliaw Oct 23, 2025
a033594
Default to waiting for v1 device connection
tpoliaw Oct 23, 2025
3837ca6
Add repr to v1 device factory
tpoliaw Oct 23, 2025
cd4b876
Split DeviceBuildResult devices and connection specs
tpoliaw Oct 24, 2025
ba075e1
Remove device_type property from factories
tpoliaw Oct 24, 2025
1612853
Include fixture overrides in built devices
tpoliaw Oct 24, 2025
79f8bdf
Fix duplication in factory repr
tpoliaw Oct 24, 2025
6a81076
Add initial device_manager tests
tpoliaw Oct 24, 2025
3175300
Revert adsim changes for now
tpoliaw Oct 27, 2025
15d7dc1
Enough tests to get full coverage
tpoliaw Oct 27, 2025
28724b4
Create DeviceManager in fixture
tpoliaw Oct 27, 2025
9434201
Add test for docstrings
tpoliaw Oct 28, 2025
fef6959
Reformat tests
tpoliaw Oct 28, 2025
3609a8f
Linting et al
tpoliaw Nov 12, 2025
14f96a3
Support single device manager name in CLI
tpoliaw Nov 12, 2025
e3ee340
Ignore mock type warnings
tpoliaw Nov 12, 2025
45716f2
Appease pre-commit lints
tpoliaw Nov 12, 2025
897d129
Add tests for device manager use in CLI
tpoliaw Nov 13, 2025
f136601
Make 'devices' default name for device manager in CLI
tpoliaw Nov 19, 2025
f33a7b5
Clean up TODO comments
tpoliaw Nov 19, 2025
caa6319
Set return_value when creating mocks
tpoliaw Nov 19, 2025
815bd47
Fix typing in v1_init decorator
tpoliaw Nov 21, 2025
9c22d22
Use ParamSpec when passing through __call__
tpoliaw Nov 21, 2025
820049d
Handle or ignore var args
tpoliaw Nov 21, 2025
f626761
Update tests
tpoliaw Nov 21, 2025
6afc663
Rename ignore skip test
tpoliaw Nov 21, 2025
4b19585
Simplify LazyFixture __getitem__
tpoliaw Nov 21, 2025
0487c9b
Used UserDict as base class for LazyFixtures
tpoliaw Nov 21, 2025
8adcc77
Merge branch 'main' into device_context
rtuck99 Nov 24, 2025
9af2428
Make pyright happy
rtuck99 Nov 24, 2025
e4740e6
Example conversion to the new device manager
tpoliaw Oct 30, 2025
62ed2da
Fix device heirachy
DominicOram Nov 18, 2025
613bb09
Fix some tests
DominicOram Nov 18, 2025
0c7d789
Ignore device_manager factories in utils
tpoliaw Nov 19, 2025
b9b096f
Fix test_device_instantiation
rtuck99 Nov 21, 2025
aa3882c
Update beamline howto docs
rtuck99 Nov 21, 2025
42e4d40
Merge branch 'main' into i03-device-manager
DominicOram Nov 27, 2025
c943f7a
Fix merge
DominicOram Nov 27, 2025
97189cb
Remove spurious comments
DominicOram Nov 27, 2025
fae9575
Merge branch 'main' into i03-device-manager
DominicOram Nov 27, 2025
042bf15
Example of shared beamline structure - i05
oliwenmandiamond Nov 28, 2025
5411a75
Move ID devices to shared and add basic sample stages
oliwenmandiamond Nov 28, 2025
4dd49c0
Fix test by testing beamlines modules in isolation
oliwenmandiamond Nov 28, 2025
f5f2934
Fix test_valid_beamline_variable_causes_get_device_module_to_return_m…
oliwenmandiamond Nov 28, 2025
fea77f8
Added combine method to get around pytest issue
oliwenmandiamond Nov 28, 2025
c4c7c44
Removed duplicate method
oliwenmandiamond Dec 1, 2025
7076edd
Merge branch 'main' into i05_example_shared_beamline_structure
oliwenmandiamond Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
14 changes: 0 additions & 14 deletions src/dodal/beamline_specific_utils/i05_shared.py

This file was deleted.

55 changes: 15 additions & 40 deletions src/dodal/beamlines/i05.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,24 @@
from dodal.beamline_specific_utils.i05_shared import pgm as i05_pgm
from dodal.common.beamlines.beamline_utils import (
device_factory,
)
from dodal.beamlines.i05_shared import PREFIX
from dodal.beamlines.i05_shared import devices as i05_shared_devices
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
from dodal.devices.apple2_undulator import (
Apple2,
UndulatorGap,
UndulatorLockedPhaseAxes,
)
from dodal.devices.pgm import PlaneGratingMonochromator
from dodal.devices.synchrotron import Synchrotron
from dodal.device_manager import DeviceManager
from dodal.devices.motors import XYZStage
from dodal.log import set_beamline as set_log_beamline
from dodal.utils import BeamlinePrefix, get_beamline_name
from dodal.utils import get_beamline_name

devices = DeviceManager()
devices.combine(i05_shared_devices)

BL = get_beamline_name("i05")
PREFIX = BeamlinePrefix(BL)
set_log_beamline(BL)
set_utils_beamline(BL)


@device_factory()
def synchrotron() -> Synchrotron:
return Synchrotron()


@device_factory()
def pgm() -> PlaneGratingMonochromator:
return i05_pgm()


@device_factory()
def id_gap() -> UndulatorGap:
return UndulatorGap(prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:")


@device_factory()
def id_phase() -> UndulatorLockedPhaseAxes:
return UndulatorLockedPhaseAxes(
prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:",
top_outer="PL",
btm_inner="PU",
@devices.factory()
def sm() -> XYZStage:
return XYZStage(
f"{PREFIX.beamline_prefix}-EA-SM-01:",
x_infix="SAX",
y_infix="SAY",
z_infix="SAZ",
)


@device_factory()
def id() -> Apple2:
"""i05 insertion device."""
return Apple2(id_gap=id_gap(), id_phase=id_phase())
28 changes: 15 additions & 13 deletions src/dodal/beamlines/i05_1.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
from dodal.beamline_specific_utils.i05_shared import pgm as i05_pgm
from dodal.common.beamlines.beamline_utils import device_factory
from dodal.beamlines.i05_shared import devices as i05_shared_devices
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
from dodal.devices.pgm import PlaneGratingMonochromator
from dodal.devices.synchrotron import Synchrotron
from dodal.device_manager import DeviceManager
from dodal.devices.motors import XYZStage
from dodal.log import set_beamline as set_log_beamline
from dodal.utils import BeamlinePrefix, get_beamline_name

devices = DeviceManager()
devices.combine(i05_shared_devices)

BL = get_beamline_name("i05-1")
PREFIX = BeamlinePrefix(BL, suffix="J")
J_PREFIX = BeamlinePrefix(BL, suffix="J")
set_log_beamline(BL)
set_utils_beamline(BL)


@device_factory()
def pgm() -> PlaneGratingMonochromator:
return i05_pgm()


@device_factory()
def synchrotron() -> Synchrotron:
return Synchrotron()
@devices.factory()
def sm() -> XYZStage:
return XYZStage(
f"{J_PREFIX.beamline_prefix}-EA-SM-01:",
x_infix="SMX",
y_infix="SMY",
z_infix="SMZ",
)
47 changes: 47 additions & 0 deletions src/dodal/beamlines/i05_shared.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from dodal.device_manager import DeviceManager
from dodal.devices.apple2_undulator import (
Apple2,
UndulatorGap,
UndulatorLockedPhaseAxes,
)
from dodal.devices.i05.enums import Grating
from dodal.devices.pgm import PlaneGratingMonochromator
from dodal.devices.synchrotron import Synchrotron
from dodal.utils import BeamlinePrefix

devices = DeviceManager()

PREFIX = BeamlinePrefix("i05", "I")


@devices.factory()
def synchrotron() -> Synchrotron:
return Synchrotron()


@devices.factory()
def pgm() -> PlaneGratingMonochromator:
return PlaneGratingMonochromator(
prefix=f"{PREFIX.beamline_prefix}-OP-PGM-01:",
grating=Grating,
)


@devices.factory()
def id_gap() -> UndulatorGap:
return UndulatorGap(prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:")


@devices.factory()
def id_phase() -> UndulatorLockedPhaseAxes:
return UndulatorLockedPhaseAxes(
prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:",
top_outer="PL",
btm_inner="PU",
)


@devices.factory()
def id() -> Apple2[UndulatorLockedPhaseAxes]:
"""i05 insertion device."""
return Apple2[UndulatorLockedPhaseAxes](id_gap=id_gap(), id_phase=id_phase())
27 changes: 25 additions & 2 deletions src/dodal/device_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,18 @@ def or_raise(self) -> Self:
return self


def combine_dicts_no_duplicates(dict1, dict2):
# Find overlapping keys (keys present in both dictionaries)
overlapping_keys = set(dict1) & set(dict2)

if overlapping_keys:
raise ValueError(f"Duplicate keys detected: {', '.join(overlapping_keys)}")

# If no duplicates, combine the dictionaries
combined_dict = {**dict1, **dict2}
return combined_dict


class DeviceManager:
"""Manager to handle building and connecting interdependent devices"""

Expand All @@ -355,6 +367,17 @@ def __init__(self):
self._v1_factories = {}
self._fixtures = {}

def combine(self, devices: Self) -> None:
self._factories = combine_dicts_no_duplicates(
self._factories,
devices._factories, # noqa: SLF001
)
self._v1_factories = combine_dicts_no_duplicates(
self._v1_factories,
devices._v1_factories, # noqa: SLF001
)
self._fixtures = combine_dicts_no_duplicates(self._fixtures, devices._fixtures) # noqa: SLF001

def fixture(self, func: Callable[[], T]) -> Callable[[], T]:
"""Add a function that can provide fixtures required by the factories"""
self._fixtures[func.__name__] = func
Expand Down Expand Up @@ -518,10 +541,10 @@ def build_devices(

return DeviceBuildResult(built, errors, connection_specs)

def __contains__(self, name):
def __contains__(self, name) -> bool:
return name in self._factories or name in self._v1_factories

def __getitem__(self, name):
def __getitem__(self, name) -> DeviceFactory | V1DeviceFactory:
return self._factories.get(name) or self._v1_factories[name]

def _expand_dependencies(
Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import importlib
import os
import sys
from collections.abc import AsyncGenerator
from pathlib import Path
from types import ModuleType
Expand Down Expand Up @@ -54,6 +55,12 @@ def module_and_devices_for_beamline(request: pytest.FixtureRequest):
factory.cache_clear()
del bl_mod

# Remove beamline module from sys.modules to make sure we are testing beamlines
# in isolated modules and doesn't leak over to the next one.
mods_to_clear = [m for m in sys.modules if m.startswith("dodal.beamlines")]
for m in mods_to_clear:
sys.modules.pop(m, None)


def mock_beamline_module_filepaths(bl_name: str, bl_module: ModuleType):
if mock_attributes := mock_attributes_table.get(bl_name):
Expand Down
9 changes: 6 additions & 3 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from collections.abc import Iterable, Mapping
from shutil import copytree
from types import ModuleType
from typing import Any, cast
from unittest.mock import ANY, MagicMock, Mock, patch

Expand Down Expand Up @@ -308,15 +309,17 @@ def device_b() -> Motor:


@pytest.mark.parametrize("bl", ["", "$%^&*", "nonexistent"])
def test_invalid_beamline_variable_causes_get_device_module_to_raise(bl):
def test_invalid_beamline_variable_causes_get_device_module_to_raise(bl: str):
with patch.dict(os.environ, {"BEAMLINE": bl}), pytest.raises(ValueError):
get_beamline_based_on_environment_variable()


@pytest.mark.parametrize("bl,module", [("i04", i04), ("i23", i23)])
def test_valid_beamline_variable_causes_get_device_module_to_return_module(bl, module):
def test_valid_beamline_variable_causes_get_device_module_to_return_module(
bl: str, module: ModuleType
):
with patch.dict(os.environ, {"BEAMLINE": bl}):
assert get_beamline_based_on_environment_variable() == module
assert get_beamline_based_on_environment_variable().__name__ == module.__name__


def test_find_next_run_number_from_files_gets_correct_number():
Expand Down