dls-dodal 1.65.0__py3-none-any.whl → 1.67.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/METADATA +3 -4
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +82 -66
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +21 -2
- dodal/beamlines/i03.py +102 -198
- dodal/beamlines/i04.py +40 -4
- dodal/beamlines/i05.py +28 -1
- dodal/beamlines/i06.py +62 -0
- dodal/beamlines/i07.py +20 -0
- dodal/beamlines/i09_1.py +32 -3
- dodal/beamlines/i09_2.py +57 -2
- dodal/beamlines/i10_optics.py +46 -17
- dodal/beamlines/i17.py +7 -3
- dodal/beamlines/i18.py +3 -3
- dodal/beamlines/i19_1.py +26 -14
- dodal/beamlines/i19_2.py +49 -38
- dodal/beamlines/i21.py +2 -2
- dodal/beamlines/i22.py +19 -4
- dodal/beamlines/p38.py +3 -3
- dodal/beamlines/training_rig.py +0 -16
- dodal/cli.py +26 -12
- dodal/common/coordination.py +3 -2
- dodal/device_manager.py +604 -0
- dodal/devices/aithre_lasershaping/goniometer.py +26 -9
- dodal/devices/aperturescatterguard.py +3 -2
- dodal/devices/areadetector/plugins/mjpg.py +10 -3
- dodal/devices/beamsize/__init__.py +0 -0
- dodal/devices/beamsize/beamsize.py +6 -0
- dodal/devices/cryostream.py +28 -57
- dodal/devices/detector/det_resolution.py +4 -2
- dodal/devices/eiger.py +26 -18
- dodal/devices/fast_grid_scan.py +14 -2
- dodal/devices/i03/beamsize.py +35 -0
- dodal/devices/i03/constants.py +7 -0
- dodal/devices/i03/undulator_dcm.py +2 -2
- dodal/devices/i04/beamsize.py +45 -0
- dodal/devices/i04/max_pixel.py +38 -0
- dodal/devices/i04/murko_results.py +36 -26
- dodal/devices/i04/transfocator.py +23 -29
- dodal/devices/i07/id.py +38 -0
- dodal/devices/i09_1_shared/__init__.py +13 -2
- dodal/devices/i09_1_shared/hard_energy.py +112 -0
- dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
- dodal/devices/i09_2_shared/__init__.py +0 -0
- dodal/devices/i09_2_shared/i09_apple2.py +86 -0
- dodal/devices/i10/i10_apple2.py +39 -331
- dodal/devices/i17/i17_apple2.py +37 -22
- dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
- dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
- dodal/devices/i19/access_controlled/shutter.py +2 -4
- dodal/devices/insertion_device/__init__.py +0 -0
- dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +122 -69
- dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
- dodal/devices/insertion_device/lookup_table_models.py +287 -0
- dodal/devices/ipin.py +20 -2
- dodal/devices/motors.py +33 -3
- dodal/devices/mx_phase1/beamstop.py +31 -12
- dodal/devices/oav/oav_calculations.py +9 -4
- dodal/devices/oav/oav_detector.py +65 -7
- dodal/devices/oav/oav_parameters.py +3 -1
- dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
- dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
- dodal/devices/oav/pin_image_recognition/utils.py +23 -1
- dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
- dodal/devices/oav/utils.py +16 -6
- dodal/devices/robot.py +33 -18
- dodal/devices/scintillator.py +36 -14
- dodal/devices/smargon.py +2 -3
- dodal/devices/thawer.py +7 -45
- dodal/devices/undulator.py +152 -68
- dodal/plans/__init__.py +1 -1
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
- dodal/plans/load_panda_yaml.py +9 -0
- dodal/plans/verify_undulator_gap.py +2 -2
- dodal/testing/fixtures/devices/__init__.py +0 -0
- dodal/testing/fixtures/devices/apple2.py +78 -0
- dodal/utils.py +6 -3
- dodal/beamline_specific_utils/i03.py +0 -17
- dodal/testing/__init__.py +0 -3
- dodal/testing/setup.py +0 -67
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/top_level.txt +0 -0
- /dodal/plans/{scanspec.py → spec_path.py} +0 -0
dodal/device_manager.py
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import typing
|
|
4
|
+
from collections import UserDict
|
|
5
|
+
from collections.abc import Callable, Iterable, Mapping, MutableMapping
|
|
6
|
+
from functools import cached_property, wraps
|
|
7
|
+
from inspect import Parameter
|
|
8
|
+
from types import NoneType
|
|
9
|
+
from typing import (
|
|
10
|
+
Annotated,
|
|
11
|
+
Any,
|
|
12
|
+
Concatenate,
|
|
13
|
+
Generic,
|
|
14
|
+
NamedTuple,
|
|
15
|
+
ParamSpec,
|
|
16
|
+
Self,
|
|
17
|
+
TypeVar,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from bluesky.run_engine import (
|
|
21
|
+
get_bluesky_event_loop,
|
|
22
|
+
)
|
|
23
|
+
from ophyd.sim import make_fake_device
|
|
24
|
+
|
|
25
|
+
from dodal.common.beamlines.beamline_utils import (
|
|
26
|
+
wait_for_connection,
|
|
27
|
+
)
|
|
28
|
+
from dodal.utils import (
|
|
29
|
+
AnyDevice,
|
|
30
|
+
OphydV1Device,
|
|
31
|
+
OphydV2Device,
|
|
32
|
+
SkipType,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
DEFAULT_TIMEOUT = 30
|
|
36
|
+
|
|
37
|
+
T = TypeVar("T")
|
|
38
|
+
Args = ParamSpec("Args")
|
|
39
|
+
|
|
40
|
+
V1 = TypeVar("V1", bound=OphydV1Device)
|
|
41
|
+
V2 = TypeVar("V2", bound=OphydV2Device)
|
|
42
|
+
|
|
43
|
+
DeviceFactoryDecorator = Callable[[Callable[Args, V2]], "DeviceFactory[Args, V2]"]
|
|
44
|
+
OphydInitialiser = Callable[Concatenate[V1, Args], V1 | None]
|
|
45
|
+
|
|
46
|
+
_EMPTY = object()
|
|
47
|
+
"""Sentinel value to distinguish between missing values and present but null values"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LazyFixtures(UserDict[str, Any]):
|
|
51
|
+
"""
|
|
52
|
+
Wrapper around fixtures and fixture generators
|
|
53
|
+
|
|
54
|
+
If a fixture is provided at runtime, the generator function does not have to be called.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
lazy: MutableMapping[str, Callable[[], Any]]
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
provided: Mapping[str, Any] | None,
|
|
62
|
+
factories: Mapping[str, Callable[[], Any]],
|
|
63
|
+
):
|
|
64
|
+
super().__init__(dict.fromkeys(factories, _EMPTY) | dict(provided or {}))
|
|
65
|
+
self.lazy = dict(factories)
|
|
66
|
+
|
|
67
|
+
def __getitem__(self, key: str) -> Any:
|
|
68
|
+
if self.data[key] is _EMPTY:
|
|
69
|
+
self.data[key] = self.lazy[key]()
|
|
70
|
+
return self.data[key]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DeviceFactory(Generic[Args, V2]):
|
|
74
|
+
"""
|
|
75
|
+
Wrapper around a device factory (any function returning a device) that holds
|
|
76
|
+
a reference to a device manager that can provide dependencies, along with
|
|
77
|
+
default connection information for how the created device should be
|
|
78
|
+
connected.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
factory: Callable[Args, V2],
|
|
84
|
+
use_factory_name: bool,
|
|
85
|
+
timeout: float,
|
|
86
|
+
mock: bool,
|
|
87
|
+
skip: SkipType,
|
|
88
|
+
manager: "DeviceManager",
|
|
89
|
+
):
|
|
90
|
+
for name, param in inspect.signature(factory).parameters.items():
|
|
91
|
+
if param.kind == Parameter.POSITIONAL_ONLY:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"{factory.__name__} has positional only argument '{name}'"
|
|
94
|
+
)
|
|
95
|
+
elif param.kind == Parameter.VAR_POSITIONAL:
|
|
96
|
+
raise ValueError(f"{factory.__name__} has variadic argument '{name}'")
|
|
97
|
+
|
|
98
|
+
self.factory = factory
|
|
99
|
+
self.use_factory_name = use_factory_name
|
|
100
|
+
self.timeout = timeout
|
|
101
|
+
self.mock = mock
|
|
102
|
+
self._skip = skip
|
|
103
|
+
self._manager = manager
|
|
104
|
+
wraps(factory)(self)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def name(self) -> str:
|
|
108
|
+
"""Name of the underlying factory function"""
|
|
109
|
+
return self.factory.__name__
|
|
110
|
+
|
|
111
|
+
@cached_property
|
|
112
|
+
def dependencies(self) -> set[str]:
|
|
113
|
+
"""Names of all parameters"""
|
|
114
|
+
sig = inspect.signature(self.factory)
|
|
115
|
+
return {
|
|
116
|
+
para.name
|
|
117
|
+
for para in sig.parameters.values()
|
|
118
|
+
if para.kind is not Parameter.VAR_KEYWORD
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@cached_property
|
|
122
|
+
def optional_dependencies(self) -> set[str]:
|
|
123
|
+
"""Names of optional dependencies"""
|
|
124
|
+
sig = inspect.signature(self.factory)
|
|
125
|
+
return {
|
|
126
|
+
para.name
|
|
127
|
+
for para in sig.parameters.values()
|
|
128
|
+
if para.default is not Parameter.empty
|
|
129
|
+
and para.kind is not Parameter.VAR_KEYWORD
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def skip(self) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Whether this device should be skipped as part of build_all - it will
|
|
136
|
+
still be built if a required device depends on it
|
|
137
|
+
"""
|
|
138
|
+
return self._skip() if callable(self._skip) else self._skip
|
|
139
|
+
|
|
140
|
+
def build(
|
|
141
|
+
self,
|
|
142
|
+
mock: bool = False,
|
|
143
|
+
connect_immediately: bool = False,
|
|
144
|
+
name: str | None = None,
|
|
145
|
+
timeout: float | None = None,
|
|
146
|
+
**fixtures,
|
|
147
|
+
) -> V2:
|
|
148
|
+
"""Build this device, building any dependencies first"""
|
|
149
|
+
devices = self._manager.build_devices(
|
|
150
|
+
self,
|
|
151
|
+
fixtures=fixtures,
|
|
152
|
+
mock=mock,
|
|
153
|
+
).or_raise()
|
|
154
|
+
if connect_immediately:
|
|
155
|
+
devices.connect(timeout=timeout or self.timeout).or_raise()
|
|
156
|
+
device = devices.devices[self.name]
|
|
157
|
+
if name:
|
|
158
|
+
device.set_name(name)
|
|
159
|
+
return device # type: ignore - it's us, honest
|
|
160
|
+
|
|
161
|
+
def create(self, *args: Args.args, **kwargs: Args.kwargs) -> V2:
|
|
162
|
+
# TODO: Remove when v1 support is no longer required - see #1718
|
|
163
|
+
return self(*args, **kwargs)
|
|
164
|
+
|
|
165
|
+
def __call__(self, *args: Args.args, **kwargs: Args.kwargs) -> V2:
|
|
166
|
+
device = self.factory(*args, **kwargs)
|
|
167
|
+
if self.use_factory_name:
|
|
168
|
+
device.set_name(self.name)
|
|
169
|
+
return device
|
|
170
|
+
|
|
171
|
+
def __repr__(self) -> str:
|
|
172
|
+
params = inspect.signature(self.factory)
|
|
173
|
+
return f"<{self.name}: DeviceFactory{params}>"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# TODO: Remove when ophyd v1 support is no longer required - see #1718
|
|
177
|
+
class V1DeviceFactory(Generic[Args, V1]):
|
|
178
|
+
"""
|
|
179
|
+
Wrapper around an ophyd v1 device that holds a reference to a device
|
|
180
|
+
manager that can provide dependencies, along with default connection
|
|
181
|
+
information for how the created device should be connected.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
*,
|
|
187
|
+
factory: type[V1],
|
|
188
|
+
prefix: str,
|
|
189
|
+
mock: bool,
|
|
190
|
+
skip: SkipType,
|
|
191
|
+
wait: bool,
|
|
192
|
+
timeout: int,
|
|
193
|
+
init: OphydInitialiser[V1, Args],
|
|
194
|
+
manager: "DeviceManager",
|
|
195
|
+
):
|
|
196
|
+
self.factory = factory
|
|
197
|
+
self.prefix = prefix
|
|
198
|
+
self.mock = mock
|
|
199
|
+
self._skip = skip
|
|
200
|
+
self.wait = wait
|
|
201
|
+
self.timeout = timeout
|
|
202
|
+
self.post_create = init or (lambda x: x)
|
|
203
|
+
self._manager = manager
|
|
204
|
+
wraps(init)(self)
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def name(self) -> str:
|
|
208
|
+
"""Name of the underlying factory function"""
|
|
209
|
+
return self.post_create.__name__
|
|
210
|
+
|
|
211
|
+
@cached_property
|
|
212
|
+
def dependencies(self) -> set[str]:
|
|
213
|
+
"""Names of all parameters"""
|
|
214
|
+
sig = inspect.signature(self.post_create)
|
|
215
|
+
# first parameter should be the device we've just built
|
|
216
|
+
_, *params = sig.parameters.values()
|
|
217
|
+
return {para.name for para in params if para.kind is not Parameter.VAR_KEYWORD}
|
|
218
|
+
|
|
219
|
+
@cached_property
|
|
220
|
+
def optional_dependencies(self) -> set[str]:
|
|
221
|
+
"""Names of optional dependencies"""
|
|
222
|
+
sig = inspect.signature(self.post_create)
|
|
223
|
+
_, *params = sig.parameters.values()
|
|
224
|
+
return {para.name for para in params if para.default is not Parameter.empty}
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def skip(self) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Whether this device should be skipped as part of build_all - it will
|
|
230
|
+
still be built if a required device depends on it
|
|
231
|
+
"""
|
|
232
|
+
return self._skip() if callable(self._skip) else self._skip
|
|
233
|
+
|
|
234
|
+
def mock_if_needed(self, mock=False) -> Self:
|
|
235
|
+
# TODO: Remove when Ophyd V1 support is no longer required - see #1718
|
|
236
|
+
factory = (
|
|
237
|
+
make_fake_device(self.factory) if (self.mock or mock) else self.factory
|
|
238
|
+
)
|
|
239
|
+
return self.__class__(
|
|
240
|
+
factory=factory,
|
|
241
|
+
prefix=self.prefix,
|
|
242
|
+
mock=mock or self.mock,
|
|
243
|
+
skip=self._skip,
|
|
244
|
+
wait=self.wait,
|
|
245
|
+
timeout=self.timeout,
|
|
246
|
+
init=self.post_create,
|
|
247
|
+
manager=self._manager,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def __call__(self, dev: V1, *args: Args.args, **kwargs: Args.kwargs):
|
|
251
|
+
"""Call the wrapped function to make decorator transparent"""
|
|
252
|
+
return self.post_create(dev, *args, **kwargs)
|
|
253
|
+
|
|
254
|
+
def create(self, *args: Args.args, **kwargs: Args.kwargs) -> V1:
|
|
255
|
+
device = self.factory(name=self.name, prefix=self.prefix)
|
|
256
|
+
if self.wait:
|
|
257
|
+
wait_for_connection(device, timeout=self.timeout)
|
|
258
|
+
self.post_create(device, *args, **kwargs)
|
|
259
|
+
return device
|
|
260
|
+
|
|
261
|
+
def build(self, mock: bool = False, fixtures: dict[str, Any] | None = None) -> V1:
|
|
262
|
+
"""Build this device, building any dependencies first"""
|
|
263
|
+
devices = self._manager.build_devices(
|
|
264
|
+
self,
|
|
265
|
+
fixtures=fixtures,
|
|
266
|
+
mock=mock,
|
|
267
|
+
).or_raise()
|
|
268
|
+
|
|
269
|
+
device = devices.devices[self.name]
|
|
270
|
+
return device # type: ignore - it's us really, promise
|
|
271
|
+
|
|
272
|
+
def __repr__(self) -> str:
|
|
273
|
+
return f"<{self.name}: V1DeviceFactory[{self.factory.__name__}]>"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class ConnectionSpec(NamedTuple):
|
|
277
|
+
"""The options used to configure a device"""
|
|
278
|
+
|
|
279
|
+
mock: bool
|
|
280
|
+
timeout: float
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class ConnectionResult(NamedTuple):
|
|
284
|
+
"""Wrapper around results of building and connecting devices"""
|
|
285
|
+
|
|
286
|
+
devices: dict[str, AnyDevice]
|
|
287
|
+
build_errors: dict[str, Exception]
|
|
288
|
+
connection_errors: dict[str, Exception]
|
|
289
|
+
|
|
290
|
+
def or_raise(self) -> dict[str, Any]:
|
|
291
|
+
"""Re-raise any errors from build or connect stage or return devices"""
|
|
292
|
+
if self.build_errors or self.connection_errors:
|
|
293
|
+
all_exc = []
|
|
294
|
+
for name, exc in (self.build_errors | self.connection_errors).items():
|
|
295
|
+
exc.add_note(name)
|
|
296
|
+
all_exc.append(exc)
|
|
297
|
+
raise ExceptionGroup("Some devices failed", tuple(all_exc))
|
|
298
|
+
return self.devices
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class DeviceBuildResult(NamedTuple):
|
|
302
|
+
"""Wrapper around the results of building devices"""
|
|
303
|
+
|
|
304
|
+
devices: dict[str, AnyDevice]
|
|
305
|
+
errors: dict[str, Exception]
|
|
306
|
+
connection_specs: dict[str, ConnectionSpec]
|
|
307
|
+
|
|
308
|
+
def connect(self, timeout: float | None = None) -> ConnectionResult:
|
|
309
|
+
"""Connect all devices that didn't fail to build"""
|
|
310
|
+
connections = {}
|
|
311
|
+
connected = {}
|
|
312
|
+
loop: asyncio.EventLoop = get_bluesky_event_loop() # type: ignore
|
|
313
|
+
for name, device in self.devices.items():
|
|
314
|
+
if not isinstance(device, OphydV2Device):
|
|
315
|
+
# TODO: Remove when ophyd v1 support is no longer required - see #1718
|
|
316
|
+
# V1 devices are connected at creation time assuming wait is not set to False
|
|
317
|
+
connected[name] = device
|
|
318
|
+
continue
|
|
319
|
+
mock, dev_timeout = self.connection_specs[name]
|
|
320
|
+
timeout = timeout or dev_timeout or DEFAULT_TIMEOUT
|
|
321
|
+
fut = asyncio.run_coroutine_threadsafe(
|
|
322
|
+
device.connect(mock=mock, timeout=timeout),
|
|
323
|
+
loop=loop,
|
|
324
|
+
)
|
|
325
|
+
connections[name] = fut
|
|
326
|
+
|
|
327
|
+
connection_errors = {}
|
|
328
|
+
for name, connection_future in connections.items():
|
|
329
|
+
try:
|
|
330
|
+
connection_future.result()
|
|
331
|
+
connected[name] = self.devices[name]
|
|
332
|
+
except Exception as e:
|
|
333
|
+
connection_errors[name] = e
|
|
334
|
+
|
|
335
|
+
return ConnectionResult(connected, self.errors, connection_errors)
|
|
336
|
+
|
|
337
|
+
def or_raise(self) -> Self:
|
|
338
|
+
"""Re-raise any build errors"""
|
|
339
|
+
if self.errors:
|
|
340
|
+
for name, exc in self.errors.items():
|
|
341
|
+
exc.add_note(name)
|
|
342
|
+
raise ExceptionGroup("Some devices failed", tuple(self.errors.values()))
|
|
343
|
+
return self
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class DeviceManager:
|
|
347
|
+
"""Manager to handle building and connecting interdependent devices"""
|
|
348
|
+
|
|
349
|
+
_factories: dict[str, DeviceFactory]
|
|
350
|
+
_fixtures: dict[str, Callable[[], Any]]
|
|
351
|
+
_v1_factories: dict[str, V1DeviceFactory]
|
|
352
|
+
|
|
353
|
+
def __init__(self):
|
|
354
|
+
self._factories = {}
|
|
355
|
+
self._v1_factories = {}
|
|
356
|
+
self._fixtures = {}
|
|
357
|
+
|
|
358
|
+
def fixture(self, func: Callable[[], T]) -> Callable[[], T]:
|
|
359
|
+
"""Add a function that can provide fixtures required by the factories"""
|
|
360
|
+
self._fixtures[func.__name__] = func
|
|
361
|
+
return func
|
|
362
|
+
|
|
363
|
+
def v1_init(
|
|
364
|
+
self,
|
|
365
|
+
factory: type[V1],
|
|
366
|
+
prefix: str,
|
|
367
|
+
mock: bool = False,
|
|
368
|
+
skip: SkipType = False,
|
|
369
|
+
wait: bool = True,
|
|
370
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
371
|
+
):
|
|
372
|
+
"""
|
|
373
|
+
Register an ophyd v1 device
|
|
374
|
+
|
|
375
|
+
The function this decorates is an initialiser that takes a built device
|
|
376
|
+
and is not used to create the device.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
def decorator(init: OphydInitialiser[V1, Args]) -> V1DeviceFactory[Args, V1]:
|
|
380
|
+
name = init.__name__
|
|
381
|
+
if name in self:
|
|
382
|
+
raise ValueError(f"Duplicate factory name: {name}")
|
|
383
|
+
device_factory = V1DeviceFactory(
|
|
384
|
+
factory=factory,
|
|
385
|
+
prefix=prefix,
|
|
386
|
+
mock=mock,
|
|
387
|
+
skip=skip,
|
|
388
|
+
wait=wait,
|
|
389
|
+
timeout=timeout,
|
|
390
|
+
init=init,
|
|
391
|
+
manager=self,
|
|
392
|
+
)
|
|
393
|
+
self._v1_factories[name] = device_factory
|
|
394
|
+
return device_factory
|
|
395
|
+
|
|
396
|
+
return decorator
|
|
397
|
+
|
|
398
|
+
# Overload for using as plain decorator, ie: @devices.factory
|
|
399
|
+
@typing.overload
|
|
400
|
+
def factory(self, func: Callable[Args, V2], /) -> DeviceFactory[Args, V2]: ...
|
|
401
|
+
|
|
402
|
+
# Overload for using as configurable decorator, eg: @devices.factory(skip=True)
|
|
403
|
+
@typing.overload
|
|
404
|
+
def factory(
|
|
405
|
+
self,
|
|
406
|
+
func: NoneType = None,
|
|
407
|
+
/,
|
|
408
|
+
use_factory_name: bool = True,
|
|
409
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
410
|
+
mock: bool = False,
|
|
411
|
+
skip: SkipType = False,
|
|
412
|
+
) -> Callable[[Callable[Args, V2]], DeviceFactory[Args, V2]]: ...
|
|
413
|
+
|
|
414
|
+
def factory(
|
|
415
|
+
self,
|
|
416
|
+
func: Callable[Args, V2] | None = None,
|
|
417
|
+
/,
|
|
418
|
+
use_factory_name: Annotated[bool, "Use factory name as name of device"] = True,
|
|
419
|
+
timeout: Annotated[
|
|
420
|
+
float, "Timeout for connecting to the device"
|
|
421
|
+
] = DEFAULT_TIMEOUT,
|
|
422
|
+
mock: Annotated[bool, "Use Signals with mock backends for device"] = False,
|
|
423
|
+
skip: Annotated[
|
|
424
|
+
SkipType,
|
|
425
|
+
"mark the factory to be (conditionally) skipped when beamline is imported by external program",
|
|
426
|
+
] = False,
|
|
427
|
+
) -> DeviceFactory[Args, V2] | DeviceFactoryDecorator[Args, V2]:
|
|
428
|
+
def decorator(func: Callable[Args, V2]) -> DeviceFactory[Args, V2]:
|
|
429
|
+
if func.__name__ in self:
|
|
430
|
+
raise ValueError(f"Duplicate factory name: {func.__name__}")
|
|
431
|
+
factory = DeviceFactory(func, use_factory_name, timeout, mock, skip, self)
|
|
432
|
+
self._factories[func.__name__] = factory
|
|
433
|
+
return factory
|
|
434
|
+
|
|
435
|
+
if func is None:
|
|
436
|
+
return decorator
|
|
437
|
+
return decorator(func)
|
|
438
|
+
|
|
439
|
+
def build_and_connect(
|
|
440
|
+
self,
|
|
441
|
+
*,
|
|
442
|
+
fixtures: dict[str, Any] | None = None,
|
|
443
|
+
mock: bool = False,
|
|
444
|
+
timeout: float | None = None,
|
|
445
|
+
) -> ConnectionResult:
|
|
446
|
+
return self.build_all(fixtures=fixtures, mock=mock).connect(timeout=timeout)
|
|
447
|
+
|
|
448
|
+
def build_all(
|
|
449
|
+
self,
|
|
450
|
+
include_skipped=False,
|
|
451
|
+
fixtures: dict[str, Any] | None = None,
|
|
452
|
+
mock: bool = False,
|
|
453
|
+
) -> DeviceBuildResult:
|
|
454
|
+
# exclude all skipped devices and those that have been overridden by fixtures
|
|
455
|
+
|
|
456
|
+
return self.build_devices(
|
|
457
|
+
*(
|
|
458
|
+
f
|
|
459
|
+
for f in (self._factories | self._v1_factories).values()
|
|
460
|
+
# allow overriding skip but still allow fixtures to override devices
|
|
461
|
+
if (include_skipped or not f.skip)
|
|
462
|
+
),
|
|
463
|
+
fixtures=fixtures,
|
|
464
|
+
mock=mock,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def build_devices(
|
|
468
|
+
self,
|
|
469
|
+
*factories: DeviceFactory | V1DeviceFactory,
|
|
470
|
+
fixtures: Mapping[str, Any] | None = None,
|
|
471
|
+
mock: bool = False,
|
|
472
|
+
) -> DeviceBuildResult:
|
|
473
|
+
"""
|
|
474
|
+
Build the devices from the given factories, ensuring that any
|
|
475
|
+
dependencies are built first and passed to later factories as required.
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
fixtures = LazyFixtures(provided=fixtures, factories=self._fixtures)
|
|
479
|
+
if common := fixtures.keys() & {f.name for f in factories}:
|
|
480
|
+
factories = tuple(f for f in factories if f.name not in common)
|
|
481
|
+
build_list = self._expand_dependencies(factories, fixtures)
|
|
482
|
+
order = self._build_order(
|
|
483
|
+
{dep: self[dep] for dep in build_list}, fixtures=fixtures
|
|
484
|
+
)
|
|
485
|
+
built: dict[str, AnyDevice] = {
|
|
486
|
+
override: fixtures[override] for override in common
|
|
487
|
+
}
|
|
488
|
+
connection_specs: dict[str, ConnectionSpec] = {}
|
|
489
|
+
errors = {}
|
|
490
|
+
for device in order:
|
|
491
|
+
factory = self[device]
|
|
492
|
+
deps = factory.dependencies
|
|
493
|
+
if dep_errs := deps & errors.keys():
|
|
494
|
+
errors[device] = ValueError(f"Errors building dependencies: {dep_errs}")
|
|
495
|
+
else:
|
|
496
|
+
# If we've made it this far, any devices that aren't available must have default
|
|
497
|
+
# values so ignore anything that's missing
|
|
498
|
+
params = {
|
|
499
|
+
dep: value
|
|
500
|
+
for dep in deps
|
|
501
|
+
# get from built if it's there, from fixtures otherwise...
|
|
502
|
+
if (value := (built.get(dep, fixtures.get(dep, _EMPTY))))
|
|
503
|
+
# ...and skip if in neither
|
|
504
|
+
is not _EMPTY
|
|
505
|
+
}
|
|
506
|
+
try:
|
|
507
|
+
if isinstance(factory, V1DeviceFactory):
|
|
508
|
+
# TODO: Remove when ophyd v1 support is no longer required - see #1718
|
|
509
|
+
factory = factory.mock_if_needed(mock)
|
|
510
|
+
built_device = factory.create(**params)
|
|
511
|
+
built[device] = built_device
|
|
512
|
+
connection_specs[device] = ConnectionSpec(
|
|
513
|
+
mock=mock or factory.mock,
|
|
514
|
+
timeout=factory.timeout,
|
|
515
|
+
)
|
|
516
|
+
except Exception as e:
|
|
517
|
+
errors[device] = e
|
|
518
|
+
|
|
519
|
+
return DeviceBuildResult(built, errors, connection_specs)
|
|
520
|
+
|
|
521
|
+
def __contains__(self, name):
|
|
522
|
+
return name in self._factories or name in self._v1_factories
|
|
523
|
+
|
|
524
|
+
def __getitem__(self, name):
|
|
525
|
+
return self._factories.get(name) or self._v1_factories[name]
|
|
526
|
+
|
|
527
|
+
def _expand_dependencies(
|
|
528
|
+
self,
|
|
529
|
+
factories: Iterable[DeviceFactory[..., V2] | V1DeviceFactory[..., V1]],
|
|
530
|
+
available_fixtures: Mapping[str, Any],
|
|
531
|
+
) -> set[str]:
|
|
532
|
+
"""
|
|
533
|
+
Determine full list of devices that are required to build the given devices.
|
|
534
|
+
If a dependency is available via the fixtures, a matching device factory
|
|
535
|
+
will not be included unless explicitly requested allowing for devices to
|
|
536
|
+
be overridden.
|
|
537
|
+
|
|
538
|
+
Errors:
|
|
539
|
+
If a required dependency is not available as either a device
|
|
540
|
+
factory or a fixture, a ValueError is raised
|
|
541
|
+
"""
|
|
542
|
+
dependencies = set()
|
|
543
|
+
factories = set(factories)
|
|
544
|
+
while factories:
|
|
545
|
+
fact = factories.pop()
|
|
546
|
+
dependencies.add(fact.name)
|
|
547
|
+
options = fact.optional_dependencies
|
|
548
|
+
for dep in fact.dependencies:
|
|
549
|
+
if dep not in dependencies and dep not in available_fixtures:
|
|
550
|
+
if dep in self._factories:
|
|
551
|
+
factories.add(self[dep])
|
|
552
|
+
elif dep not in options:
|
|
553
|
+
raise ValueError(
|
|
554
|
+
f"Missing fixture or factory for {dep}",
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
return dependencies
|
|
558
|
+
|
|
559
|
+
def _build_order(
|
|
560
|
+
self,
|
|
561
|
+
factories: dict[str, DeviceFactory[..., V2] | V1DeviceFactory[..., V1]],
|
|
562
|
+
fixtures: Mapping[str, Any],
|
|
563
|
+
) -> list[str]:
|
|
564
|
+
"""
|
|
565
|
+
Determine the order devices in which devices should be build to ensure
|
|
566
|
+
that dependencies are built before the things that depend on them
|
|
567
|
+
|
|
568
|
+
Assumes that all required devices and fixtures are included in the
|
|
569
|
+
given factory list.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
# This is not an efficient way of doing this, however, for realistic use
|
|
573
|
+
# cases, it is fast enough for now
|
|
574
|
+
order = []
|
|
575
|
+
available = set(fixtures.keys())
|
|
576
|
+
pending = factories
|
|
577
|
+
while pending:
|
|
578
|
+
buffer = {}
|
|
579
|
+
for name, factory in pending.items():
|
|
580
|
+
buildable_deps = factory.dependencies & factories.keys()
|
|
581
|
+
# We should only have been called with a resolvable set of things to build
|
|
582
|
+
# but just to double check
|
|
583
|
+
assert buildable_deps.issubset(
|
|
584
|
+
factory.dependencies - factory.optional_dependencies
|
|
585
|
+
)
|
|
586
|
+
if all(dep in available for dep in buildable_deps):
|
|
587
|
+
order.append(name)
|
|
588
|
+
available.add(name)
|
|
589
|
+
else:
|
|
590
|
+
buffer[name] = factory
|
|
591
|
+
if len(pending) == len(buffer):
|
|
592
|
+
# This should only be reachable if we have circular dependencies
|
|
593
|
+
raise ValueError(
|
|
594
|
+
f"Cannot determine build order - possibly circular dependencies ({', '.join(pending.keys())})"
|
|
595
|
+
)
|
|
596
|
+
buffer, pending = [], buffer
|
|
597
|
+
|
|
598
|
+
return order
|
|
599
|
+
|
|
600
|
+
def __len__(self) -> int:
|
|
601
|
+
return len(self._factories) + len(self._v1_factories)
|
|
602
|
+
|
|
603
|
+
def __repr__(self) -> str:
|
|
604
|
+
return f"<DeviceManager: {len(self)} devices>"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from ophyd_async.epics.motor import Motor
|
|
2
2
|
|
|
3
|
-
from dodal.devices.motors import
|
|
3
|
+
from dodal.devices.motors import XYZOmegaStage, create_axis_perp_to_rotation
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class Goniometer(
|
|
6
|
+
class Goniometer(XYZOmegaStage):
|
|
7
7
|
"""The Aithre lab goniometer and the XYZ stage it sits on.
|
|
8
8
|
|
|
9
9
|
`x`, `y` and `z` control the axes of the positioner at the base, while `sampy` and
|
|
@@ -15,11 +15,28 @@ class Goniometer(XYZStage):
|
|
|
15
15
|
regardless of the current rotation.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
def __init__(
|
|
19
|
-
self
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
prefix: str,
|
|
21
|
+
name: str = "",
|
|
22
|
+
x_infix: str = "X",
|
|
23
|
+
y_infix: str = "Y",
|
|
24
|
+
z_infix: str = "Z",
|
|
25
|
+
omega_infix: str = "OMEGA",
|
|
26
|
+
sampy_infix: str = "SAMPY",
|
|
27
|
+
sampz_infix: str = "SAMPZ",
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__(
|
|
30
|
+
prefix=prefix,
|
|
31
|
+
name=name,
|
|
32
|
+
x_infix=x_infix,
|
|
33
|
+
y_infix=y_infix,
|
|
34
|
+
z_infix=z_infix,
|
|
35
|
+
omega_infix=omega_infix,
|
|
24
36
|
)
|
|
25
|
-
|
|
37
|
+
with self.add_children_as_readables():
|
|
38
|
+
self.sampy = Motor(prefix + sampy_infix)
|
|
39
|
+
self.sampz = Motor(prefix + sampz_infix)
|
|
40
|
+
self.vertical_position = create_axis_perp_to_rotation(
|
|
41
|
+
self.omega, self.sampz, self.sampy
|
|
42
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
from math import inf
|
|
4
5
|
|
|
5
6
|
from bluesky.protocols import Preparable
|
|
6
7
|
from ophyd_async.core import (
|
|
@@ -112,7 +113,7 @@ def load_positions_from_beamline_parameters(
|
|
|
112
113
|
) -> dict[ApertureValue, AperturePosition]:
|
|
113
114
|
return {
|
|
114
115
|
ApertureValue.OUT_OF_BEAM: AperturePosition.from_gda_params(
|
|
115
|
-
_GDAParamApertureValue.ROBOT_LOAD,
|
|
116
|
+
_GDAParamApertureValue.ROBOT_LOAD, inf, params
|
|
116
117
|
),
|
|
117
118
|
ApertureValue.SMALL: AperturePosition.from_gda_params(
|
|
118
119
|
_GDAParamApertureValue.SMALL, 20, params
|
|
@@ -124,7 +125,7 @@ def load_positions_from_beamline_parameters(
|
|
|
124
125
|
_GDAParamApertureValue.LARGE, 100, params
|
|
125
126
|
),
|
|
126
127
|
ApertureValue.PARKED: AperturePosition.from_gda_params(
|
|
127
|
-
_GDAParamApertureValue.MANUAL_LOAD,
|
|
128
|
+
_GDAParamApertureValue.MANUAL_LOAD, inf, params
|
|
128
129
|
),
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -28,11 +28,18 @@ class MJPG(StandardReadable, Triggerable, ABC):
|
|
|
28
28
|
latest image from the stream to the `post_processing` method for child classes to handle.
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
prefix: str,
|
|
34
|
+
name: str = "",
|
|
35
|
+
x_size_pv: str = "ArraySize1_RBV",
|
|
36
|
+
y_size_pv: str = "ArraySize2_RBV",
|
|
37
|
+
) -> None:
|
|
32
38
|
self.url = epics_signal_rw(str, prefix + "JPG_URL_RBV")
|
|
39
|
+
self.video_url = epics_signal_rw(str, prefix + "MJPG_URL_RBV")
|
|
33
40
|
|
|
34
|
-
self.x_size = epics_signal_r(int, prefix +
|
|
35
|
-
self.y_size = epics_signal_r(int, prefix +
|
|
41
|
+
self.x_size = epics_signal_r(int, prefix + x_size_pv)
|
|
42
|
+
self.y_size = epics_signal_r(int, prefix + y_size_pv)
|
|
36
43
|
|
|
37
44
|
with self.add_children_as_readables():
|
|
38
45
|
self.filename = soft_signal_rw(str)
|
|
File without changes
|