dls-dodal 1.35.0__py3-none-any.whl → 1.36.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dls-dodal
3
- Version: 1.35.0
3
+ Version: 1.36.0
4
4
  Summary: Ophyd devices and other utils that could be used across DLS beamlines
5
5
  Author-email: Dominic Oram <dominic.oram@diamond.ac.uk>
6
6
  License: Apache License
@@ -233,11 +233,13 @@ Requires-Dist: aiofiles
233
233
  Requires-Dist: aiohttp
234
234
  Requires-Dist: redis
235
235
  Requires-Dist: deepdiff
236
+ Requires-Dist: scanspec >=0.7.3
236
237
  Provides-Extra: dev
237
238
  Requires-Dist: black ; extra == 'dev'
238
239
  Requires-Dist: diff-cover ; extra == 'dev'
239
240
  Requires-Dist: mypy ; extra == 'dev'
240
241
  Requires-Dist: myst-parser ; extra == 'dev'
242
+ Requires-Dist: ophyd-async[sim] ; extra == 'dev'
241
243
  Requires-Dist: pipdeptree ; extra == 'dev'
242
244
  Requires-Dist: pre-commit ; extra == 'dev'
243
245
  Requires-Dist: psutil ; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  dodal/__init__.py,sha256=Ksms_WJF8LTkbm38gEpm1jBpGqcQ8NGvmb2ZJlOE1j8,198
2
2
  dodal/__main__.py,sha256=kP2S2RPitnOWpNGokjZ1Yq-1umOtp5sNOZk2B3tBPLM,111
3
- dodal/_version.py,sha256=q-2n6ovtbk-r5sMORN2VAFD8HDD5aIS9gRmqA5SXG40,413
3
+ dodal/_version.py,sha256=x0utt4SxT0Rvm84NfrkoVlob-vmxNptNpw0IPhaGKKU,413
4
4
  dodal/adsim.py,sha256=OW2dcS7ciD4Yq9WFw4PN_c5Bwccrmu7R-zr-u6ZCbQM,497
5
5
  dodal/cli.py,sha256=_crmaHchxphSW8eEJB58_XZIeK82aiUv9bV7tpz-LpA,2122
6
6
  dodal/log.py,sha256=0to7CRsbzbgVfAAfKRAMhsaUuKqF2-7CGdQc-z8Uhno,9499
@@ -45,7 +45,7 @@ dodal/devices/backlight.py,sha256=nQIr3J-I-OXnOUoWmr3ruy3nhq_q2US1KXC4NrGG_2U,16
45
45
  dodal/devices/cryostream.py,sha256=K-ldpredpeDTzNt4qtQMg99nKJNjBYoXBbK0WJGexzw,656
46
46
  dodal/devices/dcm.py,sha256=cc531sQbed1yX6pWhizUvszLhQczWatMYMZ3cEp3FHA,2559
47
47
  dodal/devices/diamond_filter.py,sha256=A--RHd7WuH-IBhvCyENcRCTP4K-mm_Kqpa0pojpHZow,1098
48
- dodal/devices/eiger.py,sha256=2ZT2oQiZFLprg3CjsHRkXfxt9kSf8z8uUKH1Z5n3Hzo,14107
48
+ dodal/devices/eiger.py,sha256=VGGgkSz8AzSOP3rpehs2GwPHnOGz0OKsRP6U_FYbBA4,15337
49
49
  dodal/devices/eiger_odin.py,sha256=oZl16K-Qb2yL6tK1fyDQvqbbhhvYMSVcf_e2CjlqMa4,7409
50
50
  dodal/devices/fast_grid_scan.py,sha256=DfO4w8ivQJACurYO_OEoXYTAMa2qUc5P-bxYCkjRKkQ,11967
51
51
  dodal/devices/fluorescence_detector_motion.py,sha256=-1qCSvW0PdT0m6BcoLxrtc0OJ5UDIBsEe11EOLr-gFw,501
@@ -136,12 +136,17 @@ dodal/devices/zocalo/zocalo_constants.py,sha256=vu7Xjz7UNEpBUWEEBxDvP4bVFkZIN6NL
136
136
  dodal/devices/zocalo/zocalo_interaction.py,sha256=8V9rAHYgdrFlPURnsxl5pLheEZemsm8cqMpv0E2fOS8,3555
137
137
  dodal/devices/zocalo/zocalo_results.py,sha256=DJWCWKUhMXguUcjb2fU8a86OLp8SnJS9jqbe6EHau-8,15293
138
138
  dodal/parameters/experiment_parameter_base.py,sha256=O7JamfuJ5cYHkPf9tsHJPqn-OMHTAGouigvM1cDFehE,313
