dls-dodal 1.39.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.39.0.dist-info → dls_dodal-1.40.0.dist-info}/METADATA +2 -2
- {dls_dodal-1.39.0.dist-info → dls_dodal-1.40.0.dist-info}/RECORD +48 -39
- 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 +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/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 +3 -2
- dodal/devices/fast_grid_scan.py +16 -18
- dodal/devices/hutch_shutter.py +25 -12
- 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.39.0.dist-info → dls_dodal-1.40.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.39.0.dist-info → dls_dodal-1.40.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.39.0.dist-info → dls_dodal-1.40.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.39.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)
|
|
@@ -161,7 +162,7 @@ class EigerDetector(Device):
|
|
|
161
162
|
status_ok = self.odin.check_and_wait_for_odin_state(
|
|
162
163
|
self.timeouts.general_status_timeout
|
|
163
164
|
)
|
|
164
|
-
self.disable_roi_mode()
|
|
165
|
+
self.disable_roi_mode().wait(self.timeouts.general_status_timeout)
|
|
165
166
|
self.disarming_status.set_finished()
|
|
166
167
|
return status_ok
|
|
167
168
|
|
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
|
|
|
@@ -170,21 +170,6 @@ class MotionProgram(Device):
|
|
|
170
170
|
self.program_number = epics_signal_r(float, prefix + "CS1:PROG_NUM")
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
class ExpectedImages(SignalR[int]):
|
|
174
|
-
def __init__(self, parent: "FastGridScanCommon") -> None:
|
|
175
|
-
super().__init__(SoftSignalBackend(int))
|
|
176
|
-
self.parent = parent
|
|
177
|
-
|
|
178
|
-
async def get_value(self, cached: bool | None = None):
|
|
179
|
-
assert isinstance(self.parent, FastGridScanCommon)
|
|
180
|
-
x = await self.parent.x_steps.get_value()
|
|
181
|
-
y = await self.parent.y_steps.get_value()
|
|
182
|
-
z = await self.parent.z_steps.get_value()
|
|
183
|
-
first_grid = x * y
|
|
184
|
-
second_grid = x * z
|
|
185
|
-
return first_grid + second_grid
|
|
186
|
-
|
|
187
|
-
|
|
188
173
|
class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
189
174
|
"""Device for a general fast grid scan
|
|
190
175
|
|
|
@@ -217,7 +202,9 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
|
217
202
|
self.run_cmd = epics_signal_x(f"{prefix}RUN.PROC")
|
|
218
203
|
self.status = epics_signal_r(int, f"{prefix}SCAN_STATUS")
|
|
219
204
|
|
|
220
|
-
self.expected_images =
|
|
205
|
+
self.expected_images = create_hardware_backed_soft_signal(
|
|
206
|
+
float, self._calculate_expected_images
|
|
207
|
+
)
|
|
221
208
|
|
|
222
209
|
self.motion_program = MotionProgram(smargon_prefix)
|
|
223
210
|
|
|
@@ -243,6 +230,17 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
|
243
230
|
}
|
|
244
231
|
super().__init__(name)
|
|
245
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
|
+
|
|
246
244
|
@AsyncStatus.wrap
|
|
247
245
|
async def kickoff(self):
|
|
248
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
|
)
|
|
@@ -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
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class KBMirror(StandardReadable):
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
prefix: str,
|
|
9
|
+
name: str = "",
|
|
10
|
+
):
|
|
11
|
+
self._prefix = prefix
|
|
12
|
+
with self.add_children_as_readables():
|
|
13
|
+
self.x = epics_signal_rw(float, prefix + "X")
|
|
14
|
+
self.y = epics_signal_rw(float, prefix + "Y")
|
|
15
|
+
self.bend1 = epics_signal_rw(float, prefix + "BEND1")
|
|
16
|
+
self.bend2 = epics_signal_rw(float, prefix + "BEND2")
|
|
17
|
+
self.curve = epics_signal_rw(float, prefix + "CURVE")
|
|
18
|
+
self.ellip = epics_signal_rw(float, prefix + "ELLIP")
|
|
19
|
+
super().__init__(name=name)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from ophyd_async.core import (
|
|
2
|
+
StandardReadable,
|
|
3
|
+
)
|
|
4
|
+
from ophyd_async.epics.core import epics_signal_r
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Diode(StandardReadable):
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
prefix: str,
|
|
11
|
+
name: str = "",
|
|
12
|
+
):
|
|
13
|
+
self._prefix = prefix
|
|
14
|
+
with self.add_children_as_readables():
|
|
15
|
+
self.signal = epics_signal_r(float, prefix + "B:DIODE:I")
|
|
16
|
+
|
|
17
|
+
super().__init__(name=name)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from ophyd_async.core import (
|
|
2
|
+
StandardReadable,
|
|
3
|
+
)
|
|
4
|
+
from ophyd_async.epics.motor import Motor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Table(StandardReadable):
|
|
8
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
9
|
+
with self.add_children_as_readables():
|
|
10
|
+
self.x = Motor(prefix + "X")
|
|
11
|
+
self.y = Motor(prefix + "Y")
|
|
12
|
+
self.z = Motor(prefix + "Z")
|
|
13
|
+
self.theta = Motor(prefix + "THETA")
|
|
14
|
+
super().__init__(name=name)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from ophyd_async.core import (
|
|
2
|
+
StandardReadable,
|
|
3
|
+
)
|
|
4
|
+
from ophyd_async.epics.motor import Motor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ThorLabsStage(StandardReadable):
|
|
8
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
9
|
+
with self.add_children_as_readables():
|
|
10
|
+
self.x = Motor(prefix + "X")
|
|
11
|
+
self.y = Motor(prefix + "Y")
|
|
12
|
+
super().__init__(name=name)
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from bluesky.protocols import Movable
|
|
4
|
+
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
5
|
+
from ophyd_async.epics.core import epics_signal_r
|
|
6
|
+
|
|
7
|
+
from dodal.devices.hutch_shutter import HutchShutter, ShutterDemand
|
|
8
|
+
from dodal.log import LOGGER
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HutchInvalidError(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HutchState(str, Enum):
|
|
16
|
+
EH1 = "EH1"
|
|
17
|
+
EH2 = "EH2"
|
|
18
|
+
INVALID = "INVALID"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HutchConditionalShutter(StandardReadable, Movable):
|
|
22
|
+
""" I19-specific device to operate the hutch shutter.
|
|
23
|
+
|
|
24
|
+
This device evaluates the hutch state value to work out which of the two I19 \
|
|
25
|
+
hutches is in use and then implements the HutchShutter device to operate the \
|
|
26
|
+
experimental shutter.
|
|
27
|
+
As the two hutches are located in series, checking the hutch in use is necessary to \
|
|
28
|
+
avoid accidentally operating the shutter from one hutch while the other has beamtime.
|
|
29
|
+
|
|
30
|
+
The hutch name should be passed to the device upon instantiation. If this does not \
|
|
31
|
+
coincide with the current hutch in use, a warning will be logged and the shutter \
|
|
32
|
+
will not be operated. This is to allow for testing of plans.
|
|
33
|
+
An error will instead be raised if the hutch state reads as "INVALID".
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, prefix: str, hutch: HutchState, name: str = "") -> None:
|
|
37
|
+
self.shutter = HutchShutter(prefix=prefix, name=name)
|
|
38
|
+
bl_prefix = prefix.split("-")[0]
|
|
39
|
+
self.hutch_state = epics_signal_r(str, f"{bl_prefix}-OP-STAT-01:EHStatus.VALA")
|
|
40
|
+
self.hutch_request = hutch
|
|
41
|
+
super().__init__(name)
|
|
42
|
+
|
|
43
|
+
@AsyncStatus.wrap
|
|
44
|
+
async def set(self, value: ShutterDemand):
|
|
45
|
+
hutch_in_use = await self.hutch_state.get_value()
|
|
46
|
+
LOGGER.info(f"Current hutch in use: {hutch_in_use}")
|
|
47
|
+
if hutch_in_use == HutchState.INVALID:
|
|
48
|
+
raise HutchInvalidError(
|
|
49
|
+
"The hutch state is invalid. Contact the beamline staff."
|
|
50
|
+
)
|
|
51
|
+
if hutch_in_use != self.hutch_request:
|
|
52
|
+
# NOTE Warn but don't fail
|
|
53
|
+
LOGGER.warning(
|
|
54
|
+
f"{self.hutch_request} is not the hutch in use. Shutter will not be operated."
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
await self.shutter.set(value)
|