dls-dodal 1.65.0__py3-none-any.whl → 1.67.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.67.0.dist-info}/METADATA +3 -4
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +82 -66
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +21 -2
- dodal/beamlines/i03.py +102 -198
- dodal/beamlines/i04.py +40 -4
- dodal/beamlines/i05.py +28 -1
- dodal/beamlines/i06.py +62 -0
- dodal/beamlines/i07.py +20 -0
- dodal/beamlines/i09_1.py +32 -3
- dodal/beamlines/i09_2.py +57 -2
- dodal/beamlines/i10_optics.py +46 -17
- dodal/beamlines/i17.py +7 -3
- dodal/beamlines/i18.py +3 -3
- dodal/beamlines/i19_1.py +26 -14
- dodal/beamlines/i19_2.py +49 -38
- dodal/beamlines/i21.py +2 -2
- dodal/beamlines/i22.py +19 -4
- dodal/beamlines/p38.py +3 -3
- dodal/beamlines/training_rig.py +0 -16
- dodal/cli.py +26 -12
- dodal/common/coordination.py +3 -2
- dodal/device_manager.py +604 -0
- dodal/devices/aithre_lasershaping/goniometer.py +26 -9
- dodal/devices/aperturescatterguard.py +3 -2
- dodal/devices/areadetector/plugins/mjpg.py +10 -3
- dodal/devices/beamsize/__init__.py +0 -0
- dodal/devices/beamsize/beamsize.py +6 -0
- dodal/devices/cryostream.py +28 -57
- dodal/devices/detector/det_resolution.py +4 -2
- dodal/devices/eiger.py +26 -18
- 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/max_pixel.py +38 -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 +13 -2
- dodal/devices/i09_1_shared/hard_energy.py +112 -0
- dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
- dodal/devices/i09_2_shared/__init__.py +0 -0
- dodal/devices/i09_2_shared/i09_apple2.py +86 -0
- dodal/devices/i10/i10_apple2.py +39 -331
- dodal/devices/i17/i17_apple2.py +37 -22
- dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
- dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
- dodal/devices/i19/access_controlled/shutter.py +2 -4
- dodal/devices/insertion_device/__init__.py +0 -0
- dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +122 -69
- dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
- dodal/devices/insertion_device/lookup_table_models.py +287 -0
- dodal/devices/ipin.py +20 -2
- dodal/devices/motors.py +33 -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 +33 -18
- 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/plans/__init__.py +1 -1
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
- dodal/plans/load_panda_yaml.py +9 -0
- dodal/plans/verify_undulator_gap.py +2 -2
- dodal/testing/fixtures/devices/__init__.py +0 -0
- dodal/testing/fixtures/devices/apple2.py +78 -0
- dodal/utils.py +6 -3
- 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.67.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/top_level.txt +0 -0
- /dodal/plans/{scanspec.py → spec_path.py} +0 -0
|
@@ -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
|
|
@@ -71,6 +71,7 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
|
71
71
|
LOAD_TIMEOUT = 60
|
|
72
72
|
|
|
73
73
|
# Error codes that we do special things on
|
|
74
|
+
NO_ERROR = 0
|
|
74
75
|
NO_PIN_ERROR_CODE = 25
|
|
75
76
|
LIGHT_CURTAIN_TRIPPED = 40
|
|
76
77
|
|
|
@@ -104,7 +105,6 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
|
104
105
|
self.init = epics_signal_x(prefix + "INIT.PROC")
|
|
105
106
|
self.soak = epics_signal_x(prefix + "SOAK.PROC")
|
|
106
107
|
self.home = epics_signal_x(prefix + "GOHM.PROC")
|
|
107
|
-
self.unload = epics_signal_x(prefix + "UNLD.PROC")
|
|
108
108
|
self.dry = epics_signal_x(prefix + "DRY.PROC")
|
|
109
109
|
self.open = epics_signal_x(prefix + "COLO.PROC")
|
|
110
110
|
self.close = epics_signal_x(prefix + "COLC.PROC")
|
|
@@ -116,21 +116,25 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
|
116
116
|
)
|
|
117
117
|
super().__init__(name=name)
|
|
118
118
|
|
|
119
|
-
async def
|
|
120
|
-
"""This co-routine will finish when either
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
async def pin_state_or_error(self, expected_state=PinMounted.PIN_MOUNTED):
|
|
120
|
+
"""This co-routine will finish when either the pin sensor reaches the specified
|
|
121
|
+
state or the robot gives an error (whichever happens first). In the case where
|
|
122
|
+
there is an error a RobotLoadError error is raised.
|
|
123
123
|
"""
|
|
124
124
|
|
|
125
|
-
async def
|
|
126
|
-
await wait_for_value(
|
|
127
|
-
|
|
125
|
+
async def raise_if_error():
|
|
126
|
+
await wait_for_value(
|
|
127
|
+
self.prog_error.code, lambda value: value != self.NO_ERROR, None
|
|
128
|
+
)
|
|
129
|
+
error_code = await self.prog_error.code.get_value()
|
|
130
|
+
error_msg = await self.prog_error.str.get_value()
|
|
131
|
+
raise RobotLoadError(error_code, error_msg)
|
|
128
132
|
|
|
129
133
|
async def wfv():
|
|
130
|
-
await wait_for_value(self.gonio_pin_sensor,
|
|
134
|
+
await wait_for_value(self.gonio_pin_sensor, expected_state, None)
|
|
131
135
|
|
|
132
136
|
tasks = [
|
|
133
|
-
(Task(
|
|
137
|
+
(Task(raise_if_error())),
|
|
134
138
|
(Task(wfv())),
|
|
135
139
|
]
|
|
136
140
|
try:
|
|
@@ -169,18 +173,29 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
|
169
173
|
await self.load.trigger()
|
|
170
174
|
if await self.gonio_pin_sensor.get_value() == PinMounted.PIN_MOUNTED:
|
|
171
175
|
LOGGER.info(WAIT_FOR_OLD_PIN_MSG)
|
|
172
|
-
await
|
|
176
|
+
await self.pin_state_or_error(PinMounted.NO_PIN_MOUNTED)
|
|
173
177
|
LOGGER.info(WAIT_FOR_NEW_PIN_MSG)
|
|
174
178
|
|
|
175
|
-
await self.
|
|
179
|
+
await self.pin_state_or_error()
|
|
176
180
|
|
|
177
181
|
@AsyncStatus.wrap
|
|
178
|
-
async def set(self, value: SampleLocation):
|
|
182
|
+
async def set(self, value: SampleLocation | None):
|
|
183
|
+
"""
|
|
184
|
+
Perform a sample load from the specified sample location
|
|
185
|
+
Args:
|
|
186
|
+
value: The pin and puck to load, or None to unload the sample.
|
|
187
|
+
Raises:
|
|
188
|
+
RobotLoadError if a timeout occurs, or if an error occurs loading the smaple.
|
|
189
|
+
"""
|
|
179
190
|
try:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
191
|
+
if value is not None:
|
|
192
|
+
await wait_for(
|
|
193
|
+
self._load_pin_and_puck(value),
|
|
194
|
+
timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
await self.unload.trigger(timeout=self.LOAD_TIMEOUT)
|
|
198
|
+
await wait_for_value(self.program_running, False, self.NOT_BUSY_TIMEOUT)
|
|
184
199
|
except TimeoutError as e:
|
|
185
200
|
await self.prog_error.raise_if_error(e)
|
|
186
201
|
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)
|