139
- dodal/plans/check_topup.py,sha256=3gyLHfHNQBCgEWuAg4QE-ONx7y2Do1vVv5HP8ss0Z1I,5371
140
- dodal/plans/data_session_metadata.py,sha256=GjR4E2s3ZwYkvkBnW6TcL6OLdmyxrmkoSSI242sEPFA,1567
141
- dodal/plans/motor_util_plans.py,sha256=1s02s_Yn7vusv8o64n5p4yiibmQ5ETv8t59CD27h8gw,4655
142
- dls_dodal-1.35.0.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
143
- dls_dodal-1.35.0.dist-info/METADATA,sha256=Ddaa6HHHR3d-IxRoiQS_uiniR2ONfL62B2LVy8b46ns,16574
144
- dls_dodal-1.35.0.dist-info/WHEEL,sha256=a7TGlA-5DaHMRrarXjVbQagU3Man_dCnGIWMJr5kRWo,91
145
- dls_dodal-1.35.0.dist-info/entry_points.txt,sha256=bycw_EKUzup_rxfCetOwcauXV4kLln_OPpPT8jEnr-I,94
146
- dls_dodal-1.35.0.dist-info/top_level.txt,sha256=xIozdmZk_wmMV4wugpq9-6eZs0vgADNUKz3j2UAwlhc,6
147
- dls_dodal-1.35.0.dist-info/RECORD,,
139
+ dodal/plan_stubs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
140
+ dodal/plan_stubs/check_topup.py,sha256=3gyLHfHNQBCgEWuAg4QE-ONx7y2Do1vVv5HP8ss0Z1I,5371
141
+ dodal/plan_stubs/data_session.py,sha256=33wPwbs0mtMnle0H76mH_RNTc5omld7gNSJ9BvRdUnM,1570
142
+ dodal/plan_stubs/motor_utils.py,sha256=4c93U_WgjfmX12uNiztVW2oKxGVWa_SKQdJYCUNmsGU,4653
143
+ dodal/plan_stubs/wrapped.py,sha256=nriHKX4BF010CmrhdoUhY3-txClW5W8TPLz64kE_AXU,4533
144
+ dodal/plans/__init__.py,sha256=nH1jNxw3DzDMg9O8Uda0kqKIalRVEWBrq07OLY6Ey38,93
145
+ dodal/plans/scanspec.py,sha256=Q0AcvTKRT401iGMRDSqK-D523UX5_ofiVMZ_rNXKOx8,2074
146
+ dodal/plans/wrapped.py,sha256=Cr2iOpQCuk2ORKo5CZOh-zbQXAjoTfaLrfm7r1--GhU,2098
147
+ dls_dodal-1.36.0.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
148
+ dls_dodal-1.36.0.dist-info/METADATA,sha256=93qlfEkDtkITxpzgYXuicmGrR30ilk3Te0IcOalpaSs,16655
149
+ dls_dodal-1.36.0.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
150
+ dls_dodal-1.36.0.dist-info/entry_points.txt,sha256=bycw_EKUzup_rxfCetOwcauXV4kLln_OPpPT8jEnr-I,94
151
+ dls_dodal-1.36.0.dist-info/top_level.txt,sha256=xIozdmZk_wmMV4wugpq9-6eZs0vgADNUKz3j2UAwlhc,6
152
+ dls_dodal-1.36.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.4.0)
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
dodal/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.35.0'
16
- __version_tuple__ = version_tuple = (1, 35, 0)
15
+ __version__ = version = '1.36.0'
16
+ __version_tuple__ = version_tuple = (1, 36, 0)
dodal/devices/eiger.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # type: ignore # Eiger will soon be ophyd-async https://github.com/DiamondLightSource/dodal/issues/700
2
+ from dataclasses import dataclass
2
3
  from enum import Enum
3
4
 
4
5
  from ophyd import Component, Device, EpicsSignalRO, Signal
@@ -14,6 +15,15 @@ from dodal.log import LOGGER
14
15
  FREE_RUN_MAX_IMAGES = 1000000
15
16
 
16
17
 
18
+ @dataclass
19
+ class EigerTimeouts:
20
+ stale_params_timeout: int = 60
21
+ general_status_timeout: int = 10
22
+ meta_file_ready_timeout: int = 30
23
+ all_frames_timeout: int = 120
24
+ arming_timeout: int = 60
25
+
26
+
17
27
  class InternalEigerTriggerMode(Enum):
18
28
  INTERNAL_SERIES = 0
19
29
  INTERNAL_ENABLE = 1
@@ -21,6 +31,17 @@ class InternalEigerTriggerMode(Enum):
21
31
  EXTERNAL_ENABLE = 3
22
32
 
23
33
 
34
+ AVAILABLE_TIMEOUTS = {
35
+ "i03": EigerTimeouts(
36
+ stale_params_timeout=60,
37
+ general_status_timeout=10,
38
+ meta_file_ready_timeout=30,
39
+ all_frames_timeout=120, # Long timeout for meta file to compensate for filesystem issues
40
+ arming_timeout=60,
41
+ )
42
+ }
43
+
44
+
24
45
  class EigerDetector(Device):
25
46
  class ArmingSignal(Signal):
26
47
  def set(self, value, *, timeout=None, settle_time=None, **kwargs):
