dls-dodal 1.38.0__py3-none-any.whl → 1.40.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 (49) hide show
  1. {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/RECORD +49 -40
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/__init__.py +2 -0
  5. dodal/beamlines/adsim.py +3 -2
  6. dodal/beamlines/b01_1.py +3 -3
  7. dodal/beamlines/i03.py +144 -285
  8. dodal/beamlines/i04.py +112 -198
  9. dodal/beamlines/i13_1.py +5 -4
  10. dodal/beamlines/i18.py +124 -0
  11. dodal/beamlines/i19_1.py +74 -0
  12. dodal/beamlines/i19_2.py +61 -0
  13. dodal/beamlines/i20_1.py +37 -22
  14. dodal/beamlines/i22.py +7 -7
  15. dodal/beamlines/i24.py +100 -145
  16. dodal/beamlines/p38.py +12 -8
  17. dodal/beamlines/p45.py +5 -4
  18. dodal/beamlines/training_rig.py +4 -4
  19. dodal/common/beamlines/beamline_utils.py +2 -3
  20. dodal/common/beamlines/device_helpers.py +3 -1
  21. dodal/devices/aperturescatterguard.py +150 -64
  22. dodal/devices/apple2_undulator.py +86 -113
  23. dodal/devices/eiger.py +24 -14
  24. dodal/devices/fast_grid_scan.py +29 -20
  25. dodal/devices/hutch_shutter.py +25 -12
  26. dodal/devices/i04/transfocator.py +22 -29
  27. dodal/devices/i10/rasor/rasor_scaler_cards.py +4 -4
  28. dodal/devices/i13_1/merlin.py +4 -3
  29. dodal/devices/i13_1/merlin_controller.py +2 -7
  30. dodal/devices/i18/KBMirror.py +19 -0
  31. dodal/devices/i18/diode.py +17 -0
  32. dodal/devices/i18/table.py +14 -0
  33. dodal/devices/i18/thor_labs_stage.py +12 -0
  34. dodal/devices/i19/__init__.py +0 -0
  35. dodal/devices/i19/shutter.py +57 -0
  36. dodal/devices/i22/nxsas.py +4 -4
  37. dodal/devices/motors.py +2 -2
  38. dodal/devices/oav/oav_detector.py +10 -19
  39. dodal/devices/pressure_jump_cell.py +33 -16
  40. dodal/devices/robot.py +30 -11
  41. dodal/devices/tetramm.py +8 -3
  42. dodal/devices/turbo_slit.py +7 -6
  43. dodal/devices/zocalo/zocalo_results.py +21 -4
  44. dodal/plans/save_panda.py +30 -14
  45. dodal/utils.py +54 -15
  46. {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/LICENSE +0 -0
  47. {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/WHEEL +0 -0
  48. {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/entry_points.txt +0 -0
  49. {dls_dodal-1.38.0.dist-info → dls_dodal-1.40.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/p45.py CHANGED
@@ -9,6 +9,7 @@ from dodal.common.beamlines.beamline_utils import (
9
9
  set_path_provider,
10
10
  )
11
11
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
12
+ from dodal.common.beamlines.device_helpers import DET_SUFFIX, HDF5_SUFFIX
12
13
  from dodal.common.visit import StaticVisitPathProvider
13
14
  from dodal.devices.p45 import Choppers, TomoStageWithStretchAndSkew
14
15
  from dodal.log import set_beamline as set_log_beamline
@@ -60,8 +61,8 @@ def det(
60
61
  "-EA-MAP-01:",
61
62
  wait_for_connection,
62
63
  fake_with_ophyd_sim,
63
- drv_suffix="DET:",
64
- hdf_suffix="HDF5:",
64
+ drv_suffix=DET_SUFFIX,
65
+ fileio_suffix=HDF5_SUFFIX,
65
66
  path_provider=get_path_provider(),
66
67
  )
67
68
 
@@ -77,8 +78,8 @@ def diff(
77
78
  "-EA-DIFF-01:",
78
79
  wait_for_connection,
79
80
  fake_with_ophyd_sim,
80
- drv_suffix="DET:",
81
- hdf_suffix="HDF5:",
81
+ drv_suffix=DET_SUFFIX,
82
+ fileio_suffix=HDF5_SUFFIX,
82
83
  path_provider=get_path_provider(),
83
84
  )
84
85
 
@@ -9,7 +9,7 @@ from dodal.common.beamlines.beamline_utils import (
9
9
  set_path_provider,
10
10
  )
11
11
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
12
- from dodal.common.beamlines.device_helpers import HDF5_PREFIX
12
+ from dodal.common.beamlines.device_helpers import DET_SUFFIX, HDF5_SUFFIX
13
13
  from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider
14
14
  from dodal.devices.training_rig.sample_stage import TrainingRigSampleStage
15
15
  from dodal.log import set_beamline as set_log_beamline
@@ -34,7 +34,7 @@ set_utils_beamline(BL)
34
34
  set_path_provider(
35
35
  StaticVisitPathProvider(
36
36
  BL,
37
- Path("/data"),
37
+ Path("/exports/mybeamline/data/2025"),
38
38
  client=LocalDirectoryServiceClient(),
39
39
  )
40
40
  )
@@ -50,8 +50,8 @@ def det() -> AravisDetector:
50
50
  return AravisDetector(
51
51
  f"{PREFIX.beamline_prefix}-EA-DET-01:",
52
52
  path_provider=get_path_provider(),
53
- drv_suffix="DET:",
54
- hdf_suffix=HDF5_PREFIX,
53
+ drv_suffix=DET_SUFFIX,
54
+ fileio_suffix=HDF5_SUFFIX,
55
55
  )
56
56
 
57
57
 
@@ -13,7 +13,6 @@ from dodal.common.types import UpdatingPathProvider
13
13
  from dodal.utils import (
14
14
  AnyDevice,
15
15
  BeamlinePrefix,
16
- D,
17
16
  DeviceInitializationController,
18
17
  SkipType,
19
18
  skip_device,
@@ -141,8 +140,8 @@ def device_factory(
141
140
  SkipType,
142
141
  "mark the factory to be (conditionally) skipped when beamline is imported by external program",
143
142
  ] = False,
144
- ) -> Callable[[Callable[[], D]], DeviceInitializationController[D]]:
145
- def decorator(factory: Callable[[], D]) -> DeviceInitializationController[D]:
143
+ ) -> Callable[[Callable[[], T]], DeviceInitializationController[T]]:
144
+ def decorator(factory: Callable[[], T]) -> DeviceInitializationController[T]:
146
145
  return DeviceInitializationController(
147
146
  factory,
148
147
  use_factory_name,
@@ -2,7 +2,9 @@ from dodal.common.beamlines.beamline_utils import device_instantiation
2
2
  from dodal.devices.slits import Slits
3
3
  from dodal.utils import skip_device
4
4
 
5
- HDF5_PREFIX = "HDF5:"
5
+ HDF5_SUFFIX = "HDF5:"
6
+ CAM_SUFFIX = "CAM:"
7
+ DET_SUFFIX = "DET:"
6
8
 
7
9
 
8
10
  @skip_device()
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
 
5
- from bluesky.protocols import Movable
5
+ from bluesky.protocols import Movable, Preparable
6
6
  from ophyd_async.core import (
7
7
  AsyncStatus,
8
8
  StandardReadable,
@@ -21,6 +21,15 @@ class InvalidApertureMove(Exception):
21
21
  pass
22
22
 
23
23
 
24
+ class _GDAParamApertureValue(StrictEnum):
25
+ """Maps from a short usable name to the value name in the GDA Beamline parameters"""
26
+
27
+ ROBOT_LOAD = "ROBOT_LOAD"
28
+ SMALL = "SMALL_APERTURE"
29
+ MEDIUM = "MEDIUM_APERTURE"
30
+ LARGE = "LARGE_APERTURE"
31
+
32
+
24
33
  class AperturePosition(BaseModel):
25
34
  """
26
35
  Represents one of the available positions for the Aperture-Scatterguard.
@@ -65,7 +74,7 @@ class AperturePosition(BaseModel):
65
74
 
66
75
  @staticmethod
67
76
  def from_gda_params(
68
- name: ApertureValue,
77
+ name: _GDAParamApertureValue,
69
78
  radius: float,
70
79
  params: GDABeamlineParameters,
71
80
  ) -> AperturePosition:
@@ -80,12 +89,16 @@ class AperturePosition(BaseModel):
80
89
 
81
90
 
82
91
  class ApertureValue(StrictEnum):
83
- """Maps from a short usable name to the value name in the GDA Beamline parameters"""
92
+ """The possible apertures that can be selected.
93
+
94
+ Changing these means changing the external paramter model of Hyperion.
95
+ See https://github.com/DiamondLightSource/mx-bluesky/issues/760
96
+ """
84
97
 
85
- ROBOT_LOAD = "ROBOT_LOAD"
86
98
  SMALL = "SMALL_APERTURE"
87
99
  MEDIUM = "MEDIUM_APERTURE"
88
100
  LARGE = "LARGE_APERTURE"
101
+ OUT_OF_BEAM = "Out of beam"
89
102
 
90
103
  def __str__(self):
91
104
  return self.name.capitalize()
@@ -95,22 +108,53 @@ def load_positions_from_beamline_parameters(
95
108
  params: GDABeamlineParameters,
96
109
  ) -> dict[ApertureValue, AperturePosition]:
97
110
  return {
98
- ApertureValue.ROBOT_LOAD: AperturePosition.from_gda_params(
99
- ApertureValue.ROBOT_LOAD, 0, params
111
+ ApertureValue.OUT_OF_BEAM: AperturePosition.from_gda_params(
112
+ _GDAParamApertureValue.ROBOT_LOAD, 0, params
100
113
  ),
101
114
  ApertureValue.SMALL: AperturePosition.from_gda_params(
102
- ApertureValue.SMALL, 20, params
115
+ _GDAParamApertureValue.SMALL, 20, params
103
116
  ),
104
117
  ApertureValue.MEDIUM: AperturePosition.from_gda_params(
105
- ApertureValue.MEDIUM, 50, params
118
+ _GDAParamApertureValue.MEDIUM, 50, params
106
119
  ),
107
120
  ApertureValue.LARGE: AperturePosition.from_gda_params(
108
- ApertureValue.LARGE, 100, params
121
+ _GDAParamApertureValue.LARGE, 100, params
109
122
  ),
110
123
  }
111
124
 
112
125
 
113
- class ApertureScatterguard(StandardReadable, Movable):
126
+ class ApertureScatterguard(StandardReadable, Movable, Preparable):
127
+ """Move the aperture and scatterguard assembly in a safe way. There are two ways to
128
+ interact with the device depending on if you want simplicity or move flexibility.
129
+
130
+ Examples:
131
+ The simple interface is using::
132
+
133
+ await aperture_scatterguard.set(ApertureValue.LARGE)
134
+
135
+ This will move the assembly so that the large aperture is in the beam, regardless
136
+ of where the assembly currently is.
137
+
138
+ We may also want to move the assembly out of the beam with::
139
+
140
+ await aperture_scatterguard.set(ApertureValue.OUT_OF_BEAM)
141
+
142
+ Note, to make sure we do this as quickly as possible, the scatterguard will stay
143
+ in the same position relative to the aperture.
144
+
145
+ We may then want to keep the assembly out of the beam whilst asynchronously preparing
146
+ the other axes for the aperture that's to follow::
147
+
148
+ await aperture_scatterguard.prepare(ApertureValue.LARGE)
149
+
150
+ Then, at a later time, move back into the beam::
151
+
152
+ await aperture_scatterguard.set(ApertureValue.LARGE)
153
+
154
+ Given the prepare has been done this move will now be faster as only the y is
155
+ left to move.
156
+ """
157
+
114
158
  def __init__(
115
159
  self,
116
160
  loaded_positions: dict[ApertureValue, AperturePosition],
@@ -135,6 +179,7 @@ class ApertureScatterguard(StandardReadable, Movable):
135
179
  self.radius,
136
180
  ],
137
181
  )
182
+
138
183
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
139
184
  self.selected_aperture = create_hardware_backed_soft_signal(
140
185
  ApertureValue, self._get_current_aperture_position
@@ -142,15 +187,85 @@ class ApertureScatterguard(StandardReadable, Movable):
142
187
 
143
188
  super().__init__(name)
144
189
 
145
- def get_position_from_gda_aperture_name(
146
- self, gda_aperture_name: str
147
- ) -> ApertureValue:
148
- return ApertureValue(gda_aperture_name)
149
-
150
190
  @AsyncStatus.wrap
151
191
  async def set(self, value: ApertureValue):
192
+ """This set will move the aperture into the beam or move the whole assembly out"""
193
+
152
194
  position = self._loaded_positions[value]
153
- await self._safe_move_within_datacollection_range(position, value)
195
+ await self._check_safe_to_move(position.aperture_z)
196
+
197
+ if value == ApertureValue.OUT_OF_BEAM:
198
+ out_y = self._loaded_positions[ApertureValue.OUT_OF_BEAM].aperture_y
199
+ await self.aperture.y.set(out_y)
200
+ else:
201
+ await self._safe_move_whilst_in_beam(position)
202
+
203
+ async def _check_safe_to_move(self, expected_z_position: float):
204
+ """The assembly is moved (in z) to be under the table when the beamline is not
205
+ in use. If we try and move whilst in the incorrect Z position we will collide
206
+ with the table.
207
+
208
+ Additionally, because there are so many collision possibilities in the device we
209
+ throw an error if any of the axes are already moving.
210
+ """
211
+ current_ap_z = await self.aperture.z.user_readback.get_value()
212
+ diff_on_z = abs(current_ap_z - expected_z_position)
213
+ aperture_z_tolerance = self._tolerances.aperture_z
214
+ if diff_on_z > aperture_z_tolerance:
215
+ raise InvalidApertureMove(
216
+ f"Current aperture z ({current_ap_z}), outside of tolerance ({aperture_z_tolerance}) from target ({expected_z_position})."
217
+ )
218
+
219
+ all_axes = [
220
+ self.aperture.x,
221
+ self.aperture.y,
222
+ self.aperture.z,
223
+ self.scatterguard.x,
224
+ self.scatterguard.y,
225
+ ]
226
+ for axis in all_axes:
227
+ axis_stationary = await axis.motor_done_move.get_value()
228
+ if not axis_stationary:
229
+ raise InvalidApertureMove(
230
+ f"{axis.name} is still moving. Wait for it to finish before"
231
+ "triggering another move."
232
+ )
233
+
234
+ async def _safe_move_whilst_in_beam(self, position: AperturePosition):
235
+ """
236
+ Move the aperture and scatterguard combo safely to a new position.
237
+ See https://github.com/DiamondLightSource/hyperion/wiki/Aperture-Scatterguard-Collisions
238
+ for why this is required. TLDR is that we have a collision at the top of y so we need
239
+ to make sure we move the assembly down before we move the scatterguard up.
240
+ """
241
+ current_ap_y = await self.aperture.y.user_readback.get_value()
242
+
243
+ aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = (
244
+ position.values
245
+ )
246
+
247
+ if aperture_y > current_ap_y:
248
+ # Assembly needs to move up so move the scatterguard down first
249
+ await asyncio.gather(
250
+ self.scatterguard.x.set(scatterguard_x),
251
+ self.scatterguard.y.set(scatterguard_y),
252
+ )
253
+ await asyncio.gather(
254
+ self.aperture.x.set(aperture_x),
255
+ self.aperture.y.set(aperture_y),
256
+ self.aperture.z.set(aperture_z),
257
+ )
258
+ else:
259
+ await asyncio.gather(
260
+ self.aperture.x.set(aperture_x),
261
+ self.aperture.y.set(aperture_y),
262
+ self.aperture.z.set(aperture_z),
263
+ )
264
+
265
+ await asyncio.gather(
266
+ self.scatterguard.x.set(scatterguard_x),
267
+ self.scatterguard.y.set(scatterguard_y),
268
+ )
154
269
 
155
270
  @AsyncStatus.wrap
156
271
  async def _set_raw_unsafe(self, position: AperturePosition):
@@ -167,6 +282,11 @@ class ApertureScatterguard(StandardReadable, Movable):
167
282
  self.scatterguard.y.set(scatterguard_y),
168
283
  )
169
284
 
285
+ async def _is_out_of_beam(self) -> bool:
286
+ current_ap_y = await self.aperture.y.user_readback.get_value()
287
+ out_ap_y = self._loaded_positions[ApertureValue.OUT_OF_BEAM].aperture_y
288
+ return current_ap_y <= out_ap_y + self._tolerances.aperture_y
289
+
170
290
  async def _get_current_aperture_position(self) -> ApertureValue:
171
291
  """
172
292
  Returns the current aperture position using readback values
@@ -174,16 +294,14 @@ class ApertureScatterguard(StandardReadable, Movable):
174
294
  mini aperture y <= ROBOT_LOAD.location.aperture_y + tolerance.
175
295
  If no position is found then raises InvalidApertureMove.
176
296
  """
177
- current_ap_y = await self.aperture.y.user_readback.get_value(cached=False)
178
- robot_load_ap_y = self._loaded_positions[ApertureValue.ROBOT_LOAD].aperture_y
179
297
  if await self.aperture.large.get_value(cached=False) == 1:
180
298
  return ApertureValue.LARGE
181
299
  elif await self.aperture.medium.get_value(cached=False) == 1:
182
300
  return ApertureValue.MEDIUM
183
301
  elif await self.aperture.small.get_value(cached=False) == 1:
184
302
  return ApertureValue.SMALL
185
- elif current_ap_y <= robot_load_ap_y + self._tolerances.aperture_y:
186
- return ApertureValue.ROBOT_LOAD
303
+ elif await self._is_out_of_beam():
304
+ return ApertureValue.OUT_OF_BEAM
187
305
 
188
306
  raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
189
307
 
@@ -191,56 +309,24 @@ class ApertureScatterguard(StandardReadable, Movable):
191
309
  current_value = await self._get_current_aperture_position()
192
310
  return self._loaded_positions[current_value].radius
193
311
 
194
- async def _safe_move_within_datacollection_range(
195
- self, position: AperturePosition, value: ApertureValue
196
- ):
197
- """
198
- Move the aperture and scatterguard combo safely to a new position.
199
- See https://github.com/DiamondLightSource/hyperion/wiki/Aperture-Scatterguard-Collisions
200
- for why this is required.
201
- """
202
- assert self._loaded_positions is not None
203
-
204
- ap_z_in_position = await self.aperture.z.motor_done_move.get_value()
205
- if not ap_z_in_position:
206
- raise InvalidApertureMove(
207
- "ApertureScatterguard z is still moving. Wait for it to finish "
208
- "before triggering another move."
209
- )
312
+ @AsyncStatus.wrap
313
+ async def prepare(self, value: ApertureValue):
314
+ """Moves the assembly to the position for the specified aperture, whilst keeping
315
+ it out of the beam if it already is so.
210
316
 
211
- current_ap_z = await self.aperture.z.user_readback.get_value()
212
- diff_on_z = abs(current_ap_z - position.aperture_z)
213
- if diff_on_z > self._tolerances.aperture_z:
214
- raise InvalidApertureMove(
215
- "ApertureScatterguard safe move is not yet defined for positions "
216
- "outside of LARGE, MEDIUM, SMALL, ROBOT_LOAD. "
217
- f"Current aperture z ({current_ap_z}), outside of tolerance ({self._tolerances.aperture_z}) from target ({position.aperture_z})."
317
+ Moving the assembly whilst out of the beam has no collision risk so we can just
318
+ move all the motors together.
319
+ """
320
+ if await self._is_out_of_beam():
321
+ aperture_x, _, aperture_z, scatterguard_x, scatterguard_y = (
322
+ self._loaded_positions[value].values
218
323
  )
219
324
 
220
- current_ap_y = await self.aperture.y.user_readback.get_value()
221
-
222
- aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = (
223
- position.values
224
- )
225
-
226
- if position.aperture_y > current_ap_y:
227
- await asyncio.gather(
228
- self.scatterguard.x.set(scatterguard_x),
229
- self.scatterguard.y.set(scatterguard_y),
230
- )
231
- await asyncio.gather(
232
- self.aperture.x.set(aperture_x),
233
- self.aperture.y.set(aperture_y),
234
- self.aperture.z.set(aperture_z),
235
- )
236
- else:
237
325
  await asyncio.gather(
238
326
  self.aperture.x.set(aperture_x),
239
- self.aperture.y.set(aperture_y),
240
327
  self.aperture.z.set(aperture_z),
241
- )
242
-
243
- await asyncio.gather(
244
328
  self.scatterguard.x.set(scatterguard_x),
245
329
  self.scatterguard.y.set(scatterguard_y),
246
330
  )
331
+ else:
332
+ await self.set(value)