dls-dodal 1.38.0__py3-none-any.whl → 1.40.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.38.0.dist-info → dls_dodal-1.40.0.dist-info}/METADATA +2 -2
- {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/RECORD +49 -40
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +2 -0
- dodal/beamlines/adsim.py +3 -2
- dodal/beamlines/b01_1.py +3 -3
- dodal/beamlines/i03.py +144 -285
- 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/i24.py +100 -145
- dodal/beamlines/p38.py +12 -8
- 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 +86 -113
- dodal/devices/eiger.py +24 -14
- dodal/devices/fast_grid_scan.py +29 -20
- dodal/devices/hutch_shutter.py +25 -12
- dodal/devices/i04/transfocator.py +22 -29
- 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/motors.py +2 -2
- dodal/devices/oav/oav_detector.py +10 -19
- dodal/devices/pressure_jump_cell.py +33 -16
- dodal/devices/robot.py +30 -11
- dodal/devices/tetramm.py +8 -3
- dodal/devices/turbo_slit.py +7 -6
- dodal/devices/zocalo/zocalo_results.py +21 -4
- dodal/plans/save_panda.py +30 -14
- dodal/utils.py +54 -15
- {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/top_level.txt +0 -0
|
@@ -8,6 +8,8 @@ from bluesky.protocols import Movable
|
|
|
8
8
|
from ophyd_async.core import (
|
|
9
9
|
AsyncStatus,
|
|
10
10
|
Reference,
|
|
11
|
+
SignalR,
|
|
12
|
+
SignalW,
|
|
11
13
|
StandardReadable,
|
|
12
14
|
StandardReadableFormat,
|
|
13
15
|
StrictEnum,
|
|
@@ -88,7 +90,56 @@ MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
|
88
90
|
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
89
91
|
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
async def estimate_motor_timeout(
|
|
94
|
+
setpoint: SignalR, curr_pos: SignalR, velocity: SignalR
|
|
95
|
+
):
|
|
96
|
+
vel = await velocity.get_value()
|
|
97
|
+
cur_pos = await curr_pos.get_value()
|
|
98
|
+
target_pos = float(await setpoint.get_value())
|
|
99
|
+
return abs((target_pos - cur_pos) * 2.0 / vel) + 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SafeUndulatorMover(StandardReadable, Movable):
|
|
103
|
+
"""A device that will check it's safe to move the undulator before moving it and
|
|
104
|
+
wait for the undulator to be safe again before calling the move complete.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, set_move: SignalW, prefix: str, name: str = ""):
|
|
108
|
+
# Gate keeper open when move is requested, closed when move is completed
|
|
109
|
+
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
|
|
110
|
+
|
|
111
|
+
split_pv = prefix.split("-")
|
|
112
|
+
fault_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
|
|
113
|
+
self.fault = epics_signal_r(float, fault_pv)
|
|
114
|
+
self.set_move = set_move
|
|
115
|
+
super().__init__(name)
|
|
116
|
+
|
|
117
|
+
@AsyncStatus.wrap
|
|
118
|
+
async def set(self, value) -> None:
|
|
119
|
+
LOGGER.info(f"Setting {self.name} to {value}")
|
|
120
|
+
await self.raise_if_cannot_move()
|
|
121
|
+
await self._set_demand_positions(value)
|
|
122
|
+
timeout = await self.get_timeout()
|
|
123
|
+
LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
|
|
124
|
+
await self.set_move.set(value=1, timeout=timeout)
|
|
125
|
+
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
126
|
+
|
|
127
|
+
@abc.abstractmethod
|
|
128
|
+
async def _set_demand_positions(self, value) -> None:
|
|
129
|
+
"""Set the demand positions on the device without actually hitting move."""
|
|
130
|
+
|
|
131
|
+
@abc.abstractmethod
|
|
132
|
+
async def get_timeout(self) -> float:
|
|
133
|
+
"""Get the timeout for the move based on an estimate of how long it will take."""
|
|
134
|
+
|
|
135
|
+
async def raise_if_cannot_move(self) -> None:
|
|
136
|
+
if await self.fault.get_value() != 0:
|
|
137
|
+
raise RuntimeError(f"{self.name} is in fault state")
|
|
138
|
+
if await self.gate.get_value() == UndulatorGateStatus.OPEN:
|
|
139
|
+
raise RuntimeError(f"{self.name} is already in motion.")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class UndulatorGap(SafeUndulatorMover):
|
|
92
143
|
"""A device with a collection of epics signals to set Apple 2 undulator gap motion.
|
|
93
144
|
Only PV used by beamline are added the full list is here:
|
|
94
145
|
/dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDGapVelocityControl.template
|
|
@@ -113,21 +164,17 @@ class UndulatorGap(StandardReadable, Movable):
|
|
|
113
164
|
)
|
|
114
165
|
# Nothing move until this is set to 1 and it will return to 0 when done
|
|
115
166
|
self.set_move = epics_signal_rw(int, prefix + "BLGSETP")
|
|
116
|
-
|
|
117
|
-
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
|
|
167
|
+
|
|
118
168
|
# These are gap velocity limit.
|
|
119
169
|
self.max_velocity = epics_signal_r(float, prefix + "BLGSETVEL.HOPR")
|
|
120
170
|
self.min_velocity = epics_signal_r(float, prefix + "BLGSETVEL.LOPR")
|
|
121
171
|
# These are gap limit.
|
|
122
172
|
self.high_limit_travel = epics_signal_r(float, prefix + "BLGAPMTR.HLM")
|
|
123
173
|
self.low_limit_travel = epics_signal_r(float, prefix + "BLGAPMTR.LLM")
|
|
124
|
-
|
|
125
|
-
self.fault = epics_signal_r(
|
|
126
|
-
float,
|
|
127
|
-
f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT",
|
|
128
|
-
)
|
|
174
|
+
|
|
129
175
|
# This is calculated acceleration from speed
|
|
130
176
|
self.acceleration_time = epics_signal_r(float, prefix + "IDGSETACC")
|
|
177
|
+
|
|
131
178
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
132
179
|
# Unit
|
|
133
180
|
self.motor_egu = epics_signal_r(str, prefix + "BLGAPMTR.EGU")
|
|
@@ -136,32 +183,15 @@ class UndulatorGap(StandardReadable, Movable):
|
|
|
136
183
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
137
184
|
# Gap readback value
|
|
138
185
|
self.user_readback = epics_signal_r(float, prefix + "CURRGAPD")
|
|
139
|
-
super().__init__(name)
|
|
140
|
-
|
|
141
|
-
@AsyncStatus.wrap
|
|
142
|
-
async def set(self, value) -> None:
|
|
143
|
-
LOGGER.info(f"Setting {self.name} to {value}")
|
|
144
|
-
await self.check_id_status()
|
|
145
|
-
await self.user_setpoint.set(value=str(value))
|
|
146
|
-
timeout = await self._cal_timeout()
|
|
147
|
-
LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
|
|
148
|
-
await self.set_move.set(value=1, timeout=timeout)
|
|
149
|
-
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
150
|
-
|
|
151
|
-
async def _cal_timeout(self) -> float:
|
|
152
|
-
vel = await self.velocity.get_value()
|
|
153
|
-
cur_pos = await self.user_readback.get_value()
|
|
154
|
-
target_pos = float(await self.user_setpoint.get_value())
|
|
155
|
-
return abs((target_pos - cur_pos) * 2.0 / vel) + 1
|
|
186
|
+
super().__init__(self.set_move, prefix, name)
|
|
156
187
|
|
|
157
|
-
async def
|
|
158
|
-
|
|
159
|
-
raise RuntimeError(f"{self.name} is in fault state")
|
|
160
|
-
if await self.gate.get_value() == UndulatorGateStatus.OPEN:
|
|
161
|
-
raise RuntimeError(f"{self.name} is already in motion.")
|
|
188
|
+
async def _set_demand_positions(self, value) -> None:
|
|
189
|
+
await self.user_setpoint.set(str(value))
|
|
162
190
|
|
|
163
191
|
async def get_timeout(self) -> float:
|
|
164
|
-
return await
|
|
192
|
+
return await estimate_motor_timeout(
|
|
193
|
+
self.user_setpoint, self.user_readback, self.velocity
|
|
194
|
+
)
|
|
165
195
|
|
|
166
196
|
|
|
167
197
|
class UndulatorPhaseMotor(StandardReadable):
|
|
@@ -204,7 +234,7 @@ class UndulatorPhaseMotor(StandardReadable):
|
|
|
204
234
|
super().__init__(name=name)
|
|
205
235
|
|
|
206
236
|
|
|
207
|
-
class UndulatorPhaseAxes(
|
|
237
|
+
class UndulatorPhaseAxes(SafeUndulatorMover):
|
|
208
238
|
"""
|
|
209
239
|
A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
|
|
210
240
|
e.g. top_outer == Q1
|
|
@@ -231,66 +261,36 @@ class UndulatorPhaseAxes(StandardReadable, Movable):
|
|
|
231
261
|
self.btm_outer = UndulatorPhaseMotor(prefix=prefix, infix=btm_outer)
|
|
232
262
|
# Nothing move until this is set to 1 and it will return to 0 when done.
|
|
233
263
|
self.set_move = epics_signal_rw(int, f"{prefix}BL{top_outer}" + "MOVE")
|
|
234
|
-
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
|
|
235
|
-
split_pv = prefix.split("-")
|
|
236
|
-
temp_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
|
|
237
|
-
self.fault = epics_signal_r(float, temp_pv)
|
|
238
|
-
super().__init__(name=name)
|
|
239
|
-
|
|
240
|
-
@AsyncStatus.wrap
|
|
241
|
-
async def set(self, value: Apple2PhasesVal) -> None:
|
|
242
|
-
LOGGER.info(f"Setting {self.name} to {value}")
|
|
243
264
|
|
|
244
|
-
|
|
265
|
+
super().__init__(self.set_move, prefix, name)
|
|
245
266
|
|
|
267
|
+
async def _set_demand_positions(self, value: Apple2PhasesVal) -> None:
|
|
246
268
|
await asyncio.gather(
|
|
247
269
|
self.top_outer.user_setpoint.set(value=value.top_outer),
|
|
248
270
|
self.top_inner.user_setpoint.set(value=value.top_inner),
|
|
249
271
|
self.btm_inner.user_setpoint.set(value=value.btm_inner),
|
|
250
272
|
self.btm_outer.user_setpoint.set(value=value.btm_outer),
|
|
251
273
|
)
|
|
252
|
-
timeout = await self._cal_timeout()
|
|
253
|
-
await self.set_move.set(value=1, timeout=timeout)
|
|
254
|
-
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
255
274
|
|
|
256
|
-
async def
|
|
275
|
+
async def get_timeout(self) -> float:
|
|
257
276
|
"""
|
|
258
277
|
Get all four motor speed, current positions and target positions to calculate required timeout.
|
|
259
278
|
"""
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
self.btm_outer.user_setpoint_demand_readback.get_value(),
|
|
271
|
-
)
|
|
272
|
-
cur_pos = await asyncio.gather(
|
|
273
|
-
self.top_outer.user_setpoint_readback.get_value(),
|
|
274
|
-
self.top_inner.user_setpoint_readback.get_value(),
|
|
275
|
-
self.btm_inner.user_setpoint_readback.get_value(),
|
|
276
|
-
self.btm_outer.user_setpoint_readback.get_value(),
|
|
279
|
+
axes = [self.top_outer, self.top_inner, self.btm_inner, self.btm_outer]
|
|
280
|
+
timeouts = await asyncio.gather(
|
|
281
|
+
*[
|
|
282
|
+
estimate_motor_timeout(
|
|
283
|
+
axis.user_setpoint_demand_readback,
|
|
284
|
+
axis.user_setpoint_readback,
|
|
285
|
+
axis.velocity,
|
|
286
|
+
)
|
|
287
|
+
for axis in axes
|
|
288
|
+
]
|
|
277
289
|
)
|
|
278
|
-
|
|
279
|
-
move_times = np.abs(np.divide(move_distances, velos))
|
|
280
|
-
longest_move_time = np.max(move_times)
|
|
281
|
-
return longest_move_time * 2 + 1
|
|
282
|
-
|
|
283
|
-
async def check_id_status(self) -> None:
|
|
284
|
-
if await self.fault.get_value() != 0:
|
|
285
|
-
raise RuntimeError(f"{self.name} is in fault state")
|
|
286
|
-
if await self.gate.get_value() == UndulatorGateStatus.OPEN:
|
|
287
|
-
raise RuntimeError(f"{self.name} is already in motion.")
|
|
288
|
-
|
|
289
|
-
async def get_timeout(self) -> float:
|
|
290
|
-
return await self._cal_timeout()
|
|
290
|
+
return np.max(timeouts)
|
|
291
291
|
|
|
292
292
|
|
|
293
|
-
class UndulatorJawPhase(
|
|
293
|
+
class UndulatorJawPhase(SafeUndulatorMover):
|
|
294
294
|
"""
|
|
295
295
|
A JawPhase movable, this is use for moving the jaw phase which is use to control the
|
|
296
296
|
linear arbitrary polarisation but only one some of the beamline.
|
|
@@ -308,49 +308,22 @@ class UndulatorJawPhase(StandardReadable, Movable):
|
|
|
308
308
|
self.jaw_phase = UndulatorPhaseMotor(prefix=prefix, infix=jaw_phase)
|
|
309
309
|
# Nothing move until this is set to 1 and it will return to 0 when done
|
|
310
310
|
self.set_move = epics_signal_rw(int, f"{prefix}BL{move_pv}" + "MOVE")
|
|
311
|
-
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
|
|
312
|
-
split_pv = prefix.split("-")
|
|
313
|
-
temp_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
|
|
314
|
-
self.fault = epics_signal_r(float, temp_pv)
|
|
315
|
-
super().__init__(name=name)
|
|
316
311
|
|
|
317
|
-
|
|
318
|
-
async def set(self, value: float) -> None:
|
|
319
|
-
LOGGER.info(f"Setting {self.name} to {value}")
|
|
320
|
-
|
|
321
|
-
await self.check_id_status()
|
|
312
|
+
super().__init__(self.set_move, prefix, name)
|
|
322
313
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
)
|
|
326
|
-
timeout = await self._cal_timeout()
|
|
327
|
-
await self.set_move.set(value=1, timeout=timeout)
|
|
328
|
-
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
314
|
+
async def _set_demand_positions(self, value: float) -> None:
|
|
315
|
+
await self.jaw_phase.user_setpoint.set(value=str(value))
|
|
329
316
|
|
|
330
|
-
async def
|
|
317
|
+
async def get_timeout(self) -> float:
|
|
331
318
|
"""
|
|
332
319
|
Get motor speed, current position and target position to calculate required timeout.
|
|
333
320
|
"""
|
|
334
|
-
|
|
335
|
-
self.jaw_phase.
|
|
336
|
-
self.jaw_phase.
|
|
337
|
-
self.jaw_phase.
|
|
321
|
+
return await estimate_motor_timeout(
|
|
322
|
+
self.jaw_phase.user_setpoint_demand_readback,
|
|
323
|
+
self.jaw_phase.user_setpoint_readback,
|
|
324
|
+
self.jaw_phase.velocity,
|
|
338
325
|
)
|
|
339
326
|
|
|
340
|
-
move_distances = target_pos - cur_pos
|
|
341
|
-
move_times = np.abs(move_distances / velo)
|
|
342
|
-
|
|
343
|
-
return move_times * 2 + 1
|
|
344
|
-
|
|
345
|
-
async def check_id_status(self) -> None:
|
|
346
|
-
if await self.fault.get_value() != 0:
|
|
347
|
-
raise RuntimeError(f"{self.name} is in fault state")
|
|
348
|
-
if await self.gate.get_value() == UndulatorGateStatus.OPEN:
|
|
349
|
-
raise RuntimeError(f"{self.name} is already in motion.")
|
|
350
|
-
|
|
351
|
-
async def get_timeout(self) -> float:
|
|
352
|
-
return await self._cal_timeout()
|
|
353
|
-
|
|
354
327
|
|
|
355
328
|
class Apple2(StandardReadable, Movable):
|
|
356
329
|
"""
|
|
@@ -437,7 +410,7 @@ class Apple2(StandardReadable, Movable):
|
|
|
437
410
|
"""
|
|
438
411
|
|
|
439
412
|
# Only need to check gap as the phase motors share both fault and gate with gap.
|
|
440
|
-
await self.gap().
|
|
413
|
+
await self.gap().raise_if_cannot_move()
|
|
441
414
|
await asyncio.gather(
|
|
442
415
|
self.phase().top_outer.user_setpoint.set(value=value.top_outer),
|
|
443
416
|
self.phase().top_inner.user_setpoint.set(value=value.top_inner),
|
dodal/devices/eiger.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from enum import Enum
|
|
4
4
|
|
|
5
|
+
from bluesky.protocols import Stageable
|
|
5
6
|
from ophyd import Component, Device, EpicsSignalRO, Signal
|
|
6
7
|
from ophyd.areadetector.cam import EigerDetectorCam
|
|
7
8
|
from ophyd.status import AndStatus, Status, StatusBase
|
|
@@ -42,7 +43,7 @@ AVAILABLE_TIMEOUTS = {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
class EigerDetector(Device):
|
|
46
|
+
class EigerDetector(Device, Stageable):
|
|
46
47
|
class ArmingSignal(Signal):
|
|
47
48
|
def set(self, value, *, timeout=None, settle_time=None, **kwargs):
|
|
48
49
|
assert isinstance(self.parent, EigerDetector)
|
|
@@ -62,6 +63,9 @@ class EigerDetector(Device):
|
|
|
62
63
|
arming_status = Status()
|
|
63
64
|
arming_status.set_finished()
|
|
64
65
|
|
|
66
|
+
disarming_status = Status()
|
|
67
|
+
disarming_status.set_finished()
|
|
68
|
+
|
|
65
69
|
def __init__(self, beamline: str = "i03", *args, **kwargs):
|
|
66
70
|
super().__init__(*args, **kwargs)
|
|
67
71
|
self.beamline = beamline
|
|
@@ -140,6 +144,7 @@ class EigerDetector(Device):
|
|
|
140
144
|
def unstage(self) -> bool:
|
|
141
145
|
assert self.detector_params is not None
|
|
142
146
|
try:
|
|
147
|
+
self.disarming_status = Status()
|
|
143
148
|
self.wait_on_arming_if_started()
|
|
144
149
|
if self.detector_params.trigger_mode == TriggerMode.FREE_RUN:
|
|
145
150
|
# In free run mode we have to manually stop odin
|
|
@@ -157,24 +162,29 @@ class EigerDetector(Device):
|
|
|
157
162
|
status_ok = self.odin.check_and_wait_for_odin_state(
|
|
158
163
|
self.timeouts.general_status_timeout
|
|
159
164
|
)
|
|
160
|
-
self.disable_roi_mode()
|
|
165
|
+
self.disable_roi_mode().wait(self.timeouts.general_status_timeout)
|
|
166
|
+
self.disarming_status.set_finished()
|
|
161
167
|
return status_ok
|
|
162
168
|
|
|
163
169
|
def stop(self, *args):
|
|
164
170
|
"""Emergency stop the device, mainly used to clean up after error."""
|
|
165
171
|
LOGGER.info("Eiger stop() called - cleaning up...")
|
|
166
|
-
self.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
172
|
+
if not self.disarming_status.done:
|
|
173
|
+
LOGGER.info("Eiger still disarming, waiting on disarm")
|
|
174
|
+
self.disarming_status.wait(self.timeouts.arming_timeout)
|
|
175
|
+
else:
|
|
176
|
+
self.wait_on_arming_if_started()
|
|
177
|
+
stop_status = self.odin.stop()
|
|
178
|
+
self.odin.file_writer.start_timeout.set(1).wait(
|
|
179
|
+
self.timeouts.general_status_timeout
|
|
180
|
+
)
|
|
181
|
+
self.disarm_detector()
|
|
182
|
+
stop_status &= self.disable_roi_mode()
|
|
183
|
+
stop_status.wait(self.timeouts.general_status_timeout)
|
|
184
|
+
# See https://github.com/DiamondLightSource/hyperion/issues/1395
|
|
185
|
+
LOGGER.info("Turning off Eiger dev/shm streaming")
|
|
186
|
+
self.odin.fan.dev_shm_enable.set(0).wait()
|
|
187
|
+
LOGGER.info("Eiger has successfully been stopped")
|
|
178
188
|
|
|
179
189
|
def disable_roi_mode(self):
|
|
180
190
|
return self.change_roi_mode(False)
|
dodal/devices/fast_grid_scan.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from abc import ABC, abstractmethod
|
|
2
3
|
from typing import Generic, TypeVar
|
|
3
4
|
|
|
@@ -9,9 +10,7 @@ from ophyd_async.core import (
|
|
|
9
10
|
AsyncStatus,
|
|
10
11
|
Device,
|
|
11
12
|
Signal,
|
|
12
|
-
SignalR,
|
|
13
13
|
SignalRW,
|
|
14
|
-
SoftSignalBackend,
|
|
15
14
|
StandardReadable,
|
|
16
15
|
wait_for_value,
|
|
17
16
|
)
|
|
@@ -24,6 +23,7 @@ from ophyd_async.epics.core import (
|
|
|
24
23
|
from pydantic import field_validator
|
|
25
24
|
from pydantic.dataclasses import dataclass
|
|
26
25
|
|
|
26
|
+
from dodal.common.signal_utils import create_hardware_backed_soft_signal
|
|
27
27
|
from dodal.log import LOGGER
|
|
28
28
|
from dodal.parameters.experiment_parameter_base import AbstractExperimentWithBeamParams
|
|
29
29
|
|
|
@@ -45,8 +45,19 @@ class GridAxis:
|
|
|
45
45
|
# refering to the first position
|
|
46
46
|
return self.steps_to_motor_position(self.full_steps - 1)
|
|
47
47
|
|
|
48
|
-
def is_within(self, steps):
|
|
49
|
-
|
|
48
|
+
def is_within(self, steps: float):
|
|
49
|
+
"""
|
|
50
|
+
Determine whether a single axis coordinate is within the grid.
|
|
51
|
+
The coordinate is from a continuous coordinate space based on the
|
|
52
|
+
XRC grid where the origin corresponds to the centre of the first grid box.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
steps: The coordinate to check
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if the coordinate falls within the grid.
|
|
59
|
+
"""
|
|
60
|
+
return -0.5 <= steps <= self.full_steps - 0.5
|
|
50
61
|
|
|
51
62
|
|
|
52
63
|
class GridScanParamsCommon(AbstractExperimentWithBeamParams):
|
|
@@ -159,21 +170,6 @@ class MotionProgram(Device):
|
|
|
159
170
|
self.program_number = epics_signal_r(float, prefix + "CS1:PROG_NUM")
|
|
160
171
|
|
|
161
172
|
|
|
162
|
-
class ExpectedImages(SignalR[int]):
|
|
163
|
-
def __init__(self, parent: "FastGridScanCommon") -> None:
|
|
164
|
-
super().__init__(SoftSignalBackend(int))
|
|
165
|
-
self.parent = parent
|
|
166
|
-
|
|
167
|
-
async def get_value(self, cached: bool | None = None):
|
|
168
|
-
assert isinstance(self.parent, FastGridScanCommon)
|
|
169
|
-
x = await self.parent.x_steps.get_value()
|
|
170
|
-
y = await self.parent.y_steps.get_value()
|
|
171
|
-
z = await self.parent.z_steps.get_value()
|
|
172
|
-
first_grid = x * y
|
|
173
|
-
second_grid = x * z
|
|
174
|
-
return first_grid + second_grid
|
|
175
|
-
|
|
176
|
-
|
|
177
173
|
class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
178
174
|
"""Device for a general fast grid scan
|
|
179
175
|
|
|
@@ -206,7 +202,9 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
|
206
202
|
self.run_cmd = epics_signal_x(f"{prefix}RUN.PROC")
|
|
207
203
|
self.status = epics_signal_r(int, f"{prefix}SCAN_STATUS")
|
|
208
204
|
|
|
209
|
-
self.expected_images =
|
|
205
|
+
self.expected_images = create_hardware_backed_soft_signal(
|
|
206
|
+
float, self._calculate_expected_images
|
|
207
|
+
)
|
|
210
208
|
|
|
211
209
|
self.motion_program = MotionProgram(smargon_prefix)
|
|
212
210
|
|
|
@@ -232,6 +230,17 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
|
232
230
|
}
|
|
233
231
|
super().__init__(name)
|
|
234
232
|
|
|
233
|
+
async def _calculate_expected_images(self):
|
|
234
|
+
x, y, z = await asyncio.gather(
|
|
235
|
+
self.x_steps.get_value(),
|
|
236
|
+
self.y_steps.get_value(),
|
|
237
|
+
self.z_steps.get_value(),
|
|
238
|
+
)
|
|
239
|
+
LOGGER.info(f"Reading num of images found {x, y, z} images in each axis")
|
|
240
|
+
first_grid = x * y
|
|
241
|
+
second_grid = x * z
|
|
242
|
+
return first_grid + second_grid
|
|
243
|
+
|
|
235
244
|
@AsyncStatus.wrap
|
|
236
245
|
async def kickoff(self):
|
|
237
246
|
curr_prog = await self.motion_program.program_number.get_value()
|
dodal/devices/hutch_shutter.py
CHANGED
|
@@ -8,9 +8,16 @@ from ophyd_async.core import (
|
|
|
8
8
|
)
|
|
9
9
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_w
|
|
10
10
|
|
|
11
|
+
from dodal.log import LOGGER
|
|
12
|
+
|
|
11
13
|
HUTCH_SAFE_FOR_OPERATIONS = 0 # Hutch is locked and can't be entered
|
|
12
14
|
|
|
13
15
|
|
|
16
|
+
# Enable to allow testing when the beamline is down, do not change in production!
|
|
17
|
+
TEST_MODE = False
|
|
18
|
+
# will be made more generic in https://github.com/DiamondLightSource/dodal/issues/754
|
|
19
|
+
|
|
20
|
+
|
|
14
21
|
class ShutterNotSafeToOperateError(Exception):
|
|
15
22
|
pass
|
|
16
23
|
|
|
@@ -64,8 +71,8 @@ class HutchShutter(StandardReadable, Movable):
|
|
|
64
71
|
"""
|
|
65
72
|
|
|
66
73
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
67
|
-
self.control = epics_signal_w(ShutterDemand, prefix
|
|
68
|
-
self.status = epics_signal_r(ShutterState, prefix
|
|
74
|
+
self.control = epics_signal_w(ShutterDemand, f"{prefix}CON")
|
|
75
|
+
self.status = epics_signal_r(ShutterState, f"{prefix}STA")
|
|
69
76
|
|
|
70
77
|
bl_prefix = prefix.split("-")[0]
|
|
71
78
|
self.interlock = HutchInterlock(bl_prefix)
|
|
@@ -75,18 +82,24 @@ class HutchShutter(StandardReadable, Movable):
|
|
|
75
82
|
@AsyncStatus.wrap
|
|
76
83
|
async def set(self, value: ShutterDemand):
|
|
77
84
|
interlock_state = await self.interlock.shutter_safe_to_operate()
|
|
78
|
-
if not interlock_state:
|
|
85
|
+
if not interlock_state and not TEST_MODE:
|
|
86
|
+
# If not in test mode, fail. If in test mode, the optics hutch may be open.
|
|
79
87
|
raise ShutterNotSafeToOperateError(
|
|
80
88
|
"The hutch has not been locked, not operating shutter."
|
|
81
89
|
)
|
|
82
|
-
if
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
if not TEST_MODE:
|
|
91
|
+
if value == ShutterDemand.OPEN:
|
|
92
|
+
await self.control.set(ShutterDemand.RESET, wait=True)
|
|
93
|
+
await self.control.set(value, wait=True)
|
|
94
|
+
return await wait_for_value(
|
|
95
|
+
self.status, match=ShutterState.OPEN, timeout=DEFAULT_TIMEOUT
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
await self.control.set(value, wait=True)
|
|
99
|
+
return await wait_for_value(
|
|
100
|
+
self.status, match=ShutterState.CLOSED, timeout=DEFAULT_TIMEOUT
|
|
101
|
+
)
|
|
88
102
|
else:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
self.status, match=ShutterState.CLOSED, timeout=DEFAULT_TIMEOUT
|
|
103
|
+
LOGGER.warning(
|
|
104
|
+
"Running in test mode, will not operate the experiment shutter."
|
|
92
105
|
)
|
|
@@ -37,32 +37,18 @@ class Transfocator(StandardReadable):
|
|
|
37
37
|
|
|
38
38
|
super().__init__(name=name)
|
|
39
39
|
|
|
40
|
-
async def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
await self.number_filters_sp.set(value)
|
|
53
|
-
await self.start.set(1)
|
|
54
|
-
LOGGER.info("Waiting for start_rbv to change to 1")
|
|
55
|
-
await wait_for_value(self.start_rbv, 1, self.TIMEOUT)
|
|
56
|
-
LOGGER.info("Waiting for start_rbv to change to 0")
|
|
57
|
-
await wait_for_value(self.start_rbv, 0, self.TIMEOUT)
|
|
58
|
-
self.latest_pred_vertical_num_lenses = value
|
|
59
|
-
is_set_filters_done = True
|
|
60
|
-
|
|
61
|
-
# The value hasn't changed so assume the device is already set up correctly
|
|
62
|
-
async for value in observe_value(self.predicted_vertical_num_lenses):
|
|
63
|
-
await set_based_on_prediction(value)
|
|
64
|
-
if is_set_filters_done:
|
|
65
|
-
break
|
|
40
|
+
async def set_based_on_prediction(self, value: float):
|
|
41
|
+
# We can only put an integer number of lenses in the beam but the
|
|
42
|
+
# calculation in the IOC returns the theoretical float number of lenses
|
|
43
|
+
value = round(value)
|
|
44
|
+
LOGGER.info(f"Transfocator setting {value} filters")
|
|
45
|
+
await self.number_filters_sp.set(value)
|
|
46
|
+
await self.start.set(1)
|
|
47
|
+
LOGGER.info("Waiting for start_rbv to change to 1")
|
|
48
|
+
await wait_for_value(self.start_rbv, 1, self.TIMEOUT)
|
|
49
|
+
LOGGER.info("Waiting for start_rbv to change to 0")
|
|
50
|
+
await wait_for_value(self.start_rbv, 0, self.TIMEOUT)
|
|
51
|
+
self.latest_pred_vertical_num_lenses = value
|
|
66
52
|
|
|
67
53
|
@AsyncStatus.wrap
|
|
68
54
|
async def set(self, value: float):
|
|
@@ -81,10 +67,17 @@ class Transfocator(StandardReadable):
|
|
|
81
67
|
|
|
82
68
|
if await self.beamsize_set_microns.get_value() != value:
|
|
83
69
|
# Logic in the IOC calculates predicted_vertical_num_lenses when beam_set_microns changes
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
70
|
+
|
|
71
|
+
# Register an observer before setting beamsize_set_microns to ensure we don't miss changes
|
|
72
|
+
predicted_vertical_num_lenses_iterator = observe_value(
|
|
73
|
+
self.predicted_vertical_num_lenses, timeout=self.TIMEOUT
|
|
87
74
|
)
|
|
75
|
+
# Keep initial prediction before setting to later compare with change after setting
|
|
76
|
+
current_prediction = await anext(predicted_vertical_num_lenses_iterator)
|
|
77
|
+
await self.beamsize_set_microns.set(value)
|
|
78
|
+
accepted_prediction = await anext(predicted_vertical_num_lenses_iterator)
|
|
79
|
+
if not math.isclose(current_prediction, accepted_prediction, abs_tol=1e-8):
|
|
80
|
+
await self.set_based_on_prediction(accepted_prediction)
|
|
88
81
|
|
|
89
82
|
number_filters_rbv, vertical_lens_size_rbv = await asyncio.gather(
|
|
90
83
|
self.number_filters_sp.get_value(),
|
|
@@ -5,8 +5,8 @@ from dodal.devices.current_amplifiers import StruckScaler
|
|
|
5
5
|
|
|
6
6
|
class RasorScalerCard1(Device):
|
|
7
7
|
def __init__(self, prefix, name: str = "") -> None:
|
|
8
|
-
self.mon = StruckScaler(prefix=prefix, suffix=".
|
|
9
|
-
self.det = StruckScaler(prefix=prefix, suffix=".
|
|
10
|
-
self.fluo = StruckScaler(prefix=prefix, suffix=".
|
|
11
|
-
self.drain = StruckScaler(prefix=prefix, suffix=".
|
|
8
|
+
self.mon = StruckScaler(prefix=prefix, suffix=".S17")
|
|
9
|
+
self.det = StruckScaler(prefix=prefix, suffix=".S18")
|
|
10
|
+
self.fluo = StruckScaler(prefix=prefix, suffix=".S19")
|
|
11
|
+
self.drain = StruckScaler(prefix=prefix, suffix=".S20")
|
|
12
12
|
super().__init__(name)
|
dodal/devices/i13_1/merlin.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from ophyd_async.core import PathProvider, StandardDetector
|
|
2
2
|
from ophyd_async.epics import adcore
|
|
3
3
|
|
|
4
|
+
from dodal.common.beamlines.device_helpers import CAM_SUFFIX, HDF5_SUFFIX
|
|
4
5
|
from dodal.devices.i13_1.merlin_controller import MerlinController
|
|
5
6
|
from dodal.devices.i13_1.merlin_io import MerlinDriverIO
|
|
6
7
|
|
|
@@ -13,12 +14,12 @@ class Merlin(StandardDetector):
|
|
|
13
14
|
self,
|
|
14
15
|
prefix: str,
|
|
15
16
|
path_provider: PathProvider,
|
|
16
|
-
drv_suffix=
|
|
17
|
-
|
|
17
|
+
drv_suffix=CAM_SUFFIX,
|
|
18
|
+
fileio_suffix=HDF5_SUFFIX,
|
|
18
19
|
name: str = "",
|
|
19
20
|
):
|
|
20
21
|
self.drv = MerlinDriverIO(prefix + drv_suffix)
|
|
21
|
-
self.hdf = adcore.NDFileHDFIO(prefix +
|
|
22
|
+
self.hdf = adcore.NDFileHDFIO(prefix + fileio_suffix)
|
|
22
23
|
|
|
23
24
|
super().__init__(
|
|
24
25
|
MerlinController(self.drv),
|
|
@@ -4,15 +4,15 @@ import logging
|
|
|
4
4
|
from ophyd_async.core import (
|
|
5
5
|
DEFAULT_TIMEOUT,
|
|
6
6
|
AsyncStatus,
|
|
7
|
-
DetectorController,
|
|
8
7
|
TriggerInfo,
|
|
9
8
|
)
|
|
10
9
|
from ophyd_async.epics import adcore
|
|
10
|
+
from ophyd_async.epics.adcore import ADBaseController
|
|
11
11
|
|
|
12
12
|
from dodal.devices.i13_1.merlin_io import MerlinDriverIO, MerlinImageMode
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class MerlinController(
|
|
15
|
+
class MerlinController(ADBaseController):
|
|
16
16
|
def __init__(
|
|
17
17
|
self,
|
|
18
18
|
driver: MerlinDriverIO,
|
|
@@ -37,11 +37,6 @@ class MerlinController(DetectorController):
|
|
|
37
37
|
self.driver.image_mode.set(MerlinImageMode.MULTIPLE),
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
async def arm(self):
|
|
41
|
-
self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
|
|
42
|
-
self.driver, good_states=self.good_states, timeout=self.frame_timeout
|
|
43
|
-
)
|
|
44
|
-
|
|
45
40
|
async def wait_for_idle(self):
|
|
46
41
|
if self._arm_status:
|
|
47
42
|
await self._arm_status
|