@@ -34,13 +55,6 @@ class EigerDetector(Device):
34
55
  stale_params = Component(EpicsSignalRO, "CAM:StaleParameters_RBV")
35
56
  bit_depth = Component(EpicsSignalRO, "CAM:BitDepthImage_RBV")
36
57
 
37
- STALE_PARAMS_TIMEOUT = 60
38
- GENERAL_STATUS_TIMEOUT = 10
39
- # Long timeout for meta file to compensate for filesystem issues
40
- META_FILE_READY_TIMEOUT = 30
41
- ALL_FRAMES_TIMEOUT = 120
42
- ARMING_TIMEOUT = 60
43
-
44
58
  filewriters_finished: StatusBase
45
59
 
46
60
  detector_params: DetectorParams | None = None
@@ -48,13 +62,20 @@ class EigerDetector(Device):
48
62
  arming_status = Status()
49
63
  arming_status.set_finished()
50
64
 
65
+ def __init__(self, beamline: str = "i03", *args, **kwargs):
66
+ super().__init__(*args, **kwargs)
67
+ self.beamline = beamline
68
+ # using i03 timeouts as default
69
+ self.timeouts = AVAILABLE_TIMEOUTS.get(beamline, AVAILABLE_TIMEOUTS["i03"])
70
+
51
71
  @classmethod
52
72
  def with_params(
53
73
  cls,
54
74
  params: DetectorParams,
55
75
  name: str = "EigerDetector",
76
+ beamline: str = "i03",
56
77
  ):
57
- det = cls(name=name)
78
+ det = cls(name=name, beamline=beamline)
58
79
  det.set_detector_parameters(params)
59
80
  return det
60
81
 
@@ -82,7 +103,7 @@ class EigerDetector(Device):
82
103
  def async_stage(self):
83
104
  self.odin.nodes.clear_odin_errors()
84
105
  status_ok, error_message = self.odin.wait_for_odin_initialised(
85
- self.GENERAL_STATUS_TIMEOUT
106
+ self.timeouts.general_status_timeout
86
107
  )
87
108
  if not status_ok:
88
109
  raise Exception(f"Odin not initialised: {error_message}")
@@ -96,14 +117,14 @@ class EigerDetector(Device):
96
117
  def wait_on_arming_if_started(self):
97
118
  if not self.arming_status.done:
98
119
  LOGGER.info("Waiting for arming to finish")
99
- self.arming_status.wait(self.ARMING_TIMEOUT)
120
+ self.arming_status.wait(self.timeouts.arming_timeout)
100
121
 
101
122
  def stage(self):
102
123
  self.wait_on_arming_if_started()
103
124
  if not self.is_armed():
104
125
  LOGGER.info("Eiger not armed, arming")
105
126
 
106
- self.async_stage().wait(timeout=self.ARMING_TIMEOUT)
127
+ self.async_stage().wait(timeout=self.timeouts.arming_timeout)
107
128
 
108
129
  def stop_odin_when_all_frames_collected(self):
109
130
  LOGGER.info("Waiting on all frames")
@@ -111,7 +132,7 @@ class EigerDetector(Device):
111
132
  await_value(
112
133
  self.odin.file_writer.num_captured,
113
134
  self.detector_params.full_number_of_images,
114
- ).wait(self.ALL_FRAMES_TIMEOUT)
135
+ ).wait(self.timeouts.all_frames_timeout)
115
136
  finally:
116
137
  LOGGER.info("Stopping Odin")
117
138
  self.odin.stop().wait(5)
@@ -124,7 +145,9 @@ class EigerDetector(Device):
124
145
  # In free run mode we have to manually stop odin
125
146
  self.stop_odin_when_all_frames_collected()
126
147
 
127
- self.odin.file_writer.start_timeout.set(1).wait(self.GENERAL_STATUS_TIMEOUT)
148
+ self.odin.file_writer.start_timeout.set(1).wait(
149
+ self.timeouts.general_status_timeout
150
+ )
128
151
  LOGGER.info("Waiting on filewriter to finish")
129
152
  self.filewriters_finished.wait(30)
130
153
 
@@ -132,7 +155,7 @@ class EigerDetector(Device):
132
155
  finally:
133
156
  self.disarm_detector()
134
157
  status_ok = self.odin.check_and_wait_for_odin_state(
135
- self.GENERAL_STATUS_TIMEOUT
158
+ self.timeouts.general_status_timeout
136
159
  )
137
160
  self.disable_roi_mode()
138
161
  return status_ok
@@ -142,10 +165,12 @@ class EigerDetector(Device):
142
165
  LOGGER.info("Eiger stop() called - cleaning up...")
143
166
  self.wait_on_arming_if_started()
144
167
  stop_status = self.odin.stop()
145
- self.odin.file_writer.start_timeout.set(1).wait(self.GENERAL_STATUS_TIMEOUT)
168
+ self.odin.file_writer.start_timeout.set(1).wait(
169
+ self.timeouts.general_status_timeout
170
+ )
146
171
  self.disarm_detector()
