dls-dodal 1.67.0__py3-none-any.whl → 1.69.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.67.0.dist-info → dls_dodal-1.69.0.dist-info}/METADATA +2 -32
- {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/RECORD +79 -71
- dodal/_version.py +2 -2
- dodal/beamlines/adsim.py +30 -23
- dodal/beamlines/b07.py +1 -1
- dodal/beamlines/b07_1.py +1 -1
- dodal/beamlines/i02_1.py +14 -42
- dodal/beamlines/i02_2.py +5 -11
- dodal/beamlines/i03.py +4 -1
- dodal/beamlines/i03_supervisor.py +19 -0
- dodal/beamlines/i04.py +74 -179
- dodal/beamlines/i05.py +9 -1
- dodal/beamlines/i06.py +1 -1
- dodal/beamlines/i06_1.py +24 -0
- dodal/beamlines/i09.py +53 -9
- dodal/beamlines/i09_1.py +9 -1
- dodal/beamlines/i09_2.py +7 -6
- dodal/beamlines/i10_optics.py +1 -1
- dodal/beamlines/i16.py +34 -0
- dodal/beamlines/i17.py +1 -1
- dodal/beamlines/i20_1.py +14 -0
- dodal/beamlines/i21.py +71 -4
- dodal/beamlines/i23.py +19 -25
- dodal/beamlines/i24.py +55 -105
- dodal/beamlines/p60.py +12 -2
- dodal/common/__init__.py +2 -1
- dodal/common/maths.py +80 -0
- dodal/devices/eiger.py +44 -23
- dodal/devices/electron_analyser/__init__.py +0 -33
- dodal/devices/electron_analyser/base/__init__.py +58 -0
- dodal/devices/electron_analyser/base/base_controller.py +84 -0
- dodal/devices/electron_analyser/base/base_detector.py +214 -0
- dodal/devices/electron_analyser/{abstract → base}/base_driver_io.py +23 -42
- dodal/devices/electron_analyser/{enums.py → base/base_enums.py} +0 -5
- dodal/devices/electron_analyser/{abstract → base}/base_region.py +48 -11
- dodal/devices/electron_analyser/{util.py → base/base_util.py} +1 -1
- dodal/devices/electron_analyser/{energy_sources.py → base/energy_sources.py} +27 -26
- dodal/devices/electron_analyser/specs/__init__.py +4 -4
- dodal/devices/electron_analyser/specs/specs_detector.py +47 -0
- dodal/devices/electron_analyser/specs/{driver_io.py → specs_driver_io.py} +23 -26
- dodal/devices/electron_analyser/specs/{region.py → specs_region.py} +4 -3
- dodal/devices/electron_analyser/vgscienta/__init__.py +4 -4
- dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +53 -0
- dodal/devices/electron_analyser/vgscienta/{driver_io.py → vgscienta_driver_io.py} +25 -31
- dodal/devices/electron_analyser/vgscienta/{region.py → vgscienta_region.py} +6 -6
- dodal/devices/fast_shutter.py +108 -25
- dodal/devices/i04/beam_centre.py +84 -0
- dodal/devices/i04/max_pixel.py +4 -17
- dodal/devices/i04/murko_results.py +18 -3
- dodal/devices/i09_2_shared/i09_apple2.py +0 -72
- dodal/devices/i10/i10_apple2.py +7 -7
- dodal/devices/i17/i17_apple2.py +6 -6
- dodal/devices/i21/__init__.py +3 -1
- dodal/devices/i24/commissioning_jungfrau.py +9 -10
- dodal/devices/insertion_device/__init__.py +62 -0
- dodal/devices/insertion_device/apple2_controller.py +380 -0
- dodal/devices/insertion_device/apple2_undulator.py +152 -481
- dodal/devices/insertion_device/energy.py +88 -0
- dodal/devices/insertion_device/energy_motor_lookup.py +1 -1
- dodal/devices/insertion_device/enum.py +17 -0
- dodal/devices/insertion_device/lookup_table_models.py +66 -36
- dodal/devices/insertion_device/polarisation.py +36 -0
- dodal/devices/oav/oav_detector.py +66 -1
- dodal/devices/oav/utils.py +17 -0
- dodal/devices/robot.py +35 -18
- dodal/devices/selectable_source.py +38 -0
- dodal/devices/zebra/zebra.py +15 -0
- dodal/devices/zebra/zebra_constants_mapping.py +1 -0
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +0 -1
- dodal/testing/__init__.py +0 -0
- dodal/testing/electron_analyser/device_factory.py +4 -4
- dodal/testing/fixtures/devices/apple2.py +1 -1
- dodal/testing/fixtures/run_engine.py +4 -0
- dodal/devices/electron_analyser/abstract/__init__.py +0 -25
- dodal/devices/electron_analyser/abstract/base_detector.py +0 -63
- dodal/devices/electron_analyser/abstract/types.py +0 -12
- dodal/devices/electron_analyser/detector.py +0 -143
- dodal/devices/electron_analyser/specs/detector.py +0 -34
- dodal/devices/electron_analyser/types.py +0 -57
- dodal/devices/electron_analyser/vgscienta/detector.py +0 -48
- {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/electron_analyser/specs/{enums.py → specs_enums.py} +0 -0
- /dodal/devices/electron_analyser/vgscienta/{enums.py → vgscienta_enums.py} +0 -0
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import uuid
|
|
2
|
-
from typing import Generic
|
|
2
|
+
from typing import Generic, TypeVar
|
|
3
3
|
|
|
4
|
+
from ophyd_async.core import StrictEnum
|
|
4
5
|
from pydantic import Field, field_validator
|
|
5
6
|
|
|
6
|
-
from dodal.devices.electron_analyser.
|
|
7
|
+
from dodal.devices.electron_analyser.base.base_region import (
|
|
7
8
|
AbstractBaseRegion,
|
|
8
9
|
AbstractBaseSequence,
|
|
9
|
-
)
|
|
10
|
-
from dodal.devices.electron_analyser.abstract.types import (
|
|
11
10
|
TLensMode,
|
|
12
|
-
TPassEnergyEnum,
|
|
13
11
|
TPsuMode,
|
|
14
12
|
)
|
|
15
|
-
from dodal.devices.electron_analyser.vgscienta.
|
|
13
|
+
from dodal.devices.electron_analyser.vgscienta.vgscienta_enums import (
|
|
16
14
|
AcquisitionMode,
|
|
17
15
|
DetectorMode,
|
|
18
16
|
)
|
|
19
17
|
|
|
18
|
+
TPassEnergyEnum = TypeVar("TPassEnergyEnum", bound=StrictEnum)
|
|
19
|
+
|
|
20
20
|
|
|
21
21
|
class VGScientaRegion(
|
|
22
22
|
AbstractBaseRegion[AcquisitionMode, TLensMode, TPassEnergyEnum],
|
dodal/devices/fast_shutter.py
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
from typing import TypeVar
|
|
1
|
+
from typing import Generic, Protocol, TypeVar
|
|
2
2
|
|
|
3
3
|
from bluesky.protocols import Movable
|
|
4
4
|
from ophyd_async.core import (
|
|
5
5
|
AsyncStatus,
|
|
6
6
|
EnumTypes,
|
|
7
|
+
Reference,
|
|
8
|
+
SignalRW,
|
|
7
9
|
StandardReadable,
|
|
10
|
+
StandardReadableFormat,
|
|
11
|
+
derived_signal_rw,
|
|
12
|
+
soft_signal_r_and_setter,
|
|
8
13
|
)
|
|
9
14
|
from ophyd_async.epics.core import epics_signal_rw
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
from dodal.devices.selectable_source import SelectedSource, get_obj_from_selected_source
|
|
12
17
|
|
|
18
|
+
EnumTypesT = TypeVar("EnumTypesT", bound=EnumTypes)
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
|
|
21
|
+
class FastShutter(Movable[EnumTypesT], Protocol, Generic[EnumTypesT]):
|
|
15
22
|
"""
|
|
16
|
-
|
|
23
|
+
Enum device specialised for a fast shutter with configured open_state and
|
|
17
24
|
close_state so it is generic enough to be used with any device or plan without
|
|
18
25
|
knowing the specific enum to use.
|
|
19
26
|
|
|
@@ -25,11 +32,28 @@ class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
|
|
|
25
32
|
run_engine(bps.mv(shutter, shutter.close_state))
|
|
26
33
|
"""
|
|
27
34
|
|
|
35
|
+
open_state: EnumTypesT
|
|
36
|
+
close_state: EnumTypesT
|
|
37
|
+
shutter_state: SignalRW[EnumTypesT]
|
|
38
|
+
|
|
39
|
+
@AsyncStatus.wrap
|
|
40
|
+
async def set(self, state: EnumTypesT):
|
|
41
|
+
await self.shutter_state.set(state)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GenericFastShutter(
|
|
45
|
+
StandardReadable, FastShutter[EnumTypesT], Generic[EnumTypesT]
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Implementation of fast shutter that connects to an epics pv. This pv is an enum that
|
|
49
|
+
controls the open and close state of the shutter.
|
|
50
|
+
"""
|
|
51
|
+
|
|
28
52
|
def __init__(
|
|
29
53
|
self,
|
|
30
54
|
pv: str,
|
|
31
|
-
open_state:
|
|
32
|
-
close_state:
|
|
55
|
+
open_state: EnumTypesT,
|
|
56
|
+
close_state: EnumTypesT,
|
|
33
57
|
name: str = "",
|
|
34
58
|
):
|
|
35
59
|
"""
|
|
@@ -41,29 +65,88 @@ class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
|
|
|
41
65
|
self.open_state = open_state
|
|
42
66
|
self.close_state = close_state
|
|
43
67
|
with self.add_children_as_readables():
|
|
44
|
-
self.
|
|
68
|
+
self.shutter_state = epics_signal_rw(type(self.open_state), pv)
|
|
45
69
|
super().__init__(name)
|
|
46
70
|
|
|
47
|
-
@AsyncStatus.wrap
|
|
48
|
-
async def set(self, value: StrictEnumT) -> None:
|
|
49
|
-
await self.state.set(value)
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
class DualFastShutter(StandardReadable, FastShutter[EnumTypesT], Generic[EnumTypesT]):
|
|
73
|
+
"""
|
|
74
|
+
A fast shutter device that handles the positions of two other fast shutters. The
|
|
75
|
+
"active" shutter is the one that corrosponds to the selected_shutter signal. For
|
|
76
|
+
example, active shutter is shutter1 if selected_source is at SelectedSource.SOURCE1
|
|
77
|
+
and vise versa for shutter2 and SelectedSource.SOURCE2. Whenever a move is done on
|
|
78
|
+
this device, the inactive shutter is always set to the close_state.
|
|
79
|
+
"""
|
|
55
80
|
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
shutter1: GenericFastShutter[EnumTypesT],
|
|
84
|
+
shutter2: GenericFastShutter[EnumTypesT],
|
|
85
|
+
selected_source: SignalRW[SelectedSource],
|
|
86
|
+
name: str = "",
|
|
87
|
+
):
|
|
58
88
|
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
Arguments:
|
|
90
|
+
shutter1: Active shutter that corrosponds to SelectedSource.SOURCE1.
|
|
91
|
+
shutter2: Active shutter that corrosponds to SelectedSource.SOURCE2.
|
|
92
|
+
selected_source: Signal that decides the active shutter.
|
|
93
|
+
name: Name of this device.
|
|
62
94
|
"""
|
|
63
|
-
|
|
64
|
-
|
|
95
|
+
self._validate_shutter_states(shutter1.open_state, shutter2.open_state)
|
|
96
|
+
self._validate_shutter_states(shutter1.close_state, shutter2.close_state)
|
|
97
|
+
self.open_state = shutter1.open_state
|
|
98
|
+
self.close_state = shutter1.close_state
|
|
65
99
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
self._shutter1_ref = Reference(shutter1)
|
|
101
|
+
self._shutter2_ref = Reference(shutter2)
|
|
102
|
+
self._selected_shutter_ref = Reference(selected_source)
|
|
103
|
+
|
|
104
|
+
with self.add_children_as_readables():
|
|
105
|
+
self.shutter_state = derived_signal_rw(
|
|
106
|
+
self._read_shutter_state,
|
|
107
|
+
self._set_shutter_state,
|
|
108
|
+
selected_shutter=selected_source,
|
|
109
|
+
shutter1=shutter1.shutter_state,
|
|
110
|
+
shutter2=shutter2.shutter_state,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
114
|
+
self.shutter1_device_name, _ = soft_signal_r_and_setter(
|
|
115
|
+
str, initial_value=shutter1.name
|
|
116
|
+
)
|
|
117
|
+
self.shutter2_device_name, _ = soft_signal_r_and_setter(
|
|
118
|
+
str, initial_value=shutter2.name
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self.add_readables([shutter1, shutter2, selected_source])
|
|
122
|
+
|
|
123
|
+
super().__init__(name)
|
|
124
|
+
|
|
125
|
+
def _validate_shutter_states(self, state1: EnumTypesT, state2: EnumTypesT) -> None:
|
|
126
|
+
if state1 is not state2:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"{state1} is not same value as {state2}. They must be the same to be compatible."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _read_shutter_state(
|
|
132
|
+
self,
|
|
133
|
+
selected_shutter: SelectedSource,
|
|
134
|
+
shutter1: EnumTypesT,
|
|
135
|
+
shutter2: EnumTypesT,
|
|
136
|
+
) -> EnumTypesT:
|
|
137
|
+
return get_obj_from_selected_source(selected_shutter, shutter1, shutter2)
|
|
138
|
+
|
|
139
|
+
async def _set_shutter_state(self, value: EnumTypesT):
|
|
140
|
+
selected_shutter = await self._selected_shutter_ref().get_value()
|
|
141
|
+
active_shutter = get_obj_from_selected_source(
|
|
142
|
+
selected_shutter,
|
|
143
|
+
self._shutter1_ref(),
|
|
144
|
+
self._shutter2_ref(),
|
|
145
|
+
)
|
|
146
|
+
inactive_shutter = get_obj_from_selected_source(
|
|
147
|
+
selected_shutter,
|
|
148
|
+
self._shutter2_ref(),
|
|
149
|
+
self._shutter1_ref(),
|
|
150
|
+
)
|
|
151
|
+
await inactive_shutter.set(inactive_shutter.close_state)
|
|
152
|
+
await active_shutter.set(value)
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
from dodal.devices.oav.utils import convert_to_gray_and_blur
|
|
10
|
+
from dodal.log import LOGGER
|
|
11
|
+
|
|
12
|
+
# Constant was chosen from trial and error with test images
|
|
13
|
+
ADDITIONAL_BINARY_THRESH = 20
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def convert_image_to_binary(image: np.ndarray):
|
|
17
|
+
"""
|
|
18
|
+
Creates a binary image from OAV image array data.
|
|
19
|
+
|
|
20
|
+
Pixels of the input image are converted to one of two values (a high and a low value).
|
|
21
|
+
Otsu's method is used for automatic thresholding.
|
|
22
|
+
See https://docs.opencv.org/4.x/d7/d4d/tutorial_py_thresholding.html.
|
|
23
|
+
The threshold is increased by ADDITIONAL_BINARY_THRESH in order to get more of
|
|
24
|
+
the centre of the beam.
|
|
25
|
+
"""
|
|
26
|
+
max_pixel_value = 255
|
|
27
|
+
|
|
28
|
+
blurred_image = convert_to_gray_and_blur(image)
|
|
29
|
+
|
|
30
|
+
threshold_value, _ = cv2.threshold(
|
|
31
|
+
blurred_image, 0, max_pixel_value, cv2.THRESH_BINARY + cv2.THRESH_OTSU
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Adjusting because the inner beam is less noisy compared to the outer
|
|
35
|
+
threshold_value += ADDITIONAL_BINARY_THRESH
|
|
36
|
+
|
|
37
|
+
_, thresholded_image = cv2.threshold(
|
|
38
|
+
blurred_image, threshold_value, max_pixel_value, cv2.THRESH_BINARY
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
LOGGER.info(f"Image binarised with threshold of {threshold_value}")
|
|
42
|
+
return thresholded_image
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CentreEllipseMethod(StandardReadable, Triggerable):
|
|
46
|
+
"""
|
|
47
|
+
Upon triggering, fits an ellipse to a binary image from the area detector defined by
|
|
48
|
+
the prefix.
|
|
49
|
+
|
|
50
|
+
This is used, in conjunction with a scintillator, to determine the centre of the beam
|
|
51
|
+
on the image.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
55
|
+
self.oav_array_signal = epics_signal_r(np.ndarray, f"pva://{prefix}PVA:ARRAY")
|
|
56
|
+
|
|
57
|
+
self.center_x_val, self._center_x_val_setter = soft_signal_r_and_setter(float)
|
|
58
|
+
self.center_y_val, self._center_y_val_setter = soft_signal_r_and_setter(float)
|
|
59
|
+
super().__init__(name)
|
|
60
|
+
|
|
61
|
+
def _fit_ellipse(self, binary_img: cv2.typing.MatLike) -> cv2.typing.RotatedRect:
|
|
62
|
+
contours, _ = cv2.findContours(
|
|
63
|
+
binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE
|
|
64
|
+
)
|
|
65
|
+
if not contours:
|
|
66
|
+
raise ValueError("No contours found in image.")
|
|
67
|
+
|
|
68
|
+
largest_contour = max(contours, key=cv2.contourArea)
|
|
69
|
+
if len(largest_contour) < 5:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Not enough points to fit an ellipse. Found {largest_contour} points and need at least 5."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return cv2.fitEllipse(largest_contour)
|
|
75
|
+
|
|
76
|
+
@AsyncStatus.wrap
|
|
77
|
+
async def trigger(self):
|
|
78
|
+
array_data = await self.oav_array_signal.get_value()
|
|
79
|
+
binary = convert_image_to_binary(array_data)
|
|
80
|
+
ellipse_fit = self._fit_ellipse(binary)
|
|
81
|
+
centre_x = ellipse_fit[0][0]
|
|
82
|
+
centre_y = ellipse_fit[0][1]
|
|
83
|
+
self._center_x_val_setter(centre_x)
|
|
84
|
+
self._center_y_val_setter(centre_y)
|
dodal/devices/i04/max_pixel.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import cv2
|
|
2
1
|
import numpy as np
|
|
3
2
|
from bluesky.protocols import Triggerable
|
|
4
3
|
from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_r_and_setter
|
|
@@ -6,9 +5,7 @@ from ophyd_async.epics.core import (
|
|
|
6
5
|
epics_signal_r,
|
|
7
6
|
)
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
# higher kernal size means more of a blur effect
|
|
11
|
-
KERNAL_SIZE = (7, 7)
|
|
8
|
+
from dodal.devices.oav.utils import convert_to_gray_and_blur
|
|
12
9
|
|
|
13
10
|
|
|
14
11
|
class MaxPixel(StandardReadable, Triggerable):
|
|
@@ -19,20 +16,10 @@ class MaxPixel(StandardReadable, Triggerable):
|
|
|
19
16
|
self.max_pixel_val, self._max_val_setter = soft_signal_r_and_setter(float)
|
|
20
17
|
super().__init__(name)
|
|
21
18
|
|
|
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
19
|
@AsyncStatus.wrap
|
|
34
20
|
async def trigger(self):
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
img_data = await self.array_data.get_value()
|
|
22
|
+
arr = convert_to_gray_and_blur(img_data)
|
|
23
|
+
max_val = float(np.max(arr))
|
|
37
24
|
assert isinstance(max_val, float)
|
|
38
25
|
self._max_val_setter(max_val)
|
|
@@ -13,7 +13,7 @@ from ophyd_async.core import (
|
|
|
13
13
|
soft_signal_r_and_setter,
|
|
14
14
|
soft_signal_rw,
|
|
15
15
|
)
|
|
16
|
-
from redis.asyncio import StrictRedis
|
|
16
|
+
from redis.asyncio import ConnectionError, StrictRedis
|
|
17
17
|
|
|
18
18
|
from dodal.devices.i04.constants import RedisConstants
|
|
19
19
|
from dodal.devices.oav.oav_calculations import (
|
|
@@ -103,13 +103,25 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
103
103
|
self.z_mm, self._z_mm_setter = soft_signal_r_and_setter(float)
|
|
104
104
|
super().__init__(name=name)
|
|
105
105
|
|
|
106
|
+
async def _check_redis_connection(self):
|
|
107
|
+
try:
|
|
108
|
+
await self.redis_client.ping() # type: ignore
|
|
109
|
+
return True
|
|
110
|
+
except ConnectionError:
|
|
111
|
+
LOGGER.warning(
|
|
112
|
+
f"Failed to connect to redis: {self.redis_client}. Murko results device will not trigger"
|
|
113
|
+
)
|
|
114
|
+
return False
|
|
115
|
+
|
|
106
116
|
def _reset(self):
|
|
107
117
|
self._last_omega = None
|
|
108
118
|
self._results: list[MurkoResult] = []
|
|
109
119
|
|
|
110
120
|
@AsyncStatus.wrap
|
|
111
121
|
async def stage(self):
|
|
112
|
-
await self.
|
|
122
|
+
self.redis_connected = await self._check_redis_connection()
|
|
123
|
+
if self.redis_connected:
|
|
124
|
+
await self.pubsub.subscribe("murko-results")
|
|
113
125
|
self._x_mm_setter(0)
|
|
114
126
|
self._y_mm_setter(0)
|
|
115
127
|
self._z_mm_setter(0)
|
|
@@ -117,10 +129,13 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
117
129
|
@AsyncStatus.wrap
|
|
118
130
|
async def unstage(self):
|
|
119
131
|
self._reset()
|
|
120
|
-
|
|
132
|
+
if self.redis_connected:
|
|
133
|
+
await self.pubsub.unsubscribe()
|
|
121
134
|
|
|
122
135
|
@AsyncStatus.wrap
|
|
123
136
|
async def trigger(self):
|
|
137
|
+
if not self.redis_connected:
|
|
138
|
+
return
|
|
124
139
|
sample_id = await self.sample_id.get_value()
|
|
125
140
|
t_last_result = time.time()
|
|
126
141
|
while True:
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
from dodal.devices.insertion_device.apple2_undulator import (
|
|
2
|
-
MAXIMUM_MOVE_TIME,
|
|
3
|
-
Apple2,
|
|
4
|
-
Apple2Controller,
|
|
5
|
-
Apple2PhasesVal,
|
|
6
|
-
Apple2Val,
|
|
7
|
-
Pol,
|
|
8
|
-
UndulatorPhaseAxes,
|
|
9
|
-
)
|
|
10
|
-
from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
|
|
11
|
-
|
|
12
1
|
J09_GAP_POLY_DEG_COLUMNS = [
|
|
13
2
|
"9th-order",
|
|
14
3
|
"8th-order",
|
|
@@ -23,64 +12,3 @@ J09_GAP_POLY_DEG_COLUMNS = [
|
|
|
23
12
|
]
|
|
24
13
|
|
|
25
14
|
J09_PHASE_POLY_DEG_COLUMNS = ["0th-order"]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class J09Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
|
|
29
|
-
def __init__(
|
|
30
|
-
self,
|
|
31
|
-
apple2: Apple2[UndulatorPhaseAxes],
|
|
32
|
-
gap_energy_motor_lut: EnergyMotorLookup,
|
|
33
|
-
phase_energy_motor_lut: EnergyMotorLookup,
|
|
34
|
-
units: str = "keV",
|
|
35
|
-
name: str = "",
|
|
36
|
-
) -> None:
|
|
37
|
-
"""
|
|
38
|
-
Parameters:
|
|
39
|
-
-----------
|
|
40
|
-
apple2 : Apple2
|
|
41
|
-
An Apple2 device.
|
|
42
|
-
gap_energy_motor_lut: EnergyMotorLookup
|
|
43
|
-
The class that handles the gap look up table logic for the insertion device.
|
|
44
|
-
phase_energy_motor_lut: EnergyMotorLookup
|
|
45
|
-
The class that handles the phase look up table logic for the insertion device.
|
|
46
|
-
units:
|
|
47
|
-
the units of this device. Defaults to eV.
|
|
48
|
-
name : str, optional
|
|
49
|
-
New device name.
|
|
50
|
-
"""
|
|
51
|
-
self.gap_energy_motor_lut = gap_energy_motor_lut
|
|
52
|
-
self.phase_energy_motor_lut = phase_energy_motor_lut
|
|
53
|
-
super().__init__(
|
|
54
|
-
apple2=apple2,
|
|
55
|
-
gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
|
|
56
|
-
phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
|
|
57
|
-
units=units,
|
|
58
|
-
name=name,
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
|
|
62
|
-
return Apple2Val(
|
|
63
|
-
gap=f"{gap:.6f}",
|
|
64
|
-
phase=Apple2PhasesVal(
|
|
65
|
-
top_outer=f"{phase:.6f}",
|
|
66
|
-
top_inner=f"{0.0:.6f}",
|
|
67
|
-
btm_inner=f"{phase:.6f}",
|
|
68
|
-
btm_outer=f"{0.0:.6f}",
|
|
69
|
-
),
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
async def _set_pol(
|
|
73
|
-
self,
|
|
74
|
-
value: Pol,
|
|
75
|
-
) -> None:
|
|
76
|
-
# I09 require all palarisation change to go via LH.
|
|
77
|
-
target_energy = await self.energy.get_value()
|
|
78
|
-
if value is not Pol.LH:
|
|
79
|
-
self._polarisation_setpoint_set(Pol.LH)
|
|
80
|
-
max_lh_energy = self.gap_energy_motor_lut.lut.root[Pol.LH].max_energy
|
|
81
|
-
lh_setpoint = (
|
|
82
|
-
max_lh_energy if target_energy > max_lh_energy else target_energy
|
|
83
|
-
)
|
|
84
|
-
await self.energy.set(lh_setpoint, timeout=MAXIMUM_MOVE_TIME)
|
|
85
|
-
self._polarisation_setpoint_set(value)
|
|
86
|
-
await self.energy.set(target_energy, timeout=MAXIMUM_MOVE_TIME)
|
dodal/devices/i10/i10_apple2.py
CHANGED
|
@@ -11,18 +11,18 @@ from ophyd_async.core import (
|
|
|
11
11
|
soft_signal_rw,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
from dodal.devices.insertion_device
|
|
14
|
+
from dodal.devices.insertion_device import (
|
|
15
15
|
MAXIMUM_MOVE_TIME,
|
|
16
16
|
Apple2,
|
|
17
17
|
Apple2Controller,
|
|
18
18
|
Apple2PhasesVal,
|
|
19
19
|
Apple2Val,
|
|
20
|
-
Pol,
|
|
21
20
|
UndulatorGap,
|
|
22
21
|
UndulatorJawPhase,
|
|
23
22
|
UndulatorPhaseAxes,
|
|
24
23
|
)
|
|
25
24
|
from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
|
|
25
|
+
from dodal.devices.insertion_device.enum import Pol
|
|
26
26
|
|
|
27
27
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
28
28
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
@@ -141,12 +141,12 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
141
141
|
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
|
|
142
142
|
phase3 = phase * (-1 if pol == Pol.LA else 1)
|
|
143
143
|
return Apple2Val(
|
|
144
|
-
gap=
|
|
144
|
+
gap=gap,
|
|
145
145
|
phase=Apple2PhasesVal(
|
|
146
|
-
top_outer=
|
|
147
|
-
top_inner=
|
|
148
|
-
btm_inner=
|
|
149
|
-
btm_outer=
|
|
146
|
+
top_outer=phase,
|
|
147
|
+
top_inner=0.0,
|
|
148
|
+
btm_inner=phase3,
|
|
149
|
+
btm_outer=0.0,
|
|
150
150
|
),
|
|
151
151
|
)
|
|
152
152
|
|
dodal/devices/i17/i17_apple2.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dodal.devices.insertion_device
|
|
1
|
+
from dodal.devices.insertion_device import (
|
|
2
2
|
Apple2,
|
|
3
3
|
Apple2Controller,
|
|
4
4
|
Apple2PhasesVal,
|
|
@@ -56,11 +56,11 @@ class I17Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
|
|
|
56
56
|
|
|
57
57
|
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
|
|
58
58
|
return Apple2Val(
|
|
59
|
-
gap=
|
|
59
|
+
gap=gap,
|
|
60
60
|
phase=Apple2PhasesVal(
|
|
61
|
-
top_outer=
|
|
62
|
-
top_inner=
|
|
63
|
-
btm_inner=
|
|
64
|
-
btm_outer=
|
|
61
|
+
top_outer=phase,
|
|
62
|
+
top_inner=0.0,
|
|
63
|
+
btm_inner=phase,
|
|
64
|
+
btm_outer=0.0,
|
|
65
65
|
),
|
|
66
66
|
)
|
dodal/devices/i21/__init__.py
CHANGED
|
@@ -6,11 +6,10 @@ from bluesky.protocols import StreamAsset
|
|
|
6
6
|
from event_model import DataKey # type: ignore
|
|
7
7
|
from ophyd_async.core import (
|
|
8
8
|
AsyncStatus,
|
|
9
|
-
AutoIncrementingPathProvider,
|
|
10
9
|
DetectorWriter,
|
|
10
|
+
PathProvider,
|
|
11
11
|
StandardDetector,
|
|
12
12
|
StandardReadable,
|
|
13
|
-
StaticPathProvider,
|
|
14
13
|
TriggerInfo,
|
|
15
14
|
observe_value,
|
|
16
15
|
wait_for_value,
|
|
@@ -22,7 +21,7 @@ from ophyd_async.fastcs.jungfrau._signals import JungfrauDriverIO
|
|
|
22
21
|
from dodal.log import LOGGER
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
class
|
|
24
|
+
class JungfrauCommissioningWriter(DetectorWriter, StandardReadable):
|
|
26
25
|
"""Implementation of the temporary filewriter used for Jungfrau commissioning on i24.
|
|
27
26
|
|
|
28
27
|
The PVs on this device are responsible for writing files of a specified name
|
|
@@ -33,11 +32,11 @@ class JunfrauCommissioningWriter(DetectorWriter, StandardReadable):
|
|
|
33
32
|
def __init__(
|
|
34
33
|
self,
|
|
35
34
|
prefix,
|
|
36
|
-
path_provider:
|
|
35
|
+
path_provider: PathProvider,
|
|
37
36
|
name="",
|
|
38
37
|
) -> None:
|
|
39
38
|
with self.add_children_as_readables():
|
|
40
|
-
self.
|
|
39
|
+
self._path_provider = path_provider
|
|
41
40
|
self.frame_counter = epics_signal_rw(int, f"{prefix}NumCaptured")
|
|
42
41
|
self.file_name = epics_signal_rw_rbv(str, f"{prefix}FileName")
|
|
43
42
|
self.file_path = epics_signal_rw_rbv(str, f"{prefix}FilePath")
|
|
@@ -47,9 +46,8 @@ class JunfrauCommissioningWriter(DetectorWriter, StandardReadable):
|
|
|
47
46
|
|
|
48
47
|
async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]:
|
|
49
48
|
self._exposures_per_event = exposures_per_event
|
|
50
|
-
_path_info = self.
|
|
49
|
+
_path_info = self._path_provider()
|
|
51
50
|
|
|
52
|
-
# Commissioning Jungfrau plans allow you to override path, so check to see if file exists
|
|
53
51
|
requested_filepath = Path(_path_info.directory_path) / _path_info.filename
|
|
54
52
|
if requested_filepath.exists():
|
|
55
53
|
raise FileExistsError(
|
|
@@ -65,6 +63,7 @@ class JunfrauCommissioningWriter(DetectorWriter, StandardReadable):
|
|
|
65
63
|
f"Jungfrau writing to folder {_path_info.directory_path} with filename {_path_info.filename}"
|
|
66
64
|
)
|
|
67
65
|
await wait_for_value(self.writer_ready, 1, timeout=10)
|
|
66
|
+
self.final_path = requested_filepath
|
|
68
67
|
return await self._describe()
|
|
69
68
|
|
|
70
69
|
async def _describe(self) -> dict[str, DataKey]:
|
|
@@ -99,7 +98,7 @@ class JunfrauCommissioningWriter(DetectorWriter, StandardReadable):
|
|
|
99
98
|
|
|
100
99
|
|
|
101
100
|
class CommissioningJungfrau(
|
|
102
|
-
StandardDetector[JungfrauController,
|
|
101
|
+
StandardDetector[JungfrauController, JungfrauCommissioningWriter]
|
|
103
102
|
):
|
|
104
103
|
"""Ophyd-async implementation of a Jungfrau 9M Detector, using a temporary
|
|
105
104
|
filewriter in place of Odin"""
|
|
@@ -108,11 +107,11 @@ class CommissioningJungfrau(
|
|
|
108
107
|
self,
|
|
109
108
|
prefix: str,
|
|
110
109
|
writer_prefix: str,
|
|
111
|
-
path_provider:
|
|
110
|
+
path_provider: PathProvider,
|
|
112
111
|
name="",
|
|
113
112
|
):
|
|
114
113
|
self.drv = JungfrauDriverIO(prefix)
|
|
115
|
-
writer =
|
|
114
|
+
writer = JungfrauCommissioningWriter(writer_prefix, path_provider)
|
|
116
115
|
controller = JungfrauController(self.drv)
|
|
117
116
|
super().__init__(controller, writer, name=name)
|
|
118
117
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from .apple2_controller import (
|
|
2
|
+
MAXIMUM_MOVE_TIME,
|
|
3
|
+
Apple2Controller,
|
|
4
|
+
Apple2EnforceLHMoveController,
|
|
5
|
+
EnergyMotorConvertor,
|
|
6
|
+
)
|
|
7
|
+
from .apple2_undulator import (
|
|
8
|
+
DEFAULT_MOTOR_MIN_TIMEOUT,
|
|
9
|
+
Apple2,
|
|
10
|
+
Apple2LockedPhasesVal,
|
|
11
|
+
Apple2PhasesVal,
|
|
12
|
+
Apple2Val,
|
|
13
|
+
EnabledDisabledUpper,
|
|
14
|
+
UndulatorGap,
|
|
15
|
+
UndulatorJawPhase,
|
|
16
|
+
UndulatorLockedPhaseAxes,
|
|
17
|
+
UndulatorPhaseAxes,
|
|
18
|
+
UnstoppableMotor,
|
|
19
|
+
)
|
|
20
|
+
from .energy import BeamEnergy, InsertionDeviceEnergy, InsertionDeviceEnergyBase
|
|
21
|
+
from .energy_motor_lookup import (
|
|
22
|
+
ConfigServerEnergyMotorLookup,
|
|
23
|
+
EnergyMotorLookup,
|
|
24
|
+
)
|
|
25
|
+
from .enum import Pol, UndulatorGateStatus
|
|
26
|
+
from .lookup_table_models import (
|
|
27
|
+
EnergyCoverage,
|
|
28
|
+
LookupTable,
|
|
29
|
+
LookupTableColumnConfig,
|
|
30
|
+
convert_csv_to_lookup,
|
|
31
|
+
)
|
|
32
|
+
from .polarisation import InsertionDevicePolarisation
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"Apple2",
|
|
36
|
+
"Apple2Controller",
|
|
37
|
+
"Apple2EnforceLHMoveController",
|
|
38
|
+
"UndulatorGap",
|
|
39
|
+
"UndulatorPhaseAxes",
|
|
40
|
+
"UndulatorJawPhase",
|
|
41
|
+
"Apple2Val",
|
|
42
|
+
"Apple2PhasesVal",
|
|
43
|
+
"MAXIMUM_MOVE_TIME",
|
|
44
|
+
"LookupTable",
|
|
45
|
+
"LookupTableColumnConfig",
|
|
46
|
+
"convert_csv_to_lookup",
|
|
47
|
+
"InsertionDeviceEnergy",
|
|
48
|
+
"InsertionDevicePolarisation",
|
|
49
|
+
"BeamEnergy",
|
|
50
|
+
"UndulatorLockedPhaseAxes",
|
|
51
|
+
"EnergyCoverage",
|
|
52
|
+
"Pol",
|
|
53
|
+
"DEFAULT_MOTOR_MIN_TIMEOUT",
|
|
54
|
+
"EnabledDisabledUpper",
|
|
55
|
+
"UndulatorGateStatus",
|
|
56
|
+
"Apple2LockedPhasesVal",
|
|
57
|
+
"EnergyMotorLookup",
|
|
58
|
+
"ConfigServerEnergyMotorLookup",
|
|
59
|
+
"EnergyMotorConvertor",
|
|
60
|
+
"UnstoppableMotor",
|
|
61
|
+
"InsertionDeviceEnergyBase",
|
|
62
|
+
]
|