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.
Files changed (85) hide show
  1. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +82 -66
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/aithre.py +21 -2
  5. dodal/beamlines/i03.py +102 -198
  6. dodal/beamlines/i04.py +40 -4
  7. dodal/beamlines/i05.py +28 -1
  8. dodal/beamlines/i06.py +62 -0
  9. dodal/beamlines/i07.py +20 -0
  10. dodal/beamlines/i09_1.py +32 -3
  11. dodal/beamlines/i09_2.py +57 -2
  12. dodal/beamlines/i10_optics.py +46 -17
  13. dodal/beamlines/i17.py +7 -3
  14. dodal/beamlines/i18.py +3 -3
  15. dodal/beamlines/i19_1.py +26 -14
  16. dodal/beamlines/i19_2.py +49 -38
  17. dodal/beamlines/i21.py +2 -2
  18. dodal/beamlines/i22.py +19 -4
  19. dodal/beamlines/p38.py +3 -3
  20. dodal/beamlines/training_rig.py +0 -16
  21. dodal/cli.py +26 -12
  22. dodal/common/coordination.py +3 -2
  23. dodal/device_manager.py +604 -0
  24. dodal/devices/aithre_lasershaping/goniometer.py +26 -9
  25. dodal/devices/aperturescatterguard.py +3 -2
  26. dodal/devices/areadetector/plugins/mjpg.py +10 -3
  27. dodal/devices/beamsize/__init__.py +0 -0
  28. dodal/devices/beamsize/beamsize.py +6 -0
  29. dodal/devices/cryostream.py +28 -57
  30. dodal/devices/detector/det_resolution.py +4 -2
  31. dodal/devices/eiger.py +26 -18
  32. dodal/devices/fast_grid_scan.py +14 -2
  33. dodal/devices/i03/beamsize.py +35 -0
  34. dodal/devices/i03/constants.py +7 -0
  35. dodal/devices/i03/undulator_dcm.py +2 -2
  36. dodal/devices/i04/beamsize.py +45 -0
  37. dodal/devices/i04/max_pixel.py +38 -0
  38. dodal/devices/i04/murko_results.py +36 -26
  39. dodal/devices/i04/transfocator.py +23 -29
  40. dodal/devices/i07/id.py +38 -0
  41. dodal/devices/i09_1_shared/__init__.py +13 -2
  42. dodal/devices/i09_1_shared/hard_energy.py +112 -0
  43. dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
  44. dodal/devices/i09_2_shared/__init__.py +0 -0
  45. dodal/devices/i09_2_shared/i09_apple2.py +86 -0
  46. dodal/devices/i10/i10_apple2.py +39 -331
  47. dodal/devices/i17/i17_apple2.py +37 -22
  48. dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
  49. dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
  50. dodal/devices/i19/access_controlled/shutter.py +2 -4
  51. dodal/devices/insertion_device/__init__.py +0 -0
  52. dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +122 -69
  53. dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
  54. dodal/devices/insertion_device/lookup_table_models.py +287 -0
  55. dodal/devices/ipin.py +20 -2
  56. dodal/devices/motors.py +33 -3
  57. dodal/devices/mx_phase1/beamstop.py +31 -12
  58. dodal/devices/oav/oav_calculations.py +9 -4
  59. dodal/devices/oav/oav_detector.py +65 -7
  60. dodal/devices/oav/oav_parameters.py +3 -1
  61. dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
  62. dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
  63. dodal/devices/oav/pin_image_recognition/utils.py +23 -1
  64. dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
  65. dodal/devices/oav/utils.py +16 -6
  66. dodal/devices/robot.py +33 -18
  67. dodal/devices/scintillator.py +36 -14
  68. dodal/devices/smargon.py +2 -3
  69. dodal/devices/thawer.py +7 -45
  70. dodal/devices/undulator.py +152 -68
  71. dodal/plans/__init__.py +1 -1
  72. dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
  73. dodal/plans/load_panda_yaml.py +9 -0
  74. dodal/plans/verify_undulator_gap.py +2 -2
  75. dodal/testing/fixtures/devices/__init__.py +0 -0
  76. dodal/testing/fixtures/devices/apple2.py +78 -0
  77. dodal/utils.py +6 -3
  78. dodal/beamline_specific_utils/i03.py +0 -17
  79. dodal/testing/__init__.py +0 -3
  80. dodal/testing/setup.py +0 -67
  81. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/WHEEL +0 -0
  82. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
  83. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
  84. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/top_level.txt +0 -0
  85. /dodal/plans/{scanspec.py → spec_path.py} +0 -0
@@ -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 XYZStage, create_axis_perp_to_rotation
3
+ from dodal.devices.motors import XYZOmegaStage, create_axis_perp_to_rotation
4
4
 
5
5
 
6
- class Goniometer(XYZStage):
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__(self, prefix: str, name: str = "") -> None:
19
- self.sampy = Motor(prefix + "SAMPY")
20
- self.sampz = Motor(prefix + "SAMPZ")
21
- self.omega = Motor(prefix + "OMEGA")
22
- self.vertical_position = create_axis_perp_to_rotation(
23
- self.omega, self.sampz, self.sampy
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
- super().__init__(name)
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, 0, params
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, 0, params
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__(self, prefix: str, name: str = "") -> None:
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 + "ArraySize1_RBV")
35
- self.y_size = epics_signal_r(int, prefix + "ArraySize2_RBV")
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