147
172
  stop_status &= self.disable_roi_mode()
148
- stop_status.wait(self.GENERAL_STATUS_TIMEOUT)
173
+ stop_status.wait(self.timeouts.general_status_timeout)
149
174
  # See https://github.com/DiamondLightSource/hyperion/issues/1395
150
175
  LOGGER.info("Turning off Eiger dev/shm streaming")
151
176
  self.odin.fan.dev_shm_enable.set(0).wait()
@@ -166,19 +191,19 @@ class EigerDetector(Device):
166
191
  )
167
192
 
168
193
  status = self.cam.roi_mode.set(
169
- 1 if enable else 0, timeout=self.GENERAL_STATUS_TIMEOUT
194
+ 1 if enable else 0, timeout=self.timeouts.general_status_timeout
170
195
  )
171
196
  status &= self.odin.file_writer.image_height.set(
172
- detector_dimensions.height, timeout=self.GENERAL_STATUS_TIMEOUT
197
+ detector_dimensions.height, timeout=self.timeouts.general_status_timeout
173
198
  )
174
199
  status &= self.odin.file_writer.image_width.set(
175
- detector_dimensions.width, timeout=self.GENERAL_STATUS_TIMEOUT
200
+ detector_dimensions.width, timeout=self.timeouts.general_status_timeout
176
201
  )
177
202
  status &= self.odin.file_writer.num_row_chunks.set(
178
- detector_dimensions.height, timeout=self.GENERAL_STATUS_TIMEOUT
203
+ detector_dimensions.height, timeout=self.timeouts.general_status_timeout
179
204
  )
180
205
  status &= self.odin.file_writer.num_col_chunks.set(
181
- detector_dimensions.width, timeout=self.GENERAL_STATUS_TIMEOUT
206
+ detector_dimensions.width, timeout=self.timeouts.general_status_timeout
182
207
  )
183
208
 
184
209
  return status
@@ -186,25 +211,29 @@ class EigerDetector(Device):
186
211
  def set_cam_pvs(self) -> AndStatus:
187
212
  assert self.detector_params is not None
188
213
  status = self.cam.acquire_time.set(
189
- self.detector_params.exposure_time, timeout=self.GENERAL_STATUS_TIMEOUT
214
+ self.detector_params.exposure_time,
215
+ timeout=self.timeouts.general_status_timeout,
190
216
  )
191
217
  status &= self.cam.acquire_period.set(
192
- self.detector_params.exposure_time, timeout=self.GENERAL_STATUS_TIMEOUT
218
+ self.detector_params.exposure_time,
219
+ timeout=self.timeouts.general_status_timeout,
220
+ )
221
+ status &= self.cam.num_exposures.set(
222
+ 1, timeout=self.timeouts.general_status_timeout
193
223
  )
194
- status &= self.cam.num_exposures.set(1, timeout=self.GENERAL_STATUS_TIMEOUT)
195
224
  status &= self.cam.image_mode.set(
196
- self.cam.ImageMode.MULTIPLE, timeout=self.GENERAL_STATUS_TIMEOUT
225
+ self.cam.ImageMode.MULTIPLE, timeout=self.timeouts.general_status_timeout
197
226
  )
198
227
  status &= self.cam.trigger_mode.set(
199
228
  InternalEigerTriggerMode.EXTERNAL_SERIES.value,
200
- timeout=self.GENERAL_STATUS_TIMEOUT,
229
+ timeout=self.timeouts.general_status_timeout,
201
230
  )
202
231
  return status
203
232
 
204
233
  def set_odin_number_of_frame_chunks(self) -> Status:
205
234
  assert self.detector_params is not None
206
235
  status = self.odin.file_writer.num_frames_chunks.set(
207
- 1, timeout=self.GENERAL_STATUS_TIMEOUT
236
+ 1, timeout=self.timeouts.general_status_timeout
208
237
  )
209
238
  return status
210
239
 
@@ -212,16 +241,20 @@ class EigerDetector(Device):
212
241
  assert self.detector_params is not None
213
242
  file_prefix = self.detector_params.full_filename
214
243
  status = self.odin.file_writer.file_path.set(
215
- self.detector_params.directory, timeout=self.GENERAL_STATUS_TIMEOUT
244
+ self.detector_params.directory, timeout=self.timeouts.general_status_timeout
216
245
  )
217
246
  status &= self.odin.file_writer.file_name.set(
218
- file_prefix, timeout=self.GENERAL_STATUS_TIMEOUT
247
+ file_prefix, timeout=self.timeouts.general_status_timeout
219
248
  )
220
249
  status &= await_value(
221
- self.odin.meta.file_name, file_prefix, timeout=self.GENERAL_STATUS_TIMEOUT
250
+ self.odin.meta.file_name,
251
+ file_prefix,
252
+ timeout=self.timeouts.general_status_timeout,
222
253
  )
