dls-dodal 1.36.0__py3-none-any.whl → 1.36.1a0__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.36.0.dist-info → dls_dodal-1.36.1a0.dist-info}/METADATA +33 -33
- {dls_dodal-1.36.0.dist-info → dls_dodal-1.36.1a0.dist-info}/RECORD +37 -33
- {dls_dodal-1.36.0.dist-info → dls_dodal-1.36.1a0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/b01_1.py +16 -31
- dodal/beamlines/i22.py +124 -265
- dodal/beamlines/i24.py +56 -7
- dodal/beamlines/p38.py +16 -1
- dodal/beamlines/p99.py +22 -53
- dodal/beamlines/training_rig.py +16 -26
- dodal/cli.py +54 -8
- dodal/common/beamlines/beamline_utils.py +32 -2
- dodal/common/beamlines/device_helpers.py +2 -0
- dodal/devices/aperture.py +7 -0
- dodal/devices/aperturescatterguard.py +195 -79
- dodal/devices/dcm.py +5 -4
- dodal/devices/fast_grid_scan.py +21 -46
- dodal/devices/focusing_mirror.py +8 -3
- dodal/devices/i24/beam_center.py +12 -0
- dodal/devices/i24/focus_mirrors.py +60 -0
- dodal/devices/i24/pilatus_metadata.py +44 -0
- dodal/devices/linkam3.py +1 -1
- dodal/devices/motors.py +14 -10
- dodal/devices/oav/oav_detector.py +2 -2
- dodal/devices/oav/pin_image_recognition/__init__.py +4 -5
- dodal/devices/oav/utils.py +1 -0
- dodal/devices/p99/sample_stage.py +12 -16
- dodal/devices/pressure_jump_cell.py +299 -0
- dodal/devices/robot.py +1 -1
- dodal/devices/tetramm.py +1 -1
- dodal/devices/undulator.py +4 -1
- dodal/devices/undulator_dcm.py +3 -19
- dodal/devices/zocalo/zocalo_results.py +7 -7
- dodal/utils.py +151 -2
- {dls_dodal-1.36.0.dist-info → dls_dodal-1.36.1a0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.36.0.dist-info → dls_dodal-1.36.1a0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.36.0.dist-info → dls_dodal-1.36.1a0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from bluesky.protocols import HasName, Movable
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
AsyncStatus,
|
|
7
|
+
DeviceVector,
|
|
8
|
+
SignalR,
|
|
9
|
+
StandardReadable,
|
|
10
|
+
StandardReadableFormat,
|
|
11
|
+
StrictEnum,
|
|
12
|
+
)
|
|
13
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
14
|
+
|
|
15
|
+
OPENSEQ_PULSE_LENGTH = 0.2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PumpState(StrictEnum):
|
|
19
|
+
MANUAL = "Manual"
|
|
20
|
+
AUTO_PRESSURE = "Auto Pressure"
|
|
21
|
+
AUTO_POSITION = "Auto Position"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StopState(StrictEnum):
|
|
25
|
+
CONTINUE = "CONTINUE"
|
|
26
|
+
STOP = "STOP"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FastValveControlRequest(StrictEnum):
|
|
30
|
+
OPEN = "Open"
|
|
31
|
+
CLOSE = "Close"
|
|
32
|
+
RESET = "Reset"
|
|
33
|
+
ARM = "Arm"
|
|
34
|
+
DISARM = "Disarm"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ValveControlRequest(StrictEnum):
|
|
38
|
+
OPEN = "Open"
|
|
39
|
+
CLOSE = "Close"
|
|
40
|
+
RESET = "Reset"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ValveOpenSeqRequest(StrictEnum):
|
|
44
|
+
INACTIVE = 0
|
|
45
|
+
OPEN_SEQ = 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PumpMotorDirectionState(StrictEnum):
|
|
49
|
+
EMPTY = ""
|
|
50
|
+
FORWARD = "Forward"
|
|
51
|
+
REVERSE = "Reverse"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ValveState(StrictEnum):
|
|
55
|
+
FAULT = "Fault"
|
|
56
|
+
OPEN = "Open"
|
|
57
|
+
OPENING = "Opening"
|
|
58
|
+
CLOSED = "Closed"
|
|
59
|
+
CLOSING = "Closing"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FastValveState(StrictEnum):
|
|
63
|
+
FAULT = "Fault"
|
|
64
|
+
OPEN = "Open"
|
|
65
|
+
OPEN_ARMED = "Open Armed"
|
|
66
|
+
CLOSED = "Closed"
|
|
67
|
+
CLOSED_ARMED = "Closed Armed"
|
|
68
|
+
NONE = "Unused"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class AllValvesControlState:
|
|
73
|
+
valve_1: ValveControlRequest | None = None
|
|
74
|
+
valve_3: ValveControlRequest | None = None
|
|
75
|
+
valve_5: FastValveControlRequest | None = None
|
|
76
|
+
valve_6: FastValveControlRequest | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AllValvesControl(StandardReadable, Movable):
|
|
80
|
+
"""
|
|
81
|
+
valves 2, 4, 7, 8 are not controlled by the IOC,
|
|
82
|
+
as they are under manual control.
|
|
83
|
+
fast_valves: tuple[int, ...] = (5, 6)
|
|
84
|
+
slow_valves: tuple[int, ...] = (1, 3)
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
prefix: str,
|
|
90
|
+
name: str = "",
|
|
91
|
+
fast_valves: tuple[int, ...] = (5, 6),
|
|
92
|
+
slow_valves: tuple[int, ...] = (1, 3),
|
|
93
|
+
) -> None:
|
|
94
|
+
self.fast_valves = fast_valves
|
|
95
|
+
self.slow_valves = slow_valves
|
|
96
|
+
with self.add_children_as_readables():
|
|
97
|
+
self.valve_states: DeviceVector[SignalR[ValveState]] = DeviceVector(
|
|
98
|
+
{
|
|
99
|
+
i: epics_signal_r(ValveState, f"{prefix}V{i}:STA")
|
|
100
|
+
for i in self.slow_valves
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
self.fast_valve_states: DeviceVector[SignalR[FastValveState]] = (
|
|
104
|
+
DeviceVector(
|
|
105
|
+
{
|
|
106
|
+
i: epics_signal_r(FastValveState, f"{prefix}V{i}:STA")
|
|
107
|
+
for i in self.fast_valves
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self.fast_valve_control: DeviceVector[FastValveControl] = DeviceVector(
|
|
113
|
+
{i: FastValveControl(f"{prefix}V{i}") for i in self.fast_valves}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self.valve_control: DeviceVector[ValveControl] = DeviceVector(
|
|
117
|
+
{i: ValveControl(f"{prefix}V{i}") for i in self.slow_valves}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
super().__init__(name)
|
|
121
|
+
|
|
122
|
+
async def set_valve(
|
|
123
|
+
self,
|
|
124
|
+
valve: int,
|
|
125
|
+
value: ValveControlRequest | FastValveControlRequest,
|
|
126
|
+
):
|
|
127
|
+
if valve in self.slow_valves and (isinstance(value, ValveControlRequest)):
|
|
128
|
+
if value == ValveControlRequest.OPEN:
|
|
129
|
+
await self.valve_control[valve].set(ValveOpenSeqRequest.OPEN_SEQ)
|
|
130
|
+
await asyncio.sleep(OPENSEQ_PULSE_LENGTH)
|
|
131
|
+
await self.valve_control[valve].set(ValveOpenSeqRequest.INACTIVE)
|
|
132
|
+
else:
|
|
133
|
+
await self.valve_control[valve].set(value)
|
|
134
|
+
|
|
135
|
+
elif valve in self.fast_valves and (isinstance(value, FastValveControlRequest)):
|
|
136
|
+
if value == FastValveControlRequest.OPEN:
|
|
137
|
+
await self.fast_valve_control[valve].set(ValveOpenSeqRequest.OPEN_SEQ)
|
|
138
|
+
await asyncio.sleep(OPENSEQ_PULSE_LENGTH)
|
|
139
|
+
await self.fast_valve_control[valve].set(ValveOpenSeqRequest.INACTIVE)
|
|
140
|
+
else:
|
|
141
|
+
await self.fast_valve_control[valve].set(value)
|
|
142
|
+
|
|
143
|
+
@AsyncStatus.wrap
|
|
144
|
+
async def set(self, value: AllValvesControlState):
|
|
145
|
+
await asyncio.gather(
|
|
146
|
+
*(
|
|
147
|
+
self.set_valve(int(i[-1]), value)
|
|
148
|
+
for i, value in value.__dict__.items()
|
|
149
|
+
if value is not None
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ValveControl(StandardReadable):
|
|
155
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
156
|
+
with self.add_children_as_readables():
|
|
157
|
+
self.close = epics_signal_rw(ValveControlRequest, prefix + ":CON")
|
|
158
|
+
self.open = epics_signal_rw(ValveOpenSeqRequest, prefix + ":OPENSEQ")
|
|
159
|
+
|
|
160
|
+
super().__init__(name)
|
|
161
|
+
|
|
162
|
+
def set(self, value: ValveControlRequest | ValveOpenSeqRequest) -> AsyncStatus:
|
|
163
|
+
set_status = None
|
|
164
|
+
|
|
165
|
+
if isinstance(value, ValveControlRequest):
|
|
166
|
+
set_status = self.close.set(value)
|
|
167
|
+
elif isinstance(value, ValveOpenSeqRequest):
|
|
168
|
+
set_status = self.open.set(value)
|
|
169
|
+
|
|
170
|
+
return set_status
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class FastValveControl(StandardReadable):
|
|
174
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
175
|
+
with self.add_children_as_readables():
|
|
176
|
+
self.close = epics_signal_rw(FastValveControlRequest, prefix + ":CON")
|
|
177
|
+
self.open = epics_signal_rw(ValveOpenSeqRequest, prefix + ":OPENSEQ")
|
|
178
|
+
|
|
179
|
+
super().__init__(name)
|
|
180
|
+
|
|
181
|
+
def set(self, value: FastValveControlRequest | ValveOpenSeqRequest) -> AsyncStatus:
|
|
182
|
+
set_status = None
|
|
183
|
+
|
|
184
|
+
if isinstance(value, FastValveControlRequest):
|
|
185
|
+
set_status = self.close.set(value)
|
|
186
|
+
elif isinstance(value, ValveOpenSeqRequest):
|
|
187
|
+
set_status = self.open.set(value)
|
|
188
|
+
|
|
189
|
+
return set_status
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Pump(StandardReadable):
|
|
193
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
194
|
+
with self.add_children_as_readables():
|
|
195
|
+
self.pump_position = epics_signal_r(float, prefix + "POS")
|
|
196
|
+
self.pump_motor_direction = epics_signal_r(
|
|
197
|
+
PumpMotorDirectionState, prefix + "MTRDIR"
|
|
198
|
+
)
|
|
199
|
+
self.pump_speed = epics_signal_rw(
|
|
200
|
+
float, write_pv=prefix + "MSPEED", read_pv=prefix + "MSPEED_RBV"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
204
|
+
self.pump_mode = epics_signal_rw(PumpState, prefix + "SP:AUTO")
|
|
205
|
+
|
|
206
|
+
super().__init__(name)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class PressureTransducer(StandardReadable):
|
|
210
|
+
"""
|
|
211
|
+
Pressure transducer for a high pressure X-ray cell.
|
|
212
|
+
This is the chamber and there are three of them.
|
|
213
|
+
1 is the start, 3 is where the sample is.
|
|
214
|
+
NOTE: the distinction between the adc prefix and the cell prefix is kept here.
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
def __init__(
|
|
219
|
+
self,
|
|
220
|
+
prefix: str,
|
|
221
|
+
cell_prefix: str,
|
|
222
|
+
number: int,
|
|
223
|
+
name: str = "",
|
|
224
|
+
full_different_prefix_adc: str = "",
|
|
225
|
+
) -> None:
|
|
226
|
+
final_prefix = f"{prefix}{cell_prefix}"
|
|
227
|
+
with self.add_children_as_readables():
|
|
228
|
+
self.omron_pressure = epics_signal_r(
|
|
229
|
+
float, f"{final_prefix}PP{number}:PRES"
|
|
230
|
+
)
|
|
231
|
+
self.omron_voltage = epics_signal_r(float, f"{final_prefix}PP{number}:RAW")
|
|
232
|
+
self.beckhoff_pressure = epics_signal_r(
|
|
233
|
+
float, f"{final_prefix}STATP{number}:MeanValue_RBV"
|
|
234
|
+
)
|
|
235
|
+
self.slow_beckhoff_voltage_readout = epics_signal_r(
|
|
236
|
+
float, f"{full_different_prefix_adc}CH1"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
super().__init__(name)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class PressureJumpCellController(HasName):
|
|
243
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
244
|
+
self.stop = epics_signal_rw(StopState, f"{prefix}STOP")
|
|
245
|
+
|
|
246
|
+
self.target_pressure = epics_signal_rw(float, f"{prefix}TARGET")
|
|
247
|
+
self.timeout = epics_signal_rw(float, f"{prefix}TIMER.HIGH")
|
|
248
|
+
self.go = epics_signal_rw(bool, f"{prefix}GO")
|
|
249
|
+
|
|
250
|
+
## Jump logic ##
|
|
251
|
+
self.start_pressure = epics_signal_rw(float, f"{prefix}JUMPF")
|
|
252
|
+
self.target_pressure = epics_signal_rw(float, f"{prefix}JUMPT")
|
|
253
|
+
self.jump_ready = epics_signal_rw(bool, f"{prefix}SETJUMP")
|
|
254
|
+
|
|
255
|
+
self._name = name
|
|
256
|
+
super().__init__()
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def name(self):
|
|
260
|
+
return self._name
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class PressureJumpCell(StandardReadable):
|
|
264
|
+
"""
|
|
265
|
+
High pressure X-ray cell, used to apply pressure or pressure jumps to a sample.
|
|
266
|
+
prefix: str
|
|
267
|
+
The prefix of beamline - SPECIAL - unusual that the cell prefix is computed separately
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def __init__(
|
|
271
|
+
self,
|
|
272
|
+
prefix: str,
|
|
273
|
+
cell_prefix: str = "-HPXC-01:",
|
|
274
|
+
adc_prefix: str = "-ADC",
|
|
275
|
+
name: str = "",
|
|
276
|
+
):
|
|
277
|
+
self.all_valves_control = AllValvesControl(f"{prefix}{cell_prefix}", name)
|
|
278
|
+
self.pump = Pump(f"{prefix}{cell_prefix}", name)
|
|
279
|
+
|
|
280
|
+
self.controller = PressureJumpCellController(
|
|
281
|
+
f"{prefix}{cell_prefix}CTRL:", name
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
with self.add_children_as_readables():
|
|
285
|
+
self.pressure_transducers: DeviceVector[PressureTransducer] = DeviceVector(
|
|
286
|
+
{
|
|
287
|
+
i: PressureTransducer(
|
|
288
|
+
prefix=prefix,
|
|
289
|
+
number=i,
|
|
290
|
+
cell_prefix=cell_prefix,
|
|
291
|
+
full_different_prefix_adc=f"{prefix}{adc_prefix}-0{i}:",
|
|
292
|
+
)
|
|
293
|
+
for i in [1, 2, 3]
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
self.cell_temperature = epics_signal_r(float, f"{prefix}{cell_prefix}TEMP")
|
|
298
|
+
|
|
299
|
+
super().__init__(name)
|
dodal/devices/robot.py
CHANGED
|
@@ -42,7 +42,7 @@ class BartRobot(StandardReadable, Movable):
|
|
|
42
42
|
"""The sample changing robot."""
|
|
43
43
|
|
|
44
44
|
# How long to wait for the robot if it is busy soaking/drying
|
|
45
|
-
NOT_BUSY_TIMEOUT = 60
|
|
45
|
+
NOT_BUSY_TIMEOUT = 5 * 60
|
|
46
46
|
# How long to wait for the actual load to happen
|
|
47
47
|
LOAD_TIMEOUT = 60
|
|
48
48
|
NO_PIN_ERROR_CODE = 25
|
dodal/devices/tetramm.py
CHANGED
dodal/devices/undulator.py
CHANGED
|
@@ -108,10 +108,13 @@ class Undulator(StandardReadable, Movable):
|
|
|
108
108
|
"""
|
|
109
109
|
await self._set_undulator_gap(value)
|
|
110
110
|
|
|
111
|
-
async def
|
|
111
|
+
async def raise_if_not_enabled(self):
|
|
112
112
|
access_level = await self.gap_access.get_value()
|
|
113
113
|
if access_level is UndulatorGapAccess.DISABLED and not TEST_MODE:
|
|
114
114
|
raise AccessError("Undulator gap access is disabled. Contact Control Room")
|
|
115
|
+
|
|
116
|
+
async def _set_undulator_gap(self, energy_kev: float) -> None:
|
|
117
|
+
await self.raise_if_not_enabled()
|
|
115
118
|
LOGGER.info(f"Setting undulator gap to {energy_kev:.2f} kev")
|
|
116
119
|
target_gap = await self._get_gap_to_match_energy(energy_kev)
|
|
117
120
|
|
dodal/devices/undulator_dcm.py
CHANGED
|
@@ -6,17 +6,10 @@ from ophyd_async.core import AsyncStatus, StandardReadable
|
|
|
6
6
|
from dodal.common.beamlines.beamline_parameters import get_beamline_parameters
|
|
7
7
|
|
|
8
8
|
from .dcm import DCM
|
|
9
|
-
from .undulator import Undulator
|
|
9
|
+
from .undulator import Undulator
|
|
10
10
|
|
|
11
11
|
ENERGY_TIMEOUT_S: float = 30.0
|
|
12
12
|
|
|
13
|
-
# Enable to allow testing when the beamline is down, do not change in production!
|
|
14
|
-
TEST_MODE = False
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class AccessError(Exception):
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
13
|
|
|
21
14
|
class UndulatorDCM(StandardReadable, Movable):
|
|
22
15
|
"""
|
|
@@ -61,17 +54,8 @@ class UndulatorDCM(StandardReadable, Movable):
|
|
|
61
54
|
|
|
62
55
|
@AsyncStatus.wrap
|
|
63
56
|
async def set(self, value: float):
|
|
57
|
+
await self.undulator.raise_if_not_enabled()
|
|
64
58
|
await asyncio.gather(
|
|
65
|
-
self.
|
|
59
|
+
self.dcm.energy_in_kev.set(value, timeout=ENERGY_TIMEOUT_S),
|
|
66
60
|
self.undulator.set(value),
|
|
67
61
|
)
|
|
68
|
-
|
|
69
|
-
async def _set_dcm_energy(self, energy_kev: float) -> None:
|
|
70
|
-
access_level = await self.undulator.gap_access.get_value()
|
|
71
|
-
if access_level is UndulatorGapAccess.DISABLED and not TEST_MODE:
|
|
72
|
-
raise AccessError("Undulator gap access is disabled. Contact Control Room")
|
|
73
|
-
|
|
74
|
-
await self.dcm.energy_in_kev.set(
|
|
75
|
-
energy_kev,
|
|
76
|
-
timeout=ENERGY_TIMEOUT_S,
|
|
77
|
-
)
|
|
@@ -12,8 +12,8 @@ import workflows.transport
|
|
|
12
12
|
from bluesky.protocols import Triggerable
|
|
13
13
|
from bluesky.utils import Msg
|
|
14
14
|
from deepdiff import DeepDiff
|
|
15
|
-
from numpy.typing import NDArray
|
|
16
15
|
from ophyd_async.core import (
|
|
16
|
+
Array1D,
|
|
17
17
|
AsyncStatus,
|
|
18
18
|
StandardReadable,
|
|
19
19
|
StandardReadableFormat,
|
|
@@ -133,22 +133,22 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
133
133
|
self.use_cpu_and_gpu = use_cpu_and_gpu
|
|
134
134
|
|
|
135
135
|
self.centre_of_mass, self._com_setter = soft_signal_r_and_setter(
|
|
136
|
-
|
|
136
|
+
Array1D[np.uint64], name="centre_of_mass"
|
|
137
137
|
)
|
|
138
138
|
self.bounding_box, self._bounding_box_setter = soft_signal_r_and_setter(
|
|
139
|
-
|
|
139
|
+
Array1D[np.uint64], name="bounding_box"
|
|
140
140
|
)
|
|
141
141
|
self.max_voxel, self._max_voxel_setter = soft_signal_r_and_setter(
|
|
142
|
-
|
|
142
|
+
Array1D[np.uint64], name="max_voxel"
|
|
143
143
|
)
|
|
144
144
|
self.max_count, self._max_count_setter = soft_signal_r_and_setter(
|
|
145
|
-
|
|
145
|
+
Array1D[np.uint64], name="max_count"
|
|
146
146
|
)
|
|
147
147
|
self.n_voxels, self._n_voxels_setter = soft_signal_r_and_setter(
|
|
148
|
-
|
|
148
|
+
Array1D[np.uint64], name="n_voxels"
|
|
149
149
|
)
|
|
150
150
|
self.total_count, self._total_count_setter = soft_signal_r_and_setter(
|
|
151
|
-
|
|
151
|
+
Array1D[np.uint64], name="total_count"
|
|
152
152
|
)
|
|
153
153
|
self.ispyb_dcid, self._ispyb_dcid_setter = soft_signal_r_and_setter(
|
|
154
154
|
int, name="ispyb_dcid"
|
dodal/utils.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import functools
|
|
1
2
|
import importlib
|
|
2
3
|
import inspect
|
|
3
4
|
import os
|
|
@@ -6,13 +7,14 @@ import socket
|
|
|
6
7
|
import string
|
|
7
8
|
from collections.abc import Callable, Iterable, Mapping
|
|
8
9
|
from dataclasses import dataclass
|
|
9
|
-
from functools import wraps
|
|
10
|
+
from functools import update_wrapper, wraps
|
|
10
11
|
from importlib import import_module
|
|
11
12
|
from inspect import signature
|
|
12
13
|
from os import environ
|
|
13
14
|
from types import ModuleType
|
|
14
15
|
from typing import (
|
|
15
16
|
Any,
|
|
17
|
+
Generic,
|
|
16
18
|
Protocol,
|
|
17
19
|
TypeGuard,
|
|
18
20
|
TypeVar,
|
|
@@ -35,6 +37,7 @@ from bluesky.protocols import (
|
|
|
35
37
|
Triggerable,
|
|
36
38
|
WritesExternalAssets,
|
|
37
39
|
)
|
|
40
|
+
from bluesky.run_engine import call_in_bluesky_event_loop
|
|
38
41
|
from ophyd.device import Device as OphydV1Device
|
|
39
42
|
from ophyd_async.core import Device as OphydV2Device
|
|
40
43
|
|
|
@@ -99,6 +102,8 @@ class BeamlinePrefix:
|
|
|
99
102
|
|
|
100
103
|
|
|
101
104
|
T = TypeVar("T", bound=AnyDevice)
|
|
105
|
+
D = TypeVar("D", bound=OphydV2Device)
|
|
106
|
+
SkipType = bool | Callable[[], bool]
|
|
102
107
|
|
|
103
108
|
|
|
104
109
|
def skip_device(precondition=lambda: True):
|
|
@@ -114,6 +119,91 @@ def skip_device(precondition=lambda: True):
|
|
|
114
119
|
return decorator
|
|
115
120
|
|
|
116
121
|
|
|
122
|
+
class DeviceInitializationController(Generic[D]):
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
factory: Callable[[], D],
|
|
126
|
+
use_factory_name: bool,
|
|
127
|
+
timeout: float,
|
|
128
|
+
mock: bool,
|
|
129
|
+
skip: SkipType,
|
|
130
|
+
):
|
|
131
|
+
self._factory: Callable[[], D] = functools.cache(factory)
|
|
132
|
+
self._use_factory_name = use_factory_name
|
|
133
|
+
self._timeout = timeout
|
|
134
|
+
self._mock = mock
|
|
135
|
+
self._skip = skip
|
|
136
|
+
update_wrapper(self, factory)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def skip(self) -> bool:
|
|
140
|
+
return self._skip() if callable(self._skip) else self._skip
|
|
141
|
+
|
|
142
|
+
def cache_clear(self) -> None:
|
|
143
|
+
"""Clears the controller's internal cached instance of the device, if present.
|
|
144
|
+
Noop if not."""
|
|
145
|
+
|
|
146
|
+
# Functools adds the cache_clear function via setattr so the type checker
|
|
147
|
+
# does not pick it up.
|
|
148
|
+
self._factory.cache_clear() # type: ignore
|
|
149
|
+
|
|
150
|
+
def __call__(
|
|
151
|
+
self,
|
|
152
|
+
connect_immediately: bool = False,
|
|
153
|
+
name: str | None = None,
|
|
154
|
+
connection_timeout: float | None = None,
|
|
155
|
+
mock: bool | None = None,
|
|
156
|
+
) -> D:
|
|
157
|
+
"""Returns an instance of the Device the wrapped factory produces: the same
|
|
158
|
+
instance will be returned if this method is called multiple times, and arguments
|
|
159
|
+
may be passed to override this Controller's configuration.
|
|
160
|
+
Once the device is connected, the value of mock must be consistent, or connect
|
|
161
|
+
must be False.
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
connect_immediately (bool, default False): whether to call connect on the
|
|
166
|
+
device before returning it- connect is idempotent for ophyd-async devices.
|
|
167
|
+
Not connecting to the device allows for the instance to be created prior
|
|
168
|
+
to the RunEngine event loop being configured or for connect to be called
|
|
169
|
+
lazily e.g. by the `ensure_connected` stub.
|
|
170
|
+
name (str | None, optional): an override name to give the device, which is
|
|
171
|
+
also used to name its children. Defaults to None, which does not name the
|
|
172
|
+
device unless the device has no name and this Controller is configured to
|
|
173
|
+
use_factory_name, which propagates the name of the wrapped factory
|
|
174
|
+
function to the device instance.
|
|
175
|
+
connection_timeout (float | None, optional): an override timeout length in
|
|
176
|
+
seconds for the connect method, if it is called. Defaults to None, which
|
|
177
|
+
defers to the timeout configured for this Controller: the default uses
|
|
178
|
+
ophyd_async's DEFAULT_TIMEOUT.
|
|
179
|
+
mock (bool | None, optional): overrides whether to connect to Mock signal
|
|
180
|
+
backends, if connect is called. Defaults to None, which uses the mock
|
|
181
|
+
parameter of this Controller. This value must be used consistently when
|
|
182
|
+
connect is called on the Device.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
D: a singleton instance of the Device class returned by the wrapped factory.
|
|
186
|
+
"""
|
|
187
|
+
device = self._factory()
|
|
188
|
+
|
|
189
|
+
if connect_immediately:
|
|
190
|
+
call_in_bluesky_event_loop(
|
|
191
|
+
device.connect(
|
|
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
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if name:
|
|
200
|
+
device.set_name(name)
|
|
201
|
+
elif not device.name and self._use_factory_name:
|
|
202
|
+
device.set_name(self._factory.__name__)
|
|
203
|
+
|
|
204
|
+
return device
|
|
205
|
+
|
|
206
|
+
|
|
117
207
|
def make_device(
|
|
118
208
|
module: str | ModuleType,
|
|
119
209
|
device_name: str,
|
|
@@ -206,7 +296,33 @@ def invoke_factories(
|
|
|
206
296
|
dependent_name = leaves.pop()
|
|
207
297
|
params = {name: devices[name] for name in dependencies[dependent_name]}
|
|
208
298
|
try:
|
|
209
|
-
|
|
299
|
+
factory = factories[dependent_name]
|
|
300
|
+
if isinstance(factory, DeviceInitializationController):
|
|
301
|
+
# For now we translate the old-style parameters that
|
|
302
|
+
# device_instantiation expects. Once device_instantiation is gone and
|
|
303
|
+
# replaced with DeviceInitializationController we can formalise the
|
|
304
|
+
# API of make_all_devices and make these parameters explicit.
|
|
305
|
+
# https://github.com/DiamondLightSource/dodal/issues/844
|
|
306
|
+
mock = kwargs.get(
|
|
307
|
+
"mock",
|
|
308
|
+
kwargs.get(
|
|
309
|
+
"fake_with_ophyd_sim",
|
|
310
|
+
False,
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
connect_immediately = kwargs.get(
|
|
314
|
+
"connect_immediately",
|
|
315
|
+
kwargs.get(
|
|
316
|
+
"wait_for_connection",
|
|
317
|
+
False,
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
devices[dependent_name] = factory(
|
|
321
|
+
mock=mock,
|
|
322
|
+
connect_immediately=connect_immediately,
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
devices[dependent_name] = factory(**params, **kwargs)
|
|
210
326
|
except Exception as e:
|
|
211
327
|
exceptions[dependent_name] = e
|
|
212
328
|
|
|
@@ -268,6 +384,8 @@ def collect_factories(
|
|
|
268
384
|
|
|
269
385
|
|
|
270
386
|
def _is_device_skipped(func: AnyDeviceFactory) -> bool:
|
|
387
|
+
if isinstance(func, DeviceInitializationController):
|
|
388
|
+
return func.skip
|
|
271
389
|
return getattr(func, "__skip__", False)
|
|
272
390
|
|
|
273
391
|
|
|
@@ -301,6 +419,37 @@ def is_v1_device_type(obj: type[Any]) -> bool:
|
|
|
301
419
|
return is_class and follows_protocols and not is_v2_device_type(obj)
|
|
302
420
|
|
|
303
421
|
|
|
422
|
+
def filter_ophyd_devices(
|
|
423
|
+
devices: Mapping[str, AnyDevice],
|
|
424
|
+
) -> tuple[Mapping[str, OphydV1Device], Mapping[str, OphydV2Device]]:
|
|
425
|
+
"""
|
|
426
|
+
Split a dictionary of ophyd and ophyd-async devices
|
|
427
|
+
(i.e. the output of make_all_devices) into 2 separate dictionaries of the
|
|
428
|
+
different types. Useful when special handling is needed for each type of device.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
devices: Dictionary of device name to ophyd or ophyd-async device.
|
|
432
|
+
|
|
433
|
+
Raises:
|
|
434
|
+
ValueError: If anything in the dictionary doesn't come from either library.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Tuple of two dictionaries, one mapping names to ophyd devices and one mapping
|
|
438
|
+
names to ophyd-async devices.
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
ophyd_devices = {}
|
|
442
|
+
ophyd_async_devices = {}
|
|
443
|
+
for name, device in devices.items():
|
|
444
|
+
if isinstance(device, OphydV1Device):
|
|
445
|
+
ophyd_devices[name] = device
|
|
446
|
+
elif isinstance(device, OphydV2Device):
|
|
447
|
+
ophyd_async_devices[name] = device
|
|
448
|
+
else:
|
|
449
|
+
raise ValueError(f"{name}: {device} is not an ophyd or ophyd-async device")
|
|
450
|
+
return ophyd_devices, ophyd_async_devices
|
|
451
|
+
|
|
452
|
+
|
|
304
453
|
def get_beamline_based_on_environment_variable() -> ModuleType:
|
|
305
454
|
"""
|
|
306
455
|
Gets the dodal module for the current beamline, as specified by the
|
|
File without changes
|
|
File without changes
|
|
File without changes
|