dls-dodal 1.65.0__py3-none-any.whl → 1.66.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.65.0.dist-info → dls_dodal-1.66.0.dist-info}/METADATA +3 -4
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/RECORD +56 -50
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +21 -2
- dodal/beamlines/i03.py +23 -3
- dodal/beamlines/i04.py +18 -3
- dodal/beamlines/i05.py +28 -1
- dodal/beamlines/i06.py +62 -0
- dodal/beamlines/i07.py +20 -0
- dodal/beamlines/i09_1.py +7 -2
- dodal/beamlines/i10_optics.py +18 -8
- dodal/beamlines/i18.py +3 -3
- dodal/beamlines/i22.py +3 -3
- dodal/beamlines/p38.py +3 -3
- dodal/devices/aithre_lasershaping/goniometer.py +26 -9
- dodal/devices/aperturescatterguard.py +3 -2
- dodal/devices/apple2_undulator.py +89 -44
- dodal/devices/areadetector/plugins/mjpg.py +10 -3
- dodal/devices/beamsize/__init__.py +0 -0
- dodal/devices/beamsize/beamsize.py +6 -0
- dodal/devices/detector/det_resolution.py +4 -2
- dodal/devices/fast_grid_scan.py +14 -2
- dodal/devices/i03/beamsize.py +35 -0
- dodal/devices/i03/constants.py +7 -0
- dodal/devices/i03/undulator_dcm.py +2 -2
- dodal/devices/i04/beamsize.py +45 -0
- dodal/devices/i04/murko_results.py +36 -26
- dodal/devices/i04/transfocator.py +23 -29
- dodal/devices/i07/id.py +38 -0
- dodal/devices/i09_1_shared/__init__.py +6 -2
- dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
- dodal/devices/i10/i10_apple2.py +22 -316
- dodal/devices/i17/i17_apple2.py +7 -4
- dodal/devices/ipin.py +20 -2
- dodal/devices/motors.py +19 -3
- dodal/devices/mx_phase1/beamstop.py +31 -12
- dodal/devices/oav/oav_calculations.py +9 -4
- dodal/devices/oav/oav_detector.py +65 -7
- dodal/devices/oav/oav_parameters.py +3 -1
- dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
- dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
- dodal/devices/oav/pin_image_recognition/utils.py +23 -1
- dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
- dodal/devices/oav/utils.py +16 -6
- dodal/devices/robot.py +17 -7
- dodal/devices/scintillator.py +36 -14
- dodal/devices/smargon.py +2 -3
- dodal/devices/thawer.py +7 -45
- dodal/devices/undulator.py +152 -68
- dodal/devices/util/lookup_tables_apple2.py +390 -0
- dodal/plans/load_panda_yaml.py +9 -0
- dodal/plans/verify_undulator_gap.py +2 -2
- dodal/beamline_specific_utils/i03.py +0 -17
- dodal/testing/__init__.py +0 -3
- dodal/testing/setup.py +0 -67
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/top_level.txt +0 -0
|
@@ -46,9 +46,14 @@ class NullZoomController(BaseZoomController):
|
|
|
46
46
|
def __init__(self):
|
|
47
47
|
self.level = soft_signal_rw(str, "1.0x")
|
|
48
48
|
self.percentage = soft_signal_rw(float, 100)
|
|
49
|
+
super().__init__()
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
@AsyncStatus.wrap
|
|
52
|
+
async def set(self, value: str) -> None:
|
|
53
|
+
if value != "1.0x":
|
|
54
|
+
raise Exception("Attempting to set zoom level of a null zoom controller")
|
|
55
|
+
else:
|
|
56
|
+
await self.level.set(value, wait=True)
|
|
52
57
|
|
|
53
58
|
|
|
54
59
|
class ZoomController(BaseZoomController):
|
|
@@ -74,6 +79,16 @@ class ZoomController(BaseZoomController):
|
|
|
74
79
|
|
|
75
80
|
|
|
76
81
|
class OAV(StandardReadable):
|
|
82
|
+
"""
|
|
83
|
+
Class for oav device
|
|
84
|
+
|
|
85
|
+
x_direction(int): Should only be 1 or -1, with 1 indicating the oav x direction is the same with motor x
|
|
86
|
+
y_direction(int): Same with x_direction but for motor y
|
|
87
|
+
z_direction(int): Same with x_direction but for motor z
|
|
88
|
+
mjpg_x_size_pv(str): PV infix for x_size in mjpg
|
|
89
|
+
mjpg_y_size_pv(str): PV infix for y_size in mjpg
|
|
90
|
+
"""
|
|
91
|
+
|
|
77
92
|
beam_centre_i: SignalR[int]
|
|
78
93
|
beam_centre_j: SignalR[int]
|
|
79
94
|
|
|
@@ -82,7 +97,13 @@ class OAV(StandardReadable):
|
|
|
82
97
|
prefix: str,
|
|
83
98
|
config: OAVConfigBase,
|
|
84
99
|
name: str = "",
|
|
100
|
+
mjpeg_prefix: str = "MJPG",
|
|
85
101
|
zoom_controller: BaseZoomController | None = None,
|
|
102
|
+
x_direction: int = -1,
|
|
103
|
+
y_direction: int = -1,
|
|
104
|
+
z_direction: int = 1,
|
|
105
|
+
mjpg_x_size_pv: str = "ArraySize1_RBV",
|
|
106
|
+
mjpg_y_size_pv: str = "ArraySize2_RBV",
|
|
86
107
|
):
|
|
87
108
|
self.oav_config = config
|
|
88
109
|
self._prefix = prefix
|
|
@@ -98,9 +119,15 @@ class OAV(StandardReadable):
|
|
|
98
119
|
|
|
99
120
|
self.cam = Cam(f"{prefix}CAM:", name=name)
|
|
100
121
|
with self.add_children_as_readables():
|
|
101
|
-
self.grid_snapshot = SnapshotWithGrid(
|
|
122
|
+
self.grid_snapshot = SnapshotWithGrid(
|
|
123
|
+
f"{prefix}{mjpeg_prefix}:", name, mjpg_x_size_pv, mjpg_y_size_pv
|
|
124
|
+
)
|
|
102
125
|
|
|
103
126
|
self.sizes = [self.grid_snapshot.x_size, self.grid_snapshot.y_size]
|
|
127
|
+
with self.add_children_as_readables():
|
|
128
|
+
self.x_direction = soft_signal_rw(int, x_direction, name="x_direction")
|
|
129
|
+
self.y_direction = soft_signal_rw(int, y_direction, name="y_direction")
|
|
130
|
+
self.z_direction = soft_signal_rw(int, z_direction, name="z_direction")
|
|
104
131
|
|
|
105
132
|
with self.add_children_as_readables():
|
|
106
133
|
self.microns_per_pixel_x = derived_signal_r(
|
|
@@ -116,7 +143,7 @@ class OAV(StandardReadable):
|
|
|
116
143
|
coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
|
|
117
144
|
)
|
|
118
145
|
self.snapshot = Snapshot(
|
|
119
|
-
f"{self._prefix}
|
|
146
|
+
f"{self._prefix}{mjpeg_prefix}:",
|
|
120
147
|
self._name,
|
|
121
148
|
)
|
|
122
149
|
|
|
@@ -143,9 +170,16 @@ class OAV(StandardReadable):
|
|
|
143
170
|
|
|
144
171
|
|
|
145
172
|
class OAVBeamCentreFile(OAV):
|
|
146
|
-
"""
|
|
173
|
+
"""
|
|
174
|
+
OAV device that reads its beam centre values from a file. The config parameter
|
|
147
175
|
must be a OAVConfigBeamCentre object, as this contains a filepath to where the beam
|
|
148
176
|
centre values are stored.
|
|
177
|
+
|
|
178
|
+
x_direction(int): Should only be 1 or -1, with 1 indicating the oav x direction is the same with motor x
|
|
179
|
+
y_direction(int): Same with x_direction but for motor y
|
|
180
|
+
z_direction(int): Same with x_direction but for motor z
|
|
181
|
+
mjpg_x_size_pv(str): PV infix for x_size in mjpg
|
|
182
|
+
mjpg_y_size_pv(str): PV infix for y_size in mjpg
|
|
149
183
|
"""
|
|
150
184
|
|
|
151
185
|
def __init__(
|
|
@@ -153,9 +187,26 @@ class OAVBeamCentreFile(OAV):
|
|
|
153
187
|
prefix: str,
|
|
154
188
|
config: OAVConfigBeamCentre,
|
|
155
189
|
name: str = "",
|
|
190
|
+
mjpeg_prefix: str = "MJPG",
|
|
156
191
|
zoom_controller: BaseZoomController | None = None,
|
|
192
|
+
mjpg_x_size_pv: str = "ArraySize1_RBV",
|
|
193
|
+
mjpg_y_size_pv: str = "ArraySize2_RBV",
|
|
194
|
+
x_direction: int = -1,
|
|
195
|
+
y_direction: int = -1,
|
|
196
|
+
z_direction: int = 1,
|
|
157
197
|
):
|
|
158
|
-
super().__init__(
|
|
198
|
+
super().__init__(
|
|
199
|
+
prefix=prefix,
|
|
200
|
+
config=config,
|
|
201
|
+
name=name,
|
|
202
|
+
mjpeg_prefix=mjpeg_prefix,
|
|
203
|
+
zoom_controller=zoom_controller,
|
|
204
|
+
mjpg_x_size_pv=mjpg_x_size_pv,
|
|
205
|
+
mjpg_y_size_pv=mjpg_y_size_pv,
|
|
206
|
+
x_direction=x_direction,
|
|
207
|
+
y_direction=y_direction,
|
|
208
|
+
z_direction=z_direction,
|
|
209
|
+
)
|
|
159
210
|
|
|
160
211
|
with self.add_children_as_readables():
|
|
161
212
|
self.beam_centre_i = derived_signal_r(
|
|
@@ -189,6 +240,7 @@ class OAVBeamCentrePV(OAV):
|
|
|
189
240
|
prefix: str,
|
|
190
241
|
config: OAVConfig,
|
|
191
242
|
name: str = "",
|
|
243
|
+
mjpeg_prefix: str = "MJPG",
|
|
192
244
|
zoom_controller: BaseZoomController | None = None,
|
|
193
245
|
overlay_channel: int = 1,
|
|
194
246
|
):
|
|
@@ -199,4 +251,10 @@ class OAVBeamCentrePV(OAV):
|
|
|
199
251
|
self.beam_centre_j = epics_signal_r(
|
|
200
252
|
int, prefix + f"OVER:{overlay_channel}:CenterY"
|
|
201
253
|
)
|
|
202
|
-
super().__init__(
|
|
254
|
+
super().__init__(
|
|
255
|
+
prefix=prefix,
|
|
256
|
+
config=config,
|
|
257
|
+
name=name,
|
|
258
|
+
mjpeg_prefix=mjpeg_prefix,
|
|
259
|
+
zoom_controller=zoom_controller,
|
|
260
|
+
)
|
|
@@ -92,8 +92,10 @@ class OAVParameters:
|
|
|
92
92
|
self.preprocess_K_size: int = update(
|
|
93
93
|
"preProcessKSize", int
|
|
94
94
|
) # length scale for blur preprocessing
|
|
95
|
+
self.preprocess_iter: int = update("preProcessIteration", int, default=5)
|
|
95
96
|
self.detection_script_filename: str = update("filename", str)
|
|
96
|
-
self.
|
|
97
|
+
self.open_ksize: int = update("open_ksize", int, default=0)
|
|
98
|
+
self.close_ksize: int = update("close_ksize", int, default=5)
|
|
97
99
|
self.min_callback_time: float = update("min_callback_time", float, default=0.08)
|
|
98
100
|
self.direction: int = update("direction", int)
|
|
99
101
|
self.max_tip_distance: float = update("max_tip_distance", float, default=300)
|
|
@@ -9,6 +9,7 @@ from bluesky.protocols import Flyable, Stoppable
|
|
|
9
9
|
from ophyd_async.core import (
|
|
10
10
|
AsyncStatus,
|
|
11
11
|
DeviceVector,
|
|
12
|
+
Reference,
|
|
12
13
|
StandardReadable,
|
|
13
14
|
observe_value,
|
|
14
15
|
soft_signal_r_and_setter,
|
|
@@ -17,6 +18,7 @@ from ophyd_async.core import (
|
|
|
17
18
|
from ophyd_async.epics.core import epics_signal_r
|
|
18
19
|
from redis.asyncio import StrictRedis
|
|
19
20
|
|
|
21
|
+
from dodal.devices.oav.oav_detector import OAV
|
|
20
22
|
from dodal.log import LOGGER
|
|
21
23
|
|
|
22
24
|
|
|
@@ -35,13 +37,10 @@ class Source(IntEnum):
|
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
class OAVSource(StandardReadable):
|
|
38
|
-
def __init__(
|
|
39
|
-
self
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
):
|
|
43
|
-
self.url = epics_signal_r(str, f"{prefix}MJPG_URL_RBV")
|
|
44
|
-
self.oav_name = oav_name
|
|
40
|
+
def __init__(self, oav: OAV, label: str):
|
|
41
|
+
self.url_ref = Reference(oav.snapshot.video_url)
|
|
42
|
+
self.oav_ref = Reference(oav)
|
|
43
|
+
self.label = label
|
|
45
44
|
super().__init__()
|
|
46
45
|
|
|
47
46
|
|
|
@@ -62,6 +61,8 @@ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
|
|
|
62
61
|
def __init__(
|
|
63
62
|
self,
|
|
64
63
|
prefix: str,
|
|
64
|
+
oav_roi: OAV,
|
|
65
|
+
oav_fs: OAV,
|
|
65
66
|
redis_host: str,
|
|
66
67
|
redis_password: str,
|
|
67
68
|
redis_db: int = 0,
|
|
@@ -78,13 +79,13 @@ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
|
|
|
78
79
|
name: str the name of this device
|
|
79
80
|
"""
|
|
80
81
|
self.counter = epics_signal_r(int, f"{prefix}CAM:ArrayCounter_RBV")
|
|
81
|
-
|
|
82
82
|
self.sources = DeviceVector(
|
|
83
83
|
{
|
|
84
|
-
Source.ROI.value: OAVSource(
|
|
85
|
-
Source.FULL_SCREEN.value: OAVSource(
|
|
84
|
+
Source.ROI.value: OAVSource(oav_roi, "roi"),
|
|
85
|
+
Source.FULL_SCREEN.value: OAVSource(oav_fs, "fullscreen"),
|
|
86
86
|
}
|
|
87
87
|
)
|
|
88
|
+
|
|
88
89
|
self.selected_source = soft_signal_rw(int)
|
|
89
90
|
|
|
90
91
|
self.forwarding_task = None
|
|
@@ -120,11 +121,11 @@ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
|
|
|
120
121
|
self, function_to_do: Callable[[ClientResponse, OAVSource], Awaitable]
|
|
121
122
|
):
|
|
122
123
|
source_idx = await self.selected_source.get_value()
|
|
124
|
+
source = self.sources[source_idx]
|
|
125
|
+
stream_url = await source.url_ref().get_value()
|
|
123
126
|
LOGGER.info(
|
|
124
|
-
f"Forwarding data from sample {await self.sample_id.get_value()} and OAV {source_idx}"
|
|
127
|
+
f"Forwarding data from sample {await self.sample_id.get_value()} and OAV {source_idx} from URL {stream_url}"
|
|
125
128
|
)
|
|
126
|
-
source = self.sources[source_idx]
|
|
127
|
-
stream_url = await source.url.get_value()
|
|
128
129
|
async with ClientSession() as session:
|
|
129
130
|
async with session.get(stream_url) as response:
|
|
130
131
|
await function_to_do(response, source)
|
|
@@ -141,12 +142,14 @@ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
|
|
|
141
142
|
asyncio.wait_for(self._stop_flag.wait(), timeout=self.TIMEOUT)
|
|
142
143
|
)
|
|
143
144
|
async for frame_count in observe_value(self.counter, done_status=done_status):
|
|
144
|
-
redis_uuid = f"{source.
|
|
145
|
+
redis_uuid = f"{source.label}-{frame_count}-{uuid4()}"
|
|
145
146
|
await self._get_frame_and_put_to_redis(redis_uuid, response)
|
|
146
147
|
|
|
147
148
|
async def _confirm_mjpg_stream(self, response: ClientResponse, source: OAVSource):
|
|
148
149
|
if response.content_type != "multipart/x-mixed-replace":
|
|
149
|
-
raise ValueError(
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"{await source.url_ref().get_value()} is not an MJPG stream"
|
|
152
|
+
)
|
|
150
153
|
|
|
151
154
|
@AsyncStatus.wrap
|
|
152
155
|
async def kickoff(self):
|
|
@@ -67,6 +67,8 @@ class PinTipDetection(StandardReadable):
|
|
|
67
67
|
)
|
|
68
68
|
self.canny_upper_threshold = soft_signal_rw(int, 100, name="canny_upper")
|
|
69
69
|
self.canny_lower_threshold = soft_signal_rw(int, 50, name="canny_lower")
|
|
70
|
+
self.open_ksize = soft_signal_rw(int, 0, name="open_ksize")
|
|
71
|
+
self.open_iterations = soft_signal_rw(int, 5, name="open_iterations")
|
|
70
72
|
self.close_ksize = soft_signal_rw(int, 5, name="close_ksize")
|
|
71
73
|
self.close_iterations = soft_signal_rw(int, 5, name="close_iterations")
|
|
72
74
|
self.scan_direction = soft_signal_rw(
|
|
@@ -105,7 +107,7 @@ class PinTipDetection(StandardReadable):
|
|
|
105
107
|
|
|
106
108
|
try:
|
|
107
109
|
preprocess_func = ARRAY_PROCESSING_FUNCTIONS_MAP[preprocess_key](
|
|
108
|
-
|
|
110
|
+
iterations=preprocess_iter, ksize=preprocess_ksize
|
|
109
111
|
)
|
|
110
112
|
except KeyError:
|
|
111
113
|
LOGGER.error("Invalid preprocessing function, using identity")
|
|
@@ -121,6 +123,8 @@ class PinTipDetection(StandardReadable):
|
|
|
121
123
|
preprocess=preprocess_func,
|
|
122
124
|
canny_lower=await self.canny_lower_threshold.get_value(),
|
|
123
125
|
canny_upper=await self.canny_upper_threshold.get_value(),
|
|
126
|
+
open_ksize=await self.open_ksize.get_value(),
|
|
127
|
+
open_iterations=await self.open_iterations.get_value(),
|
|
124
128
|
close_ksize=await self.close_ksize.get_value(),
|
|
125
129
|
close_iterations=await self.close_iterations.get_value(),
|
|
126
130
|
scan_direction=direction,
|
|
@@ -14,6 +14,12 @@ class ScanDirections(Enum):
|
|
|
14
14
|
REVERSE = -1
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
"""
|
|
18
|
+
See https://opencv24-python-tutorials.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_morphological_ops/py_morphological_ops.html
|
|
19
|
+
for description of functions below.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
17
23
|
def identity(*args, **kwargs) -> Callable[[np.ndarray], np.ndarray]:
|
|
18
24
|
return lambda arr: arr
|
|
19
25
|
|
|
@@ -109,6 +115,8 @@ class MxSampleDetect:
|
|
|
109
115
|
self,
|
|
110
116
|
*,
|
|
111
117
|
preprocess: Callable[[np.ndarray], np.ndarray] = lambda arr: arr,
|
|
118
|
+
open_ksize: int = 0,
|
|
119
|
+
open_iterations: int = 5,
|
|
112
120
|
canny_upper: int = 100,
|
|
113
121
|
canny_lower: int = 50,
|
|
114
122
|
close_ksize: int = 5,
|
|
@@ -123,6 +131,12 @@ class MxSampleDetect:
|
|
|
123
131
|
preprocess: A preprocessing function applied to the array after conversion to grayscale.
|
|
124
132
|
See implementations of common functions above for predefined conversions
|
|
125
133
|
Defaults to a no-op (i.e. no preprocessing)
|
|
134
|
+
|
|
135
|
+
for open and close operations, please read:
|
|
136
|
+
https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html
|
|
137
|
+
|
|
138
|
+
open_ksize: kernel size for "open" operation, if set to zero then ignore "open" operation
|
|
139
|
+
open_iterations: number of iterations for "open" operation
|
|
126
140
|
canny_upper: upper threshold for canny edge detection
|
|
127
141
|
canny_lower: lower threshold for canny edge detection
|
|
128
142
|
close_ksize: kernel size for "close" operation
|
|
@@ -132,6 +146,8 @@ class MxSampleDetect:
|
|
|
132
146
|
"""
|
|
133
147
|
|
|
134
148
|
self.preprocess = preprocess
|
|
149
|
+
self.open_ksize = open_ksize
|
|
150
|
+
self.open_iterations = open_iterations
|
|
135
151
|
self.canny_upper = canny_upper
|
|
136
152
|
self.canny_lower = canny_lower
|
|
137
153
|
self.close_ksize = close_ksize
|
|
@@ -148,8 +164,14 @@ class MxSampleDetect:
|
|
|
148
164
|
assert arr.ndim == 2
|
|
149
165
|
gray_arr = arr
|
|
150
166
|
|
|
167
|
+
# Remove Noises if open set to non-zero
|
|
168
|
+
if self.open_ksize == 0:
|
|
169
|
+
open_arr = gray_arr
|
|
170
|
+
else:
|
|
171
|
+
open_arr = open_morph(self.open_ksize, self.open_iterations)(gray_arr)
|
|
172
|
+
|
|
151
173
|
# Preprocess the array. (Use the greyscale one.)
|
|
152
|
-
pp_arr = self.preprocess(
|
|
174
|
+
pp_arr = self.preprocess(open_arr)
|
|
153
175
|
|
|
154
176
|
# Find some edges.
|
|
155
177
|
edge_arr = cv2.Canny(pp_arr, self.canny_upper, self.canny_lower)
|
|
@@ -12,7 +12,13 @@ from dodal.log import LOGGER
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class SnapshotWithGrid(MJPG):
|
|
15
|
-
def __init__(
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
prefix: str,
|
|
18
|
+
name: str = "",
|
|
19
|
+
x_size_pv: str = "ArraySize1_RBV",
|
|
20
|
+
y_size_pv: str = "ArraySize2_RBV",
|
|
21
|
+
) -> None:
|
|
16
22
|
with self.add_children_as_readables():
|
|
17
23
|
self.top_left_x = soft_signal_rw(float)
|
|
18
24
|
self.top_left_y = soft_signal_rw(float)
|
|
@@ -23,7 +29,7 @@ class SnapshotWithGrid(MJPG):
|
|
|
23
29
|
self.last_path_outer = soft_signal_rw(str)
|
|
24
30
|
self.last_path_full_overlay = soft_signal_rw(str)
|
|
25
31
|
|
|
26
|
-
super().__init__(prefix, name)
|
|
32
|
+
super().__init__(prefix, name, x_size_pv, y_size_pv)
|
|
27
33
|
|
|
28
34
|
async def post_processing(self, image: Image):
|
|
29
35
|
# Save an unmodified image with no suffix
|
dodal/devices/oav/utils.py
CHANGED
|
@@ -5,13 +5,13 @@ import bluesky.plan_stubs as bps
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from bluesky.utils import Msg
|
|
7
7
|
|
|
8
|
+
from dodal.devices.motors import XYZOmegaStage
|
|
8
9
|
from dodal.devices.oav.oav_calculations import (
|
|
9
10
|
calculate_beam_distance,
|
|
10
11
|
camera_coordinates_to_xyz_mm,
|
|
11
12
|
)
|
|
12
13
|
from dodal.devices.oav.oav_detector import OAV
|
|
13
14
|
from dodal.devices.oav.pin_image_recognition import PinTipDetection
|
|
14
|
-
from dodal.devices.smargon import Smargon
|
|
15
15
|
|
|
16
16
|
Pixel = tuple[int, int]
|
|
17
17
|
|
|
@@ -52,24 +52,29 @@ class EdgeOutputArrayImageType(IntEnum):
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def get_move_required_so_that_beam_is_at_pixel(
|
|
55
|
-
|
|
55
|
+
gonio: XYZOmegaStage,
|
|
56
|
+
pixel: Pixel,
|
|
57
|
+
oav: OAV,
|
|
56
58
|
) -> Generator[Msg, None, np.ndarray]:
|
|
57
59
|
"""Calculate the required move so that the given pixel is in the centre of the beam."""
|
|
58
60
|
|
|
59
61
|
current_motor_xyz = np.array(
|
|
60
62
|
[
|
|
61
|
-
(yield from bps.rd(
|
|
62
|
-
(yield from bps.rd(
|
|
63
|
-
(yield from bps.rd(
|
|
63
|
+
(yield from bps.rd(gonio.x)),
|
|
64
|
+
(yield from bps.rd(gonio.y)),
|
|
65
|
+
(yield from bps.rd(gonio.z)),
|
|
64
66
|
],
|
|
65
67
|
dtype=np.float64,
|
|
66
68
|
)
|
|
67
|
-
current_angle = yield from bps.rd(
|
|
69
|
+
current_angle = yield from bps.rd(gonio.omega)
|
|
68
70
|
|
|
69
71
|
beam_x = yield from bps.rd(oav.beam_centre_i)
|
|
70
72
|
beam_y = yield from bps.rd(oav.beam_centre_j)
|
|
71
73
|
microns_per_pixel_x = yield from bps.rd(oav.microns_per_pixel_x)
|
|
72
74
|
microns_per_pixel_y = yield from bps.rd(oav.microns_per_pixel_y)
|
|
75
|
+
x_direction = yield from bps.rd(oav.x_direction)
|
|
76
|
+
y_direction = yield from bps.rd(oav.y_direction)
|
|
77
|
+
z_direction = yield from bps.rd(oav.z_direction)
|
|
73
78
|
|
|
74
79
|
return calculate_x_y_z_of_pixel(
|
|
75
80
|
current_motor_xyz,
|
|
@@ -77,6 +82,7 @@ def get_move_required_so_that_beam_is_at_pixel(
|
|
|
77
82
|
pixel,
|
|
78
83
|
(beam_x, beam_y),
|
|
79
84
|
(microns_per_pixel_x, microns_per_pixel_y),
|
|
85
|
+
(x_direction, y_direction, z_direction),
|
|
80
86
|
)
|
|
81
87
|
|
|
82
88
|
|
|
@@ -86,6 +92,7 @@ def calculate_x_y_z_of_pixel(
|
|
|
86
92
|
pixel: Pixel,
|
|
87
93
|
beam_centre: tuple[int, int],
|
|
88
94
|
microns_per_pixel: tuple[float, float],
|
|
95
|
+
xyz_direction: tuple[int, int, int],
|
|
89
96
|
) -> np.ndarray:
|
|
90
97
|
"""Get the x, y, z position of a pixel in mm"""
|
|
91
98
|
beam_distance_px: Pixel = calculate_beam_distance(beam_centre, *pixel)
|
|
@@ -96,6 +103,9 @@ def calculate_x_y_z_of_pixel(
|
|
|
96
103
|
current_omega,
|
|
97
104
|
microns_per_pixel[0],
|
|
98
105
|
microns_per_pixel[1],
|
|
106
|
+
xyz_direction[0],
|
|
107
|
+
xyz_direction[1],
|
|
108
|
+
xyz_direction[2],
|
|
99
109
|
)
|
|
100
110
|
|
|
101
111
|
|
dodal/devices/robot.py
CHANGED
|
@@ -61,7 +61,7 @@ class ErrorStatus(Device):
|
|
|
61
61
|
raise RobotLoadError(int(error_code), error_string) from raise_from
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
64
|
+
class BartRobot(StandardReadable, Movable[SampleLocation | None]):
|
|
65
65
|
"""The sample changing robot."""
|
|
66
66
|
|
|
67
67
|
# How long to wait for the robot if it is busy soaking/drying
|
|
@@ -104,7 +104,6 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
|
104
104
|
self.init = epics_signal_x(prefix + "INIT.PROC")
|
|
105
105
|
self.soak = epics_signal_x(prefix + "SOAK.PROC")
|
|
106
106
|
self.home = epics_signal_x(prefix + "GOHM.PROC")
|
|
107
|
-
self.unload = epics_signal_x(prefix + "UNLD.PROC")
|
|
108
107
|
self.dry = epics_signal_x(prefix + "DRY.PROC")
|
|
109
108
|
self.open = epics_signal_x(prefix + "COLO.PROC")
|
|
110
109
|
self.close = epics_signal_x(prefix + "COLC.PROC")
|
|
@@ -175,12 +174,23 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
|
175
174
|
await self.pin_mounted_or_no_pin_found()
|
|
176
175
|
|
|
177
176
|
@AsyncStatus.wrap
|
|
178
|
-
async def set(self, value: SampleLocation):
|
|
177
|
+
async def set(self, value: SampleLocation | None):
|
|
178
|
+
"""
|
|
179
|
+
Perform a sample load from the specified sample location
|
|
180
|
+
Args:
|
|
181
|
+
value: The pin and puck to load, or None to unload the sample.
|
|
182
|
+
Raises:
|
|
183
|
+
RobotLoadError if a timeout occurs, or if an error occurs loading the smaple.
|
|
184
|
+
"""
|
|
179
185
|
try:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
if value is not None:
|
|
187
|
+
await wait_for(
|
|
188
|
+
self._load_pin_and_puck(value),
|
|
189
|
+
timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
await self.unload.trigger(timeout=self.LOAD_TIMEOUT)
|
|
193
|
+
await wait_for_value(self.program_running, False, self.NOT_BUSY_TIMEOUT)
|
|
184
194
|
except TimeoutError as e:
|
|
185
195
|
await self.prog_error.raise_if_error(e)
|
|
186
196
|
await self.controller_error.raise_if_error(e)
|
dodal/devices/scintillator.py
CHANGED
|
@@ -8,9 +8,10 @@ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureVal
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class InOut(StrictEnum):
|
|
11
|
-
"""
|
|
11
|
+
"""Moves scintillator in and out of the beam."""
|
|
12
12
|
|
|
13
|
-
OUT = "Out"
|
|
13
|
+
OUT = "Out" # Out of beam
|
|
14
|
+
IN = "In" # In to beam
|
|
14
15
|
UNKNOWN = "Unknown"
|
|
15
16
|
|
|
16
17
|
|
|
@@ -45,27 +46,46 @@ class Scintillator(StandardReadable):
|
|
|
45
46
|
self._scintillator_out_yz_mm = [
|
|
46
47
|
float(beamline_parameters[f"scin_{axis}_SCIN_OUT"]) for axis in ("y", "z")
|
|
47
48
|
]
|
|
49
|
+
self._scintillator_in_yz_mm = [
|
|
50
|
+
float(beamline_parameters[f"scin_{axis}_SCIN_IN"]) for axis in ("y", "z")
|
|
51
|
+
]
|
|
48
52
|
self._yz_tolerance_mm = [
|
|
49
53
|
float(beamline_parameters[f"scin_{axis}_tolerance"]) for axis in ("y", "z")
|
|
50
54
|
]
|
|
51
55
|
|
|
52
56
|
super().__init__(name)
|
|
53
57
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
if all(
|
|
58
|
+
def _check_position(self, current_pos: list[float], pos_to_check: list[float]):
|
|
59
|
+
return all(
|
|
57
60
|
isclose(axis_pos, axis_in_beam, abs_tol=axis_tolerance)
|
|
58
61
|
for axis_pos, axis_in_beam, axis_tolerance in zip(
|
|
59
62
|
current_pos,
|
|
60
|
-
|
|
63
|
+
pos_to_check,
|
|
61
64
|
self._yz_tolerance_mm,
|
|
62
65
|
strict=False,
|
|
63
66
|
)
|
|
64
|
-
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _get_selected_position(self, y: float, z: float) -> InOut:
|
|
70
|
+
current_pos = [y, z]
|
|
71
|
+
if self._check_position(current_pos, self._scintillator_out_yz_mm):
|
|
65
72
|
return InOut.OUT
|
|
73
|
+
|
|
74
|
+
elif self._check_position(current_pos, self._scintillator_in_yz_mm):
|
|
75
|
+
return InOut.IN
|
|
76
|
+
|
|
66
77
|
else:
|
|
67
78
|
return InOut.UNKNOWN
|
|
68
79
|
|
|
80
|
+
async def _check_aperture_parked(self):
|
|
81
|
+
if (
|
|
82
|
+
await self._aperture_scatterguard().selected_aperture.get_value()
|
|
83
|
+
!= ApertureValue.PARKED
|
|
84
|
+
):
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"Cannot move scintillator if aperture/scatterguard is not parked. Position is currently {await self._aperture_scatterguard().selected_aperture.get_value()}"
|
|
87
|
+
)
|
|
88
|
+
|
|
69
89
|
async def _set_selected_position(self, position: InOut) -> None:
|
|
70
90
|
match position:
|
|
71
91
|
case InOut.OUT:
|
|
@@ -73,14 +93,16 @@ class Scintillator(StandardReadable):
|
|
|
73
93
|
current_z = await self.z_mm.user_readback.get_value()
|
|
74
94
|
if self._get_selected_position(current_y, current_z) == InOut.OUT:
|
|
75
95
|
return
|
|
76
|
-
|
|
77
|
-
self._aperture_scatterguard().selected_aperture.get_value()
|
|
78
|
-
!= ApertureValue.PARKED
|
|
79
|
-
):
|
|
80
|
-
raise ValueError(
|
|
81
|
-
"Cannot move scintillator out if aperture/scatterguard is not parked"
|
|
82
|
-
)
|
|
96
|
+
await self._check_aperture_parked()
|
|
83
97
|
await self.y_mm.set(self._scintillator_out_yz_mm[0])
|
|
84
98
|
await self.z_mm.set(self._scintillator_out_yz_mm[1])
|
|
99
|
+
case InOut.IN:
|
|
100
|
+
current_y = await self.y_mm.user_readback.get_value()
|
|
101
|
+
current_z = await self.z_mm.user_readback.get_value()
|
|
102
|
+
if self._get_selected_position(current_y, current_z) == InOut.IN:
|
|
103
|
+
return
|
|
104
|
+
await self._check_aperture_parked()
|
|
105
|
+
await self.z_mm.set(self._scintillator_in_yz_mm[1])
|
|
106
|
+
await self.y_mm.set(self._scintillator_in_yz_mm[0])
|
|
85
107
|
case _:
|
|
86
108
|
raise ValueError(f"Cannot set scintillator to position {position}")
|
dodal/devices/smargon.py
CHANGED
|
@@ -14,7 +14,7 @@ from ophyd_async.core import (
|
|
|
14
14
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
15
15
|
from ophyd_async.epics.motor import Motor
|
|
16
16
|
|
|
17
|
-
from dodal.devices.motors import
|
|
17
|
+
from dodal.devices.motors import XYZOmegaStage
|
|
18
18
|
from dodal.devices.util.epics_util import SetWhenEnabled
|
|
19
19
|
|
|
20
20
|
|
|
@@ -78,7 +78,7 @@ class CombinedMove(TypedDict, total=False):
|
|
|
78
78
|
chi: float | None
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
class Smargon(
|
|
81
|
+
class Smargon(XYZOmegaStage, Movable):
|
|
82
82
|
"""
|
|
83
83
|
Real motors added to allow stops following pin load (e.g. real_x1.stop() )
|
|
84
84
|
X1 and X2 real motors provide compound chi motion as well as the compound X travel,
|
|
@@ -92,7 +92,6 @@ class Smargon(XYZStage, Movable):
|
|
|
92
92
|
with self.add_children_as_readables():
|
|
93
93
|
self.chi = Motor(prefix + "CHI")
|
|
94
94
|
self.phi = Motor(prefix + "PHI")
|
|
95
|
-
self.omega = Motor(prefix + "OMEGA")
|
|
96
95
|
self.real_x1 = Motor(prefix + "MOTOR_3")
|
|
97
96
|
self.real_x2 = Motor(prefix + "MOTOR_4")
|
|
98
97
|
self.real_y = Motor(prefix + "MOTOR_1")
|
dodal/devices/thawer.py
CHANGED
|
@@ -1,59 +1,21 @@
|
|
|
1
|
-
from asyncio import CancelledError, Task, create_task, sleep
|
|
2
|
-
|
|
3
1
|
from bluesky.protocols import Movable, Stoppable
|
|
4
2
|
from ophyd_async.core import (
|
|
5
3
|
AsyncStatus,
|
|
6
|
-
Device,
|
|
7
4
|
OnOff,
|
|
8
|
-
Reference,
|
|
9
|
-
SignalRW,
|
|
10
5
|
StandardReadable,
|
|
11
6
|
)
|
|
12
7
|
from ophyd_async.epics.core import epics_signal_rw
|
|
13
8
|
|
|
14
|
-
from dodal.log import LOGGER
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ThawingTimer(Device, Stoppable, Movable[float]):
|
|
18
|
-
def __init__(self, control_signal: SignalRW[OnOff]) -> None:
|
|
19
|
-
self._control_signal_ref = Reference(control_signal)
|
|
20
|
-
self._thawing_task: Task | None = None
|
|
21
|
-
super().__init__("thaw_for_time_s")
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
async def set(self, value: float):
|
|
25
|
-
if self._thawing_task:
|
|
26
|
-
LOGGER.info("Thawing task already in progress, resetting timer")
|
|
27
|
-
self._thawing_task.cancel()
|
|
28
|
-
else:
|
|
29
|
-
LOGGER.info("Thawing started")
|
|
30
|
-
await self._control_signal_ref().set(OnOff.ON)
|
|
31
|
-
self._thawing_task = create_task(sleep(value))
|
|
32
|
-
try:
|
|
33
|
-
await self._thawing_task
|
|
34
|
-
except CancelledError:
|
|
35
|
-
LOGGER.info("Timer task cancelled.")
|
|
36
|
-
raise
|
|
37
|
-
else:
|
|
38
|
-
LOGGER.info("Thawing completed")
|
|
39
|
-
await self._control_signal_ref().set(OnOff.OFF)
|
|
40
|
-
|
|
41
|
-
@AsyncStatus.wrap
|
|
42
|
-
async def stop(self, *args, **kwargs):
|
|
43
|
-
if self._thawing_task:
|
|
44
|
-
self._thawing_task.cancel()
|
|
45
|
-
self._thawing_task = None
|
|
46
|
-
LOGGER.info("Thawer stopped.")
|
|
47
|
-
await self._control_signal_ref().set(OnOff.OFF)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class Thawer(StandardReadable, Stoppable):
|
|
10
|
+
class Thawer(StandardReadable, Stoppable, Movable[OnOff]):
|
|
51
11
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
52
|
-
self.
|
|
53
|
-
self.thaw_for_time_s = ThawingTimer(self.control)
|
|
12
|
+
self._control = epics_signal_rw(OnOff, prefix + ":CTRL")
|
|
54
13
|
super().__init__(name)
|
|
55
14
|
|
|
15
|
+
@AsyncStatus.wrap
|
|
16
|
+
async def set(self, value: OnOff):
|
|
17
|
+
await self._control.set(value)
|
|
18
|
+
|
|
56
19
|
@AsyncStatus.wrap
|
|
57
20
|
async def stop(self, *args, **kwargs):
|
|
58
|
-
await self.
|
|
59
|
-
await self.control.set(OnOff.OFF)
|
|
21
|
+
await self._control.set(OnOff.OFF)
|