223
254
  status &= await_value(
224
- self.odin.file_writer.id, file_prefix, timeout=self.GENERAL_STATUS_TIMEOUT
255
+ self.odin.file_writer.id,
256
+ file_prefix,
257
+ timeout=self.timeouts.general_status_timeout,
225
258
  )
226
259
  return status
227
260
 
@@ -231,19 +264,22 @@ class EigerDetector(Device):
231
264
  self.detector_params.detector_distance
232
265
  )
233
266
  status = self.cam.beam_center_x.set(
234
- beam_x_pixels, timeout=self.GENERAL_STATUS_TIMEOUT
267
+ beam_x_pixels, timeout=self.timeouts.general_status_timeout
235
268
  )
236
269
  status &= self.cam.beam_center_y.set(
237
- beam_y_pixels, timeout=self.GENERAL_STATUS_TIMEOUT
270
+ beam_y_pixels, timeout=self.timeouts.general_status_timeout
238
271
  )
239
272
  status &= self.cam.det_distance.set(
240
- self.detector_params.detector_distance, timeout=self.GENERAL_STATUS_TIMEOUT
273
+ self.detector_params.detector_distance,
274
+ timeout=self.timeouts.general_status_timeout,
241
275
  )
242
276
  status &= self.cam.omega_start.set(
243
- self.detector_params.omega_start, timeout=self.GENERAL_STATUS_TIMEOUT
277
+ self.detector_params.omega_start,
278
+ timeout=self.timeouts.general_status_timeout,
244
279
  )
245
280
  status &= self.cam.omega_incr.set(
246
- self.detector_params.omega_increment, timeout=self.GENERAL_STATUS_TIMEOUT
281
+ self.detector_params.omega_increment,
282
+ timeout=self.timeouts.general_status_timeout,
247
283
  )
248
284
  return status
249
285
 
@@ -259,7 +295,7 @@ class EigerDetector(Device):
259
295
  current_energy = self.cam.photon_energy.get()
260
296
  if abs(current_energy - energy) > tolerance:
261
297
  return self.cam.photon_energy.set(
262
- energy, timeout=self.GENERAL_STATUS_TIMEOUT
298
+ energy, timeout=self.timeouts.general_status_timeout
263
299
  )
264
300
  else:
265
301
  status = Status()
@@ -275,45 +311,46 @@ class EigerDetector(Device):
275
311
  assert self.detector_params is not None
276
312
  status = self.cam.num_images.set(
277
313
  self.detector_params.num_images_per_trigger,
278
- timeout=self.GENERAL_STATUS_TIMEOUT,
314
+ timeout=self.timeouts.general_status_timeout,
279
315
  )
280
316
  if self.detector_params.trigger_mode == TriggerMode.FREE_RUN:
281
317
  # The Eiger can't actually free run so we set a very large number of frames
282
318
  status &= self.cam.num_triggers.set(
283
- FREE_RUN_MAX_IMAGES, timeout=self.GENERAL_STATUS_TIMEOUT
319
+ FREE_RUN_MAX_IMAGES, timeout=self.timeouts.general_status_timeout
284
320
  )
285
321
  # Setting Odin to write 0 frames tells it to write until externally stopped
286
322
  status &= self.odin.file_writer.num_capture.set(
287
- 0, timeout=self.GENERAL_STATUS_TIMEOUT
323
+ 0, timeout=self.timeouts.general_status_timeout
288
324
  )
289
325
  elif self.detector_params.trigger_mode == TriggerMode.SET_FRAMES:
290
326
  status &= self.cam.num_triggers.set(
291
- self.detector_params.num_triggers, timeout=self.GENERAL_STATUS_TIMEOUT
327
+ self.detector_params.num_triggers,
328
+ timeout=self.timeouts.general_status_timeout,
292
329
  )
293
330
  status &= self.odin.file_writer.num_capture.set(
294
331
  self.detector_params.full_number_of_images,
295
- timeout=self.GENERAL_STATUS_TIMEOUT,
332
+ timeout=self.timeouts.general_status_timeout,
296
333
  )
297
334
 
298
335
  return status
299
336
 
300
337
  def _wait_for_odin_status(self) -> StatusBase:
301
338
  self.forward_bit_depth_to_filewriter()
302
- await_value(self.odin.meta.active, 1).wait(self.GENERAL_STATUS_TIMEOUT)
339
+ await_value(self.odin.meta.active, 1).wait(self.timeouts.general_status_timeout)
303
340
 
304
341
  status = self.odin.file_writer.capture.set(
305
- 1, timeout=self.GENERAL_STATUS_TIMEOUT
342
+ 1, timeout=self.timeouts.general_status_timeout
306
343
  )
307
344
  LOGGER.info("Eiger staging: awaiting odin metadata")
308
345
  status &= await_value(
309
- self.odin.meta.ready, 1, timeout=self.META_FILE_READY_TIMEOUT
346
+ self.odin.meta.ready, 1, timeout=self.timeouts.meta_file_ready_timeout
310
347
  )
