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
dodal/devices/cryostream.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from ophyd_async.core import (
|
|
2
|
-
EnabledDisabled,
|
|
3
2
|
InOut,
|
|
4
3
|
StandardReadable,
|
|
5
4
|
StandardReadableFormat,
|
|
@@ -12,20 +11,6 @@ from ophyd_async.epics.core import (
|
|
|
12
11
|
)
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
class CryoStream(StandardReadable):
|
|
16
|
-
MAX_TEMP_K = 110
|
|
17
|
-
MAX_PRESSURE_BAR = 0.1
|
|
18
|
-
|
|
19
|
-
def __init__(self, prefix: str, name: str = ""):
|
|
20
|
-
self.course = epics_signal_rw(InOut, f"{prefix}-EA-CJET-01:COARSE:CTRL")
|
|
21
|
-
self.fine = epics_signal_rw(InOut, f"{prefix}-EA-CJET-01:FINE:CTRL")
|
|
22
|
-
self.temperature_k = epics_signal_r(float, f"{prefix}-EA-CSTRM-01:TEMP")
|
|
23
|
-
self.back_pressure_bar = epics_signal_r(
|
|
24
|
-
float, f"{prefix}-EA-CSTRM-01:BACKPRESS"
|
|
25
|
-
)
|
|
26
|
-
super().__init__(name)
|
|
27
|
-
|
|
28
|
-
|
|
29
14
|
class TurboEnum(StrictEnum):
|
|
30
15
|
OFF = "Off"
|
|
31
16
|
ON = "On"
|
|
@@ -37,59 +22,33 @@ class CryoStreamSelection(StrictEnum):
|
|
|
37
22
|
HC1 = "HC1"
|
|
38
23
|
|
|
39
24
|
|
|
40
|
-
class
|
|
25
|
+
class OxfordCryoStream(StandardReadable):
|
|
41
26
|
def __init__(self, prefix: str, name: str = ""):
|
|
27
|
+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
28
|
+
self.temp = epics_signal_r(float, f"{prefix}TEMP")
|
|
29
|
+
|
|
42
30
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
43
31
|
# Any signals that should be read once at the start of the scan
|
|
32
|
+
|
|
44
33
|
self.turbo = epics_signal_rw(str, f"{prefix}TURBO")
|
|
45
34
|
self.turbo_mode = epics_signal_rw(TurboEnum, f"{prefix}TURBOMODE")
|
|
46
|
-
|
|
47
|
-
self.serial_comms = epics_signal_rw(EnabledDisabled, f"{prefix}DISABLE")
|
|
48
35
|
self.status = epics_signal_r(str, f"{prefix}STATUS.SEVR")
|
|
49
|
-
|
|
50
|
-
with self.add_children_as_readables():
|
|
51
|
-
# Any signals that should be read at every point in the scan
|
|
52
|
-
|
|
53
|
-
self.purge = epics_signal_x(f"{prefix}PURGE.PROC")
|
|
54
|
-
self.hold = epics_signal_x(f"{prefix}HOLD.PROC")
|
|
55
|
-
self.start = epics_signal_x(f"{prefix}RESTART.PROC")
|
|
56
|
-
self.pause = epics_signal_x(f"{prefix}PAUSE.PROC")
|
|
57
|
-
self.resume = epics_signal_x(f"{prefix}RESUME.PROC")
|
|
58
|
-
self.end = epics_signal_x(f"{prefix}END.PROC")
|
|
59
|
-
self.stop = epics_signal_x(f"{prefix}STOP.PROC")
|
|
60
|
-
|
|
61
|
-
self.ramp_rate = epics_signal_rw(float, f"{prefix}RRATE")
|
|
62
|
-
self.ramp_temp = epics_signal_rw(float, f"{prefix}RTEMP")
|
|
63
|
-
self.ramp = epics_signal_x(f"{prefix}RAMP.PROC")
|
|
64
|
-
|
|
65
|
-
self.plat_time = epics_signal_rw(float, f"{prefix}PTIME")
|
|
66
|
-
self.plat = epics_signal_x(f"{prefix}PLAT.PROC")
|
|
67
|
-
|
|
68
|
-
self.cool_temp = epics_signal_rw(float, f"{prefix}CTEMP")
|
|
69
|
-
self.cool = epics_signal_x(f"{prefix}COOL.PROC")
|
|
70
|
-
|
|
71
|
-
self.end_rate = epics_signal_rw(float, f"{prefix}ERATE")
|
|
72
|
-
|
|
73
|
-
super().__init__(name)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class OxfordCryoStreamStatus(StandardReadable):
|
|
77
|
-
def __init__(self, prefix: str, name: str = ""):
|
|
78
|
-
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
79
|
-
# Any signals that should be read once at the start of the scan
|
|
80
|
-
|
|
81
36
|
self.pump_uptime = epics_signal_r(float, f"{prefix}RUNTIME")
|
|
82
37
|
self.controller_number = epics_signal_r(float, f"{prefix}CTRLNUM")
|
|
83
38
|
self.software_version = epics_signal_r(float, f"{prefix}VER")
|
|
84
39
|
self.evap_adjust = epics_signal_r(float, f"{prefix}EVAPADJUST")
|
|
85
40
|
self.series = epics_signal_r(str, f"{prefix}SERIES")
|
|
41
|
+
self.error = epics_signal_r(float, f"{prefix}ERROR")
|
|
42
|
+
self.mode = epics_signal_r(str, f"{prefix}RUNMODE")
|
|
86
43
|
|
|
87
44
|
with self.add_children_as_readables():
|
|
88
45
|
# Any signals that should be read at every point in the scan
|
|
46
|
+
self.ramp_rate = epics_signal_rw(float, f"{prefix}RRATE")
|
|
47
|
+
self.ramp_temp = epics_signal_rw(float, f"{prefix}RTEMP")
|
|
48
|
+
self.plat_time = epics_signal_rw(float, f"{prefix}PTIME")
|
|
49
|
+
self.cool_temp = epics_signal_rw(float, f"{prefix}CTEMP")
|
|
50
|
+
self.end_rate = epics_signal_rw(float, f"{prefix}ERATE")
|
|
89
51
|
self.setpoint = epics_signal_r(float, f"{prefix}SETPOINT")
|
|
90
|
-
self.temp = epics_signal_r(float, f"{prefix}TEMP")
|
|
91
|
-
self.error = epics_signal_r(float, f"{prefix}ERROR")
|
|
92
|
-
self.mode = epics_signal_r(str, f"{prefix}RUNMODE")
|
|
93
52
|
self.phase = epics_signal_r(str, f"{prefix}PHASE")
|
|
94
53
|
self.ramp_rate_setpoint = epics_signal_r(float, f"{prefix}RAMPRATE")
|
|
95
54
|
self.target_temp = epics_signal_r(float, f"{prefix}TARGETTEMP")
|
|
@@ -102,14 +61,27 @@ class OxfordCryoStreamStatus(StandardReadable):
|
|
|
102
61
|
self.suct_heat = epics_signal_r(float, f"{prefix}SUCTHEAT")
|
|
103
62
|
self.back_pressure = epics_signal_r(float, f"{prefix}BACKPRESS")
|
|
104
63
|
|
|
64
|
+
self.purge = epics_signal_x(f"{prefix}PURGE.PROC")
|
|
65
|
+
self.hold = epics_signal_x(f"{prefix}HOLD.PROC")
|
|
66
|
+
self.start = epics_signal_x(f"{prefix}RESTART.PROC")
|
|
67
|
+
self.pause = epics_signal_x(f"{prefix}PAUSE.PROC")
|
|
68
|
+
self.resume = epics_signal_x(f"{prefix}RESUME.PROC")
|
|
69
|
+
self.end = epics_signal_x(f"{prefix}END.PROC")
|
|
70
|
+
self.stop = epics_signal_x(f"{prefix}STOP.PROC")
|
|
71
|
+
self.plat = epics_signal_x(f"{prefix}PLAT.PROC")
|
|
72
|
+
self.cool = epics_signal_x(f"{prefix}COOL.PROC")
|
|
73
|
+
self.ramp = epics_signal_x(f"{prefix}RAMP.PROC")
|
|
74
|
+
|
|
105
75
|
super().__init__(name)
|
|
106
76
|
|
|
107
77
|
|
|
108
|
-
class
|
|
78
|
+
class OxfordCryoJet(StandardReadable):
|
|
79
|
+
# TODO: https://github.com/DiamondLightSource/dodal/issues/1486
|
|
80
|
+
# This is a placeholder implementation to get it working with I03, the actual cryojet has many more PVs
|
|
109
81
|
def __init__(self, prefix: str, name=""):
|
|
110
82
|
with self.add_children_as_readables():
|
|
111
|
-
self.
|
|
112
|
-
self.
|
|
83
|
+
self.coarse = epics_signal_rw(InOut, f"{prefix}COARSE:CTRL")
|
|
84
|
+
self.fine = epics_signal_rw(InOut, f"{prefix}FINE:CTRL")
|
|
113
85
|
|
|
114
86
|
super().__init__(name)
|
|
115
87
|
|
|
@@ -126,5 +98,4 @@ class CryoStreamGantry(StandardReadable):
|
|
|
126
98
|
self.cryostream_selected = epics_signal_r(
|
|
127
99
|
int, f"{prefix}-MO-STEP-02:GPIO_INP_BITS.B3"
|
|
128
100
|
)
|
|
129
|
-
|
|
130
101
|
super().__init__(name)
|
|
@@ -10,14 +10,16 @@ def _get_detector_radius_mm(detector_params: DetectorParams):
|
|
|
10
10
|
return 0.5 * _get_detector_max_size_mm(detector_params)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def _get_detector_max_size_mm(detector_params):
|
|
13
|
+
def _get_detector_max_size_mm(detector_params: DetectorParams):
|
|
14
14
|
return max(
|
|
15
15
|
detector_params.detector_size_constants.det_dimension.width,
|
|
16
16
|
detector_params.detector_size_constants.det_dimension.height,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def _get_beam_xy_accounting_for_roi(
|
|
20
|
+
def _get_beam_xy_accounting_for_roi(
|
|
21
|
+
detector_params: DetectorParams, det_distance_mm: float
|
|
22
|
+
):
|
|
21
23
|
beam_x = detector_params.beam_xy_converter.get_beam_xy_from_det_dist(
|
|
22
24
|
det_distance_mm, Axis.X_AXIS
|
|
23
25
|
)
|
dodal/devices/eiger.py
CHANGED
|
@@ -64,14 +64,12 @@ class EigerDetector(Device, Stageable):
|
|
|
64
64
|
arming_status = Status()
|
|
65
65
|
arming_status.set_finished()
|
|
66
66
|
|
|
67
|
-
disarming_status = Status()
|
|
68
|
-
disarming_status.set_finished()
|
|
69
|
-
|
|
70
67
|
def __init__(self, beamline: str = "i03", *args, **kwargs):
|
|
71
68
|
super().__init__(*args, **kwargs)
|
|
72
69
|
self.beamline = beamline
|
|
73
70
|
# using i03 timeouts as default
|
|
74
71
|
self.timeouts = AVAILABLE_TIMEOUTS.get(beamline, AVAILABLE_TIMEOUTS["i03"])
|
|
72
|
+
self.disarming_status = None
|
|
75
73
|
|
|
76
74
|
@classmethod
|
|
77
75
|
def with_params(
|
|
@@ -106,6 +104,7 @@ class EigerDetector(Device, Stageable):
|
|
|
106
104
|
raise Exception("\n".join(errors))
|
|
107
105
|
|
|
108
106
|
def async_stage(self):
|
|
107
|
+
self.disarming_status = None
|
|
109
108
|
self.odin.nodes.clear_odin_errors()
|
|
110
109
|
status_ok, error_message = self.odin.wait_for_odin_initialised(
|
|
111
110
|
self.timeouts.general_status_timeout
|
|
@@ -170,22 +169,31 @@ class EigerDetector(Device, Stageable):
|
|
|
170
169
|
def stop(self, *args):
|
|
171
170
|
"""Emergency stop the device, mainly used to clean up after error."""
|
|
172
171
|
LOGGER.info("Eiger stop() called - cleaning up...")
|
|
173
|
-
if not self.disarming_status.done:
|
|
172
|
+
if self.disarming_status and not self.disarming_status.done:
|
|
174
173
|
LOGGER.info("Eiger still disarming, waiting on disarm")
|
|
175
174
|
self.disarming_status.wait(self.timeouts.arming_timeout)
|
|
175
|
+
elif not self.disarming_status:
|
|
176
|
+
self.disarming_status = Status()
|
|
177
|
+
try:
|
|
178
|
+
self.wait_on_arming_if_started()
|
|
179
|
+
stop_status = self.odin.stop()
|
|
180
|
+
self.odin.file_writer.start_timeout.set(1).wait(
|
|
181
|
+
self.timeouts.general_status_timeout
|
|
182
|
+
)
|
|
183
|
+
self.disarm_detector()
|
|
184
|
+
stop_status &= self.disable_roi_mode()
|
|
185
|
+
LOGGER.info("Waiting on stop status")
|
|
186
|
+
stop_status.wait(self.timeouts.general_status_timeout)
|
|
187
|
+
# See https://github.com/DiamondLightSource/hyperion/issues/1395
|
|
188
|
+
LOGGER.info("Turning off Eiger dev/shm streaming")
|
|
189
|
+
self.odin.fan.dev_shm_enable.set(0).wait(
|
|
190
|
+
self.timeouts.general_status_timeout
|
|
191
|
+
)
|
|
192
|
+
LOGGER.info("Eiger has successfully been stopped")
|
|
193
|
+
finally:
|
|
194
|
+
self.disarming_status.set_finished()
|
|
176
195
|
else:
|
|
177
|
-
|
|
178
|
-
stop_status = self.odin.stop()
|
|
179
|
-
self.odin.file_writer.start_timeout.set(1).wait(
|
|
180
|
-
self.timeouts.general_status_timeout
|
|
181
|
-
)
|
|
182
|
-
self.disarm_detector()
|
|
183
|
-
stop_status &= self.disable_roi_mode()
|
|
184
|
-
stop_status.wait(self.timeouts.general_status_timeout)
|
|
185
|
-
# See https://github.com/DiamondLightSource/hyperion/issues/1395
|
|
186
|
-
LOGGER.info("Turning off Eiger dev/shm streaming")
|
|
187
|
-
self.odin.fan.dev_shm_enable.set(0).wait()
|
|
188
|
-
LOGGER.info("Eiger has successfully been stopped")
|
|
196
|
+
LOGGER.info("Already disarmed, doing nothing")
|
|
189
197
|
|
|
190
198
|
def disable_roi_mode(self):
|
|
191
199
|
return self.change_roi_mode(False)
|
|
@@ -194,13 +202,13 @@ class EigerDetector(Device, Stageable):
|
|
|
194
202
|
return self.change_roi_mode(True)
|
|
195
203
|
|
|
196
204
|
def change_roi_mode(self, enable: bool) -> StatusBase:
|
|
205
|
+
LOGGER.info(f"Changing ROI mode to {enable}")
|
|
197
206
|
assert self.detector_params is not None
|
|
198
207
|
detector_dimensions = (
|
|
199
208
|
self.detector_params.detector_size_constants.roi_size_pixels
|
|
200
209
|
if enable
|
|
201
210
|
else self.detector_params.detector_size_constants.det_size_pixels
|
|
202
211
|
)
|
|
203
|
-
|
|
204
212
|
status = self.cam.roi_mode.set(
|
|
205
213
|
1 if enable else 0, timeout=self.timeouts.general_status_timeout
|
|
206
214
|
)
|
|
@@ -216,7 +224,6 @@ class EigerDetector(Device, Stageable):
|
|
|
216
224
|
status &= self.odin.file_writer.num_col_chunks.set(
|
|
217
225
|
detector_dimensions.width, timeout=self.timeouts.general_status_timeout
|
|
218
226
|
)
|
|
219
|
-
|
|
220
227
|
return status
|
|
221
228
|
|
|
222
229
|
def set_cam_pvs(self) -> AndStatus:
|
|
@@ -296,6 +303,7 @@ class EigerDetector(Device, Stageable):
|
|
|
296
303
|
self.detector_params.omega_increment,
|
|
297
304
|
timeout=self.timeouts.general_status_timeout,
|
|
298
305
|
)
|
|
306
|
+
|
|
299
307
|
return status
|
|
300
308
|
|
|
301
309
|
def set_detector_threshold(self, energy: float, tolerance: float = 0.1) -> Status:
|
dodal/devices/fast_grid_scan.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
|
+
from functools import partial
|
|
4
|
+
from math import isclose
|
|
3
5
|
from typing import Generic, TypeVar
|
|
4
6
|
|
|
5
7
|
import numpy as np
|
|
@@ -307,13 +309,23 @@ class FastGridScanCommon(
|
|
|
307
309
|
set_statuses = []
|
|
308
310
|
|
|
309
311
|
LOGGER.info("Applying gridscan parameters...")
|
|
312
|
+
|
|
310
313
|
# Create arguments for bps.mv
|
|
311
314
|
for key, signal in self._movable_params.items():
|
|
312
315
|
param_value = value.__dict__[key]
|
|
313
|
-
|
|
316
|
+
|
|
317
|
+
matcher = partial(isclose, param_value, abs_tol=0.001)
|
|
318
|
+
|
|
319
|
+
set_statuses.append(
|
|
320
|
+
set_and_wait_for_value(
|
|
321
|
+
signal, # type: ignore
|
|
322
|
+
param_value,
|
|
323
|
+
match_value=matcher,
|
|
324
|
+
)
|
|
325
|
+
)
|
|
314
326
|
|
|
315
327
|
# Counter should always start at 0
|
|
316
|
-
set_statuses.append(
|
|
328
|
+
set_statuses.append(set_and_wait_for_value(self.position_counter, 0))
|
|
317
329
|
|
|
318
330
|
LOGGER.info("Gridscan parameters applied, waiting for sets to complete...")
|
|
319
331
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from ophyd_async.core import Reference, derived_signal_r
|
|
2
|
+
|
|
3
|
+
from dodal.devices.aperturescatterguard import ApertureScatterguard
|
|
4
|
+
from dodal.devices.beamsize.beamsize import BeamsizeBase
|
|
5
|
+
from dodal.devices.i03.constants import BeamsizeConstants
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Beamsize(BeamsizeBase):
|
|
9
|
+
def __init__(self, aperture_scatterguard: ApertureScatterguard, name=""):
|
|
10
|
+
super().__init__(name=name)
|
|
11
|
+
self._aperture_scatterguard_ref = Reference(aperture_scatterguard)
|
|
12
|
+
|
|
13
|
+
with self.add_children_as_readables():
|
|
14
|
+
self.x_um = derived_signal_r(
|
|
15
|
+
self._get_beamsize_x,
|
|
16
|
+
aperture_radius=self._aperture_scatterguard_ref().radius,
|
|
17
|
+
derived_units="µm",
|
|
18
|
+
)
|
|
19
|
+
self.y_um = derived_signal_r(
|
|
20
|
+
self._get_beamsize_y,
|
|
21
|
+
aperture_radius=self._aperture_scatterguard_ref().radius,
|
|
22
|
+
derived_units="µm",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def _get_beamsize_x(
|
|
26
|
+
self,
|
|
27
|
+
aperture_radius: float,
|
|
28
|
+
) -> float:
|
|
29
|
+
return min(aperture_radius, BeamsizeConstants.BEAM_WIDTH_UM)
|
|
30
|
+
|
|
31
|
+
def _get_beamsize_y(
|
|
32
|
+
self,
|
|
33
|
+
aperture_radius: float,
|
|
34
|
+
) -> float:
|
|
35
|
+
return min(aperture_radius, BeamsizeConstants.BEAM_HEIGHT_UM)
|
|
@@ -5,7 +5,7 @@ from ophyd_async.core import AsyncStatus, Reference, StandardReadable
|
|
|
5
5
|
|
|
6
6
|
from dodal.common.beamlines.beamline_parameters import get_beamline_parameters
|
|
7
7
|
from dodal.devices.i03.dcm import DCM
|
|
8
|
-
from dodal.devices.undulator import
|
|
8
|
+
from dodal.devices.undulator import UndulatorInKeV
|
|
9
9
|
from dodal.log import LOGGER
|
|
10
10
|
|
|
11
11
|
ENERGY_TIMEOUT_S: float = 30.0
|
|
@@ -31,7 +31,7 @@ class UndulatorDCM(StandardReadable, Movable[float]):
|
|
|
31
31
|
|
|
32
32
|
def __init__(
|
|
33
33
|
self,
|
|
34
|
-
undulator:
|
|
34
|
+
undulator: UndulatorInKeV,
|
|
35
35
|
dcm: DCM,
|
|
36
36
|
daq_configuration_path: str,
|
|
37
37
|
name: str = "",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from ophyd_async.core import Reference, derived_signal_r
|
|
2
|
+
|
|
3
|
+
from dodal.devices.aperturescatterguard import ApertureScatterguard
|
|
4
|
+
from dodal.devices.beamsize.beamsize import BeamsizeBase
|
|
5
|
+
from dodal.devices.i04.transfocator import Transfocator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Beamsize(BeamsizeBase):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
transfocator: Transfocator,
|
|
12
|
+
aperture_scatterguard: ApertureScatterguard,
|
|
13
|
+
name="",
|
|
14
|
+
):
|
|
15
|
+
super().__init__(name=name)
|
|
16
|
+
self._transfocator_ref = Reference(transfocator)
|
|
17
|
+
self._aperture_scatterguard_ref = Reference(aperture_scatterguard)
|
|
18
|
+
|
|
19
|
+
with self.add_children_as_readables():
|
|
20
|
+
self.x_um = derived_signal_r(
|
|
21
|
+
self._get_beamsize_x,
|
|
22
|
+
transfocator_size_x=self._transfocator_ref().current_horizontal_size_rbv,
|
|
23
|
+
aperture_radius=self._aperture_scatterguard_ref().radius,
|
|
24
|
+
derived_units="µm",
|
|
25
|
+
)
|
|
26
|
+
self.y_um = derived_signal_r(
|
|
27
|
+
self._get_beamsize_y,
|
|
28
|
+
transfocator_size_y=self._transfocator_ref().current_vertical_size_rbv,
|
|
29
|
+
aperture_radius=self._aperture_scatterguard_ref().radius,
|
|
30
|
+
derived_units="µm",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def _get_beamsize_x(
|
|
34
|
+
self,
|
|
35
|
+
transfocator_size_x: float,
|
|
36
|
+
aperture_radius: float,
|
|
37
|
+
) -> float:
|
|
38
|
+
return min(transfocator_size_x, aperture_radius)
|
|
39
|
+
|
|
40
|
+
def _get_beamsize_y(
|
|
41
|
+
self,
|
|
42
|
+
transfocator_size_y: float,
|
|
43
|
+
aperture_radius: float,
|
|
44
|
+
) -> float:
|
|
45
|
+
return min(transfocator_size_y, aperture_radius)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
from bluesky.protocols import Triggerable
|
|
4
|
+
from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_r_and_setter
|
|
5
|
+
from ophyd_async.epics.core import (
|
|
6
|
+
epics_signal_r,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
# kernal size describes how many of the neigbouring pixels are used for the blur,
|
|
10
|
+
# higher kernal size means more of a blur effect
|
|
11
|
+
KERNAL_SIZE = (7, 7)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MaxPixel(StandardReadable, Triggerable):
|
|
15
|
+
"""Gets the max pixel (brightest pixel) from the image after some image processing."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
18
|
+
self.array_data = epics_signal_r(np.ndarray, f"pva://{prefix}PVA:ARRAY")
|
|
19
|
+
self.max_pixel_val, self._max_val_setter = soft_signal_r_and_setter(float)
|
|
20
|
+
super().__init__(name)
|
|
21
|
+
|
|
22
|
+
async def _convert_to_gray_and_blur(self):
|
|
23
|
+
"""
|
|
24
|
+
Preprocess the image array data (convert to grayscale and apply a gaussian blur)
|
|
25
|
+
Image is converted to grayscale (using a weighted mean as green contributes more to brightness)
|
|
26
|
+
as we aren't interested in data relating to colour. A blur is then applied to mitigate
|
|
27
|
+
errors due to rogue hot pixels.
|
|
28
|
+
"""
|
|
29
|
+
data = await self.array_data.get_value()
|
|
30
|
+
gray_arr = cv2.cvtColor(data, cv2.COLOR_BGR2GRAY)
|
|
31
|
+
return cv2.GaussianBlur(gray_arr, KERNAL_SIZE, 0)
|
|
32
|
+
|
|
33
|
+
@AsyncStatus.wrap
|
|
34
|
+
async def trigger(self):
|
|
35
|
+
arr = await self._convert_to_gray_and_blur()
|
|
36
|
+
max_val = float(np.max(arr)) # np.int64
|
|
37
|
+
assert isinstance(max_val, float)
|
|
38
|
+
self._max_val_setter(max_val)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import pickle
|
|
3
|
+
import time
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from enum import Enum
|
|
5
6
|
from typing import TypedDict
|
|
@@ -21,6 +22,7 @@ from dodal.devices.oav.oav_calculations import (
|
|
|
21
22
|
from dodal.log import LOGGER
|
|
22
23
|
|
|
23
24
|
NO_MURKO_RESULT = (-1, -1)
|
|
25
|
+
RESULTS_COMPLETE_MESSAGE = "murko_results_complete"
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
class MurkoMetadata(TypedDict):
|
|
@@ -71,7 +73,8 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
71
73
|
solutions for y and z can be calculated using numpy's linear algebra library.
|
|
72
74
|
"""
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
GET_MESSAGE_TIMEOUT_S = 2
|
|
77
|
+
RESULTS_COMPLETE_TIMEOUT_S = 5
|
|
75
78
|
PERCENTAGE_TO_USE = 25
|
|
76
79
|
LEFTMOST_PIXEL_TO_USE = 10
|
|
77
80
|
NUMBER_OF_WRONG_RESULTS_TO_LOG = 5
|
|
@@ -82,7 +85,6 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
82
85
|
redis_password=RedisConstants.REDIS_PASSWORD,
|
|
83
86
|
redis_db=RedisConstants.MURKO_REDIS_DB,
|
|
84
87
|
name="",
|
|
85
|
-
stop_angle=350,
|
|
86
88
|
):
|
|
87
89
|
self.redis_client = StrictRedis(
|
|
88
90
|
host=redis_host,
|
|
@@ -91,7 +93,6 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
91
93
|
)
|
|
92
94
|
self.pubsub = self.redis_client.pubsub()
|
|
93
95
|
self.sample_id = soft_signal_rw(str) # Should get from redis
|
|
94
|
-
self.stop_angle = stop_angle
|
|
95
96
|
|
|
96
97
|
self._reset()
|
|
97
98
|
|
|
@@ -103,7 +104,7 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
103
104
|
super().__init__(name=name)
|
|
104
105
|
|
|
105
106
|
def _reset(self):
|
|
106
|
-
self._last_omega =
|
|
107
|
+
self._last_omega = None
|
|
107
108
|
self._results: list[MurkoResult] = []
|
|
108
109
|
|
|
109
110
|
@AsyncStatus.wrap
|
|
@@ -120,14 +121,27 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
120
121
|
|
|
121
122
|
@AsyncStatus.wrap
|
|
122
123
|
async def trigger(self):
|
|
123
|
-
# Wait for results
|
|
124
124
|
sample_id = await self.sample_id.get_value()
|
|
125
|
-
|
|
125
|
+
t_last_result = time.time()
|
|
126
|
+
while True:
|
|
127
|
+
if time.time() - t_last_result > self.RESULTS_COMPLETE_TIMEOUT_S:
|
|
128
|
+
LOGGER.warning(
|
|
129
|
+
f"Time since last result > {self.RESULTS_COMPLETE_TIMEOUT_S}, expected to receive {RESULTS_COMPLETE_MESSAGE}"
|
|
130
|
+
)
|
|
131
|
+
break
|
|
126
132
|
# waits here for next batch to be received
|
|
127
|
-
message = await self.pubsub.get_message(timeout=self.
|
|
128
|
-
if message
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
message = await self.pubsub.get_message(timeout=self.GET_MESSAGE_TIMEOUT_S)
|
|
134
|
+
if message and message["type"] == "message":
|
|
135
|
+
t_last_result = time.time()
|
|
136
|
+
data = pickle.loads(message["data"])
|
|
137
|
+
|
|
138
|
+
if data == RESULTS_COMPLETE_MESSAGE:
|
|
139
|
+
LOGGER.info(
|
|
140
|
+
f"Received results complete message: {RESULTS_COMPLETE_MESSAGE}"
|
|
141
|
+
)
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
await self.process_batch(data, sample_id)
|
|
131
145
|
|
|
132
146
|
if not self._results:
|
|
133
147
|
raise NoResultsFoundError("No results retrieved from Murko")
|
|
@@ -155,22 +169,18 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
155
169
|
f"murko:{sample_id}:metadata", result.uuid, json.dumps(result.metadata)
|
|
156
170
|
)
|
|
157
171
|
|
|
158
|
-
async def process_batch(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
result, MurkoMetadata(json.loads(metadata_str))
|
|
171
|
-
)
|
|
172
|
-
else:
|
|
173
|
-
LOGGER.info(f"Found no metadata for uuid {uuid}")
|
|
172
|
+
async def process_batch(
|
|
173
|
+
self, batch_results: list[tuple[str, dict]], sample_id: str
|
|
174
|
+
):
|
|
175
|
+
for result_with_uuid in batch_results:
|
|
176
|
+
uuid, result = result_with_uuid
|
|
177
|
+
if metadata_str := await self.redis_client.hget( # type: ignore
|
|
178
|
+
f"murko:{sample_id}:metadata", uuid
|
|
179
|
+
):
|
|
180
|
+
LOGGER.info(f"Found metadata for uuid {uuid}, processing result")
|
|
181
|
+
self.process_result(result, MurkoMetadata(json.loads(metadata_str)))
|
|
182
|
+
else:
|
|
183
|
+
LOGGER.info(f"Found no metadata for uuid {uuid}")
|
|
174
184
|
|
|
175
185
|
def process_result(self, result: dict, metadata: MurkoMetadata):
|
|
176
186
|
"""Uses the 'most_likely_click' coordinates from Murko to calculate the
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import math
|
|
3
2
|
|
|
4
3
|
from ophyd_async.core import (
|
|
5
4
|
AsyncStatus,
|
|
@@ -9,6 +8,7 @@ from ophyd_async.core import (
|
|
|
9
8
|
)
|
|
10
9
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
11
10
|
|
|
11
|
+
from dodal.common.device_utils import periodic_reminder
|
|
12
12
|
from dodal.log import LOGGER
|
|
13
13
|
|
|
14
14
|
|
|
@@ -23,15 +23,15 @@ class Transfocator(StandardReadable):
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
def __init__(self, prefix: str, name: str = ""):
|
|
26
|
+
self._vert_size_calc_sp = epics_signal_rw(float, prefix + "VERT_REQ")
|
|
27
|
+
self._num_lenses_calc_rbv = epics_signal_r(float, prefix + "LENS_PRED")
|
|
28
|
+
self.start = epics_signal_rw(int, prefix + "START.PROC")
|
|
29
|
+
self.start_rbv = epics_signal_r(int, prefix + "START_RBV")
|
|
30
|
+
|
|
26
31
|
with self.add_children_as_readables():
|
|
27
|
-
self.beamsize_set_microns = epics_signal_rw(float, prefix + "VERT_REQ")
|
|
28
|
-
self.predicted_vertical_num_lenses = epics_signal_rw(
|
|
29
|
-
float, prefix + "LENS_PRED"
|
|
30
|
-
)
|
|
31
32
|
self.number_filters_sp = epics_signal_rw(int, prefix + "NUM_FILTERS")
|
|
32
|
-
self.
|
|
33
|
-
self.
|
|
34
|
-
self.vertical_lens_rbv = epics_signal_r(float, prefix + "VER")
|
|
33
|
+
self.current_horizontal_size_rbv = epics_signal_r(float, prefix + "HOR")
|
|
34
|
+
self.current_vertical_size_rbv = epics_signal_r(float, prefix + "VER")
|
|
35
35
|
|
|
36
36
|
self.TIMEOUT = 120
|
|
37
37
|
|
|
@@ -41,14 +41,10 @@ class Transfocator(StandardReadable):
|
|
|
41
41
|
# We can only put an integer number of lenses in the beam but the
|
|
42
42
|
# calculation in the IOC returns the theoretical float number of lenses
|
|
43
43
|
value = round(value)
|
|
44
|
-
LOGGER.info(f"Transfocator setting {value} filters")
|
|
45
44
|
await self.number_filters_sp.set(value)
|
|
46
45
|
await self.start.set(1)
|
|
47
|
-
LOGGER.info("Waiting for start_rbv to change to 1")
|
|
48
46
|
await wait_for_value(self.start_rbv, 1, self.TIMEOUT)
|
|
49
|
-
LOGGER.info("Waiting for start_rbv to change to 0")
|
|
50
47
|
await wait_for_value(self.start_rbv, 0, self.TIMEOUT)
|
|
51
|
-
self.latest_pred_vertical_num_lenses = value
|
|
52
48
|
|
|
53
49
|
@AsyncStatus.wrap
|
|
54
50
|
async def set(self, value: float):
|
|
@@ -59,29 +55,27 @@ class Transfocator(StandardReadable):
|
|
|
59
55
|
4. Start the device moving
|
|
60
56
|
5. Wait for the start_rbv goes high and low again
|
|
61
57
|
"""
|
|
62
|
-
self.latest_pred_vertical_num_lenses = (
|
|
63
|
-
await self.predicted_vertical_num_lenses.get_value()
|
|
64
|
-
)
|
|
65
|
-
|
|
66
58
|
LOGGER.info(f"Transfocator setting {value} beamsize")
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
# Logic in the IOC calculates _num_lenses_calc_rbv when _vert_size_calc_sp changes
|
|
61
|
+
|
|
62
|
+
# Register an observer before setting _vert_size_calc_sp to ensure we don't miss changes
|
|
63
|
+
num_lenses_calc_iterator = observe_value(
|
|
64
|
+
self._num_lenses_calc_rbv, timeout=self.TIMEOUT
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
await anext(num_lenses_calc_iterator)
|
|
68
|
+
await self._vert_size_calc_sp.set(value)
|
|
69
|
+
calc_lenses = await anext(num_lenses_calc_iterator)
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
# Keep initial prediction before setting to later compare with change after setting
|
|
76
|
-
current_prediction = await anext(predicted_vertical_num_lenses_iterator)
|
|
77
|
-
await self.beamsize_set_microns.set(value)
|
|
78
|
-
accepted_prediction = await anext(predicted_vertical_num_lenses_iterator)
|
|
79
|
-
if not math.isclose(current_prediction, accepted_prediction, abs_tol=1e-8):
|
|
80
|
-
await self.set_based_on_prediction(accepted_prediction)
|
|
71
|
+
async with periodic_reminder(
|
|
72
|
+
f"Waiting for transfocator to insert {calc_lenses} into beam"
|
|
73
|
+
):
|
|
74
|
+
await self.set_based_on_prediction(calc_lenses)
|
|
81
75
|
|
|
82
76
|
number_filters_rbv, vertical_lens_size_rbv = await asyncio.gather(
|
|
83
77
|
self.number_filters_sp.get_value(),
|
|
84
|
-
self.
|
|
78
|
+
self.current_vertical_size_rbv.get_value(),
|
|
85
79
|
)
|
|
86
80
|
|
|
87
81
|
LOGGER.info(
|