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.
Files changed (75) hide show
  1. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/RECORD +72 -66
  3. dodal/_version.py +2 -2
  4. dodal/beamline_specific_utils/i05_shared.py +6 -3
  5. dodal/beamlines/aithre.py +21 -2
  6. dodal/beamlines/b01_1.py +1 -1
  7. dodal/beamlines/b07.py +6 -3
  8. dodal/beamlines/b07_1.py +6 -3
  9. dodal/beamlines/i03.py +32 -4
  10. dodal/beamlines/i04.py +18 -3
  11. dodal/beamlines/i05.py +30 -3
  12. dodal/beamlines/i05_1.py +2 -2
  13. dodal/beamlines/i06.py +62 -0
  14. dodal/beamlines/i07.py +20 -0
  15. dodal/beamlines/i09.py +3 -3
  16. dodal/beamlines/i09_1.py +12 -1
  17. dodal/beamlines/i09_2.py +6 -3
  18. dodal/beamlines/i10_optics.py +21 -11
  19. dodal/beamlines/i17.py +3 -3
  20. dodal/beamlines/i18.py +3 -3
  21. dodal/beamlines/i19_2.py +22 -0
  22. dodal/beamlines/i21.py +3 -3
  23. dodal/beamlines/i22.py +3 -20
  24. dodal/beamlines/k07.py +6 -3
  25. dodal/beamlines/p38.py +3 -3
  26. dodal/devices/aithre_lasershaping/goniometer.py +26 -9
  27. dodal/devices/aperturescatterguard.py +3 -2
  28. dodal/devices/apple2_undulator.py +89 -44
  29. dodal/devices/areadetector/plugins/mjpg.py +10 -3
  30. dodal/devices/beamsize/__init__.py +0 -0
  31. dodal/devices/beamsize/beamsize.py +6 -0
  32. dodal/devices/cryostream.py +21 -0
  33. dodal/devices/detector/det_resolution.py +4 -2
  34. dodal/devices/fast_grid_scan.py +14 -2
  35. dodal/devices/i03/beamsize.py +35 -0
  36. dodal/devices/i03/constants.py +7 -0
  37. dodal/devices/i03/undulator_dcm.py +2 -2
  38. dodal/devices/i04/beamsize.py +45 -0
  39. dodal/devices/i04/murko_results.py +36 -26
  40. dodal/devices/i04/transfocator.py +23 -29
  41. dodal/devices/i07/id.py +38 -0
  42. dodal/devices/i09_1_shared/__init__.py +6 -2
  43. dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
  44. dodal/devices/i10/i10_apple2.py +22 -316
  45. dodal/devices/i17/i17_apple2.py +7 -4
  46. dodal/devices/i22/nxsas.py +5 -24
  47. dodal/devices/ipin.py +20 -2
  48. dodal/devices/motors.py +19 -3
  49. dodal/devices/mx_phase1/beamstop.py +31 -12
  50. dodal/devices/oav/oav_calculations.py +9 -4
  51. dodal/devices/oav/oav_detector.py +65 -7
  52. dodal/devices/oav/oav_parameters.py +3 -1
  53. dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
  54. dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
  55. dodal/devices/oav/pin_image_recognition/utils.py +23 -1
  56. dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
  57. dodal/devices/oav/utils.py +16 -6
  58. dodal/devices/pgm.py +1 -1
  59. dodal/devices/robot.py +17 -7
  60. dodal/devices/scintillator.py +40 -14
  61. dodal/devices/smargon.py +2 -3
  62. dodal/devices/thawer.py +7 -45
  63. dodal/devices/undulator.py +178 -66
  64. dodal/devices/util/lookup_tables_apple2.py +390 -0
  65. dodal/plan_stubs/__init__.py +3 -0
  66. dodal/plans/load_panda_yaml.py +9 -0
  67. dodal/plans/verify_undulator_gap.py +2 -2
  68. dodal/testing/fixtures/run_engine.py +79 -7
  69. dodal/beamline_specific_utils/i03.py +0 -17
  70. dodal/testing/__init__.py +0 -3
  71. dodal/testing/setup.py +0 -67
  72. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/WHEEL +0 -0
  73. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/entry_points.txt +0 -0
  74. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/licenses/LICENSE +0 -0
  75. {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
- @AsyncStatus.wrap
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.control = epics_signal_rw(OnOff, prefix + ":CTRL")
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.thaw_for_time_s.stop()
59
- await self.control.set(OnOff.OFF)
21
+ await self._control.set(OnOff.OFF)
@@ -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 Undulator(StandardReadable, Movable[float]):
43
+ class BaseUndulator(StandardReadable, Movable[float], ABC):
42
44
  """
43
- An Undulator-type insertion device, used to control photon emission at a given
44
- beam energy.
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
- """Constructor
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
- self.id_gap_lookup_table_path = id_gap_lookup_table_path
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
- if poles is not None:
78
- self.poles, _ = soft_signal_r_and_setter(
79
- int,
80
- initial_value=poles,
81
- )
82
- else:
83
- self.poles = None
84
-
85
- if length is not None:
86
- self.length, _ = soft_signal_r_and_setter(
87
- float,
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
- Set the undulator gap to a given energy in keV
95
+ Move undulator to a given position.
96
+ Abstract method - must be implemented by subclasses.
99
97
 
100
98
  Args:
101
- value: energy in keV
99
+ value: target position - units depend on implementation
102
100
  """
103
- await self._set_undulator_gap(value)
101
+ ...
104
102
 
105
- async def raise_if_not_enabled(self):
106
- access_level = await self.gap_access.get_value()
107
- commissioning_mode = await self._is_commissioning_mode_enabled()
108
- if access_level is EnabledDisabledUpper.DISABLED and not commissioning_mode:
109
- raise AccessError("Undulator gap access is disabled. Contact Control Room")
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"Setting undulator gap to {target_gap:.3f}mm based on {energy_kev:.2f}kev"
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
- # Check if undulator gap is close enough to the value from the DCM
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
- difference = abs(target_gap - current_gap)
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()