311
348
  return status
312
349
 
313
350
  def _wait_fan_ready(self) -> StatusBase:
314
351
  self.filewriters_finished = self.odin.create_finished_status()
315
352
  LOGGER.info("Eiger staging: awaiting odin fan ready")
316
- return await_value(self.odin.fan.ready, 1, self.GENERAL_STATUS_TIMEOUT)
353
+ return await_value(self.odin.fan.ready, 1, self.timeouts.general_status_timeout)
317
354
 
318
355
  def _finish_arm(self) -> Status:
319
356
  LOGGER.info("Eiger staging: Finishing arming")
@@ -324,7 +361,7 @@ class EigerDetector(Device):
324
361
  def forward_bit_depth_to_filewriter(self):
325
362
  bit_depth = self.bit_depth.get()
326
363
  self.odin.file_writer.data_type.set(f"UInt{bit_depth}").wait(
327
- self.GENERAL_STATUS_TIMEOUT
364
+ self.timeouts.general_status_timeout
328
365
  )
329
366
 
330
367
  def change_dev_shm(self, enable_dev_shm: bool):
@@ -332,7 +369,7 @@ class EigerDetector(Device):
332
369
  return self.odin.fan.dev_shm_enable.set(1 if enable_dev_shm else 0)
333
370
 
334
371
  def disarm_detector(self):
335
- self.cam.acquire.set(0).wait(self.GENERAL_STATUS_TIMEOUT)
372
+ self.cam.acquire.set(0).wait(self.timeouts.general_status_timeout)
336
373
 
337
374
  def do_arming_chain(self) -> Status:
338
375
  functions_to_do_arm = []
@@ -355,7 +392,9 @@ class EigerDetector(Device):
355
392
  self.set_num_triggers_and_captures,
356
393
  lambda: await_value(self.stale_params, 0, 60),
357
394
  self._wait_for_odin_status,
358
- lambda: self.cam.acquire.set(1, timeout=self.GENERAL_STATUS_TIMEOUT),
395
+ lambda: self.cam.acquire.set(
396
+ 1, timeout=self.timeouts.general_status_timeout
397
+ ),
359
398
  self._wait_fan_ready,
360
399
  self._finish_arm,
361
400
  ]
File without changes
@@ -2,7 +2,7 @@ from bluesky import plan_stubs as bps
2
2
  from bluesky import preprocessors as bpp
3
3
  from bluesky.utils import MsgGenerator, make_decorator
4
4
 
5
- from dodal.common.beamlines import beamline_utils
5
+ from dodal.common.beamlines.beamline_utils import get_path_provider
6
6
  from dodal.common.types import UpdatingPathProvider
7
7
 
8
8
  DATA_SESSION = "data_session"
