dls-dodal 1.29.4__py3-none-any.whl → 1.30.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.29.4.dist-info → dls_dodal-1.30.0.dist-info}/METADATA +27 -42
- dls_dodal-1.30.0.dist-info/RECORD +132 -0
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/WHEEL +1 -1
- dls_dodal-1.30.0.dist-info/entry_points.txt +3 -0
- dodal/__init__.py +1 -4
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +3 -1
- dodal/beamlines/i03.py +28 -23
- dodal/beamlines/i04.py +34 -12
- dodal/beamlines/i13_1.py +66 -0
- dodal/beamlines/i22.py +5 -5
- dodal/beamlines/i24.py +1 -1
- dodal/beamlines/p38.py +7 -7
- dodal/beamlines/p45.py +7 -5
- dodal/beamlines/p99.py +61 -0
- dodal/cli.py +6 -3
- dodal/common/beamlines/beamline_parameters.py +2 -2
- dodal/common/beamlines/beamline_utils.py +6 -5
- dodal/common/maths.py +1 -3
- dodal/common/types.py +2 -3
- dodal/common/udc_directory_provider.py +14 -3
- dodal/common/visit.py +2 -3
- dodal/devices/CTAB.py +22 -17
- dodal/devices/aperturescatterguard.py +114 -136
- dodal/devices/areadetector/adaravis.py +8 -6
- dodal/devices/areadetector/adsim.py +2 -3
- dodal/devices/areadetector/adutils.py +20 -12
- dodal/devices/cryostream.py +19 -7
- dodal/devices/detector/__init__.py +13 -2
- dodal/devices/detector/det_dim_constants.py +2 -2
- dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
- dodal/devices/detector/detector.py +5 -5
- dodal/devices/detector/detector_motion.py +38 -31
- dodal/devices/eiger.py +11 -15
- dodal/devices/eiger_odin.py +9 -10
- dodal/devices/fast_grid_scan.py +4 -3
- dodal/devices/fluorescence_detector_motion.py +13 -4
- dodal/devices/focusing_mirror.py +4 -4
- dodal/devices/hutch_shutter.py +4 -4
- dodal/devices/i22/dcm.py +4 -3
- dodal/devices/i22/fswitch.py +4 -4
- dodal/devices/i22/nxsas.py +23 -32
- dodal/devices/i24/pmac.py +47 -8
- dodal/devices/ipin.py +7 -4
- dodal/devices/linkam3.py +11 -5
- dodal/devices/logging_ophyd_device.py +1 -1
- dodal/devices/motors.py +31 -5
- dodal/devices/oav/grid_overlay.py +1 -0
- dodal/devices/oav/microns_for_zoom_levels.json +1 -1
- dodal/devices/oav/oav_detector.py +2 -1
- dodal/devices/oav/oav_parameters.py +18 -10
- dodal/devices/oav/oav_to_redis_forwarder.py +100 -0
- dodal/devices/oav/pin_image_recognition/__init__.py +6 -6
- dodal/devices/oav/pin_image_recognition/utils.py +5 -6
- dodal/devices/oav/utils.py +2 -2
- dodal/devices/p99/__init__.py +0 -0
- dodal/devices/p99/sample_stage.py +43 -0
- dodal/devices/robot.py +30 -18
- dodal/devices/scintillator.py +8 -5
- dodal/devices/smargon.py +3 -3
- dodal/devices/status.py +2 -31
- dodal/devices/tetramm.py +4 -4
- dodal/devices/thawer.py +5 -3
- dodal/devices/undulator_dcm.py +6 -8
- dodal/devices/util/adjuster_plans.py +2 -2
- dodal/devices/util/epics_util.py +5 -7
- dodal/devices/util/lookup_tables.py +2 -3
- dodal/devices/util/save_panda.py +87 -0
- dodal/devices/util/test_utils.py +17 -0
- dodal/devices/webcam.py +3 -3
- dodal/devices/xbpm_feedback.py +0 -23
- dodal/devices/zebra.py +10 -10
- dodal/devices/zebra_controlled_shutter.py +3 -3
- dodal/devices/zocalo/zocalo_interaction.py +10 -2
- dodal/devices/zocalo/zocalo_results.py +31 -18
- dodal/log.py +14 -5
- dodal/plans/data_session_metadata.py +1 -0
- dodal/plans/motor_util_plans.py +117 -0
- dodal/utils.py +65 -22
- dls_dodal-1.29.4.dist-info/RECORD +0 -125
- dls_dodal-1.29.4.dist-info/entry_points.txt +0 -2
- dodal/devices/qbpm1.py +0 -8
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +0 -0
dodal/devices/motors.py
CHANGED
|
@@ -3,8 +3,34 @@ from ophyd_async.epics.motion import Motor
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class XYZPositioner(Device):
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
Standard ophyd_async xyz motor stage, by combining 3 Motors,
|
|
9
|
+
with added infix for extra flexibliy to allow different axes other than x,y,z.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
prefix:
|
|
14
|
+
EPICS PV (Common part up to and including :).
|
|
15
|
+
name:
|
|
16
|
+
name for the stage.
|
|
17
|
+
infix:
|
|
18
|
+
EPICS PV, default is the ["X", "Y", "Z"].
|
|
19
|
+
Notes
|
|
20
|
+
-----
|
|
21
|
+
Example usage::
|
|
22
|
+
async with DeviceCollector():
|
|
23
|
+
xyz_stage = XYZPositioner("BLXX-MO-STAGE-XX:")
|
|
24
|
+
Or::
|
|
25
|
+
with DeviceCollector():
|
|
26
|
+
xyz_stage = XYZPositioner("BLXX-MO-STAGE-XX:", suffix = ["A", "B", "C"])
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, prefix: str, name: str, infix: list[str] | None = None):
|
|
31
|
+
if infix is None:
|
|
32
|
+
infix = ["X", "Y", "Z"]
|
|
33
|
+
self.x = Motor(prefix + infix[0])
|
|
34
|
+
self.y = Motor(prefix + infix[1])
|
|
35
|
+
self.z = Motor(prefix + infix[2])
|
|
36
|
+
super().__init__(name=name)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# type: ignore # OAV will soon be ophyd-async, see https://github.com/DiamondLightSource/dodal/issues/716
|
|
1
2
|
from functools import partial
|
|
2
3
|
|
|
3
4
|
from ophyd import ADComponent as ADC
|
|
@@ -46,7 +47,7 @@ class ZoomController(Device):
|
|
|
46
47
|
sxst = Component(EpicsSignal, "MP:SELECT.SXST")
|
|
47
48
|
|
|
48
49
|
def set_flatfield_on_zoom_level_one(self, value):
|
|
49
|
-
self.parent:
|
|
50
|
+
self.parent: OAV
|
|
50
51
|
flat_applied = self.parent.proc.port_name.get()
|
|
51
52
|
no_flat_applied = self.parent.cam.port_name.get()
|
|
52
53
|
return self.parent.grid_snapshot.input_plugin.set(
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import xml.etree.
|
|
2
|
+
import xml.etree.ElementTree as et
|
|
3
3
|
from collections import ChainMap
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
|
+
from xml.etree.ElementTree import Element
|
|
5
6
|
|
|
6
7
|
from dodal.devices.oav.oav_errors import (
|
|
7
8
|
OAVError_BeamPositionNotFound,
|
|
@@ -20,6 +21,13 @@ OAV_CONFIG_JSON = (
|
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def _get_element_as_float(node: Element, element_name: str) -> float:
|
|
25
|
+
element = node.find(element_name)
|
|
26
|
+
assert element is not None, f"{element_name} not found in {node}"
|
|
27
|
+
assert element.text
|
|
28
|
+
return float(element.text)
|
|
29
|
+
|
|
30
|
+
|
|
23
31
|
class OAVParameters:
|
|
24
32
|
"""
|
|
25
33
|
The parameters to set up the OAV depending on the context.
|
|
@@ -65,11 +73,11 @@ class OAVParameters:
|
|
|
65
73
|
try:
|
|
66
74
|
param = param_type(param)
|
|
67
75
|
return param
|
|
68
|
-
except AssertionError:
|
|
76
|
+
except AssertionError as e:
|
|
69
77
|
raise TypeError(
|
|
70
78
|
f"OAV param {name} from the OAV centring params json file has the "
|
|
71
79
|
f"wrong type, should be {param_type} but is {type(param)}."
|
|
72
|
-
)
|
|
80
|
+
) from e
|
|
73
81
|
|
|
74
82
|
self.exposure: float = update("exposure", float)
|
|
75
83
|
self.acquire_period: float = update("acqPeriod", float)
|
|
@@ -134,14 +142,14 @@ class OAVConfigParams:
|
|
|
134
142
|
root = tree.getroot()
|
|
135
143
|
levels = root.findall(".//zoomLevel")
|
|
136
144
|
for node in levels:
|
|
137
|
-
if
|
|
145
|
+
if _get_element_as_float(node, "level") == zoom:
|
|
138
146
|
self.micronsPerXPixel = (
|
|
139
|
-
|
|
147
|
+
_get_element_as_float(node, "micronsPerXPixel")
|
|
140
148
|
* DEFAULT_OAV_WINDOW[0]
|
|
141
149
|
/ xsize
|
|
142
150
|
)
|
|
143
151
|
self.micronsPerYPixel = (
|
|
144
|
-
|
|
152
|
+
_get_element_as_float(node, "micronsPerYPixel")
|
|
145
153
|
* DEFAULT_OAV_WINDOW[1]
|
|
146
154
|
/ ysize
|
|
147
155
|
)
|
|
@@ -155,7 +163,7 @@ class OAVConfigParams:
|
|
|
155
163
|
|
|
156
164
|
def get_beam_position_from_zoom(
|
|
157
165
|
self, zoom: float, xsize: int, ysize: int
|
|
158
|
-
) ->
|
|
166
|
+
) -> tuple[int, int]:
|
|
159
167
|
"""
|
|
160
168
|
Extracts the beam location in pixels `xCentre` `yCentre`, for a requested zoom \
|
|
161
169
|
level. The beam location is manually inputted by the beamline operator on GDA \
|
|
@@ -164,7 +172,7 @@ class OAVConfigParams:
|
|
|
164
172
|
"""
|
|
165
173
|
crosshair_x_line = None
|
|
166
174
|
crosshair_y_line = None
|
|
167
|
-
with open(self.display_config
|
|
175
|
+
with open(self.display_config) as f:
|
|
168
176
|
file_lines = f.readlines()
|
|
169
177
|
for i in range(len(file_lines)):
|
|
170
178
|
if file_lines[i].startswith("zoomLevel = " + str(zoom)):
|
|
@@ -188,7 +196,7 @@ class OAVConfigParams:
|
|
|
188
196
|
|
|
189
197
|
def calculate_beam_distance(
|
|
190
198
|
self, horizontal_pixels: int, vertical_pixels: int
|
|
191
|
-
) ->
|
|
199
|
+
) -> tuple[int, int]:
|
|
192
200
|
"""
|
|
193
201
|
Calculates the distance between the beam centre and the given (horizontal, vertical).
|
|
194
202
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import io
|
|
3
|
+
import pickle
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from aiohttp import ClientResponse, ClientSession
|
|
8
|
+
from bluesky.protocols import Flyable
|
|
9
|
+
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
10
|
+
from ophyd_async.core.signal import soft_signal_r_and_setter
|
|
11
|
+
from ophyd_async.epics.signal import epics_signal_r
|
|
12
|
+
from PIL import Image
|
|
13
|
+
from redis.asyncio import StrictRedis
|
|
14
|
+
|
|
15
|
+
from dodal.log import LOGGER
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def get_next_jpeg(response: ClientResponse) -> bytes:
|
|
19
|
+
JPEG_START_BYTE = b"\xff\xd8"
|
|
20
|
+
JPEG_STOP_BYTE = b"\xff\xd9"
|
|
21
|
+
while True:
|
|
22
|
+
line = await response.content.readline()
|
|
23
|
+
if line.startswith(JPEG_START_BYTE):
|
|
24
|
+
return line + await response.content.readuntil(JPEG_STOP_BYTE)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OAVToRedisForwarder(StandardReadable, Flyable):
|
|
28
|
+
"""Forwards OAV image data to redis. To use call:
|
|
29
|
+
|
|
30
|
+
> bps.kickoff(oav_forwarder)
|
|
31
|
+
> bps.monitor(oav_forwarder.uuid)
|
|
32
|
+
> bps.complete(oav_forwarder)
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
prefix: str,
|
|
39
|
+
redis_host: str,
|
|
40
|
+
redis_password: str,
|
|
41
|
+
redis_db: int = 0,
|
|
42
|
+
name: str = "",
|
|
43
|
+
redis_key: str = "test-image",
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Reads image data from the MJPEG stream on an OAV and forwards it into a
|
|
46
|
+
redis database. This is currently only used for murko integration.
|
|
47
|
+
|
|
48
|
+
Arguments:
|
|
49
|
+
prefix: str the PV prefix of the OAV
|
|
50
|
+
redis_host: str the host where the redis database is running
|
|
51
|
+
redis_password: str the password for the redis database
|
|
52
|
+
redis_db: int which redis database to connect to, defaults to 0
|
|
53
|
+
name: str the name of this device
|
|
54
|
+
redis_key: str the key to store data in, defaults to "test-image"
|
|
55
|
+
"""
|
|
56
|
+
self.stream_url = epics_signal_r(str, f"{prefix}-DI-OAV-01:MJPG:HOST_RBV")
|
|
57
|
+
|
|
58
|
+
with self.add_children_as_readables():
|
|
59
|
+
self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
|
|
60
|
+
|
|
61
|
+
self.forwarding_task = None
|
|
62
|
+
self.redis_client = StrictRedis(
|
|
63
|
+
host=redis_host, password=redis_password, db=redis_db
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.redis_key = redis_key
|
|
67
|
+
|
|
68
|
+
# The uuid that images are being saved under, this should be monitored for
|
|
69
|
+
# callbacks to correlate the data
|
|
70
|
+
self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
|
|
71
|
+
|
|
72
|
+
super().__init__(name=name)
|
|
73
|
+
|
|
74
|
+
async def _get_frame_and_put_to_redis(self, response: ClientResponse):
|
|
75
|
+
"""Converts the data that comes in as a jpeg byte stream into a numpy array of
|
|
76
|
+
RGB values, pickles this array then writes it to redis.
|
|
77
|
+
"""
|
|
78
|
+
jpeg_bytes = await get_next_jpeg(response)
|
|
79
|
+
self.uuid_setter(image_uuid := str(uuid.uuid4()))
|
|
80
|
+
img = Image.open(io.BytesIO(jpeg_bytes))
|
|
81
|
+
image_data = pickle.dumps(np.asarray(img))
|
|
82
|
+
await self.redis_client.hset(self.redis_key, image_uuid, image_data) # type: ignore
|
|
83
|
+
LOGGER.debug(f"Sent frame to redis key {self.redis_key} with uuid {image_uuid}")
|
|
84
|
+
|
|
85
|
+
async def _stream_to_redis(self):
|
|
86
|
+
stream_url = await self.stream_url.get_value()
|
|
87
|
+
async with ClientSession() as session:
|
|
88
|
+
async with session.get(stream_url) as response:
|
|
89
|
+
while True:
|
|
90
|
+
await self._get_frame_and_put_to_redis(response)
|
|
91
|
+
await asyncio.sleep(0.01)
|
|
92
|
+
|
|
93
|
+
@AsyncStatus.wrap
|
|
94
|
+
async def kickoff(self):
|
|
95
|
+
self.forwarding_task = asyncio.create_task(self._stream_to_redis())
|
|
96
|
+
|
|
97
|
+
@AsyncStatus.wrap
|
|
98
|
+
async def complete(self):
|
|
99
|
+
assert self.forwarding_task, "Device not kicked off"
|
|
100
|
+
self.forwarding_task.cancel()
|
|
@@ -5,6 +5,7 @@ import numpy as np
|
|
|
5
5
|
from numpy.typing import NDArray
|
|
6
6
|
from ophyd_async.core import (
|
|
7
7
|
AsyncStatus,
|
|
8
|
+
HintedSignal,
|
|
8
9
|
StandardReadable,
|
|
9
10
|
observe_value,
|
|
10
11
|
soft_signal_r_and_setter,
|
|
@@ -77,12 +78,13 @@ class PinTipDetection(StandardReadable):
|
|
|
77
78
|
self.min_tip_height = soft_signal_rw(int, 5, name="min_tip_height")
|
|
78
79
|
self.validity_timeout = soft_signal_rw(float, 5.0, name="validity_timeout")
|
|
79
80
|
|
|
80
|
-
self.
|
|
81
|
-
|
|
81
|
+
self.add_readables(
|
|
82
|
+
[
|
|
82
83
|
self.triggered_tip,
|
|
83
84
|
self.triggered_top_edge,
|
|
84
85
|
self.triggered_bottom_edge,
|
|
85
86
|
],
|
|
87
|
+
wrapper=HintedSignal,
|
|
86
88
|
)
|
|
87
89
|
|
|
88
90
|
super().__init__(name=name)
|
|
@@ -134,9 +136,7 @@ class PinTipDetection(StandardReadable):
|
|
|
134
136
|
location = sample_detection.processArray(array_data)
|
|
135
137
|
end_time = time.time()
|
|
136
138
|
LOGGER.debug(
|
|
137
|
-
"Sample location detection took {}ms"
|
|
138
|
-
(end_time - start_time) * 1000.0
|
|
139
|
-
)
|
|
139
|
+
f"Sample location detection took {(end_time - start_time) * 1000.0}ms"
|
|
140
140
|
)
|
|
141
141
|
return location
|
|
142
142
|
|
|
@@ -154,7 +154,7 @@ class PinTipDetection(StandardReadable):
|
|
|
154
154
|
location = await self._get_tip_and_edge_data(value)
|
|
155
155
|
self._set_triggered_values(location)
|
|
156
156
|
except Exception as e:
|
|
157
|
-
LOGGER.
|
|
157
|
+
LOGGER.warning(
|
|
158
158
|
f"Failed to detect pin-tip location, will retry with next image: {e}"
|
|
159
159
|
)
|
|
160
160
|
else:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from enum import Enum
|
|
3
|
-
from typing import
|
|
4
|
+
from typing import Final
|
|
4
5
|
|
|
5
6
|
import cv2
|
|
6
7
|
import numpy as np
|
|
@@ -103,7 +104,7 @@ class SampleLocation:
|
|
|
103
104
|
edge_bottom: np.ndarray
|
|
104
105
|
|
|
105
106
|
|
|
106
|
-
class MxSampleDetect
|
|
107
|
+
class MxSampleDetect:
|
|
107
108
|
def __init__(
|
|
108
109
|
self,
|
|
109
110
|
*,
|
|
@@ -161,7 +162,7 @@ class MxSampleDetect(object):
|
|
|
161
162
|
@staticmethod
|
|
162
163
|
def _first_and_last_nonzero_by_columns(
|
|
163
164
|
arr: np.ndarray,
|
|
164
|
-
) ->
|
|
165
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
165
166
|
"""
|
|
166
167
|
Finds the indexes of the first & last non-zero values by column in a 2d array.
|
|
167
168
|
|
|
@@ -243,9 +244,7 @@ class MxSampleDetect(object):
|
|
|
243
244
|
bottom[x + 1 :] = NONE_VALUE
|
|
244
245
|
|
|
245
246
|
LOGGER.info(
|
|
246
|
-
"pin-tip detection: Successfully located pin tip at (x={}, y={})"
|
|
247
|
-
tip_x, tip_y
|
|
248
|
-
)
|
|
247
|
+
f"pin-tip detection: Successfully located pin tip at (x={tip_x}, y={tip_y})"
|
|
249
248
|
)
|
|
250
249
|
return SampleLocation(
|
|
251
250
|
tip_x=tip_x, tip_y=tip_y, edge_bottom=bottom, edge_top=top
|
dodal/devices/oav/utils.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
1
2
|
from enum import IntEnum
|
|
2
|
-
from typing import Generator, Tuple
|
|
3
3
|
|
|
4
4
|
import bluesky.plan_stubs as bps
|
|
5
5
|
import numpy as np
|
|
@@ -10,7 +10,7 @@ from dodal.devices.oav.oav_detector import OAVConfigParams
|
|
|
10
10
|
from dodal.devices.oav.pin_image_recognition import PinTipDetection
|
|
11
11
|
from dodal.devices.smargon import Smargon
|
|
12
12
|
|
|
13
|
-
Pixel =
|
|
13
|
+
Pixel = tuple[int, int]
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class PinNotFoundException(Exception):
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import Device
|
|
4
|
+
from ophyd_async.epics.signal import epics_signal_rw
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SampleAngleStage(Device):
|
|
8
|
+
def __init__(self, prefix: str, name: str):
|
|
9
|
+
self.theta = epics_signal_rw(
|
|
10
|
+
float, prefix + "WRITETHETA:RBV", prefix + "WRITETHETA"
|
|
11
|
+
)
|
|
12
|
+
self.roll = epics_signal_rw(
|
|
13
|
+
float, prefix + "WRITEROLL:RBV", prefix + "WRITEROLL"
|
|
14
|
+
)
|
|
15
|
+
self.pitch = epics_signal_rw(
|
|
16
|
+
float, prefix + "WRITEPITCH:RBV", prefix + "WRITEPITCH"
|
|
17
|
+
)
|
|
18
|
+
super().__init__(name=name)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class p99StageSelections(str, Enum):
|
|
22
|
+
Empty = "Empty"
|
|
23
|
+
Mn5um = "Mn 5um"
|
|
24
|
+
Fe = "Fe (empty)"
|
|
25
|
+
Co5um = "Co 5um"
|
|
26
|
+
Ni5um = "Ni 5um"
|
|
27
|
+
Cu5um = "Cu 5um"
|
|
28
|
+
Zn5um = "Zn 5um"
|
|
29
|
+
Zr = "Zr (empty)"
|
|
30
|
+
Mo = "Mo (empty)"
|
|
31
|
+
Rh = "Rh (empty)"
|
|
32
|
+
Pd = "Pd (empty)"
|
|
33
|
+
Ag = "Ag (empty)"
|
|
34
|
+
Cd25um = "Cd 25um"
|
|
35
|
+
W = "W (empty)"
|
|
36
|
+
Pt = "Pt (empty)"
|
|
37
|
+
User = "User"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FilterMotor(Device):
|
|
41
|
+
def __init__(self, prefix: str, name: str):
|
|
42
|
+
self.user_setpoint = epics_signal_rw(p99StageSelections, prefix)
|
|
43
|
+
super().__init__(name=name)
|
dodal/devices/robot.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from asyncio import FIRST_COMPLETED, Task
|
|
2
|
+
from asyncio import FIRST_COMPLETED, CancelledError, Task
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from enum import Enum
|
|
5
5
|
|
|
@@ -45,6 +45,9 @@ class BartRobot(StandardReadable, Movable):
|
|
|
45
45
|
LOAD_TIMEOUT = 60
|
|
46
46
|
NO_PIN_ERROR_CODE = 25
|
|
47
47
|
|
|
48
|
+
# How far the gonio position can be out before loading will fail
|
|
49
|
+
LOAD_TOLERANCE_MM = 0.02
|
|
50
|
+
|
|
48
51
|
def __init__(
|
|
49
52
|
self,
|
|
50
53
|
name: str,
|
|
@@ -74,19 +77,28 @@ class BartRobot(StandardReadable, Movable):
|
|
|
74
77
|
await wait_for_value(self.error_code, self.NO_PIN_ERROR_CODE, None)
|
|
75
78
|
raise RobotLoadFailed(self.NO_PIN_ERROR_CODE, "Pin was not detected")
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
async def wfv():
|
|
81
|
+
await wait_for_value(self.gonio_pin_sensor, PinMounted.PIN_MOUNTED, None)
|
|
82
|
+
|
|
83
|
+
tasks = [
|
|
84
|
+
(Task(raise_if_no_pin())),
|
|
85
|
+
(Task(wfv())),
|
|
86
|
+
]
|
|
87
|
+
try:
|
|
88
|
+
finished, unfinished = await asyncio.wait(
|
|
89
|
+
tasks,
|
|
90
|
+
return_when=FIRST_COMPLETED,
|
|
91
|
+
)
|
|
92
|
+
for task in unfinished:
|
|
93
|
+
task.cancel()
|
|
94
|
+
for task in finished:
|
|
95
|
+
await task
|
|
96
|
+
except CancelledError:
|
|
97
|
+
# If the outer enclosing task cancels after LOAD_TIMEOUT, this causes CancelledError to be raised
|
|
98
|
+
# in the current task, when it propagates to here we should cancel all pending tasks before bubbling up
|
|
99
|
+
for task in tasks:
|
|
100
|
+
task.cancel()
|
|
101
|
+
raise
|
|
90
102
|
|
|
91
103
|
async def _load_pin_and_puck(self, sample_location: SampleLocation):
|
|
92
104
|
LOGGER.info(f"Loading pin {sample_location}")
|
|
@@ -108,12 +120,12 @@ class BartRobot(StandardReadable, Movable):
|
|
|
108
120
|
await self.pin_mounted_or_no_pin_found()
|
|
109
121
|
|
|
110
122
|
@AsyncStatus.wrap
|
|
111
|
-
async def set(self,
|
|
123
|
+
async def set(self, value: SampleLocation):
|
|
112
124
|
try:
|
|
113
125
|
await asyncio.wait_for(
|
|
114
|
-
self._load_pin_and_puck(
|
|
126
|
+
self._load_pin_and_puck(value), timeout=self.LOAD_TIMEOUT
|
|
115
127
|
)
|
|
116
|
-
except asyncio.TimeoutError:
|
|
128
|
+
except asyncio.TimeoutError as e:
|
|
117
129
|
error_code = await self.error_code.get_value()
|
|
118
130
|
error_string = await self.error_str.get_value()
|
|
119
|
-
raise RobotLoadFailed(error_code, error_string)
|
|
131
|
+
raise RobotLoadFailed(int(error_code), error_string) from e
|
dodal/devices/scintillator.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
1
|
+
from ophyd_async.core import StandardReadable
|
|
2
|
+
from ophyd_async.epics.motion import Motor
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
class Scintillator(
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
class Scintillator(StandardReadable):
|
|
6
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
7
|
+
with self.add_children_as_readables():
|
|
8
|
+
self.y = Motor(prefix + "-MO-SCIN-01:Y")
|
|
9
|
+
self.z = Motor(prefix + "-MO-SCIN-01:Z")
|
|
10
|
+
super().__init__(name)
|
dodal/devices/smargon.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from collections.abc import Generator
|
|
1
|
+
from collections.abc import Collection, Generator
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from math import isclose
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import cast
|
|
6
6
|
|
|
7
7
|
from bluesky import plan_stubs as bps
|
|
8
8
|
from bluesky.utils import Msg
|
|
@@ -87,7 +87,7 @@ class XYZLimits:
|
|
|
87
87
|
def position_valid(self, pos: Collection[float]) -> bool:
|
|
88
88
|
return all(
|
|
89
89
|
axis_limits.contains(value)
|
|
90
|
-
for axis_limits, value in zip([self.x, self.y, self.z], pos)
|
|
90
|
+
for axis_limits, value in zip([self.x, self.y, self.z], pos, strict=False)
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
|
dodal/devices/status.py
CHANGED
|
@@ -1,41 +1,12 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any
|
|
2
2
|
|
|
3
3
|
from ophyd.status import SubscriptionStatus
|
|
4
4
|
|
|
5
|
-
T = TypeVar("T")
|
|
6
|
-
|
|
7
5
|
|
|
8
6
|
def await_value(
|
|
9
|
-
subscribable: Any, expected_value:
|
|
7
|
+
subscribable: Any, expected_value: object, timeout: None | int = None
|
|
10
8
|
) -> SubscriptionStatus:
|
|
11
9
|
def value_is(value, **_):
|
|
12
10
|
return value == expected_value
|
|
13
11
|
|
|
14
12
|
return SubscriptionStatus(subscribable, value_is, timeout=timeout)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def await_value_in_list(
|
|
18
|
-
subscribable: Any, expected_value: list, timeout: None | int = None
|
|
19
|
-
) -> SubscriptionStatus:
|
|
20
|
-
"""Returns a status which is completed when the subscriptable contains a value
|
|
21
|
-
within the expected_value list"""
|
|
22
|
-
|
|
23
|
-
def value_is(value, **_):
|
|
24
|
-
return value in expected_value
|
|
25
|
-
|
|
26
|
-
if not isinstance(expected_value, list):
|
|
27
|
-
raise TypeError(f"expected value {expected_value} is not a list")
|
|
28
|
-
else:
|
|
29
|
-
return SubscriptionStatus(subscribable, value_is, timeout=timeout)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def await_approx_value(
|
|
33
|
-
subscribable: Any,
|
|
34
|
-
expected_value: T,
|
|
35
|
-
deadband: float = 1e-09,
|
|
36
|
-
timeout: None | int = None,
|
|
37
|
-
) -> SubscriptionStatus:
|
|
38
|
-
def value_is_approx(value, **_):
|
|
39
|
-
return abs(value - expected_value) <= deadband
|
|
40
|
-
|
|
41
|
-
return SubscriptionStatus(subscribable, value_is_approx, timeout=timeout)
|
dodal/devices/tetramm.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from collections.abc import Sequence
|
|
2
3
|
from enum import Enum
|
|
3
|
-
from typing import Sequence
|
|
4
4
|
|
|
5
5
|
from bluesky.protocols import Hints
|
|
6
6
|
from ophyd_async.core import (
|
|
@@ -115,7 +115,7 @@ class TetrammController(DetectorControl):
|
|
|
115
115
|
async def arm(
|
|
116
116
|
self,
|
|
117
117
|
num: int,
|
|
118
|
-
trigger: DetectorTrigger,
|
|
118
|
+
trigger: DetectorTrigger = DetectorTrigger.edge_trigger,
|
|
119
119
|
exposure: float | None = None,
|
|
120
120
|
) -> AsyncStatus:
|
|
121
121
|
if exposure is None:
|
|
@@ -132,7 +132,7 @@ class TetrammController(DetectorControl):
|
|
|
132
132
|
self._drv.averaging_time.set(exposure), self.set_exposure(exposure)
|
|
133
133
|
)
|
|
134
134
|
|
|
135
|
-
status = await set_and_wait_for_value(self._drv.acquire,
|
|
135
|
+
status = await set_and_wait_for_value(self._drv.acquire, True)
|
|
136
136
|
|
|
137
137
|
return status
|
|
138
138
|
|
|
@@ -150,7 +150,7 @@ class TetrammController(DetectorControl):
|
|
|
150
150
|
)
|
|
151
151
|
|
|
152
152
|
async def disarm(self):
|
|
153
|
-
await stop_busy_record(self._drv.acquire,
|
|
153
|
+
await stop_busy_record(self._drv.acquire, False, timeout=1)
|
|
154
154
|
|
|
155
155
|
async def set_exposure(self, exposure: float):
|
|
156
156
|
"""Tries to set the exposure time of a single frame.
|
dodal/devices/thawer.py
CHANGED
|
@@ -15,7 +15,7 @@ class ThawerStates(str, Enum):
|
|
|
15
15
|
ON = "On"
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class ThawingTimer(Device):
|
|
18
|
+
class ThawingTimer(Device, Stoppable):
|
|
19
19
|
def __init__(self, control_signal: SignalRW[ThawerStates]) -> None:
|
|
20
20
|
self._control_signal = control_signal
|
|
21
21
|
self._thawing_task: Task | None = None
|
|
@@ -32,7 +32,8 @@ class ThawingTimer(Device):
|
|
|
32
32
|
finally:
|
|
33
33
|
await self._control_signal.set(ThawerStates.OFF)
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
@AsyncStatus.wrap
|
|
36
|
+
async def stop(self, *args, **kwargs):
|
|
36
37
|
if self._thawing_task:
|
|
37
38
|
self._thawing_task.cancel()
|
|
38
39
|
|
|
@@ -43,6 +44,7 @@ class Thawer(StandardReadable, Stoppable):
|
|
|
43
44
|
self.thaw_for_time_s = ThawingTimer(self.control)
|
|
44
45
|
super().__init__(name)
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
@AsyncStatus.wrap
|
|
48
|
+
async def stop(self, *args, **kwargs):
|
|
47
49
|
await self.thaw_for_time_s.stop()
|
|
48
50
|
await self.control.set(ThawerStates.OFF)
|
dodal/devices/undulator_dcm.py
CHANGED
|
@@ -74,14 +74,12 @@ class UndulatorDCM(StandardReadable, Movable):
|
|
|
74
74
|
daq_configuration_path + "/domain/beamlineParameters"
|
|
75
75
|
)["DCM_Perp_Offset_FIXED"]
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return AsyncStatus(_set())
|
|
77
|
+
@AsyncStatus.wrap
|
|
78
|
+
async def set(self, value: float):
|
|
79
|
+
await asyncio.gather(
|
|
80
|
+
self._set_dcm_energy(value),
|
|
81
|
+
self._set_undulator_gap_if_required(value),
|
|
82
|
+
)
|
|
85
83
|
|
|
86
84
|
async def _set_dcm_energy(self, energy_kev: float) -> None:
|
|
87
85
|
access_level = await self.undulator.gap_access.get_value()
|
|
@@ -3,10 +3,10 @@ All the methods in this module return a bluesky plan generator that adjusts a va
|
|
|
3
3
|
according to some criteria either via feedback, preset positions, lookup tables etc.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Callable, Generator
|
|
7
7
|
|
|
8
8
|
from bluesky import plan_stubs as bps
|
|
9
|
-
from bluesky.
|
|
9
|
+
from bluesky.utils import Msg
|
|
10
10
|
from ophyd.epics_motor import EpicsMotor
|
|
11
11
|
from ophyd_async.epics.motion import Motor
|
|
12
12
|
|