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
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
|
|
5
|
-
from bluesky.protocols import Movable
|
|
5
|
+
from bluesky.protocols import Movable, Preparable
|
|
6
6
|
from ophyd_async.core import (
|
|
7
7
|
AsyncStatus,
|
|
8
8
|
StandardReadable,
|
|
@@ -21,6 +21,15 @@ class InvalidApertureMove(Exception):
|
|
|
21
21
|
pass
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
class _GDAParamApertureValue(StrictEnum):
|
|
25
|
+
"""Maps from a short usable name to the value name in the GDA Beamline parameters"""
|
|
26
|
+
|
|
27
|
+
ROBOT_LOAD = "ROBOT_LOAD"
|
|
28
|
+
SMALL = "SMALL_APERTURE"
|
|
29
|
+
MEDIUM = "MEDIUM_APERTURE"
|
|
30
|
+
LARGE = "LARGE_APERTURE"
|
|
31
|
+
|
|
32
|
+
|
|
24
33
|
class AperturePosition(BaseModel):
|
|
25
34
|
"""
|
|
26
35
|
Represents one of the available positions for the Aperture-Scatterguard.
|
|
@@ -65,7 +74,7 @@ class AperturePosition(BaseModel):
|
|
|
65
74
|
|
|
66
75
|
@staticmethod
|
|
67
76
|
def from_gda_params(
|
|
68
|
-
name:
|
|
77
|
+
name: _GDAParamApertureValue,
|
|
69
78
|
radius: float,
|
|
70
79
|
params: GDABeamlineParameters,
|
|
71
80
|
) -> AperturePosition:
|
|
@@ -80,12 +89,16 @@ class AperturePosition(BaseModel):
|
|
|
80
89
|
|
|
81
90
|
|
|
82
91
|
class ApertureValue(StrictEnum):
|
|
83
|
-
"""
|
|
92
|
+
"""The possible apertures that can be selected.
|
|
93
|
+
|
|
94
|
+
Changing these means changing the external paramter model of Hyperion.
|
|
95
|
+
See https://github.com/DiamondLightSource/mx-bluesky/issues/760
|
|
96
|
+
"""
|
|
84
97
|
|
|
85
|
-
ROBOT_LOAD = "ROBOT_LOAD"
|
|
86
98
|
SMALL = "SMALL_APERTURE"
|
|
87
99
|
MEDIUM = "MEDIUM_APERTURE"
|
|
88
100
|
LARGE = "LARGE_APERTURE"
|
|
101
|
+
OUT_OF_BEAM = "Out of beam"
|
|
89
102
|
|
|
90
103
|
def __str__(self):
|
|
91
104
|
return self.name.capitalize()
|
|
@@ -95,22 +108,53 @@ def load_positions_from_beamline_parameters(
|
|
|
95
108
|
params: GDABeamlineParameters,
|
|
96
109
|
) -> dict[ApertureValue, AperturePosition]:
|
|
97
110
|
return {
|
|
98
|
-
ApertureValue.
|
|
99
|
-
|
|
111
|
+
ApertureValue.OUT_OF_BEAM: AperturePosition.from_gda_params(
|
|
112
|
+
_GDAParamApertureValue.ROBOT_LOAD, 0, params
|
|
100
113
|
),
|
|
101
114
|
ApertureValue.SMALL: AperturePosition.from_gda_params(
|
|
102
|
-
|
|
115
|
+
_GDAParamApertureValue.SMALL, 20, params
|
|
103
116
|
),
|
|
104
117
|
ApertureValue.MEDIUM: AperturePosition.from_gda_params(
|
|
105
|
-
|
|
118
|
+
_GDAParamApertureValue.MEDIUM, 50, params
|
|
106
119
|
),
|
|
107
120
|
ApertureValue.LARGE: AperturePosition.from_gda_params(
|
|
108
|
-
|
|
121
|
+
_GDAParamApertureValue.LARGE, 100, params
|
|
109
122
|
),
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
|
|
113
|
-
class ApertureScatterguard(StandardReadable, Movable):
|
|
126
|
+
class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable):
|
|
127
|
+
"""Move the aperture and scatterguard assembly in a safe way. There are two ways to
|
|
128
|
+
interact with the device depending on if you want simplicity or move flexibility.
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
The simple interface is using::
|
|
132
|
+
|
|
133
|
+
await aperture_scatterguard.set(ApertureValue.LARGE)
|
|
134
|
+
|
|
135
|
+
This will move the assembly so that the large aperture is in the beam, regardless
|
|
136
|
+
of where the assembly currently is.
|
|
137
|
+
|
|
138
|
+
We may also want to move the assembly out of the beam with::
|
|
139
|
+
|
|
140
|
+
await aperture_scatterguard.set(ApertureValue.OUT_OF_BEAM)
|
|
141
|
+
|
|
142
|
+
Note, to make sure we do this as quickly as possible, the scatterguard will stay
|
|
143
|
+
in the same position relative to the aperture.
|
|
144
|
+
|
|
145
|
+
We may then want to keep the assembly out of the beam whilst asynchronously preparing
|
|
146
|
+
the other axes for the aperture that's to follow::
|
|
147
|
+
|
|
148
|
+
await aperture_scatterguard.prepare(ApertureValue.LARGE)
|
|
149
|
+
|
|
150
|
+
Then, at a later time, move back into the beam::
|
|
151
|
+
|
|
152
|
+
await aperture_scatterguard.set(ApertureValue.LARGE)
|
|
153
|
+
|
|
154
|
+
Given the prepare has been done this move will now be faster as only the y is
|
|
155
|
+
left to move.
|
|
156
|
+
"""
|
|
157
|
+
|
|
114
158
|
def __init__(
|
|
115
159
|
self,
|
|
116
160
|
loaded_positions: dict[ApertureValue, AperturePosition],
|
|
@@ -135,6 +179,7 @@ class ApertureScatterguard(StandardReadable, Movable):
|
|
|
135
179
|
self.radius,
|
|
136
180
|
],
|
|
137
181
|
)
|
|
182
|
+
|
|
138
183
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
139
184
|
self.selected_aperture = create_hardware_backed_soft_signal(
|
|
140
185
|
ApertureValue, self._get_current_aperture_position
|
|
@@ -142,15 +187,85 @@ class ApertureScatterguard(StandardReadable, Movable):
|
|
|
142
187
|
|
|
143
188
|
super().__init__(name)
|
|
144
189
|
|
|
145
|
-
def get_position_from_gda_aperture_name(
|
|
146
|
-
self, gda_aperture_name: str
|
|
147
|
-
) -> ApertureValue:
|
|
148
|
-
return ApertureValue(gda_aperture_name)
|
|
149
|
-
|
|
150
190
|
@AsyncStatus.wrap
|
|
151
191
|
async def set(self, value: ApertureValue):
|
|
192
|
+
"""This set will move the aperture into the beam or move the whole assembly out"""
|
|
193
|
+
|
|
152
194
|
position = self._loaded_positions[value]
|
|
153
|
-
await self.
|
|
195
|
+
await self._check_safe_to_move(position.aperture_z)
|
|
196
|
+
|
|
197
|
+
if value == ApertureValue.OUT_OF_BEAM:
|
|
198
|
+
out_y = self._loaded_positions[ApertureValue.OUT_OF_BEAM].aperture_y
|
|
199
|
+
await self.aperture.y.set(out_y)
|
|
200
|
+
else:
|
|
201
|
+
await self._safe_move_whilst_in_beam(position)
|
|
202
|
+
|
|
203
|
+
async def _check_safe_to_move(self, expected_z_position: float):
|
|
204
|
+
"""The assembly is moved (in z) to be under the table when the beamline is not
|
|
205
|
+
in use. If we try and move whilst in the incorrect Z position we will collide
|
|
206
|
+
with the table.
|
|
207
|
+
|
|
208
|
+
Additionally, because there are so many collision possibilities in the device we
|
|
209
|
+
throw an error if any of the axes are already moving.
|
|
210
|
+
"""
|
|
211
|
+
current_ap_z = await self.aperture.z.user_readback.get_value()
|
|
212
|
+
diff_on_z = abs(current_ap_z - expected_z_position)
|
|
213
|
+
aperture_z_tolerance = self._tolerances.aperture_z
|
|
214
|
+
if diff_on_z > aperture_z_tolerance:
|
|
215
|
+
raise InvalidApertureMove(
|
|
216
|
+
f"Current aperture z ({current_ap_z}), outside of tolerance ({aperture_z_tolerance}) from target ({expected_z_position})."
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
all_axes = [
|
|
220
|
+
self.aperture.x,
|
|
221
|
+
self.aperture.y,
|
|
222
|
+
self.aperture.z,
|
|
223
|
+
self.scatterguard.x,
|
|
224
|
+
self.scatterguard.y,
|
|
225
|
+
]
|
|
226
|
+
for axis in all_axes:
|
|
227
|
+
axis_stationary = await axis.motor_done_move.get_value()
|
|
228
|
+
if not axis_stationary:
|
|
229
|
+
raise InvalidApertureMove(
|
|
230
|
+
f"{axis.name} is still moving. Wait for it to finish before"
|
|
231
|
+
"triggering another move."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
async def _safe_move_whilst_in_beam(self, position: AperturePosition):
|
|
235
|
+
"""
|
|
236
|
+
Move the aperture and scatterguard combo safely to a new position.
|
|
237
|
+
See https://github.com/DiamondLightSource/hyperion/wiki/Aperture-Scatterguard-Collisions
|
|
238
|
+
for why this is required. TLDR is that we have a collision at the top of y so we need
|
|
239
|
+
to make sure we move the assembly down before we move the scatterguard up.
|
|
240
|
+
"""
|
|
241
|
+
current_ap_y = await self.aperture.y.user_readback.get_value()
|
|
242
|
+
|
|
243
|
+
aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = (
|
|
244
|
+
position.values
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if aperture_y > current_ap_y:
|
|
248
|
+
# Assembly needs to move up so move the scatterguard down first
|
|
249
|
+
await asyncio.gather(
|
|
250
|
+
self.scatterguard.x.set(scatterguard_x),
|
|
251
|
+
self.scatterguard.y.set(scatterguard_y),
|
|
252
|
+
)
|
|
253
|
+
await asyncio.gather(
|
|
254
|
+
self.aperture.x.set(aperture_x),
|
|
255
|
+
self.aperture.y.set(aperture_y),
|
|
256
|
+
self.aperture.z.set(aperture_z),
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
await asyncio.gather(
|
|
260
|
+
self.aperture.x.set(aperture_x),
|
|
261
|
+
self.aperture.y.set(aperture_y),
|
|
262
|
+
self.aperture.z.set(aperture_z),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
await asyncio.gather(
|
|
266
|
+
self.scatterguard.x.set(scatterguard_x),
|
|
267
|
+
self.scatterguard.y.set(scatterguard_y),
|
|
268
|
+
)
|
|
154
269
|
|
|
155
270
|
@AsyncStatus.wrap
|
|
156
271
|
async def _set_raw_unsafe(self, position: AperturePosition):
|
|
@@ -167,6 +282,11 @@ class ApertureScatterguard(StandardReadable, Movable):
|
|
|
167
282
|
self.scatterguard.y.set(scatterguard_y),
|
|
168
283
|
)
|
|
169
284
|
|
|
285
|
+
async def _is_out_of_beam(self) -> bool:
|
|
286
|
+
current_ap_y = await self.aperture.y.user_readback.get_value()
|
|
287
|
+
out_ap_y = self._loaded_positions[ApertureValue.OUT_OF_BEAM].aperture_y
|
|
288
|
+
return current_ap_y <= out_ap_y + self._tolerances.aperture_y
|
|
289
|
+
|
|
170
290
|
async def _get_current_aperture_position(self) -> ApertureValue:
|
|
171
291
|
"""
|
|
172
292
|
Returns the current aperture position using readback values
|
|
@@ -174,16 +294,14 @@ class ApertureScatterguard(StandardReadable, Movable):
|
|
|
174
294
|
mini aperture y <= ROBOT_LOAD.location.aperture_y + tolerance.
|
|
175
295
|
If no position is found then raises InvalidApertureMove.
|
|
176
296
|
"""
|
|
177
|
-
current_ap_y = await self.aperture.y.user_readback.get_value(cached=False)
|
|
178
|
-
robot_load_ap_y = self._loaded_positions[ApertureValue.ROBOT_LOAD].aperture_y
|
|
179
297
|
if await self.aperture.large.get_value(cached=False) == 1:
|
|
180
298
|
return ApertureValue.LARGE
|
|
181
299
|
elif await self.aperture.medium.get_value(cached=False) == 1:
|
|
182
300
|
return ApertureValue.MEDIUM
|
|
183
301
|
elif await self.aperture.small.get_value(cached=False) == 1:
|
|
184
302
|
return ApertureValue.SMALL
|
|
185
|
-
elif
|
|
186
|
-
return ApertureValue.
|
|
303
|
+
elif await self._is_out_of_beam():
|
|
304
|
+
return ApertureValue.OUT_OF_BEAM
|
|
187
305
|
|
|
188
306
|
raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
|
|
189
307
|
|
|
@@ -191,56 +309,24 @@ class ApertureScatterguard(StandardReadable, Movable):
|
|
|
191
309
|
current_value = await self._get_current_aperture_position()
|
|
192
310
|
return self._loaded_positions[current_value].radius
|
|
193
311
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
Move the aperture and scatterguard combo safely to a new position.
|
|
199
|
-
See https://github.com/DiamondLightSource/hyperion/wiki/Aperture-Scatterguard-Collisions
|
|
200
|
-
for why this is required.
|
|
201
|
-
"""
|
|
202
|
-
assert self._loaded_positions is not None
|
|
203
|
-
|
|
204
|
-
ap_z_in_position = await self.aperture.z.motor_done_move.get_value()
|
|
205
|
-
if not ap_z_in_position:
|
|
206
|
-
raise InvalidApertureMove(
|
|
207
|
-
"ApertureScatterguard z is still moving. Wait for it to finish "
|
|
208
|
-
"before triggering another move."
|
|
209
|
-
)
|
|
312
|
+
@AsyncStatus.wrap
|
|
313
|
+
async def prepare(self, value: ApertureValue):
|
|
314
|
+
"""Moves the assembly to the position for the specified aperture, whilst keeping
|
|
315
|
+
it out of the beam if it already is so.
|
|
210
316
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
f"Current aperture z ({current_ap_z}), outside of tolerance ({self._tolerances.aperture_z}) from target ({position.aperture_z})."
|
|
317
|
+
Moving the assembly whilst out of the beam has no collision risk so we can just
|
|
318
|
+
move all the motors together.
|
|
319
|
+
"""
|
|
320
|
+
if await self._is_out_of_beam():
|
|
321
|
+
aperture_x, _, aperture_z, scatterguard_x, scatterguard_y = (
|
|
322
|
+
self._loaded_positions[value].values
|
|
218
323
|
)
|
|
219
324
|
|
|
220
|
-
current_ap_y = await self.aperture.y.user_readback.get_value()
|
|
221
|
-
|
|
222
|
-
aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = (
|
|
223
|
-
position.values
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
if position.aperture_y > current_ap_y:
|
|
227
|
-
await asyncio.gather(
|
|
228
|
-
self.scatterguard.x.set(scatterguard_x),
|
|
229
|
-
self.scatterguard.y.set(scatterguard_y),
|
|
230
|
-
)
|
|
231
|
-
await asyncio.gather(
|
|
232
|
-
self.aperture.x.set(aperture_x),
|
|
233
|
-
self.aperture.y.set(aperture_y),
|
|
234
|
-
self.aperture.z.set(aperture_z),
|
|
235
|
-
)
|
|
236
|
-
else:
|
|
237
325
|
await asyncio.gather(
|
|
238
326
|
self.aperture.x.set(aperture_x),
|
|
239
|
-
self.aperture.y.set(aperture_y),
|
|
240
327
|
self.aperture.z.set(aperture_z),
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
await asyncio.gather(
|
|
244
328
|
self.scatterguard.x.set(scatterguard_x),
|
|
245
329
|
self.scatterguard.y.set(scatterguard_y),
|
|
246
330
|
)
|
|
331
|
+
else:
|
|
332
|
+
await self.set(value)
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import asyncio
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, Generic, TypeVar
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
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,
|
|
@@ -19,6 +21,8 @@ from pydantic import BaseModel, ConfigDict, RootModel
|
|
|
19
21
|
|
|
20
22
|
from dodal.log import LOGGER
|
|
21
23
|
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
class UndulatorGateStatus(StrictEnum):
|
|
24
28
|
OPEN = "Open"
|
|
@@ -88,7 +92,56 @@ MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
|
88
92
|
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
89
93
|
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
async def estimate_motor_timeout(
|
|
96
|
+
setpoint: SignalR, curr_pos: SignalR, velocity: SignalR
|
|
97
|
+
):
|
|
98
|
+
vel = await velocity.get_value()
|
|
99
|
+
cur_pos = await curr_pos.get_value()
|
|
100
|
+
target_pos = float(await setpoint.get_value())
|
|
101
|
+
return abs((target_pos - cur_pos) * 2.0 / vel) + 1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
|
|
105
|
+
"""A device that will check it's safe to move the undulator before moving it and
|
|
106
|
+
wait for the undulator to be safe again before calling the move complete.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, set_move: SignalW, prefix: str, name: str = ""):
|
|
110
|
+
# Gate keeper open when move is requested, closed when move is completed
|
|
111
|
+
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
|
|
112
|
+
|
|
113
|
+
split_pv = prefix.split("-")
|
|
114
|
+
fault_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
|
|
115
|
+
self.fault = epics_signal_r(float, fault_pv)
|
|
116
|
+
self.set_move = set_move
|
|
117
|
+
super().__init__(name)
|
|
118
|
+
|
|
119
|
+
@AsyncStatus.wrap
|
|
120
|
+
async def set(self, value: T) -> None:
|
|
121
|
+
LOGGER.info(f"Setting {self.name} to {value}")
|
|
122
|
+
await self.raise_if_cannot_move()
|
|
123
|
+
await self._set_demand_positions(value)
|
|
124
|
+
timeout = await self.get_timeout()
|
|
125
|
+
LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
|
|
126
|
+
await self.set_move.set(value=1, timeout=timeout)
|
|
127
|
+
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
128
|
+
|
|
129
|
+
@abc.abstractmethod
|
|
130
|
+
async def _set_demand_positions(self, value: T) -> None:
|
|
131
|
+
"""Set the demand positions on the device without actually hitting move."""
|
|
132
|
+
|
|
133
|
+
@abc.abstractmethod
|
|
134
|
+
async def get_timeout(self) -> float:
|
|
135
|
+
"""Get the timeout for the move based on an estimate of how long it will take."""
|
|
136
|
+
|
|
137
|
+
async def raise_if_cannot_move(self) -> None:
|
|
138
|
+
if await self.fault.get_value() != 0:
|
|
139
|
+
raise RuntimeError(f"{self.name} is in fault state")
|
|
140
|
+
if await self.gate.get_value() == UndulatorGateStatus.OPEN:
|
|
141
|
+
raise RuntimeError(f"{self.name} is already in motion.")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class UndulatorGap(SafeUndulatorMover[float]):
|
|
92
145
|
"""A device with a collection of epics signals to set Apple 2 undulator gap motion.
|
|
93
146
|
Only PV used by beamline are added the full list is here:
|
|
94
147
|
/dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDGapVelocityControl.template
|
|
@@ -113,21 +166,17 @@ class UndulatorGap(StandardReadable, Movable):
|
|
|
113
166
|
)
|
|
114
167
|
# Nothing move until this is set to 1 and it will return to 0 when done
|
|
115
168
|
self.set_move = epics_signal_rw(int, prefix + "BLGSETP")
|
|
116
|
-
|
|
117
|
-
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
|
|
169
|
+
|
|
118
170
|
# These are gap velocity limit.
|
|
119
171
|
self.max_velocity = epics_signal_r(float, prefix + "BLGSETVEL.HOPR")
|
|
120
172
|
self.min_velocity = epics_signal_r(float, prefix + "BLGSETVEL.LOPR")
|
|
121
173
|
# These are gap limit.
|
|
122
174
|
self.high_limit_travel = epics_signal_r(float, prefix + "BLGAPMTR.HLM")
|
|
123
175
|
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
|
-
)
|
|
176
|
+
|
|
129
177
|
# This is calculated acceleration from speed
|
|
130
178
|
self.acceleration_time = epics_signal_r(float, prefix + "IDGSETACC")
|
|
179
|
+
|
|
131
180
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
132
181
|
# Unit
|
|
133
182
|
self.motor_egu = epics_signal_r(str, prefix + "BLGAPMTR.EGU")
|
|
@@ -136,32 +185,15 @@ class UndulatorGap(StandardReadable, Movable):
|
|
|
136
185
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
137
186
|
# Gap readback value
|
|
138
187
|
self.user_readback = epics_signal_r(float, prefix + "CURRGAPD")
|
|
139
|
-
super().__init__(name)
|
|
188
|
+
super().__init__(self.set_move, prefix, name)
|
|
140
189
|
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
async def check_id_status(self) -> None:
|
|
158
|
-
if await self.fault.get_value() != 0:
|
|
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.")
|
|
190
|
+
async def _set_demand_positions(self, value: float) -> None:
|
|
191
|
+
await self.user_setpoint.set(str(value))
|
|
162
192
|
|
|
163
193
|
async def get_timeout(self) -> float:
|
|
164
|
-
return await
|
|
194
|
+
return await estimate_motor_timeout(
|
|
195
|
+
self.user_setpoint, self.user_readback, self.velocity
|
|
196
|
+
)
|
|
165
197
|
|
|
166
198
|
|
|
167
199
|
class UndulatorPhaseMotor(StandardReadable):
|
|
@@ -204,7 +236,7 @@ class UndulatorPhaseMotor(StandardReadable):
|
|
|
204
236
|
super().__init__(name=name)
|
|
205
237
|
|
|
206
238
|
|
|
207
|
-
class UndulatorPhaseAxes(
|
|
239
|
+
class UndulatorPhaseAxes(SafeUndulatorMover[Apple2PhasesVal]):
|
|
208
240
|
"""
|
|
209
241
|
A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
|
|
210
242
|
e.g. top_outer == Q1
|
|
@@ -231,66 +263,36 @@ class UndulatorPhaseAxes(StandardReadable, Movable):
|
|
|
231
263
|
self.btm_outer = UndulatorPhaseMotor(prefix=prefix, infix=btm_outer)
|
|
232
264
|
# Nothing move until this is set to 1 and it will return to 0 when done.
|
|
233
265
|
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
266
|
|
|
240
|
-
|
|
241
|
-
async def set(self, value: Apple2PhasesVal) -> None:
|
|
242
|
-
LOGGER.info(f"Setting {self.name} to {value}")
|
|
243
|
-
|
|
244
|
-
await self.check_id_status()
|
|
267
|
+
super().__init__(self.set_move, prefix, name)
|
|
245
268
|
|
|
269
|
+
async def _set_demand_positions(self, value: Apple2PhasesVal) -> None:
|
|
246
270
|
await asyncio.gather(
|
|
247
271
|
self.top_outer.user_setpoint.set(value=value.top_outer),
|
|
248
272
|
self.top_inner.user_setpoint.set(value=value.top_inner),
|
|
249
273
|
self.btm_inner.user_setpoint.set(value=value.btm_inner),
|
|
250
274
|
self.btm_outer.user_setpoint.set(value=value.btm_outer),
|
|
251
275
|
)
|
|
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
276
|
|
|
256
|
-
async def
|
|
277
|
+
async def get_timeout(self) -> float:
|
|
257
278
|
"""
|
|
258
279
|
Get all four motor speed, current positions and target positions to calculate required timeout.
|
|
259
280
|
"""
|
|
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(),
|
|
281
|
+
axes = [self.top_outer, self.top_inner, self.btm_inner, self.btm_outer]
|
|
282
|
+
timeouts = await asyncio.gather(
|
|
283
|
+
*[
|
|
284
|
+
estimate_motor_timeout(
|
|
285
|
+
axis.user_setpoint_demand_readback,
|
|
286
|
+
axis.user_setpoint_readback,
|
|
287
|
+
axis.velocity,
|
|
288
|
+
)
|
|
289
|
+
for axis in axes
|
|
290
|
+
]
|
|
277
291
|
)
|
|
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
|
|
292
|
+
return np.max(timeouts)
|
|
282
293
|
|
|
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()
|
|
291
294
|
|
|
292
|
-
|
|
293
|
-
class UndulatorJawPhase(StandardReadable, Movable):
|
|
295
|
+
class UndulatorJawPhase(SafeUndulatorMover[float]):
|
|
294
296
|
"""
|
|
295
297
|
A JawPhase movable, this is use for moving the jaw phase which is use to control the
|
|
296
298
|
linear arbitrary polarisation but only one some of the beamline.
|
|
@@ -308,49 +310,22 @@ class UndulatorJawPhase(StandardReadable, Movable):
|
|
|
308
310
|
self.jaw_phase = UndulatorPhaseMotor(prefix=prefix, infix=jaw_phase)
|
|
309
311
|
# Nothing move until this is set to 1 and it will return to 0 when done
|
|
310
312
|
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
313
|
|
|
317
|
-
|
|
318
|
-
async def set(self, value: float) -> None:
|
|
319
|
-
LOGGER.info(f"Setting {self.name} to {value}")
|
|
314
|
+
super().__init__(self.set_move, prefix, name)
|
|
320
315
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
await asyncio.gather(
|
|
324
|
-
self.jaw_phase.user_setpoint.set(value=str(value)),
|
|
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)
|
|
316
|
+
async def _set_demand_positions(self, value: float) -> None:
|
|
317
|
+
await self.jaw_phase.user_setpoint.set(value=str(value))
|
|
329
318
|
|
|
330
|
-
async def
|
|
319
|
+
async def get_timeout(self) -> float:
|
|
331
320
|
"""
|
|
332
321
|
Get motor speed, current position and target position to calculate required timeout.
|
|
333
322
|
"""
|
|
334
|
-
|
|
335
|
-
self.jaw_phase.
|
|
336
|
-
self.jaw_phase.
|
|
337
|
-
self.jaw_phase.
|
|
323
|
+
return await estimate_motor_timeout(
|
|
324
|
+
self.jaw_phase.user_setpoint_demand_readback,
|
|
325
|
+
self.jaw_phase.user_setpoint_readback,
|
|
326
|
+
self.jaw_phase.velocity,
|
|
338
327
|
)
|
|
339
328
|
|
|
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
329
|
|
|
355
330
|
class Apple2(StandardReadable, Movable):
|
|
356
331
|
"""
|
|
@@ -437,7 +412,7 @@ class Apple2(StandardReadable, Movable):
|
|
|
437
412
|
"""
|
|
438
413
|
|
|
439
414
|
# Only need to check gap as the phase motors share both fault and gate with gap.
|
|
440
|
-
await self.gap().
|
|
415
|
+
await self.gap().raise_if_cannot_move()
|
|
441
416
|
await asyncio.gather(
|
|
442
417
|
self.phase().top_outer.user_setpoint.set(value=value.top_outer),
|
|
443
418
|
self.phase().top_inner.user_setpoint.set(value=value.top_inner),
|
|
@@ -29,7 +29,7 @@ class ReadOnlyAttenuator(StandardReadable):
|
|
|
29
29
|
super().__init__(name)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable):
|
|
32
|
+
class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable[float]):
|
|
33
33
|
"""The attenuator will insert filters into the beam to reduce its transmission.
|
|
34
34
|
In this attenuator, each filter can be in one of two states: IN or OUT
|
|
35
35
|
|
dodal/devices/backlight.py
CHANGED
|
@@ -15,7 +15,7 @@ class BacklightPosition(StrictEnum):
|
|
|
15
15
|
OUT = "Out"
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class Backlight(StandardReadable, Movable):
|
|
18
|
+
class Backlight(StandardReadable, Movable[BacklightPosition]):
|
|
19
19
|
"""Simple device to trigger the pneumatic in/out."""
|
|
20
20
|
|
|
21
21
|
TIME_TO_MOVE_S = 1.0 # Tested using a stopwatch on the beamline 09/2024
|
dodal/devices/bimorph_mirror.py
CHANGED
|
@@ -41,7 +41,7 @@ class BimorphMirrorStatus(StrictEnum):
|
|
|
41
41
|
ERROR = "Error"
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
class BimorphMirrorChannel(StandardReadable, Movable, EpicsDevice):
|
|
44
|
+
class BimorphMirrorChannel(StandardReadable, Movable[float], EpicsDevice):
|
|
45
45
|
"""Collection of PVs comprising a single bimorph channel.
|
|
46
46
|
|
|
47
47
|
Attributes:
|
|
@@ -66,7 +66,7 @@ class BimorphMirrorChannel(StandardReadable, Movable, EpicsDevice):
|
|
|
66
66
|
await self.output_voltage.set(value)
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
class BimorphMirror(StandardReadable, Movable):
|
|
69
|
+
class BimorphMirror(StandardReadable, Movable[Mapping[int, float]]):
|
|
70
70
|
"""Class to represent CAENels Bimorph Mirrors.
|
|
71
71
|
|
|
72
72
|
Attributes:
|
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
|
|