dls-dodal 1.64.0__py3-none-any.whl → 1.66.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.64.0.dist-info → dls_dodal-1.66.0.dist-info}/METADATA +3 -4
- {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/RECORD +72 -66
- dodal/_version.py +2 -2
- dodal/beamline_specific_utils/i05_shared.py +6 -3
- dodal/beamlines/aithre.py +21 -2
- dodal/beamlines/b01_1.py +1 -1
- dodal/beamlines/b07.py +6 -3
- dodal/beamlines/b07_1.py +6 -3
- dodal/beamlines/i03.py +32 -4
- dodal/beamlines/i04.py +18 -3
- dodal/beamlines/i05.py +30 -3
- dodal/beamlines/i05_1.py +2 -2
- dodal/beamlines/i06.py +62 -0
- dodal/beamlines/i07.py +20 -0
- dodal/beamlines/i09.py +3 -3
- dodal/beamlines/i09_1.py +12 -1
- dodal/beamlines/i09_2.py +6 -3
- dodal/beamlines/i10_optics.py +21 -11
- dodal/beamlines/i17.py +3 -3
- dodal/beamlines/i18.py +3 -3
- dodal/beamlines/i19_2.py +22 -0
- dodal/beamlines/i21.py +3 -3
- dodal/beamlines/i22.py +3 -20
- dodal/beamlines/k07.py +6 -3
- dodal/beamlines/p38.py +3 -3
- dodal/devices/aithre_lasershaping/goniometer.py +26 -9
- dodal/devices/aperturescatterguard.py +3 -2
- dodal/devices/apple2_undulator.py +89 -44
- 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 +21 -0
- dodal/devices/detector/det_resolution.py +4 -2
- 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/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 +6 -2
- dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
- dodal/devices/i10/i10_apple2.py +22 -316
- dodal/devices/i17/i17_apple2.py +7 -4
- dodal/devices/i22/nxsas.py +5 -24
- dodal/devices/ipin.py +20 -2
- dodal/devices/motors.py +19 -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/pgm.py +1 -1
- dodal/devices/robot.py +17 -7
- dodal/devices/scintillator.py +40 -14
- dodal/devices/smargon.py +2 -3
- dodal/devices/thawer.py +7 -45
- dodal/devices/undulator.py +178 -66
- dodal/devices/util/lookup_tables_apple2.py +390 -0
- dodal/plan_stubs/__init__.py +3 -0
- dodal/plans/load_panda_yaml.py +9 -0
- dodal/plans/verify_undulator_gap.py +2 -2
- dodal/testing/fixtures/run_engine.py +79 -7
- dodal/beamline_specific_utils/i03.py +0 -17
- dodal/testing/__init__.py +0 -3
- dodal/testing/setup.py +0 -67
- {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/top_level.txt +0 -0
dodal/devices/thawer.py
CHANGED
|
@@ -1,59 +1,21 @@
|
|
|
1
|
-
from asyncio import CancelledError, Task, create_task, sleep
|
|
2
|
-
|
|
3
1
|
from bluesky.protocols import Movable, Stoppable
|
|
4
2
|
from ophyd_async.core import (
|
|
5
3
|
AsyncStatus,
|
|
6
|
-
Device,
|
|
7
4
|
OnOff,
|
|
8
|
-
Reference,
|
|
9
|
-
SignalRW,
|
|
10
5
|
StandardReadable,
|
|
11
6
|
)
|
|
12
7
|
from ophyd_async.epics.core import epics_signal_rw
|
|
13
8
|
|
|
14
|
-
from dodal.log import LOGGER
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ThawingTimer(Device, Stoppable, Movable[float]):
|
|
18
|
-
def __init__(self, control_signal: SignalRW[OnOff]) -> None:
|
|
19
|
-
self._control_signal_ref = Reference(control_signal)
|
|
20
|
-
self._thawing_task: Task | None = None
|
|
21
|
-
super().__init__("thaw_for_time_s")
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
async def set(self, value: float):
|
|
25
|
-
if self._thawing_task:
|
|
26
|
-
LOGGER.info("Thawing task already in progress, resetting timer")
|
|
27
|
-
self._thawing_task.cancel()
|
|
28
|
-
else:
|
|
29
|
-
LOGGER.info("Thawing started")
|
|
30
|
-
await self._control_signal_ref().set(OnOff.ON)
|
|
31
|
-
self._thawing_task = create_task(sleep(value))
|
|
32
|
-
try:
|
|
33
|
-
await self._thawing_task
|
|
34
|
-
except CancelledError:
|
|
35
|
-
LOGGER.info("Timer task cancelled.")
|
|
36
|
-
raise
|
|
37
|
-
else:
|
|
38
|
-
LOGGER.info("Thawing completed")
|
|
39
|
-
await self._control_signal_ref().set(OnOff.OFF)
|
|
40
|
-
|
|
41
|
-
@AsyncStatus.wrap
|
|
42
|
-
async def stop(self, *args, **kwargs):
|
|
43
|
-
if self._thawing_task:
|
|
44
|
-
self._thawing_task.cancel()
|
|
45
|
-
self._thawing_task = None
|
|
46
|
-
LOGGER.info("Thawer stopped.")
|
|
47
|
-
await self._control_signal_ref().set(OnOff.OFF)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class Thawer(StandardReadable, Stoppable):
|
|
10
|
+
class Thawer(StandardReadable, Stoppable, Movable[OnOff]):
|
|
51
11
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
52
|
-
self.
|
|
53
|
-
self.thaw_for_time_s = ThawingTimer(self.control)
|
|
12
|
+
self._control = epics_signal_rw(OnOff, prefix + ":CTRL")
|
|
54
13
|
super().__init__(name)
|
|
55
14
|
|
|
15
|
+
@AsyncStatus.wrap
|
|
16
|
+
async def set(self, value: OnOff):
|
|
17
|
+
await self._control.set(value)
|
|
18
|
+
|
|
56
19
|
@AsyncStatus.wrap
|
|
57
20
|
async def stop(self, *args, **kwargs):
|
|
58
|
-
await self.
|
|
59
|
-
await self.control.set(OnOff.OFF)
|
|
21
|
+
await self._control.set(OnOff.OFF)
|
dodal/devices/undulator.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
|
-
from bluesky.protocols import Movable
|
|
5
|
+
from bluesky.protocols import Locatable, Location, Movable
|
|
5
6
|
from numpy import ndarray
|
|
6
7
|
from ophyd_async.core import (
|
|
7
8
|
AsyncStatus,
|
|
@@ -9,6 +10,7 @@ from ophyd_async.core import (
|
|
|
9
10
|
StandardReadable,
|
|
10
11
|
StandardReadableFormat,
|
|
11
12
|
soft_signal_r_and_setter,
|
|
13
|
+
soft_signal_rw,
|
|
12
14
|
)
|
|
13
15
|
from ophyd_async.epics.core import epics_signal_r
|
|
14
16
|
from ophyd_async.epics.motor import Motor
|
|
@@ -38,111 +40,177 @@ def _get_gap_for_energy(
|
|
|
38
40
|
)
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
class
|
|
43
|
+
class BaseUndulator(StandardReadable, Movable[float], ABC):
|
|
42
44
|
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
Base class for undulator devices providing gap control and access management.
|
|
46
|
+
This class expects target gap value [mm] passed in set method.
|
|
45
47
|
"""
|
|
46
48
|
|
|
47
49
|
def __init__(
|
|
48
50
|
self,
|
|
49
51
|
prefix: str,
|
|
50
|
-
id_gap_lookup_table_path: str = os.devnull,
|
|
51
|
-
name: str = "",
|
|
52
52
|
poles: int | None = None,
|
|
53
53
|
length: float | None = None,
|
|
54
|
+
undulator_period: int | None = None,
|
|
54
55
|
baton: Baton | None = None,
|
|
56
|
+
name: str = "",
|
|
55
57
|
) -> None:
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
+
"""
|
|
58
59
|
Args:
|
|
59
60
|
prefix: PV prefix
|
|
60
|
-
poles (int): Number of magnetic poles built into the undulator
|
|
61
|
-
length (float): Length of the undulator in meters
|
|
61
|
+
poles (int, optional): Number of magnetic poles built into the undulator
|
|
62
|
+
length (float, optional): Length of the undulator in meters
|
|
63
|
+
undulator_period(int, optional): Undulator period
|
|
64
|
+
baton (optional): Baton object if provided.
|
|
62
65
|
name (str, optional): Name for device. Defaults to "".
|
|
63
66
|
"""
|
|
64
|
-
|
|
65
67
|
self.baton_ref = Reference(baton) if baton else None
|
|
66
|
-
|
|
68
|
+
|
|
67
69
|
with self.add_children_as_readables():
|
|
70
|
+
self.gap_access = epics_signal_r(EnabledDisabledUpper, prefix + "IDBLENA")
|
|
68
71
|
self.gap_motor = Motor(prefix + "BLGAPMTR")
|
|
69
72
|
self.current_gap = epics_signal_r(float, prefix + "CURRGAPD")
|
|
70
|
-
self.gap_access = epics_signal_r(EnabledDisabledUpper, prefix + "IDBLENA")
|
|
71
73
|
|
|
72
74
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
73
75
|
self.gap_discrepancy_tolerance_mm, _ = soft_signal_r_and_setter(
|
|
74
76
|
float,
|
|
75
77
|
initial_value=UNDULATOR_DISCREPANCY_THRESHOLD_MM,
|
|
76
78
|
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
initial_value=length,
|
|
89
|
-
)
|
|
90
|
-
else:
|
|
91
|
-
self.length = None
|
|
92
|
-
|
|
93
|
-
super().__init__(name)
|
|
79
|
+
self.poles = self._make_signal_if_not_none(poles, int)
|
|
80
|
+
self.length = self._make_signal_if_not_none(length, float)
|
|
81
|
+
self.undulator_period = self._make_signal_if_not_none(undulator_period, int)
|
|
82
|
+
|
|
83
|
+
super().__init__(name=name)
|
|
84
|
+
|
|
85
|
+
def _make_signal_if_not_none(self, initial_value, type):
|
|
86
|
+
if initial_value is None:
|
|
87
|
+
return None
|
|
88
|
+
signal, _ = soft_signal_r_and_setter(type, initial_value=initial_value)
|
|
89
|
+
return signal
|
|
94
90
|
|
|
91
|
+
@abstractmethod
|
|
95
92
|
@AsyncStatus.wrap
|
|
96
|
-
async def set(self, value: float):
|
|
93
|
+
async def set(self, value: float) -> None:
|
|
97
94
|
"""
|
|
98
|
-
|
|
95
|
+
Move undulator to a given position.
|
|
96
|
+
Abstract method - must be implemented by subclasses.
|
|
99
97
|
|
|
100
98
|
Args:
|
|
101
|
-
value:
|
|
99
|
+
value: target position - units depend on implementation
|
|
102
100
|
"""
|
|
103
|
-
|
|
101
|
+
...
|
|
104
102
|
|
|
105
|
-
async def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
async def _set_gap(self, value: float) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Set the undulator gap to a given value in mm.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
value: gap in mm
|
|
109
|
+
"""
|
|
110
|
+
await self.raise_if_not_enabled() # Check access
|
|
111
|
+
if await self._check_gap_within_threshold(value):
|
|
112
|
+
LOGGER.debug(
|
|
113
|
+
"Gap is already in the correct place, no need to ask it to move"
|
|
114
|
+
)
|
|
115
|
+
return
|
|
110
116
|
|
|
111
|
-
async def _set_undulator_gap(self, energy_kev: float) -> None:
|
|
112
|
-
await self.raise_if_not_enabled()
|
|
113
|
-
target_gap = await self._get_gap_to_match_energy(energy_kev)
|
|
114
117
|
LOGGER.info(
|
|
115
|
-
f"
|
|
118
|
+
f"Undulator gap mismatch. Moving gap to nominal value, {value:.3f}mm"
|
|
116
119
|
)
|
|
120
|
+
commissioning_mode = await self._is_commissioning_mode_enabled()
|
|
121
|
+
if not commissioning_mode:
|
|
122
|
+
# Only move if the gap is sufficiently different to the value from the
|
|
123
|
+
# DCM lookup table AND we're not in commissioning mode
|
|
124
|
+
await self.gap_motor.set(
|
|
125
|
+
value,
|
|
126
|
+
timeout=STATUS_TIMEOUT_S,
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
LOGGER.warning("In test mode, not moving ID gap")
|
|
130
|
+
|
|
131
|
+
async def _check_gap_within_threshold(self, target_gap: float) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Check if the undulator gap is within the acceptable threshold of the target gap.
|
|
117
134
|
|
|
118
|
-
|
|
135
|
+
Args:
|
|
136
|
+
target_gap: target gap in mm
|
|
137
|
+
Returns:
|
|
138
|
+
True if the gap is within the threshold, False otherwise
|
|
139
|
+
"""
|
|
119
140
|
current_gap = await self.current_gap.get_value()
|
|
120
141
|
tolerance = await self.gap_discrepancy_tolerance_mm.get_value()
|
|
121
|
-
|
|
122
|
-
if difference > tolerance:
|
|
123
|
-
LOGGER.info(
|
|
124
|
-
f"Undulator gap mismatch. {difference:.3f}mm is outside tolerance.\
|
|
125
|
-
Moving gap to nominal value, {target_gap:.3f}mm"
|
|
126
|
-
)
|
|
127
|
-
commissioning_mode = await self._is_commissioning_mode_enabled()
|
|
128
|
-
if not commissioning_mode:
|
|
129
|
-
# Only move if the gap is sufficiently different to the value from the
|
|
130
|
-
# DCM lookup table AND we're not in commissioning mode
|
|
131
|
-
await self.gap_motor.set(
|
|
132
|
-
target_gap,
|
|
133
|
-
timeout=STATUS_TIMEOUT_S,
|
|
134
|
-
)
|
|
135
|
-
else:
|
|
136
|
-
LOGGER.warning("In test mode, not moving ID gap")
|
|
137
|
-
else:
|
|
138
|
-
LOGGER.debug(
|
|
139
|
-
"Gap is already in the correct place for the new energy value "
|
|
140
|
-
f"{energy_kev}, no need to ask it to move"
|
|
141
|
-
)
|
|
142
|
+
return abs(target_gap - current_gap) <= tolerance
|
|
142
143
|
|
|
143
|
-
async def _is_commissioning_mode_enabled(self):
|
|
144
|
+
async def _is_commissioning_mode_enabled(self) -> bool | None:
|
|
145
|
+
"""
|
|
146
|
+
Asynchronously checks if commissioning mode is enabled via the baton reference.
|
|
147
|
+
"""
|
|
144
148
|
return self.baton_ref and await self.baton_ref().commissioning.get_value()
|
|
145
149
|
|
|
150
|
+
async def raise_if_not_enabled(self) -> AccessError | None:
|
|
151
|
+
"""
|
|
152
|
+
Asynchronously raises AccessError if gap access is disabled and not in commissioning mode.
|
|
153
|
+
"""
|
|
154
|
+
access_level = await self.gap_access.get_value()
|
|
155
|
+
commissioning_mode = await self._is_commissioning_mode_enabled()
|
|
156
|
+
if access_level is EnabledDisabledUpper.DISABLED and not commissioning_mode:
|
|
157
|
+
raise AccessError("Undulator gap access is disabled. Contact Control Room")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class UndulatorInKeV(BaseUndulator):
|
|
161
|
+
"""
|
|
162
|
+
An Undulator-type insertion device, used to control photon emission at a given beam energy.
|
|
163
|
+
This class expects energy [keV] passed in set method and does conversion to gap
|
|
164
|
+
internally, for which it requires path to lookup table file in constructor.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
prefix: str,
|
|
170
|
+
id_gap_lookup_table_path: str = os.devnull,
|
|
171
|
+
poles: int | None = None,
|
|
172
|
+
length: float | None = None,
|
|
173
|
+
undulator_period: int | None = None,
|
|
174
|
+
baton: Baton | None = None,
|
|
175
|
+
name: str = "",
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Constructor
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
prefix: PV prefix
|
|
181
|
+
id_gap_lookup_table_path (str): Path to a lookup table file
|
|
182
|
+
poles (int, optional): Number of magnetic poles built into the undulator
|
|
183
|
+
length (float, optional): Length of the undulator in meters
|
|
184
|
+
undulator_period(int, optional): Undulator period
|
|
185
|
+
baton (optional): Baton object if provided.
|
|
186
|
+
name (str, optional): Name for device. Defaults to "".
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
self.id_gap_lookup_table_path = id_gap_lookup_table_path
|
|
190
|
+
super().__init__(
|
|
191
|
+
prefix=prefix,
|
|
192
|
+
poles=poles,
|
|
193
|
+
length=length,
|
|
194
|
+
undulator_period=undulator_period,
|
|
195
|
+
baton=baton,
|
|
196
|
+
name=name,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@AsyncStatus.wrap
|
|
200
|
+
async def set(self, value: float):
|
|
201
|
+
"""
|
|
202
|
+
Check conditions and Set undulator gap to a given energy in keV
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
value: energy in keV
|
|
206
|
+
"""
|
|
207
|
+
# Convert energy in keV to gap in mm first
|
|
208
|
+
target_gap = await self._get_gap_to_match_energy(value)
|
|
209
|
+
LOGGER.info(
|
|
210
|
+
f"Setting undulator gap to {target_gap:.3f}mm based on {value:.2f}kev"
|
|
211
|
+
)
|
|
212
|
+
await self._set_gap(target_gap)
|
|
213
|
+
|
|
146
214
|
async def _get_gap_to_match_energy(self, energy_kev: float) -> float:
|
|
147
215
|
"""
|
|
148
216
|
get a 2d np.array from lookup table that
|
|
@@ -157,3 +225,47 @@ class Undulator(StandardReadable, Movable[float]):
|
|
|
157
225
|
energy_kev * 1000,
|
|
158
226
|
energy_to_distance_table,
|
|
159
227
|
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class UndulatorInMm(BaseUndulator):
|
|
231
|
+
"""
|
|
232
|
+
An Undulator-type insertion device, used to control photon emission.
|
|
233
|
+
This class expects gap [mm] passed in set method.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
@AsyncStatus.wrap
|
|
237
|
+
async def set(self, value: float):
|
|
238
|
+
"""
|
|
239
|
+
Check conditions and Set undulator gap to a given value in mm
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
value: value in mm
|
|
243
|
+
"""
|
|
244
|
+
await self._set_gap(value)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class UndulatorOrder(StandardReadable, Locatable[int]):
|
|
248
|
+
"""
|
|
249
|
+
Represents the order of an undulator device. Allows setting and locating the order.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, name: str = "") -> None:
|
|
253
|
+
"""
|
|
254
|
+
Args:
|
|
255
|
+
name: Name for device. Defaults to ""
|
|
256
|
+
"""
|
|
257
|
+
with self.add_children_as_readables():
|
|
258
|
+
self.value = soft_signal_rw(int, initial_value=3)
|
|
259
|
+
super().__init__(name=name)
|
|
260
|
+
|
|
261
|
+
@AsyncStatus.wrap
|
|
262
|
+
async def set(self, value: int) -> None:
|
|
263
|
+
if (value >= 0) and isinstance(value, int):
|
|
264
|
+
await self.value.set(value)
|
|
265
|
+
else:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f"Undulator order must be a positive integer. Requested value: {value}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
async def locate(self) -> Location[int]:
|
|
271
|
+
return await self.value.locate()
|