dls-dodal 1.39.0__py3-none-any.whl → 1.41.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.39.0.dist-info → dls_dodal-1.41.0.dist-info}/METADATA +5 -3
- {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/RECORD +61 -52
- {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +9 -4
- dodal/beamlines/__init__.py +2 -0
- dodal/beamlines/adsim.py +3 -2
- dodal/beamlines/b01_1.py +3 -3
- dodal/beamlines/i03.py +141 -292
- dodal/beamlines/i04.py +112 -198
- dodal/beamlines/i13_1.py +5 -4
- dodal/beamlines/i18.py +124 -0
- dodal/beamlines/i19_1.py +74 -0
- dodal/beamlines/i19_2.py +61 -0
- dodal/beamlines/i20_1.py +37 -22
- dodal/beamlines/i22.py +7 -7
- dodal/beamlines/i23.py +8 -11
- dodal/beamlines/i24.py +100 -145
- dodal/beamlines/p38.py +84 -220
- dodal/beamlines/p45.py +5 -4
- dodal/beamlines/training_rig.py +4 -4
- dodal/common/beamlines/beamline_utils.py +2 -3
- dodal/common/beamlines/device_helpers.py +3 -1
- dodal/devices/aperturescatterguard.py +150 -64
- dodal/devices/apple2_undulator.py +89 -114
- dodal/devices/attenuator/attenuator.py +1 -1
- dodal/devices/backlight.py +1 -1
- dodal/devices/bimorph_mirror.py +2 -2
- dodal/devices/eiger.py +3 -2
- dodal/devices/fast_grid_scan.py +26 -19
- dodal/devices/hutch_shutter.py +26 -13
- dodal/devices/i10/i10_apple2.py +3 -3
- dodal/devices/i10/rasor/rasor_scaler_cards.py +4 -4
- dodal/devices/i13_1/merlin.py +4 -3
- dodal/devices/i13_1/merlin_controller.py +2 -7
- dodal/devices/i18/KBMirror.py +19 -0
- dodal/devices/i18/diode.py +17 -0
- dodal/devices/i18/table.py +14 -0
- dodal/devices/i18/thor_labs_stage.py +12 -0
- dodal/devices/i19/__init__.py +0 -0
- dodal/devices/i19/shutter.py +57 -0
- dodal/devices/i22/nxsas.py +4 -4
- dodal/devices/i24/pmac.py +2 -2
- dodal/devices/motors.py +2 -2
- dodal/devices/oav/oav_detector.py +10 -19
- dodal/devices/pressure_jump_cell.py +43 -19
- dodal/devices/robot.py +31 -12
- dodal/devices/tetramm.py +8 -3
- dodal/devices/thawer.py +4 -4
- dodal/devices/turbo_slit.py +7 -6
- dodal/devices/undulator.py +1 -1
- dodal/devices/undulator_dcm.py +1 -1
- dodal/devices/util/epics_util.py +1 -1
- dodal/devices/zebra/zebra.py +4 -3
- dodal/devices/zebra/zebra_controlled_shutter.py +1 -1
- dodal/devices/zocalo/zocalo_results.py +21 -4
- dodal/plan_stubs/wrapped.py +10 -12
- dodal/plans/save_panda.py +30 -14
- dodal/utils.py +55 -21
- {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/top_level.txt +0 -0
dodal/devices/robot.py
CHANGED
|
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
|
|
5
5
|
from bluesky.protocols import Movable
|
|
6
6
|
from ophyd_async.core import (
|
|
7
7
|
AsyncStatus,
|
|
8
|
+
Device,
|
|
8
9
|
StandardReadable,
|
|
9
10
|
StrictEnum,
|
|
10
11
|
set_and_wait_for_value,
|
|
@@ -38,23 +39,36 @@ class PinMounted(StrictEnum):
|
|
|
38
39
|
PIN_MOUNTED = "Pin Mounted"
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
class
|
|
42
|
+
class ErrorStatus(Device):
|
|
43
|
+
def __init__(self, prefix: str) -> None:
|
|
44
|
+
self.str = epics_signal_r(str, prefix + "_ERR_MSG")
|
|
45
|
+
self.code = epics_signal_r(int, prefix + "_ERR_CODE")
|
|
46
|
+
super().__init__()
|
|
47
|
+
|
|
48
|
+
async def raise_if_error(self, raise_from: Exception):
|
|
49
|
+
error_code = await self.code.get_value()
|
|
50
|
+
if error_code:
|
|
51
|
+
error_string = await self.str.get_value()
|
|
52
|
+
raise RobotLoadFailed(int(error_code), error_string) from raise_from
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
42
56
|
"""The sample changing robot."""
|
|
43
57
|
|
|
44
58
|
# How long to wait for the robot if it is busy soaking/drying
|
|
45
59
|
NOT_BUSY_TIMEOUT = 5 * 60
|
|
60
|
+
|
|
46
61
|
# How long to wait for the actual load to happen
|
|
47
62
|
LOAD_TIMEOUT = 60
|
|
63
|
+
|
|
64
|
+
# Error codes that we do special things on
|
|
48
65
|
NO_PIN_ERROR_CODE = 25
|
|
66
|
+
LIGHT_CURTAIN_TRIPPED = 40
|
|
49
67
|
|
|
50
68
|
# How far the gonio position can be out before loading will fail
|
|
51
69
|
LOAD_TOLERANCE_MM = 0.02
|
|
52
70
|
|
|
53
|
-
def __init__(
|
|
54
|
-
self,
|
|
55
|
-
name: str,
|
|
56
|
-
prefix: str,
|
|
57
|
-
) -> None:
|
|
71
|
+
def __init__(self, name: str, prefix: str) -> None:
|
|
58
72
|
self.barcode = epics_signal_r(str, prefix + "BARCODE")
|
|
59
73
|
self.gonio_pin_sensor = epics_signal_r(PinMounted, prefix + "PIN_MOUNTED")
|
|
60
74
|
|
|
@@ -69,8 +83,10 @@ class BartRobot(StandardReadable, Movable):
|
|
|
69
83
|
self.load = epics_signal_x(prefix + "LOAD.PROC")
|
|
70
84
|
self.program_running = epics_signal_r(bool, prefix + "PROGRAM_RUNNING")
|
|
71
85
|
self.program_name = epics_signal_r(str, prefix + "PROGRAM_NAME")
|
|
72
|
-
|
|
73
|
-
self.
|
|
86
|
+
|
|
87
|
+
self.prog_error = ErrorStatus(prefix + "PRG")
|
|
88
|
+
self.controller_error = ErrorStatus(prefix + "CNTL")
|
|
89
|
+
|
|
74
90
|
self.reset = epics_signal_x(prefix + "RESET.PROC")
|
|
75
91
|
super().__init__(name=name)
|
|
76
92
|
|
|
@@ -81,7 +97,7 @@ class BartRobot(StandardReadable, Movable):
|
|
|
81
97
|
"""
|
|
82
98
|
|
|
83
99
|
async def raise_if_no_pin():
|
|
84
|
-
await wait_for_value(self.
|
|
100
|
+
await wait_for_value(self.prog_error.code, self.NO_PIN_ERROR_CODE, None)
|
|
85
101
|
raise RobotLoadFailed(self.NO_PIN_ERROR_CODE, "Pin was not detected")
|
|
86
102
|
|
|
87
103
|
async def wfv():
|
|
@@ -108,6 +124,9 @@ class BartRobot(StandardReadable, Movable):
|
|
|
108
124
|
raise
|
|
109
125
|
|
|
110
126
|
async def _load_pin_and_puck(self, sample_location: SampleLocation):
|
|
127
|
+
if await self.controller_error.code.get_value() == self.LIGHT_CURTAIN_TRIPPED:
|
|
128
|
+
LOGGER.info("Light curtain tripped, trying again")
|
|
129
|
+
await self.reset.trigger()
|
|
111
130
|
LOGGER.info(f"Loading pin {sample_location}")
|
|
112
131
|
if await self.program_running.get_value():
|
|
113
132
|
LOGGER.info(
|
|
@@ -137,6 +156,6 @@ class BartRobot(StandardReadable, Movable):
|
|
|
137
156
|
)
|
|
138
157
|
except (asyncio.TimeoutError, TimeoutError) as e:
|
|
139
158
|
# Will only need to catch asyncio.TimeoutError after https://github.com/bluesky/ophyd-async/issues/572
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
raise RobotLoadFailed(
|
|
159
|
+
await self.prog_error.raise_if_error(e)
|
|
160
|
+
await self.controller_error.raise_if_error(e)
|
|
161
|
+
raise RobotLoadFailed(0, "Robot timed out") from e
|
dodal/devices/tetramm.py
CHANGED
|
@@ -13,7 +13,12 @@ from ophyd_async.core import (
|
|
|
13
13
|
set_and_wait_for_value,
|
|
14
14
|
soft_signal_r_and_setter,
|
|
15
15
|
)
|
|
16
|
-
from ophyd_async.epics.adcore import
|
|
16
|
+
from ophyd_async.epics.adcore import (
|
|
17
|
+
ADHDFWriter,
|
|
18
|
+
NDFileHDFIO,
|
|
19
|
+
NDPluginBaseIO,
|
|
20
|
+
stop_busy_record,
|
|
21
|
+
)
|
|
17
22
|
from ophyd_async.epics.core import (
|
|
18
23
|
epics_signal_r,
|
|
19
24
|
epics_signal_rw,
|
|
@@ -221,7 +226,7 @@ class TetrammDetector(StandardDetector):
|
|
|
221
226
|
path_provider: PathProvider,
|
|
222
227
|
name: str = "",
|
|
223
228
|
type: str | None = None,
|
|
224
|
-
|
|
229
|
+
plugins: dict[str, NDPluginBaseIO] | None = None,
|
|
225
230
|
) -> None:
|
|
226
231
|
self.drv = TetrammDriver(prefix + "DRV:")
|
|
227
232
|
self.hdf = NDFileHDFIO(prefix + "HDF5:")
|
|
@@ -243,7 +248,7 @@ class TetrammDetector(StandardDetector):
|
|
|
243
248
|
path_provider,
|
|
244
249
|
lambda: self.name,
|
|
245
250
|
TetrammDatasetDescriber(controller),
|
|
246
|
-
|
|
251
|
+
plugins=plugins,
|
|
247
252
|
),
|
|
248
253
|
config_signals,
|
|
249
254
|
name,
|
dodal/devices/thawer.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from asyncio import Task, create_task, sleep
|
|
2
2
|
|
|
3
|
-
from bluesky.protocols import Stoppable
|
|
3
|
+
from bluesky.protocols import Movable, Stoppable
|
|
4
4
|
from ophyd_async.core import (
|
|
5
5
|
AsyncStatus,
|
|
6
6
|
Device,
|
|
@@ -21,18 +21,18 @@ class ThawerStates(StrictEnum):
|
|
|
21
21
|
ON = "On"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
class ThawingTimer(Device, Stoppable):
|
|
24
|
+
class ThawingTimer(Device, Stoppable, Movable[float]):
|
|
25
25
|
def __init__(self, control_signal: SignalRW[ThawerStates]) -> None:
|
|
26
26
|
self._control_signal_ref = Reference(control_signal)
|
|
27
27
|
self._thawing_task: Task | None = None
|
|
28
28
|
super().__init__("thaw_for_time_s")
|
|
29
29
|
|
|
30
30
|
@AsyncStatus.wrap
|
|
31
|
-
async def set(self,
|
|
31
|
+
async def set(self, value: float):
|
|
32
32
|
await self._control_signal_ref().set(ThawerStates.ON)
|
|
33
33
|
if self._thawing_task and not self._thawing_task.done():
|
|
34
34
|
raise ThawingException("Thawing task already in progress")
|
|
35
|
-
self._thawing_task = create_task(sleep(
|
|
35
|
+
self._thawing_task = create_task(sleep(value))
|
|
36
36
|
try:
|
|
37
37
|
await self._thawing_task
|
|
38
38
|
finally:
|
dodal/devices/turbo_slit.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from ophyd_async.core import
|
|
1
|
+
from ophyd_async.core import StandardReadable
|
|
2
2
|
from ophyd_async.epics.motor import Motor
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
class TurboSlit(
|
|
5
|
+
class TurboSlit(StandardReadable):
|
|
6
6
|
"""
|
|
7
7
|
This collection of motors coordinates time resolved XAS experiments.
|
|
8
8
|
It selects a beam out of the polychromatic fan.
|
|
@@ -17,8 +17,9 @@ class TurboSlit(Device):
|
|
|
17
17
|
- xfine selects the energy as part of the high frequency scan
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
def __init__(self, prefix: str, name: str):
|
|
21
|
-
self.
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
21
|
+
with self.add_children_as_readables():
|
|
22
|
+
self.gap = Motor(prefix=prefix + "GAP")
|
|
23
|
+
self.arc = Motor(prefix=prefix + "ARC")
|
|
24
|
+
self.xfine = Motor(prefix=prefix + "XFINE")
|
|
24
25
|
super().__init__(name=name)
|
dodal/devices/undulator.py
CHANGED
|
@@ -46,7 +46,7 @@ def _get_closest_gap_for_energy(
|
|
|
46
46
|
return table[1][idx]
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
class Undulator(StandardReadable, Movable):
|
|
49
|
+
class Undulator(StandardReadable, Movable[float]):
|
|
50
50
|
"""
|
|
51
51
|
An Undulator-type insertion device, used to control photon emission at a given
|
|
52
52
|
beam energy.
|
dodal/devices/undulator_dcm.py
CHANGED
|
@@ -12,7 +12,7 @@ from .undulator import Undulator
|
|
|
12
12
|
ENERGY_TIMEOUT_S: float = 30.0
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class UndulatorDCM(StandardReadable, Movable):
|
|
15
|
+
class UndulatorDCM(StandardReadable, Movable[float]):
|
|
16
16
|
"""
|
|
17
17
|
Composite device to handle changing beamline energies, wraps the Undulator and the
|
|
18
18
|
DCM. The DCM has a motor which controls the beam energy, when it moves, the
|
dodal/devices/util/epics_util.py
CHANGED
|
@@ -114,7 +114,7 @@ def call_func(func: Callable[[], StatusBase]) -> StatusBase:
|
|
|
114
114
|
return func()
|
|
115
115
|
|
|
116
116
|
|
|
117
|
-
class SetWhenEnabled(OphydAsyncDevice, Movable):
|
|
117
|
+
class SetWhenEnabled(OphydAsyncDevice, Movable[int]):
|
|
118
118
|
"""A device that sets the proc field of a PV when it becomes enabled."""
|
|
119
119
|
|
|
120
120
|
def __init__(self, name: str = "", prefix: str = ""):
|
dodal/devices/zebra/zebra.py
CHANGED
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from functools import partialmethod
|
|
6
6
|
|
|
7
|
+
from bluesky.protocols import Movable
|
|
7
8
|
from ophyd_async.core import (
|
|
8
9
|
AsyncStatus,
|
|
9
10
|
DeviceVector,
|
|
@@ -74,7 +75,7 @@ class SoftInState(StrictEnum):
|
|
|
74
75
|
NO = "No"
|
|
75
76
|
|
|
76
77
|
|
|
77
|
-
class ArmingDevice(StandardReadable):
|
|
78
|
+
class ArmingDevice(StandardReadable, Movable[ArmDemand]):
|
|
78
79
|
"""A useful device that can abstract some of the logic of arming.
|
|
79
80
|
Allows a user to just call arm.set(ArmDemand.ARM)"""
|
|
80
81
|
|
|
@@ -94,8 +95,8 @@ class ArmingDevice(StandardReadable):
|
|
|
94
95
|
return
|
|
95
96
|
|
|
96
97
|
@AsyncStatus.wrap
|
|
97
|
-
async def set(self,
|
|
98
|
-
await asyncio.wait_for(self._set_armed(
|
|
98
|
+
async def set(self, value: ArmDemand):
|
|
99
|
+
await asyncio.wait_for(self._set_armed(value), timeout=self.TIMEOUT)
|
|
99
100
|
|
|
100
101
|
|
|
101
102
|
class PositionCompare(StandardReadable):
|
|
@@ -19,7 +19,7 @@ class ZebraShutterControl(StrictEnum):
|
|
|
19
19
|
AUTO = "Auto"
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class ZebraShutter(StandardReadable, Movable):
|
|
22
|
+
class ZebraShutter(StandardReadable, Movable[ZebraShutterState]):
|
|
23
23
|
"""The shutter on most MX beamlines is controlled by the zebra.
|
|
24
24
|
|
|
25
25
|
Internally in the zebra there are two AND gates, one for manual control and one for
|
|
@@ -129,7 +129,12 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
129
129
|
|
|
130
130
|
prefix (str): EPICS PV prefix for the device
|
|
131
131
|
|
|
132
|
-
use_cpu_and_gpu (bool): When True, ZocaloResults will wait for results from the
|
|
132
|
+
use_cpu_and_gpu (bool): When True, ZocaloResults will wait for results from the
|
|
133
|
+
CPU and the GPU, compare them, and provide a warning if the results differ. When
|
|
134
|
+
False, ZocaloResults will only use results from the CPU
|
|
135
|
+
|
|
136
|
+
use_gpu (bool): When True, ZocaloResults will take the first set of
|
|
137
|
+
results that it receives (which are likely the GPU results)
|
|
133
138
|
|
|
134
139
|
"""
|
|
135
140
|
|
|
@@ -142,6 +147,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
142
147
|
timeout_s: float = DEFAULT_TIMEOUT,
|
|
143
148
|
prefix: str = "",
|
|
144
149
|
use_cpu_and_gpu: bool = False,
|
|
150
|
+
use_gpu: bool = False,
|
|
145
151
|
) -> None:
|
|
146
152
|
self.zocalo_environment = zocalo_environment
|
|
147
153
|
self.sort_key = SortKeys[sort_key]
|
|
@@ -151,6 +157,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
151
157
|
self._raw_results_received: Queue = Queue()
|
|
152
158
|
self.transport: CommonTransport | None = None
|
|
153
159
|
self.use_cpu_and_gpu = use_cpu_and_gpu
|
|
160
|
+
self.use_gpu = use_gpu
|
|
154
161
|
|
|
155
162
|
self.centre_of_mass, self._com_setter = soft_signal_r_and_setter(
|
|
156
163
|
Array1D[np.float64], name="centre_of_mass"
|
|
@@ -213,6 +220,11 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
213
220
|
clearing the queue. Plans using this device should wait on ZOCALO_STAGE_GROUP
|
|
214
221
|
before triggering processing for the experiment"""
|
|
215
222
|
|
|
223
|
+
if self.use_cpu_and_gpu and self.use_gpu:
|
|
224
|
+
raise ValueError(
|
|
225
|
+
"Cannot compare GPU and CPU results and use GPU results at the same time."
|
|
226
|
+
)
|
|
227
|
+
|
|
216
228
|
LOGGER.info("Subscribing to results queue")
|
|
217
229
|
try:
|
|
218
230
|
self._subscribe_to_results()
|
|
@@ -253,8 +265,13 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
253
265
|
raw_results = self._raw_results_received.get(timeout=self.timeout_s)
|
|
254
266
|
source_of_first_results = source_from_results(raw_results)
|
|
255
267
|
|
|
256
|
-
|
|
268
|
+
if self.use_gpu and source_of_first_results == ZocaloSource.CPU:
|
|
269
|
+
LOGGER.warning(
|
|
270
|
+
"Configured to use GPU results but CPU came first, using CPU results."
|
|
271
|
+
)
|
|
272
|
+
|
|
257
273
|
if self.use_cpu_and_gpu:
|
|
274
|
+
# Wait for results from CPU and GPU, warn and continue if only GPU times out. Error if CPU times out
|
|
258
275
|
if source_of_first_results == ZocaloSource.CPU:
|
|
259
276
|
LOGGER.warning("Received zocalo results from CPU before GPU")
|
|
260
277
|
raw_results_two_sources = [raw_results]
|
|
@@ -303,7 +320,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
303
320
|
raise err
|
|
304
321
|
|
|
305
322
|
LOGGER.info(
|
|
306
|
-
f"Zocalo results from {
|
|
323
|
+
f"Zocalo results from {source_from_results(raw_results)} processing: found {len(raw_results['results'])} crystals."
|
|
307
324
|
)
|
|
308
325
|
# Sort from strongest to weakest in case of multiple crystals
|
|
309
326
|
await self._put_results(
|
|
@@ -335,7 +352,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
335
352
|
|
|
336
353
|
results = message.get("results", [])
|
|
337
354
|
|
|
338
|
-
if self.use_cpu_and_gpu:
|
|
355
|
+
if self.use_cpu_and_gpu or self.use_gpu:
|
|
339
356
|
self._raw_results_received.put(
|
|
340
357
|
{"results": results, "recipe_parameters": recipe_parameters}
|
|
341
358
|
)
|
dodal/plan_stubs/wrapped.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
from collections.abc import Mapping
|
|
3
|
-
from typing import Annotated,
|
|
3
|
+
from typing import Annotated, TypeVar
|
|
4
4
|
|
|
5
5
|
import bluesky.plan_stubs as bps
|
|
6
6
|
from bluesky.protocols import Movable
|
|
@@ -12,17 +12,17 @@ Wrappers for Bluesky built-in plan stubs with type hinting
|
|
|
12
12
|
|
|
13
13
|
Group = Annotated[str, "String identifier used by 'wait' or stubs that await"]
|
|
14
14
|
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
15
17
|
|
|
16
|
-
# After bluesky 1.14, bounds for stubs that move can be narrowed
|
|
17
|
-
# https://github.com/bluesky/bluesky/issues/1821
|
|
18
18
|
def set_absolute(
|
|
19
|
-
movable: Movable, value:
|
|
19
|
+
movable: Movable[T], value: T, group: Group | None = None, wait: bool = False
|
|
20
20
|
) -> MsgGenerator:
|
|
21
21
|
"""
|
|
22
22
|
Set a device, wrapper for `bp.abs_set`.
|
|
23
23
|
|
|
24
24
|
Args:
|
|
25
|
-
movable (Movable): The device to set
|
|
25
|
+
movable (Movable[T]): The device to set
|
|
26
26
|
value (T): The new value
|
|
27
27
|
group (Group | None, optional): The message group to associate with the
|
|
28
28
|
setting, for sequencing. Defaults to None.
|
|
@@ -39,7 +39,7 @@ def set_absolute(
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def set_relative(
|
|
42
|
-
movable: Movable, value:
|
|
42
|
+
movable: Movable[T], value: T, group: Group | None = None, wait: bool = False
|
|
43
43
|
) -> MsgGenerator:
|
|
44
44
|
"""
|
|
45
45
|
Change a device, wrapper for `bp.rel_set`.
|
|
@@ -62,7 +62,7 @@ def set_relative(
|
|
|
62
62
|
return (yield from bps.rel_set(movable, value, group=group, wait=wait))
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
def move(moves: Mapping[Movable,
|
|
65
|
+
def move(moves: Mapping[Movable[T], T], group: Group | None = None) -> MsgGenerator:
|
|
66
66
|
"""
|
|
67
67
|
Move a device, wrapper for `bp.mv`.
|
|
68
68
|
|
|
@@ -79,13 +79,12 @@ def move(moves: Mapping[Movable, Any], group: Group | None = None) -> MsgGenerat
|
|
|
79
79
|
"""
|
|
80
80
|
|
|
81
81
|
return (
|
|
82
|
-
|
|
83
|
-
yield from bps.mv(*itertools.chain.from_iterable(moves.items()), group=group) # type: ignore
|
|
82
|
+
yield from bps.mv(*itertools.chain.from_iterable(moves.items()), group=group)
|
|
84
83
|
)
|
|
85
84
|
|
|
86
85
|
|
|
87
86
|
def move_relative(
|
|
88
|
-
moves: Mapping[Movable,
|
|
87
|
+
moves: Mapping[Movable[T], T], group: Group | None = None
|
|
89
88
|
) -> MsgGenerator:
|
|
90
89
|
"""
|
|
91
90
|
Move a device relative to its current position, wrapper for `bp.mvr`.
|
|
@@ -103,8 +102,7 @@ def move_relative(
|
|
|
103
102
|
"""
|
|
104
103
|
|
|
105
104
|
return (
|
|
106
|
-
|
|
107
|
-
yield from bps.mvr(*itertools.chain.from_iterable(moves.items()), group=group) # type: ignore
|
|
105
|
+
yield from bps.mvr(*itertools.chain.from_iterable(moves.items()), group=group)
|
|
108
106
|
)
|
|
109
107
|
|
|
110
108
|
|
dodal/plans/save_panda.py
CHANGED
|
@@ -6,8 +6,10 @@ from pathlib import Path
|
|
|
6
6
|
from typing import cast
|
|
7
7
|
|
|
8
8
|
from bluesky.run_engine import RunEngine
|
|
9
|
-
from ophyd_async.core import Device,
|
|
10
|
-
from ophyd_async.
|
|
9
|
+
from ophyd_async.core import Device, YamlSettingsProvider
|
|
10
|
+
from ophyd_async.plan_stubs import (
|
|
11
|
+
store_settings,
|
|
12
|
+
)
|
|
11
13
|
|
|
12
14
|
from dodal.beamlines import module_name_for_beamline
|
|
13
15
|
from dodal.utils import make_device
|
|
@@ -17,20 +19,24 @@ def main(argv: list[str]):
|
|
|
17
19
|
"""CLI Utility to save the panda configuration."""
|
|
18
20
|
parser = ArgumentParser(description="Save an ophyd_async panda to yaml")
|
|
19
21
|
parser.add_argument(
|
|
20
|
-
"--beamline", help="
|
|
22
|
+
"--beamline", help="Beamline to save from e.g. i03. Defaults to BEAMLINE"
|
|
21
23
|
)
|
|
22
24
|
parser.add_argument(
|
|
23
25
|
"--device-name",
|
|
24
|
-
help='
|
|
26
|
+
help='Name of the device. The default is "panda"',
|
|
25
27
|
default="panda",
|
|
26
28
|
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--output-file",
|
|
31
|
+
help="Path to output file, including filename, eg '/scratch/panda_settings'. '.yaml' is appended to the file name automatically",
|
|
32
|
+
required=True,
|
|
33
|
+
)
|
|
27
34
|
parser.add_argument(
|
|
28
35
|
"-f",
|
|
29
36
|
"--force",
|
|
30
37
|
action=argparse.BooleanOptionalAction,
|
|
31
38
|
help="Force overwriting an existing file",
|
|
32
39
|
)
|
|
33
|
-
parser.add_argument("output_file", help="output filename")
|
|
34
40
|
|
|
35
41
|
# this exit()s with message/help unless args parsed successfully
|
|
36
42
|
args = parser.parse_args(argv[1:])
|
|
@@ -40,6 +46,9 @@ def main(argv: list[str]):
|
|
|
40
46
|
output_file = args.output_file
|
|
41
47
|
force = args.force
|
|
42
48
|
|
|
49
|
+
p = Path(output_file)
|
|
50
|
+
output_directory, file_name = str(p.parent), str(p.name)
|
|
51
|
+
|
|
43
52
|
if beamline:
|
|
44
53
|
os.environ["BEAMLINE"] = beamline
|
|
45
54
|
else:
|
|
@@ -49,36 +58,43 @@ def main(argv: list[str]):
|
|
|
49
58
|
sys.stderr.write("BEAMLINE not set and --beamline not specified\n")
|
|
50
59
|
return 1
|
|
51
60
|
|
|
52
|
-
if Path(
|
|
61
|
+
if Path(f"{output_directory}/{file_name}").exists() and not force:
|
|
53
62
|
sys.stderr.write(
|
|
54
|
-
f"Output file {
|
|
63
|
+
f"Output file {output_directory}/{file_name} already exists and --force not specified."
|
|
55
64
|
)
|
|
56
65
|
return 1
|
|
57
66
|
|
|
58
|
-
_save_panda(beamline, device_name,
|
|
67
|
+
_save_panda(beamline, device_name, output_directory, file_name)
|
|
59
68
|
|
|
60
69
|
print("Done.")
|
|
61
70
|
return 0
|
|
62
71
|
|
|
63
72
|
|
|
64
|
-
def _save_panda(beamline, device_name,
|
|
73
|
+
def _save_panda(beamline, device_name, output_directory, file_name):
|
|
65
74
|
RE = RunEngine()
|
|
66
75
|
print("Creating devices...")
|
|
67
76
|
module_name = module_name_for_beamline(beamline)
|
|
68
77
|
try:
|
|
69
|
-
devices = make_device(
|
|
78
|
+
devices = make_device(
|
|
79
|
+
f"dodal.beamlines.{module_name}", device_name, connect_immediately=True
|
|
80
|
+
)
|
|
70
81
|
except Exception as error:
|
|
71
82
|
sys.stderr.write(f"Couldn't create device {device_name}: {error}\n")
|
|
72
83
|
sys.exit(1)
|
|
73
84
|
|
|
74
85
|
panda = devices[device_name]
|
|
75
|
-
print(
|
|
76
|
-
|
|
86
|
+
print(
|
|
87
|
+
f"Saving to {output_directory}/{file_name} from {device_name} on {beamline}..."
|
|
88
|
+
)
|
|
89
|
+
_save_panda_to_yaml(RE, cast(Device, panda), file_name, output_directory)
|
|
77
90
|
|
|
78
91
|
|
|
79
|
-
def
|
|
92
|
+
def _save_panda_to_yaml(
|
|
93
|
+
RE: RunEngine, panda: Device, file_name: str, output_directory: str
|
|
94
|
+
):
|
|
80
95
|
def save_to_file():
|
|
81
|
-
|
|
96
|
+
provider = YamlSettingsProvider(output_directory)
|
|
97
|
+
yield from store_settings(provider, file_name, panda)
|
|
82
98
|
|
|
83
99
|
RE(save_to_file())
|
|
84
100
|
|
dodal/utils.py
CHANGED
|
@@ -16,6 +16,7 @@ from typing import (
|
|
|
16
16
|
Any,
|
|
17
17
|
Generic,
|
|
18
18
|
Protocol,
|
|
19
|
+
TypeAlias,
|
|
19
20
|
TypeGuard,
|
|
20
21
|
TypeVar,
|
|
21
22
|
runtime_checkable,
|
|
@@ -43,12 +44,6 @@ from ophyd_async.core import Device as OphydV2Device
|
|
|
43
44
|
|
|
44
45
|
import dodal.log
|
|
45
46
|
|
|
46
|
-
try:
|
|
47
|
-
from typing import TypeAlias
|
|
48
|
-
except ImportError:
|
|
49
|
-
from typing import TypeAlias
|
|
50
|
-
|
|
51
|
-
|
|
52
47
|
#: Protocols defining interface to hardware
|
|
53
48
|
BLUESKY_PROTOCOLS = [
|
|
54
49
|
Checkable,
|
|
@@ -102,7 +97,7 @@ class BeamlinePrefix:
|
|
|
102
97
|
|
|
103
98
|
|
|
104
99
|
T = TypeVar("T", bound=AnyDevice)
|
|
105
|
-
|
|
100
|
+
|
|
106
101
|
SkipType = bool | Callable[[], bool]
|
|
107
102
|
|
|
108
103
|
|
|
@@ -119,16 +114,16 @@ def skip_device(precondition=lambda: True):
|
|
|
119
114
|
return decorator
|
|
120
115
|
|
|
121
116
|
|
|
122
|
-
class DeviceInitializationController(Generic[
|
|
117
|
+
class DeviceInitializationController(Generic[T]):
|
|
123
118
|
def __init__(
|
|
124
119
|
self,
|
|
125
|
-
factory: Callable[[],
|
|
120
|
+
factory: Callable[[], T],
|
|
126
121
|
use_factory_name: bool,
|
|
127
122
|
timeout: float,
|
|
128
123
|
mock: bool,
|
|
129
124
|
skip: SkipType,
|
|
130
125
|
):
|
|
131
|
-
self._factory: Callable[
|
|
126
|
+
self._factory: Callable[..., T] = functools.cache(factory)
|
|
132
127
|
self._use_factory_name = use_factory_name
|
|
133
128
|
self._timeout = timeout
|
|
134
129
|
self._mock = mock
|
|
@@ -153,13 +148,15 @@ class DeviceInitializationController(Generic[D]):
|
|
|
153
148
|
name: str | None = None,
|
|
154
149
|
connection_timeout: float | None = None,
|
|
155
150
|
mock: bool | None = None,
|
|
156
|
-
|
|
151
|
+
**kwargs,
|
|
152
|
+
) -> T:
|
|
157
153
|
"""Returns an instance of the Device the wrapped factory produces: the same
|
|
158
154
|
instance will be returned if this method is called multiple times, and arguments
|
|
159
155
|
may be passed to override this Controller's configuration.
|
|
160
156
|
Once the device is connected, the value of mock must be consistent, or connect
|
|
161
157
|
must be False.
|
|
162
158
|
|
|
159
|
+
Additional keyword arguments will be passed through to the wrapped factory function.
|
|
163
160
|
|
|
164
161
|
Args:
|
|
165
162
|
connect_immediately (bool, default False): whether to call connect on the
|
|
@@ -182,19 +179,36 @@ class DeviceInitializationController(Generic[D]):
|
|
|
182
179
|
connect is called on the Device.
|
|
183
180
|
|
|
184
181
|
Returns:
|
|
185
|
-
|
|
182
|
+
T: a singleton instance of the Device class returned by the wrapped factory.
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
RuntimeError: If the device factory was invoked again with different
|
|
186
|
+
keyword arguments, without previously invoking cache_clear()
|
|
186
187
|
"""
|
|
187
|
-
|
|
188
|
+
is_v2_device = is_v2_device_factory(self._factory)
|
|
189
|
+
is_mock = mock if mock is not None else self._mock
|
|
190
|
+
if is_v2_device:
|
|
191
|
+
device: T = self._factory(**kwargs)
|
|
192
|
+
else:
|
|
193
|
+
device: T = self._factory(mock=is_mock, **kwargs)
|
|
194
|
+
|
|
195
|
+
if self._factory.cache_info().currsize > 1: # type: ignore
|
|
196
|
+
raise RuntimeError(
|
|
197
|
+
f"Device factory method called multiple times with different parameters: "
|
|
198
|
+
f"{self.__name__}" # type: ignore
|
|
199
|
+
)
|
|
188
200
|
|
|
189
201
|
if connect_immediately:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
timeout=connection_timeout
|
|
193
|
-
if connection_timeout is not None
|
|
194
|
-
else self._timeout,
|
|
195
|
-
mock=mock if mock is not None else self._mock,
|
|
196
|
-
)
|
|
202
|
+
timeout = (
|
|
203
|
+
connection_timeout if connection_timeout is not None else self._timeout
|
|
197
204
|
)
|
|
205
|
+
if is_v2_device:
|
|
206
|
+
call_in_bluesky_event_loop(
|
|
207
|
+
device.connect(timeout=timeout, mock=is_mock)
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
assert is_v1_device_type(type(device))
|
|
211
|
+
device.wait_for_connection(timeout=timeout) # type: ignore
|
|
198
212
|
|
|
199
213
|
if name:
|
|
200
214
|
device.set_name(name)
|
|
@@ -410,7 +424,27 @@ def is_any_device_factory(func: Callable) -> TypeGuard[AnyDeviceFactory]:
|
|
|
410
424
|
|
|
411
425
|
|
|
412
426
|
def is_v2_device_type(obj: type[Any]) -> bool:
|
|
413
|
-
|
|
427
|
+
non_parameterized_class = None
|
|
428
|
+
if obj != inspect.Signature.empty:
|
|
429
|
+
if inspect.isclass(obj):
|
|
430
|
+
non_parameterized_class = obj
|
|
431
|
+
elif hasattr(obj, "__origin__"):
|
|
432
|
+
# typing._GenericAlias is the same as types.GenericAlias, maybe?
|
|
433
|
+
# This is all very badly documented and possibly prone to change in future versions of Python
|
|
434
|
+
non_parameterized_class = obj.__origin__
|
|
435
|
+
if non_parameterized_class:
|
|
436
|
+
try:
|
|
437
|
+
return non_parameterized_class and issubclass(
|
|
438
|
+
non_parameterized_class, OphydV2Device
|
|
439
|
+
)
|
|
440
|
+
except TypeError:
|
|
441
|
+
# Python 3.10 will return inspect.isclass(t) == True but then
|
|
442
|
+
# raise TypeError: issubclass() arg 1 must be a class
|
|
443
|
+
# when inspecting device_factory decorator function itself
|
|
444
|
+
# Later versions of Python seem not to be affected
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
return False
|
|
414
448
|
|
|
415
449
|
|
|
416
450
|
def is_v1_device_type(obj: type[Any]) -> bool:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|