@@ -31,7 +31,7 @@ def attach_data_session_metadata_wrapper(
31
31
  Iterator[Msg]: Plan messages
32
32
  """
33
33
  if provider is None:
34
- provider = beamline_utils.get_path_provider()
34
+ provider = get_path_provider()
35
35
  yield from bps.wait_for([provider.update])
36
36
  ress = yield from bps.wait_for([provider.data_session])
37
37
  data_session = ress[0].result()
@@ -23,7 +23,7 @@ class MoveTooLarge(Exception):
23
23
  super().__init__(*args)
24
24
 
25
25
 
26
- def _check_and_cache_values(
26
+ def check_and_cache_values(
27
27
  devices_and_positions: dict[MovableReadableDevice, float],
28
28
  smallest_move: float,
29
29
  maximum_move: float,
@@ -89,7 +89,7 @@ def move_and_reset_wrapper(
89
89
  on. If false it is left up to the caller to wait on
90
90
  them. Defaults to True.
91
91
  """
92
- initial_positions = yield from _check_and_cache_values(
92
+ initial_positions = yield from check_and_cache_values(
93
93
  device_and_positions, smallest_move, maximum_move
94
94
  )
95
95
 
@@ -0,0 +1,150 @@
1
+ import itertools
2
+ from collections.abc import Mapping
3
+ from typing import Annotated, Any
4
+
5
+ import bluesky.plan_stubs as bps
6
+ from bluesky.protocols import Movable
7
+ from bluesky.utils import MsgGenerator
8
+
9
+ """
10
+ Wrappers for Bluesky built-in plan stubs with type hinting
11
+ """
12
+
13
+ Group = Annotated[str, "String identifier used by 'wait' or stubs that await"]
14
+
15
+
16
+ # After bluesky 1.14, bounds for stubs that move can be narrowed
17
+ # https://github.com/bluesky/bluesky/issues/1821
18
+ def set_absolute(
19
+ movable: Movable, value: Any, group: Group | None = None, wait: bool = False
20
+ ) -> MsgGenerator:
21
+ """
22
+ Set a device, wrapper for `bp.abs_set`.
23
+
24
+ Args:
25
+ movable (Movable): The device to set
26
+ value (T): The new value
27
+ group (Group | None, optional): The message group to associate with the
28
+ setting, for sequencing. Defaults to None.
29
+ wait (bool, optional): The group should wait until all setting is complete
30
+ (e.g. a motor has finished moving). Defaults to False.
31
+
32
+ Returns:
33
+ MsgGenerator: Plan
34
+
35
+ Yields:
36
+ Iterator[MsgGenerator]: Bluesky messages
37
+ """
38
+ return (yield from bps.abs_set(movable, value, group=group, wait=wait))
39
+
40
+
41
+ def set_relative(
42
+ movable: Movable, value: Any, group: Group | None = None, wait: bool = False
43
+ ) -> MsgGenerator:
44
+ """
45
+ Change a device, wrapper for `bp.rel_set`.
46
+
47
+ Args:
48
+ movable (Movable): The device to set
49
+ value (T): The new value
50
+ group (Group | None, optional): The message group to associate with the
51
+ setting, for sequencing. Defaults to None.
52
+ wait (bool, optional): The group should wait until all setting is complete
53
+ (e.g. a motor has finished moving). Defaults to False.
54
+
55
+ Returns:
56
+ MsgGenerator: Plan
57
+
58
+ Yields:
59
+ Iterator[MsgGenerator]: Bluesky messages
60
+ """
61
+
62
+ return (yield from bps.rel_set(movable, value, group=group, wait=wait))
63
+
64
+
65
+ def move(moves: Mapping[Movable, Any], group: Group | None = None) -> MsgGenerator:
66
+ """
67
+ Move a device, wrapper for `bp.mv`.
68
+
69
+ Args:
70
+ moves (Mapping[Movable, T]): Mapping of Movables to target positions
71
+ group (Group | None, optional): The message group to associate with the
72
+ setting, for sequencing. Defaults to None.
73
+
74
+ Returns:
75
+ MsgGenerator: Plan
76
+
77
+ Yields:
78
+ Iterator[MsgGenerator]: Bluesky messages
79
+ """
80
+
81
+ return (
82
+ # type ignore until https://github.com/bluesky/bluesky/issues/1809
83
+ yield from bps.mv(*itertools.chain.from_iterable(moves.items()), group=group) # type: ignore
84
+ )
85
+
86
+
87
+ def move_relative(
88
+ moves: Mapping[Movable, Any], group: Group | None = None
89
+ ) -> MsgGenerator:
90
+ """
91
+ Move a device relative to its current position, wrapper for `bp.mvr`.
92
+
93
+ Args:
94
+ moves (Mapping[Movable, T]): Mapping of Movables to target deltas
95
+ group (Group | None, optional): The message group to associate with the
96
+ setting, for sequencing. Defaults to None.
97
+
98
+ Returns:
99
+ MsgGenerator: Plan
100
+
101
+ Yields:
102
+ Iterator[MsgGenerator]: Bluesky messages
103
+ """
104
+
105
+ return (
106
+ # type ignore until https://github.com/bluesky/bluesky/issues/1809
107
+ yield from bps.mvr(*itertools.chain.from_iterable(moves.items()), group=group) # type: ignore
108
+ )
109
+
110
+
111
+ def sleep(time: float) -> MsgGenerator:
112
+ """
113
+ Suspend all action for a given time, wrapper for `bp.sleep`
114
+
115
+ Args:
116
+ time (float): Time to wait in seconds
117
+
118
+ Returns:
119
+ MsgGenerator: Plan
120
+
121
+ Yields:
122
+ Iterator[MsgGenerator]: Bluesky messages
123
+ """
124
+
125
+ return (yield from bps.sleep(time))
126
+
127
+
128
+ def wait(
129
+ group: Group | None = None,
130
+ timeout: float | None = None,
131
+ ) -> MsgGenerator:
132
+ """
133
+ Wait for a group status to complete, wrapper for `bp.wait`.
134
+ Does not expose move_on, as when used as a stub will not fail on Timeout.
135
+
136
+ Args:
137
+ group (Group | None, optional): The name of the group to wait for, defaults
138
+ to None, in which case waits for all
139
+ groups that have not yet been awaited.
140
+ timeout (float | None, default=None): a timeout in seconds
141
+
142
+
143
+ Returns:
144
+ MsgGenerator: Plan
145
+
146
+ Yields:
147
+ Iterator[MsgGenerator]: Bluesky messages
148
+ """
149
+
150
+ return (yield from bps.wait(group, timeout=timeout))
@@ -0,0 +1,4 @@
1
+ from .scanspec import spec_scan
2
+ from .wrapped import count
3
+
4
+ __all__ = ["count", "spec_scan"]
@@ -0,0 +1,66 @@
1
+ import operator
2
+ from functools import reduce
3
+ from typing import Annotated, Any
4
+
5
+ import bluesky.plans as bp
6
+ from bluesky.protocols import Movable, Readable
7
+ from cycler import Cycler, cycler
8
+ from pydantic import Field, validate_call
9
+ from scanspec.specs import Spec
10
+
11
+ from dodal.common import MsgGenerator
12
+ from dodal.plan_stubs.data_session import attach_data_session_metadata_decorator
13
+
14
+
15
+ @attach_data_session_metadata_decorator()
16
+ @validate_call(config={"arbitrary_types_allowed": True})
17
+ def spec_scan(
18
+ detectors: Annotated[
19
+ set[Readable],
20
+ Field(
21
+ description="Set of readable devices, will take a reading at each point, \
22
+ in addition to any Movables in the Spec",
23
+ ),
24
+ ],
25
+ spec: Annotated[
26
+ Spec[Movable],
27
+ Field(description="ScanSpec modelling the path of the scan"),
28
+ ],
29
+ metadata: dict[str, Any] | None = None,
30
+ ) -> MsgGenerator:
31
+ """Generic plan for reading `detectors` at every point of a ScanSpec `Spec`.
32
+ A `Spec` is an N-dimensional path.
33
+ """
34
+ # TODO: https://github.com/bluesky/scanspec/issues/154
35
+ # support Static.duration: Spec[Literal["DURATION"]]
36
+
37
+ _md = {
38
+ "plan_args": {
39
+ "detectors": {det.name for det in detectors},
40
+ "spec": repr(spec),
41
+ },
42
+ "plan_name": "spec_scan",
43
+ "shape": spec.shape(),
44
+ **(metadata or {}),
45
+ }
46
+
47
+ yield from bp.scan_nd(tuple(detectors), _as_cycler(spec), md=_md)
48
+
49
+
50
+ def _as_cycler(spec: Spec[Movable]) -> Cycler:
51
+ """
52
+ Convert a scanspec to a cycler for compatibility with legacy Bluesky plans such as
53
+ `bp.scan_nd`. Use the midpoints of the scanspec since cyclers are normally used
54
+ for software triggered scans.
55
+
56
+ Args:
57
+ spec: A scanspec
58
+
59
+ Returns:
60
+ Cycler: A new cycler
61
+ """
62
+
63
+ midpoints = spec.frames().midpoints
64
+ # Need to "add" the cyclers for all the axes together. The code below is
65
+ # effectively: cycler(motor1, [...]) + cycler(motor2, [...]) + ...
66
+ return reduce(operator.add, (cycler(*args) for args in midpoints.items()))
dodal/plans/wrapped.py ADDED
@@ -0,0 +1,57 @@
1
+ from collections.abc import Sequence
2
+ from typing import Annotated, Any
3
+
4
+ import bluesky.plans as bp
5
+ from bluesky.protocols import Readable
6
+ from pydantic import Field, NonNegativeFloat, validate_call
7
+
8
+ from dodal.common import MsgGenerator
9
+ from dodal.plan_stubs.data_session import attach_data_session_metadata_decorator
10
+
11
+ """This module wraps plan(s) from bluesky.plans until required handling for them is
12
+ moved into bluesky or better handled in downstream services.
13
+
14
+ Required decorators are installed on plan import
15
+ https://github.com/DiamondLightSource/blueapi/issues/474
16
+
17
+ Non-serialisable fields are ignored when they are optional
18
+ https://github.com/DiamondLightSource/blueapi/issues/711
19
+
20
+ We may also need other adjustments for UI purposes, e.g.
21
+ Forcing uniqueness or orderedness of Readables
22
+ Limits and metadata (e.g. units)
23
+ """
24
+
25
+
26
+ @attach_data_session_metadata_decorator()
27
+ @validate_call(config={"arbitrary_types_allowed": True})
28
+ def count(
29
+ detectors: Annotated[
30
+ set[Readable],
31
+ Field(
32
+ description="Set of readable devices, will take a reading at each point",
33
+ min_length=1,
34
+ ),
35
+ ],
36
+ num: Annotated[int, Field(description="Number of frames to collect", ge=1)] = 1,
37
+ delay: Annotated[
38
+ NonNegativeFloat | Sequence[NonNegativeFloat],
39
+ Field(
40
+ description="Delay between readings: if tuple, len(delay) == num - 1 and \
41
+ the delays are between each point, if value or None is the delay for every \
42
+ gap",
43
+ json_schema_extra={"units": "s"},
44
+ ),
45
+ ] = 0.0,
46
+ metadata: dict[str, Any] | None = None,
47
+ ) -> MsgGenerator:
48
+ """Reads from a number of devices.
49
+ Wraps bluesky.plans.count(det, num, delay, md=metadata) exposing only serializable
50
+ parameters and metadata."""
51
+ if isinstance(delay, Sequence):
52
+ assert (
53
+ len(delay) == num - 1
54
+ ), f"Number of delays given must be {num - 1}: was given {len(delay)}"
55
+ metadata = metadata or {}
56
+ metadata["shape"] = (num,)
57
+ yield from bp.count(tuple(detectors), num, delay=delay, md=metadata)
File without changes