dls-dodal 1.30.0__py3-none-any.whl → 1.31.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.30.0.dist-info → dls_dodal-1.31.0.dist-info}/METADATA +4 -4
- {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/RECORD +64 -62
- {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamline_specific_utils/i03.py +1 -4
- dodal/beamlines/__init__.py +4 -0
- dodal/beamlines/i03.py +8 -8
- dodal/beamlines/i04.py +10 -9
- dodal/beamlines/i13_1.py +7 -7
- dodal/beamlines/i22.py +18 -18
- dodal/beamlines/p38.py +14 -14
- dodal/beamlines/p45.py +11 -11
- dodal/beamlines/training_rig.py +64 -0
- dodal/common/beamlines/beamline_parameters.py +5 -4
- dodal/common/beamlines/beamline_utils.py +9 -9
- dodal/common/types.py +4 -2
- dodal/common/udc_directory_provider.py +29 -22
- dodal/common/visit.py +59 -60
- dodal/devices/CTAB.py +1 -1
- dodal/devices/aperture.py +1 -1
- dodal/devices/aperturescatterguard.py +140 -188
- dodal/devices/areadetector/plugins/MJPG.py +2 -1
- dodal/devices/backlight.py +12 -1
- dodal/devices/dcm.py +1 -1
- dodal/devices/detector/detector.py +31 -30
- dodal/devices/detector/detector_motion.py +1 -1
- dodal/devices/fast_grid_scan.py +14 -24
- dodal/devices/focusing_mirror.py +2 -2
- dodal/devices/i22/dcm.py +1 -1
- dodal/devices/i22/fswitch.py +6 -2
- dodal/devices/i22/nxsas.py +32 -11
- dodal/devices/i24/aperture.py +1 -1
- dodal/devices/i24/beamstop.py +1 -1
- dodal/devices/i24/dcm.py +1 -1
- dodal/devices/i24/i24_detector_motion.py +1 -1
- dodal/devices/i24/pmac.py +24 -8
- dodal/devices/linkam3.py +1 -1
- dodal/devices/motors.py +1 -1
- dodal/devices/oav/oav_to_redis_forwarder.py +46 -17
- dodal/devices/robot.py +1 -2
- dodal/devices/scatterguard.py +1 -1
- dodal/devices/scintillator.py +1 -1
- dodal/devices/slits.py +1 -1
- dodal/devices/smargon.py +1 -1
- dodal/devices/tetramm.py +20 -16
- dodal/devices/training_rig/__init__.py +0 -0
- dodal/devices/training_rig/sample_stage.py +10 -0
- dodal/devices/turbo_slit.py +1 -1
- dodal/devices/undulator.py +1 -1
- dodal/devices/util/adjuster_plans.py +1 -1
- dodal/devices/util/save_panda.py +1 -1
- dodal/devices/util/test_utils.py +1 -1
- dodal/devices/xbpm_feedback.py +1 -2
- dodal/devices/xspress3/xspress3.py +1 -1
- dodal/devices/zebra.py +5 -0
- dodal/devices/zebra_controlled_shutter.py +24 -9
- dodal/devices/zocalo/zocalo_results.py +6 -2
- dodal/log.py +32 -10
- dodal/plans/check_topup.py +65 -10
- dodal/plans/data_session_metadata.py +8 -10
- dodal/plans/motor_util_plans.py +1 -1
- dodal/devices/beamstop.py +0 -8
- {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
|
-
from collections import namedtuple
|
|
3
|
-
from dataclasses import asdict, dataclass
|
|
4
4
|
from enum import Enum
|
|
5
5
|
|
|
6
|
-
from bluesky.protocols import Movable
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
from bluesky.protocols import Movable
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
AsyncStatus,
|
|
9
|
+
HintedSignal,
|
|
10
|
+
StandardReadable,
|
|
11
|
+
soft_signal_rw,
|
|
12
|
+
)
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
10
14
|
|
|
11
15
|
from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
|
|
12
16
|
from dodal.devices.aperture import Aperture
|
|
@@ -17,100 +21,76 @@ class InvalidApertureMove(Exception):
|
|
|
17
21
|
pass
|
|
18
22
|
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
|
|
24
|
+
class AperturePosition(BaseModel):
|
|
25
|
+
aperture_x: float
|
|
26
|
+
aperture_y: float
|
|
27
|
+
aperture_z: float
|
|
28
|
+
scatterguard_x: float
|
|
29
|
+
scatterguard_y: float
|
|
30
|
+
radius: float | None = Field(json_schema_extra={"units": "µm"}, default=None)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def values(self) -> tuple[float, float, float, float, float]:
|
|
34
|
+
return (
|
|
35
|
+
self.aperture_x,
|
|
36
|
+
self.aperture_y,
|
|
37
|
+
self.aperture_z,
|
|
38
|
+
self.scatterguard_x,
|
|
39
|
+
self.scatterguard_y,
|
|
40
|
+
)
|
|
31
41
|
|
|
32
|
-
@
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
@staticmethod
|
|
43
|
+
def tolerances_from_gda_params(
|
|
44
|
+
params: GDABeamlineParameters,
|
|
45
|
+
) -> AperturePosition:
|
|
46
|
+
return AperturePosition(
|
|
47
|
+
aperture_x=params["miniap_x_tolerance"],
|
|
48
|
+
aperture_y=params["miniap_y_tolerance"],
|
|
49
|
+
aperture_z=params["miniap_z_tolerance"],
|
|
50
|
+
scatterguard_x=params["sg_x_tolerance"],
|
|
51
|
+
scatterguard_y=params["sg_y_tolerance"],
|
|
52
|
+
)
|
|
39
53
|
|
|
54
|
+
@staticmethod
|
|
55
|
+
def from_gda_params(
|
|
56
|
+
name: ApertureValue,
|
|
57
|
+
radius: float | None,
|
|
58
|
+
params: GDABeamlineParameters,
|
|
59
|
+
) -> AperturePosition:
|
|
60
|
+
return AperturePosition(
|
|
61
|
+
aperture_x=params[f"miniap_x_{name.value}"],
|
|
62
|
+
aperture_y=params[f"miniap_y_{name.value}"],
|
|
63
|
+
aperture_z=params[f"miniap_z_{name.value}"],
|
|
64
|
+
scatterguard_x=params[f"sg_x_{name.value}"],
|
|
65
|
+
scatterguard_y=params[f"sg_y_{name.value}"],
|
|
66
|
+
radius=radius,
|
|
67
|
+
)
|
|
40
68
|
|
|
41
|
-
@dataclass
|
|
42
|
-
class SingleAperturePosition:
|
|
43
|
-
name: str
|
|
44
|
-
GDA_name: str
|
|
45
|
-
radius_microns: float | None
|
|
46
|
-
location: ApertureFiveDimensionalLocation
|
|
47
69
|
|
|
70
|
+
class ApertureValue(str, Enum):
|
|
71
|
+
"""Maps from a short usable name to the value name in the GDA Beamline parameters"""
|
|
48
72
|
|
|
49
|
-
# Use StrEnum once we stop python 3.10 support
|
|
50
|
-
class AperturePositionGDANames(str, Enum):
|
|
51
|
-
LARGE_APERTURE = "LARGE_APERTURE"
|
|
52
|
-
MEDIUM_APERTURE = "MEDIUM_APERTURE"
|
|
53
|
-
SMALL_APERTURE = "SMALL_APERTURE"
|
|
54
73
|
ROBOT_LOAD = "ROBOT_LOAD"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def position_from_params(
|
|
61
|
-
name: str,
|
|
62
|
-
GDA_name: AperturePositionGDANames,
|
|
63
|
-
radius_microns: float | None,
|
|
64
|
-
params: GDABeamlineParameters,
|
|
65
|
-
) -> SingleAperturePosition:
|
|
66
|
-
return SingleAperturePosition(
|
|
67
|
-
name,
|
|
68
|
-
GDA_name,
|
|
69
|
-
radius_microns,
|
|
70
|
-
ApertureFiveDimensionalLocation(
|
|
71
|
-
params[f"miniap_x_{GDA_name}"],
|
|
72
|
-
params[f"miniap_y_{GDA_name}"],
|
|
73
|
-
params[f"miniap_z_{GDA_name}"],
|
|
74
|
-
params[f"sg_x_{GDA_name}"],
|
|
75
|
-
params[f"sg_y_{GDA_name}"],
|
|
76
|
-
),
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def load_tolerances_from_beamline_params(
|
|
81
|
-
params: GDABeamlineParameters,
|
|
82
|
-
) -> ApertureScatterguardTolerances:
|
|
83
|
-
return ApertureScatterguardTolerances(
|
|
84
|
-
ap_x=params["miniap_x_tolerance"],
|
|
85
|
-
ap_y=params["miniap_y_tolerance"],
|
|
86
|
-
ap_z=params["miniap_z_tolerance"],
|
|
87
|
-
sg_x=params["sg_x_tolerance"],
|
|
88
|
-
sg_y=params["sg_y_tolerance"],
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
class AperturePosition(Enum):
|
|
93
|
-
ROBOT_LOAD = 0
|
|
94
|
-
SMALL = 1
|
|
95
|
-
MEDIUM = 2
|
|
96
|
-
LARGE = 3
|
|
74
|
+
SMALL = "SMALL_APERTURE"
|
|
75
|
+
MEDIUM = "MEDIUM_APERTURE"
|
|
76
|
+
LARGE = "LARGE_APERTURE"
|
|
97
77
|
|
|
98
78
|
|
|
99
79
|
def load_positions_from_beamline_parameters(
|
|
100
80
|
params: GDABeamlineParameters,
|
|
101
|
-
) -> dict[
|
|
81
|
+
) -> dict[ApertureValue, AperturePosition]:
|
|
102
82
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
83
|
+
ApertureValue.ROBOT_LOAD: AperturePosition.from_gda_params(
|
|
84
|
+
ApertureValue.ROBOT_LOAD, None, params
|
|
105
85
|
),
|
|
106
|
-
|
|
107
|
-
|
|
86
|
+
ApertureValue.SMALL: AperturePosition.from_gda_params(
|
|
87
|
+
ApertureValue.SMALL, 20, params
|
|
108
88
|
),
|
|
109
|
-
|
|
110
|
-
|
|
89
|
+
ApertureValue.MEDIUM: AperturePosition.from_gda_params(
|
|
90
|
+
ApertureValue.MEDIUM, 50, params
|
|
111
91
|
),
|
|
112
|
-
|
|
113
|
-
|
|
92
|
+
ApertureValue.LARGE: AperturePosition.from_gda_params(
|
|
93
|
+
ApertureValue.LARGE, 100, params
|
|
114
94
|
),
|
|
115
95
|
}
|
|
116
96
|
|
|
@@ -118,119 +98,86 @@ def load_positions_from_beamline_parameters(
|
|
|
118
98
|
class ApertureScatterguard(StandardReadable, Movable):
|
|
119
99
|
def __init__(
|
|
120
100
|
self,
|
|
121
|
-
loaded_positions: dict[
|
|
122
|
-
tolerances:
|
|
101
|
+
loaded_positions: dict[ApertureValue, AperturePosition],
|
|
102
|
+
tolerances: AperturePosition,
|
|
123
103
|
prefix: str = "",
|
|
124
104
|
name: str = "",
|
|
125
105
|
) -> None:
|
|
126
|
-
self.
|
|
127
|
-
self.
|
|
106
|
+
self.aperture = Aperture(prefix + "-MO-MAPT-01:")
|
|
107
|
+
self.scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
|
|
108
|
+
self.radius = soft_signal_rw(float, units="µm")
|
|
128
109
|
self._loaded_positions = loaded_positions
|
|
129
110
|
self._tolerances = tolerances
|
|
130
|
-
|
|
131
|
-
|
|
111
|
+
self.add_readables(
|
|
112
|
+
[
|
|
113
|
+
self.aperture.x.user_readback,
|
|
114
|
+
self.aperture.y.user_readback,
|
|
115
|
+
self.aperture.z.user_readback,
|
|
116
|
+
self.scatterguard.x.user_readback,
|
|
117
|
+
self.scatterguard.y.user_readback,
|
|
118
|
+
self.radius,
|
|
119
|
+
],
|
|
132
120
|
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
self.add_readables([self.selected_aperture], wrapper=HintedSignal)
|
|
136
|
-
super().__init__(name)
|
|
137
|
-
|
|
138
|
-
class ApertureConverter(SoftConverter):
|
|
139
|
-
# Ophyd-async #311 should add a default converter for dataclasses to do this
|
|
140
|
-
def reading(
|
|
141
|
-
self, value: SingleAperturePosition, timestamp: float, severity: int
|
|
142
|
-
) -> Reading:
|
|
143
|
-
return Reading(
|
|
144
|
-
value=asdict(value),
|
|
145
|
-
timestamp=timestamp,
|
|
146
|
-
alarm_severity=-1 if severity > 2 else severity,
|
|
147
|
-
)
|
|
121
|
+
with self.add_children_as_readables(HintedSignal):
|
|
122
|
+
self.selected_aperture = soft_signal_rw(ApertureValue)
|
|
148
123
|
|
|
149
|
-
|
|
150
|
-
async def read(self, *args, **kwargs):
|
|
151
|
-
assert isinstance(self.parent, ApertureScatterguard)
|
|
152
|
-
assert self._backend
|
|
153
|
-
await self._backend.put(await self.parent.get_current_aperture_position())
|
|
154
|
-
return {self.name: await self._backend.get_reading()}
|
|
155
|
-
|
|
156
|
-
async def describe(self) -> dict[str, DataKey]:
|
|
157
|
-
return {
|
|
158
|
-
self._name: DataKey(
|
|
159
|
-
dtype="array",
|
|
160
|
-
shape=[
|
|
161
|
-
-1,
|
|
162
|
-
], # TODO describe properly - see https://github.com/DiamondLightSource/dodal/issues/253,
|
|
163
|
-
source=self._backend.source(self._name), # type: ignore
|
|
164
|
-
)
|
|
165
|
-
}
|
|
124
|
+
super().__init__(name)
|
|
166
125
|
|
|
167
126
|
def get_position_from_gda_aperture_name(
|
|
168
|
-
self, gda_aperture_name:
|
|
169
|
-
) ->
|
|
170
|
-
|
|
171
|
-
if detail.GDA_name == gda_aperture_name:
|
|
172
|
-
return aperture
|
|
173
|
-
raise ValueError(
|
|
174
|
-
f"Tried to convert unknown aperture name {gda_aperture_name} to a SingleAperturePosition"
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
def get_gda_name_for_position(self, position: AperturePosition) -> str:
|
|
178
|
-
detailed_position = self._loaded_positions[position]
|
|
179
|
-
return detailed_position.GDA_name
|
|
127
|
+
self, gda_aperture_name: str
|
|
128
|
+
) -> ApertureValue:
|
|
129
|
+
return ApertureValue(gda_aperture_name)
|
|
180
130
|
|
|
181
131
|
@AsyncStatus.wrap
|
|
182
|
-
async def set(self, value:
|
|
132
|
+
async def set(self, value: ApertureValue):
|
|
183
133
|
position = self._loaded_positions[value]
|
|
184
|
-
await self._safe_move_within_datacollection_range(position
|
|
185
|
-
|
|
186
|
-
def _get_motor_list(self):
|
|
187
|
-
return [
|
|
188
|
-
self._aperture.x,
|
|
189
|
-
self._aperture.y,
|
|
190
|
-
self._aperture.z,
|
|
191
|
-
self._scatterguard.x,
|
|
192
|
-
self._scatterguard.y,
|
|
193
|
-
]
|
|
134
|
+
await self._safe_move_within_datacollection_range(position, value)
|
|
194
135
|
|
|
195
136
|
@AsyncStatus.wrap
|
|
196
|
-
async def _set_raw_unsafe(self,
|
|
137
|
+
async def _set_raw_unsafe(self, position: AperturePosition):
|
|
197
138
|
"""Accept the risks and move in an unsafe way. Collisions possible."""
|
|
139
|
+
if position.radius is not None:
|
|
140
|
+
await self.radius.set(position.radius)
|
|
198
141
|
|
|
199
|
-
|
|
200
|
-
|
|
142
|
+
aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = (
|
|
143
|
+
position.values
|
|
144
|
+
)
|
|
201
145
|
|
|
202
146
|
await asyncio.gather(
|
|
203
|
-
self.
|
|
204
|
-
self.
|
|
205
|
-
self.
|
|
206
|
-
self.
|
|
207
|
-
self.
|
|
147
|
+
self.aperture.x.set(aperture_x),
|
|
148
|
+
self.aperture.y.set(aperture_y),
|
|
149
|
+
self.aperture.z.set(aperture_z),
|
|
150
|
+
self.scatterguard.x.set(scatterguard_x),
|
|
151
|
+
self.scatterguard.y.set(scatterguard_y),
|
|
208
152
|
)
|
|
153
|
+
try:
|
|
154
|
+
value = await self.get_current_aperture_position()
|
|
155
|
+
self.selected_aperture.set(value)
|
|
156
|
+
except InvalidApertureMove:
|
|
157
|
+
self.selected_aperture.set(None) # type: ignore
|
|
209
158
|
|
|
210
|
-
async def get_current_aperture_position(self) ->
|
|
159
|
+
async def get_current_aperture_position(self) -> ApertureValue:
|
|
211
160
|
"""
|
|
212
161
|
Returns the current aperture position using readback values
|
|
213
162
|
for SMALL, MEDIUM, LARGE. ROBOT_LOAD position defined when
|
|
214
163
|
mini aperture y <= ROBOT_LOAD.location.aperture_y + tolerance.
|
|
215
164
|
If no position is found then raises InvalidApertureMove.
|
|
216
165
|
"""
|
|
217
|
-
current_ap_y = await self.
|
|
218
|
-
robot_load_ap_y = self._loaded_positions[
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return
|
|
223
|
-
elif await self.
|
|
224
|
-
return
|
|
225
|
-
elif
|
|
226
|
-
return
|
|
227
|
-
elif current_ap_y <= robot_load_ap_y + self._tolerances.ap_y:
|
|
228
|
-
return self._loaded_positions[AperturePosition.ROBOT_LOAD]
|
|
166
|
+
current_ap_y = await self.aperture.y.user_readback.get_value(cached=False)
|
|
167
|
+
robot_load_ap_y = self._loaded_positions[ApertureValue.ROBOT_LOAD].aperture_y
|
|
168
|
+
if await self.aperture.large.get_value(cached=False) == 1:
|
|
169
|
+
return ApertureValue.LARGE
|
|
170
|
+
elif await self.aperture.medium.get_value(cached=False) == 1:
|
|
171
|
+
return ApertureValue.MEDIUM
|
|
172
|
+
elif await self.aperture.small.get_value(cached=False) == 1:
|
|
173
|
+
return ApertureValue.SMALL
|
|
174
|
+
elif current_ap_y <= robot_load_ap_y + self._tolerances.aperture_y:
|
|
175
|
+
return ApertureValue.ROBOT_LOAD
|
|
229
176
|
|
|
230
177
|
raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
|
|
231
178
|
|
|
232
179
|
async def _safe_move_within_datacollection_range(
|
|
233
|
-
self,
|
|
180
|
+
self, position: AperturePosition, value: ApertureValue
|
|
234
181
|
):
|
|
235
182
|
"""
|
|
236
183
|
Move the aperture and scatterguard combo safely to a new position.
|
|
@@ -238,45 +185,50 @@ class ApertureScatterguard(StandardReadable, Movable):
|
|
|
238
185
|
for why this is required.
|
|
239
186
|
"""
|
|
240
187
|
assert self._loaded_positions is not None
|
|
241
|
-
# unpacking the position
|
|
242
|
-
aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = pos
|
|
243
188
|
|
|
244
|
-
ap_z_in_position = await self.
|
|
189
|
+
ap_z_in_position = await self.aperture.z.motor_done_move.get_value()
|
|
245
190
|
if not ap_z_in_position:
|
|
246
191
|
raise InvalidApertureMove(
|
|
247
192
|
"ApertureScatterguard z is still moving. Wait for it to finish "
|
|
248
193
|
"before triggering another move."
|
|
249
194
|
)
|
|
250
195
|
|
|
251
|
-
current_ap_z = await self.
|
|
252
|
-
diff_on_z = abs(current_ap_z - aperture_z)
|
|
253
|
-
if diff_on_z > self._tolerances.
|
|
196
|
+
current_ap_z = await self.aperture.z.user_readback.get_value()
|
|
197
|
+
diff_on_z = abs(current_ap_z - position.aperture_z)
|
|
198
|
+
if diff_on_z > self._tolerances.aperture_z:
|
|
254
199
|
raise InvalidApertureMove(
|
|
255
200
|
"ApertureScatterguard safe move is not yet defined for positions "
|
|
256
201
|
"outside of LARGE, MEDIUM, SMALL, ROBOT_LOAD. "
|
|
257
|
-
f"Current aperture z ({current_ap_z}), outside of tolerance ({self._tolerances.
|
|
202
|
+
f"Current aperture z ({current_ap_z}), outside of tolerance ({self._tolerances.aperture_z}) from target ({position.aperture_z})."
|
|
258
203
|
)
|
|
259
204
|
|
|
260
|
-
current_ap_y = await self.
|
|
205
|
+
current_ap_y = await self.aperture.y.user_readback.get_value()
|
|
206
|
+
if position.radius is not None:
|
|
207
|
+
await self.radius.set(position.radius)
|
|
208
|
+
|
|
209
|
+
aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = (
|
|
210
|
+
position.values
|
|
211
|
+
)
|
|
261
212
|
|
|
262
|
-
if aperture_y > current_ap_y:
|
|
213
|
+
if position.aperture_y > current_ap_y:
|
|
263
214
|
await asyncio.gather(
|
|
264
|
-
self.
|
|
265
|
-
self.
|
|
215
|
+
self.scatterguard.x.set(scatterguard_x),
|
|
216
|
+
self.scatterguard.y.set(scatterguard_y),
|
|
266
217
|
)
|
|
267
218
|
await asyncio.gather(
|
|
268
|
-
self.
|
|
269
|
-
self.
|
|
270
|
-
self.
|
|
219
|
+
self.aperture.x.set(aperture_x),
|
|
220
|
+
self.aperture.y.set(aperture_y),
|
|
221
|
+
self.aperture.z.set(aperture_z),
|
|
271
222
|
)
|
|
272
223
|
return
|
|
273
224
|
await asyncio.gather(
|
|
274
|
-
self.
|
|
275
|
-
self.
|
|
276
|
-
self.
|
|
225
|
+
self.aperture.x.set(aperture_x),
|
|
226
|
+
self.aperture.y.set(aperture_y),
|
|
227
|
+
self.aperture.z.set(aperture_z),
|
|
277
228
|
)
|
|
278
229
|
|
|
279
230
|
await asyncio.gather(
|
|
280
|
-
self.
|
|
281
|
-
self.
|
|
231
|
+
self.scatterguard.x.set(scatterguard_x),
|
|
232
|
+
self.scatterguard.y.set(scatterguard_y),
|
|
282
233
|
)
|
|
234
|
+
await self.selected_aperture.set(value)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import threading
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
+
from io import BytesIO
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import requests
|
|
@@ -70,7 +71,7 @@ class MJPG(Device, ABC):
|
|
|
70
71
|
try:
|
|
71
72
|
response = requests.get(url_str, stream=True)
|
|
72
73
|
response.raise_for_status()
|
|
73
|
-
with Image.open(response.
|
|
74
|
+
with Image.open(BytesIO(response.content)) as image:
|
|
74
75
|
self.post_processing(image)
|
|
75
76
|
st.set_finished()
|
|
76
77
|
except requests.HTTPError as e:
|
dodal/devices/backlight.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from asyncio import sleep
|
|
1
2
|
from enum import Enum
|
|
2
3
|
|
|
3
4
|
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
@@ -17,6 +18,8 @@ class BacklightPosition(str, Enum):
|
|
|
17
18
|
class Backlight(StandardReadable):
|
|
18
19
|
"""Simple device to trigger the pneumatic in/out."""
|
|
19
20
|
|
|
21
|
+
TIME_TO_MOVE_S = 1 # Tested using a stopwatch on the beamline 09/2024
|
|
22
|
+
|
|
20
23
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
21
24
|
with self.add_children_as_readables():
|
|
22
25
|
self.power = epics_signal_rw(BacklightPower, prefix + "-EA-BLIT-01:TOGGLE")
|
|
@@ -28,9 +31,17 @@ class Backlight(StandardReadable):
|
|
|
28
31
|
@AsyncStatus.wrap
|
|
29
32
|
async def set(self, position: BacklightPosition):
|
|
30
33
|
"""This setter will turn the backlight on when we move it in to the beam and off
|
|
31
|
-
when we move it out.
|
|
34
|
+
when we move it out.
|
|
35
|
+
|
|
36
|
+
Moving the backlight in/out is a pneumatic axis and we have no readback on its
|
|
37
|
+
position so it appears to us to instantly move. In fact it does take some time
|
|
38
|
+
to move completely in/out so we sleep here to simulate this.
|
|
39
|
+
"""
|
|
40
|
+
old_position = await self.position.get_value()
|
|
32
41
|
await self.position.set(position)
|
|
33
42
|
if position == BacklightPosition.OUT:
|
|
34
43
|
await self.power.set(BacklightPower.OFF)
|
|
35
44
|
else:
|
|
36
45
|
await self.power.set(BacklightPower.ON)
|
|
46
|
+
if old_position != position:
|
|
47
|
+
await sleep(self.TIME_TO_MOVE_S)
|
dodal/devices/dcm.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from enum import Enum, auto
|
|
2
|
-
from
|
|
2
|
+
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel,
|
|
4
|
+
from pydantic import BaseModel, Field, field_serializer, field_validator
|
|
5
5
|
|
|
6
6
|
from dodal.devices.detector.det_dim_constants import (
|
|
7
7
|
EIGER2_X_16M_SIZE,
|
|
@@ -28,9 +28,12 @@ class DetectorParams(BaseModel):
|
|
|
28
28
|
"""Holds parameters for the detector. Provides access to a list of Dectris detector
|
|
29
29
|
sizes and a converter for distance to beam centre."""
|
|
30
30
|
|
|
31
|
+
# https://github.com/pydantic/pydantic/issues/8379
|
|
32
|
+
# Must use model_dump(by_alias=True) if serialising!
|
|
33
|
+
|
|
31
34
|
expected_energy_ev: float | None = None
|
|
32
35
|
exposure_time: float
|
|
33
|
-
directory: str
|
|
36
|
+
directory: str # : Path https://github.com/DiamondLightSource/dodal/issues/774
|
|
34
37
|
prefix: str
|
|
35
38
|
detector_distance: float
|
|
36
39
|
omega_start: float
|
|
@@ -39,46 +42,44 @@ class DetectorParams(BaseModel):
|
|
|
39
42
|
num_triggers: int
|
|
40
43
|
use_roi_mode: bool
|
|
41
44
|
det_dist_to_beam_converter_path: str
|
|
45
|
+
override_run_number: int | None = Field(default=None, alias="run_number")
|
|
42
46
|
trigger_mode: TriggerMode = TriggerMode.SET_FRAMES
|
|
43
47
|
detector_size_constants: DetectorSizeConstants = EIGER2_X_16M_SIZE
|
|
44
|
-
beam_xy_converter: DetectorDistanceToBeamXYConverter = None # type: ignore # Filled in by validator
|
|
45
|
-
run_number: int = None # type: ignore # Filled in by validator
|
|
46
48
|
enable_dev_shm: bool = (
|
|
47
49
|
False # Remove in https://github.com/DiamondLightSource/hyperion/issues/1395
|
|
48
50
|
)
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
values["beam_xy_converter"] = DetectorDistanceToBeamXYConverter(
|
|
61
|
-
values["det_dist_to_beam_converter_path"]
|
|
52
|
+
@property
|
|
53
|
+
def beam_xy_converter(self) -> DetectorDistanceToBeamXYConverter:
|
|
54
|
+
return DetectorDistanceToBeamXYConverter(self.det_dist_to_beam_converter_path)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def run_number(self) -> int:
|
|
58
|
+
return (
|
|
59
|
+
get_run_number(self.directory, self.prefix)
|
|
60
|
+
if self.override_run_number is None
|
|
61
|
+
else self.override_run_number
|
|
62
62
|
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
) -> DetectorSizeConstants:
|
|
63
|
+
|
|
64
|
+
@field_serializer("detector_size_constants")
|
|
65
|
+
def serialize_detector_size_constants(self, size: DetectorSizeConstants):
|
|
66
|
+
return size.det_type_string
|
|
67
|
+
|
|
68
|
+
@field_validator("detector_size_constants", mode="before")
|
|
69
|
+
@classmethod
|
|
70
|
+
def _parse_detector_size_constants(cls, det_type: str) -> DetectorSizeConstants:
|
|
71
71
|
return (
|
|
72
72
|
det_type
|
|
73
73
|
if isinstance(det_type, DetectorSizeConstants)
|
|
74
74
|
else constants_from_type(det_type)
|
|
75
75
|
)
|
|
76
76
|
|
|
77
|
-
@
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
@field_validator("directory", mode="before")
|
|
78
|
+
@classmethod
|
|
79
|
+
def _parse_directory(cls, directory: str | Path) -> str:
|
|
80
|
+
path = Path(directory)
|
|
81
|
+
assert path.is_dir()
|
|
82
|
+
return str(path)
|
|
82
83
|
|
|
83
84
|
def get_beam_position_mm(self, detector_distance: float) -> tuple[float, float]:
|
|
84
85
|
x_beam_mm = self.beam_xy_converter.get_beam_xy_from_det_dist(
|
dodal/devices/fast_grid_scan.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Generic, TypeVar
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from bluesky.plan_stubs import mv
|
|
@@ -20,7 +20,7 @@ from ophyd_async.epics.signal import (
|
|
|
20
20
|
epics_signal_rw_rbv,
|
|
21
21
|
epics_signal_x,
|
|
22
22
|
)
|
|
23
|
-
from pydantic import
|
|
23
|
+
from pydantic import field_validator
|
|
24
24
|
from pydantic.dataclasses import dataclass
|
|
25
25
|
|
|
26
26
|
from dodal.log import LOGGER
|
|
@@ -69,9 +69,6 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
|
|
|
69
69
|
y2_start: float = 0.1
|
|
70
70
|
z1_start: float = 0.1
|
|
71
71
|
z2_start: float = 0.1
|
|
72
|
-
x_axis: GridAxis = GridAxis(0, 0, 0)
|
|
73
|
-
y_axis: GridAxis = GridAxis(0, 0, 0)
|
|
74
|
-
z_axis: GridAxis = GridAxis(0, 0, 0)
|
|
75
72
|
|
|
76
73
|
# Whether to set the stub offsets after centering
|
|
77
74
|
set_stub_offsets: bool = False
|
|
@@ -91,28 +88,20 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
|
|
|
91
88
|
"z2_start": self.z2_start,
|
|
92
89
|
}
|
|
93
90
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"x_axis": {"exclude": True},
|
|
98
|
-
"y_axis": {"exclude": True},
|
|
99
|
-
"z_axis": {"exclude": True},
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
@validator("x_axis", always=True)
|
|
103
|
-
def _get_x_axis(cls, x_axis: GridAxis, values: dict[str, Any]) -> GridAxis:
|
|
104
|
-
return GridAxis(values["x_start"], values["x_step_size"], values["x_steps"])
|
|
91
|
+
@property
|
|
92
|
+
def x_axis(self) -> GridAxis:
|
|
93
|
+
return GridAxis(self.x_start, self.x_step_size, self.x_steps)
|
|
105
94
|
|
|
106
|
-
@
|
|
107
|
-
def
|
|
108
|
-
return GridAxis(
|
|
95
|
+
@property
|
|
96
|
+
def y_axis(self) -> GridAxis:
|
|
97
|
+
return GridAxis(self.y1_start, self.y_step_size, self.y_steps)
|
|
109
98
|
|
|
110
|
-
@
|
|
111
|
-
def
|
|
112
|
-
return GridAxis(
|
|
99
|
+
@property
|
|
100
|
+
def z_axis(self) -> GridAxis:
|
|
101
|
+
return GridAxis(self.z2_start, self.z_step_size, self.z_steps)
|
|
113
102
|
|
|
114
103
|
def get_num_images(self):
|
|
115
|
-
return self.x_steps * self.y_steps + self.
|
|
104
|
+
return self.x_steps * (self.y_steps + self.z_steps)
|
|
116
105
|
|
|
117
106
|
@property
|
|
118
107
|
def is_3d_grid_scan(self):
|
|
@@ -155,7 +144,8 @@ class ZebraGridScanParams(GridScanParamsCommon):
|
|
|
155
144
|
param_positions["dwell_time_ms"] = self.dwell_time_ms
|
|
156
145
|
return param_positions
|
|
157
146
|
|
|
158
|
-
@
|
|
147
|
+
@field_validator("dwell_time_ms")
|
|
148
|
+
@classmethod
|
|
159
149
|
def non_integer_dwell_time(cls, dwell_time_ms: float) -> float:
|
|
160
150
|
dwell_time_floor_rounded = np.floor(dwell_time_ms)
|
|
161
151
|
dwell_time_is_close = np.isclose(
|
dodal/devices/focusing_mirror.py
CHANGED
|
@@ -8,9 +8,9 @@ from ophyd_async.core import (
|
|
|
8
8
|
HintedSignal,
|
|
9
9
|
StandardReadable,
|
|
10
10
|
observe_value,
|
|
11
|
+
soft_signal_r_and_setter,
|
|
11
12
|
)
|
|
12
|
-
from ophyd_async.
|
|
13
|
-
from ophyd_async.epics.motion import Motor
|
|
13
|
+
from ophyd_async.epics.motor import Motor
|
|
14
14
|
from ophyd_async.epics.signal import (
|
|
15
15
|
epics_signal_r,
|
|
16
16
|
epics_signal_rw,
|