dls-dodal 1.66.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.66.0.dist-info → dls_dodal-1.67.0.dist-info}/METADATA +1 -1
- {dls_dodal-1.66.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +47 -37
- dodal/_version.py +2 -2
- dodal/beamlines/i03.py +92 -208
- dodal/beamlines/i04.py +22 -1
- dodal/beamlines/i05.py +1 -1
- dodal/beamlines/i06.py +1 -1
- dodal/beamlines/i09_1.py +26 -2
- dodal/beamlines/i09_2.py +57 -2
- dodal/beamlines/i10_optics.py +44 -25
- dodal/beamlines/i17.py +7 -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 +16 -1
- 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/cryostream.py +28 -57
- dodal/devices/eiger.py +26 -18
- dodal/devices/i04/max_pixel.py +38 -0
- dodal/devices/i09_1_shared/__init__.py +8 -1
- dodal/devices/i09_1_shared/hard_energy.py +112 -0
- 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 +23 -21
- dodal/devices/i17/i17_apple2.py +32 -20
- 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} +36 -28
- dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
- dodal/devices/insertion_device/lookup_table_models.py +287 -0
- dodal/devices/motors.py +14 -0
- dodal/devices/robot.py +16 -11
- dodal/plans/__init__.py +1 -1
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
- dodal/testing/fixtures/devices/__init__.py +0 -0
- dodal/testing/fixtures/devices/apple2.py +78 -0
- dodal/utils.py +6 -3
- dodal/devices/util/lookup_tables_apple2.py +0 -390
- {dls_dodal-1.66.0.dist-info → dls_dodal-1.67.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.66.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.66.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.66.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)
|
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:
|
|
@@ -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,7 +1,14 @@
|
|
|
1
|
+
from .hard_energy import HardEnergy, HardInsertionDeviceEnergy
|
|
1
2
|
from .hard_undulator_functions import (
|
|
2
3
|
calculate_energy_i09_hu,
|
|
3
4
|
calculate_gap_i09_hu,
|
|
4
5
|
get_hu_lut_as_dict,
|
|
5
6
|
)
|
|
6
7
|
|
|
7
|
-
__all__ = [
|
|
8
|
+
__all__ = [
|
|
9
|
+
"calculate_gap_i09_hu",
|
|
10
|
+
"get_hu_lut_as_dict",
|
|
11
|
+
"calculate_energy_i09_hu",
|
|
12
|
+
"HardInsertionDeviceEnergy",
|
|
13
|
+
"HardEnergy",
|
|
14
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from asyncio import gather
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
|
|
4
|
+
from bluesky.protocols import Locatable, Location, Movable
|
|
5
|
+
from numpy import ndarray
|
|
6
|
+
from ophyd_async.core import (
|
|
7
|
+
AsyncStatus,
|
|
8
|
+
Reference,
|
|
9
|
+
StandardReadable,
|
|
10
|
+
StandardReadableFormat,
|
|
11
|
+
derived_signal_rw,
|
|
12
|
+
soft_signal_rw,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from dodal.devices.common_dcm import DoubleCrystalMonochromatorBase
|
|
16
|
+
from dodal.devices.i09_1_shared.hard_undulator_functions import (
|
|
17
|
+
MAX_ENERGY_COLUMN,
|
|
18
|
+
MIN_ENERGY_COLUMN,
|
|
19
|
+
)
|
|
20
|
+
from dodal.devices.undulator import UndulatorInMm, UndulatorOrder
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HardInsertionDeviceEnergy(StandardReadable, Movable[float]):
|
|
24
|
+
"""
|
|
25
|
+
Compound device to link hard x-ray undulator gap and order to photon energy.
|
|
26
|
+
Setting the energy adjusts the undulator gap accordingly.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
undulator_order: UndulatorOrder,
|
|
32
|
+
undulator: UndulatorInMm,
|
|
33
|
+
lut: dict[int, ndarray],
|
|
34
|
+
gap_to_energy_func: Callable[..., float],
|
|
35
|
+
energy_to_gap_func: Callable[..., float],
|
|
36
|
+
name: str = "",
|
|
37
|
+
) -> None:
|
|
38
|
+
self._lut = lut
|
|
39
|
+
self.gap_to_energy_func = gap_to_energy_func
|
|
40
|
+
self.energy_to_gap_func = energy_to_gap_func
|
|
41
|
+
self._undulator_order_ref = Reference(undulator_order)
|
|
42
|
+
self._undulator_ref = Reference(undulator)
|
|
43
|
+
|
|
44
|
+
self.add_readables([undulator_order, undulator.current_gap])
|
|
45
|
+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
46
|
+
self.energy_demand = soft_signal_rw(float)
|
|
47
|
+
self.energy = derived_signal_rw(
|
|
48
|
+
raw_to_derived=self._read_energy,
|
|
49
|
+
set_derived=self._set_energy,
|
|
50
|
+
current_gap=self._undulator_ref().gap_motor.user_readback,
|
|
51
|
+
current_order=self._undulator_order_ref().value,
|
|
52
|
+
derived_units="keV",
|
|
53
|
+
)
|
|
54
|
+
super().__init__(name=name)
|
|
55
|
+
|
|
56
|
+
def _read_energy(self, current_gap: float, current_order: int) -> float:
|
|
57
|
+
return self.gap_to_energy_func(
|
|
58
|
+
gap=current_gap,
|
|
59
|
+
look_up_table=self._lut,
|
|
60
|
+
order=current_order,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def _set_energy(self, energy: float) -> None:
|
|
64
|
+
current_order = await self._undulator_order_ref().value.get_value()
|
|
65
|
+
min_energy, max_energy = self._lut[current_order][
|
|
66
|
+
MIN_ENERGY_COLUMN : MAX_ENERGY_COLUMN + 1
|
|
67
|
+
]
|
|
68
|
+
if not (min_energy <= energy <= max_energy):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Requested energy {energy} keV is out of range for harmonic {current_order}: "
|
|
71
|
+
f"[{min_energy}, {max_energy}] keV"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
target_gap = self.energy_to_gap_func(
|
|
75
|
+
photon_energy_kev=energy, look_up_table=self._lut, order=current_order
|
|
76
|
+
)
|
|
77
|
+
await self._undulator_ref().set(target_gap)
|
|
78
|
+
|
|
79
|
+
@AsyncStatus.wrap
|
|
80
|
+
async def set(self, value: float) -> None:
|
|
81
|
+
self.energy_demand.set(value)
|
|
82
|
+
await self.energy.set(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class HardEnergy(StandardReadable, Locatable[float]):
|
|
86
|
+
"""
|
|
87
|
+
Energy compound device that provides combined change of both DCM energy and undulator gap accordingly.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
dcm: DoubleCrystalMonochromatorBase,
|
|
93
|
+
undulator_energy: HardInsertionDeviceEnergy,
|
|
94
|
+
name: str = "",
|
|
95
|
+
) -> None:
|
|
96
|
+
self._dcm_ref = Reference(dcm)
|
|
97
|
+
self._undulator_energy_ref = Reference(undulator_energy)
|
|
98
|
+
self.add_readables([undulator_energy, dcm.energy_in_keV])
|
|
99
|
+
super().__init__(name=name)
|
|
100
|
+
|
|
101
|
+
@AsyncStatus.wrap
|
|
102
|
+
async def set(self, value: float) -> None:
|
|
103
|
+
await gather(
|
|
104
|
+
self._dcm_ref().energy_in_keV.set(value),
|
|
105
|
+
self._undulator_energy_ref().set(value),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def locate(self) -> Location[float]:
|
|
109
|
+
return Location(
|
|
110
|
+
setpoint=await self._dcm_ref().energy_in_keV.user_setpoint.get_value(),
|
|
111
|
+
readback=await self._dcm_ref().energy_in_keV.user_readback.get_value(),
|
|
112
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
J09_GAP_POLY_DEG_COLUMNS = [
|
|
13
|
+
"9th-order",
|
|
14
|
+
"8th-order",
|
|
15
|
+
"7th-order",
|
|
16
|
+
"6th-order",
|
|
17
|
+
"5th-order",
|
|
18
|
+
"4th-order",
|
|
19
|
+
"3rd-order",
|
|
20
|
+
"2nd-order",
|
|
21
|
+
"1st-order",
|
|
22
|
+
"0th-order",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
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,7 +11,7 @@ from ophyd_async.core import (
|
|
|
11
11
|
soft_signal_rw,
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
from dodal.devices.apple2_undulator import (
|
|
14
|
+
from dodal.devices.insertion_device.apple2_undulator import (
|
|
15
15
|
MAXIMUM_MOVE_TIME,
|
|
16
16
|
Apple2,
|
|
17
17
|
Apple2Controller,
|
|
@@ -22,8 +22,7 @@ from dodal.devices.apple2_undulator import (
|
|
|
22
22
|
UndulatorJawPhase,
|
|
23
23
|
UndulatorPhaseAxes,
|
|
24
24
|
)
|
|
25
|
-
from dodal.devices.
|
|
26
|
-
from dodal.log import LOGGER
|
|
25
|
+
from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
|
|
27
26
|
|
|
28
27
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
29
28
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
@@ -62,16 +61,18 @@ class I10Apple2(Apple2[UndulatorPhaseAxes]):
|
|
|
62
61
|
class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
63
62
|
"""
|
|
64
63
|
I10Apple2Controller is a extension of Apple2Controller which provide linear
|
|
65
|
-
|
|
64
|
+
arbitrary angle control.
|
|
66
65
|
"""
|
|
67
66
|
|
|
68
67
|
def __init__(
|
|
69
68
|
self,
|
|
70
69
|
apple2: I10Apple2,
|
|
71
|
-
|
|
70
|
+
gap_energy_motor_lut: EnergyMotorLookup,
|
|
71
|
+
phase_energy_motor_lut: EnergyMotorLookup,
|
|
72
72
|
jaw_phase_limit: float = 12.0,
|
|
73
73
|
jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
|
|
74
74
|
angle_threshold_deg=30.0,
|
|
75
|
+
units: str = "eV",
|
|
75
76
|
name: str = "",
|
|
76
77
|
) -> None:
|
|
77
78
|
"""
|
|
@@ -79,25 +80,30 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
79
80
|
-----------
|
|
80
81
|
apple2 : I10Apple2
|
|
81
82
|
An I10Apple2 device.
|
|
82
|
-
|
|
83
|
-
The class that handles the look up table logic for the insertion device.
|
|
83
|
+
gap_energy_motor_lut: EnergyMotorLookup
|
|
84
|
+
The class that handles the gap look up table logic for the insertion device.
|
|
85
|
+
phase_energy_motor_lut: EnergyMotorLookup
|
|
86
|
+
The class that handles the phase look up table logic for the insertion device.
|
|
84
87
|
jaw_phase_limit : float, optional
|
|
85
88
|
The maximum allowed jaw_phase movement., by default 12.0
|
|
86
89
|
jaw_phase_poly_param : list[float], optional
|
|
87
90
|
polynomial parameters highest power first., by default DEFAULT_JAW_PHASE_POLY_PARAMS
|
|
88
91
|
angle_threshold_deg : float, optional
|
|
89
92
|
The angle threshold to switch between 0-180 and 180-360 range., by default 30.0
|
|
93
|
+
units:
|
|
94
|
+
the units of this device. Defaults to eV.
|
|
90
95
|
name : str, optional
|
|
91
96
|
New device name.
|
|
92
97
|
"""
|
|
93
|
-
|
|
94
|
-
self.
|
|
98
|
+
self.gap_energy_motor_lut = gap_energy_motor_lut
|
|
99
|
+
self.phase_energy_motor_lut = phase_energy_motor_lut
|
|
95
100
|
super().__init__(
|
|
96
101
|
apple2=apple2,
|
|
97
|
-
|
|
102
|
+
gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
|
|
103
|
+
phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
|
|
104
|
+
units=units,
|
|
98
105
|
name=name,
|
|
99
106
|
)
|
|
100
|
-
|
|
101
107
|
self.jaw_phase_from_angle = np.poly1d(jaw_phase_poly_param)
|
|
102
108
|
self.angle_threshold_deg = angle_threshold_deg
|
|
103
109
|
self.jaw_phase_limit = jaw_phase_limit
|
|
@@ -132,15 +138,9 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
132
138
|
await self.apple2().jaw_phase().set(jaw_phase)
|
|
133
139
|
await self._linear_arbitrary_angle.set(pol_angle)
|
|
134
140
|
|
|
135
|
-
|
|
136
|
-
"""
|
|
137
|
-
Set the undulator motors for a given energy and polarisation.
|
|
138
|
-
"""
|
|
139
|
-
|
|
140
|
-
pol = await self._check_and_get_pol_setpoint()
|
|
141
|
-
gap, phase = self.energy_to_motor(energy=value, pol=pol)
|
|
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
144
|
gap=f"{gap:.6f}",
|
|
145
145
|
phase=Apple2PhasesVal(
|
|
146
146
|
top_outer=f"{phase:.6f}",
|
|
@@ -150,8 +150,10 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
|
|
|
150
150
|
),
|
|
151
151
|
)
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
async def _set_motors_from_energy_and_polarisation(
|
|
154
|
+
self, energy: float, pol: Pol
|
|
155
|
+
) -> None:
|
|
156
|
+
await super()._set_motors_from_energy_and_polarisation(energy, pol)
|
|
155
157
|
if pol != Pol.LA:
|
|
156
158
|
await self.apple2().jaw_phase().set(0)
|
|
157
159
|
await self.apple2().jaw_phase().set_move.set(1)
|
dodal/devices/i17/i17_apple2.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
from dodal.devices.apple2_undulator import (
|
|
1
|
+
from dodal.devices.insertion_device.apple2_undulator import (
|
|
2
2
|
Apple2,
|
|
3
3
|
Apple2Controller,
|
|
4
4
|
Apple2PhasesVal,
|
|
5
5
|
Apple2Val,
|
|
6
|
-
|
|
6
|
+
Pol,
|
|
7
|
+
UndulatorPhaseAxes,
|
|
7
8
|
)
|
|
8
|
-
from dodal.
|
|
9
|
+
from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
|
|
9
10
|
|
|
10
11
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
11
12
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
@@ -15,7 +16,7 @@ ALPHA_OFFSET = 180
|
|
|
15
16
|
MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class I17Apple2Controller(Apple2Controller[Apple2]):
|
|
19
|
+
class I17Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
|
|
19
20
|
"""
|
|
20
21
|
I10Apple2Controller is a extension of Apple2Controller which provide linear
|
|
21
22
|
arbitrary angle control.
|
|
@@ -23,32 +24,43 @@ class I17Apple2Controller(Apple2Controller[Apple2]):
|
|
|
23
24
|
|
|
24
25
|
def __init__(
|
|
25
26
|
self,
|
|
26
|
-
apple2: Apple2,
|
|
27
|
-
|
|
27
|
+
apple2: Apple2[UndulatorPhaseAxes],
|
|
28
|
+
gap_energy_motor_lut: EnergyMotorLookup,
|
|
29
|
+
phase_energy_motor_lut: EnergyMotorLookup,
|
|
30
|
+
units: str = "eV",
|
|
28
31
|
name: str = "",
|
|
29
32
|
) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Parameters:
|
|
35
|
+
-----------
|
|
36
|
+
apple2 : Apple2
|
|
37
|
+
An Apple2 device.
|
|
38
|
+
gap_energy_motor_lut: EnergyMotorLookup
|
|
39
|
+
The class that handles the gap look up table logic for the insertion device.
|
|
40
|
+
phase_energy_motor_lut: EnergyMotorLookup
|
|
41
|
+
The class that handles the phase look up table logic for the insertion device.
|
|
42
|
+
units:
|
|
43
|
+
the units of this device. Defaults to eV.
|
|
44
|
+
name : str, optional
|
|
45
|
+
New device name.
|
|
46
|
+
"""
|
|
47
|
+
self.gap_energy_motor_lut = gap_energy_motor_lut
|
|
48
|
+
self.phase_energy_motor_lut = phase_energy_motor_lut
|
|
30
49
|
super().__init__(
|
|
31
50
|
apple2=apple2,
|
|
32
|
-
|
|
51
|
+
gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
|
|
52
|
+
phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
|
|
53
|
+
units=units,
|
|
33
54
|
name=name,
|
|
34
55
|
)
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Set the undulator motors for a given energy and polarisation.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
pol = await self._check_and_get_pol_setpoint()
|
|
42
|
-
gap, phase = self.energy_to_motor(energy=value, pol=pol)
|
|
43
|
-
id_set_val = Apple2Val(
|
|
57
|
+
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
|
|
58
|
+
return Apple2Val(
|
|
44
59
|
gap=f"{gap:.6f}",
|
|
45
60
|
phase=Apple2PhasesVal(
|
|
46
61
|
top_outer=f"{phase:.6f}",
|
|
47
|
-
top_inner="0.0",
|
|
62
|
+
top_inner=f"{0.0:.6f}",
|
|
48
63
|
btm_inner=f"{phase:.6f}",
|
|
49
|
-
btm_outer="0.0",
|
|
64
|
+
btm_outer=f"{0.0:.6f}",
|
|
50
65
|
),
|
|
51
66
|
)
|
|
52
|
-
|
|
53
|
-
LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
|
|
54
|
-
await self.apple2().set(id_motor_values=id_set_val)
|