dls-dodal 1.29.4__py3-none-any.whl → 1.30.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 (85) hide show
  1. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/METADATA +27 -42
  2. dls_dodal-1.30.0.dist-info/RECORD +132 -0
  3. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/WHEEL +1 -1
  4. dls_dodal-1.30.0.dist-info/entry_points.txt +3 -0
  5. dodal/__init__.py +1 -4
  6. dodal/_version.py +2 -2
  7. dodal/beamlines/__init__.py +3 -1
  8. dodal/beamlines/i03.py +28 -23
  9. dodal/beamlines/i04.py +34 -12
  10. dodal/beamlines/i13_1.py +66 -0
  11. dodal/beamlines/i22.py +5 -5
  12. dodal/beamlines/i24.py +1 -1
  13. dodal/beamlines/p38.py +7 -7
  14. dodal/beamlines/p45.py +7 -5
  15. dodal/beamlines/p99.py +61 -0
  16. dodal/cli.py +6 -3
  17. dodal/common/beamlines/beamline_parameters.py +2 -2
  18. dodal/common/beamlines/beamline_utils.py +6 -5
  19. dodal/common/maths.py +1 -3
  20. dodal/common/types.py +2 -3
  21. dodal/common/udc_directory_provider.py +14 -3
  22. dodal/common/visit.py +2 -3
  23. dodal/devices/CTAB.py +22 -17
  24. dodal/devices/aperturescatterguard.py +114 -136
  25. dodal/devices/areadetector/adaravis.py +8 -6
  26. dodal/devices/areadetector/adsim.py +2 -3
  27. dodal/devices/areadetector/adutils.py +20 -12
  28. dodal/devices/cryostream.py +19 -7
  29. dodal/devices/detector/__init__.py +13 -2
  30. dodal/devices/detector/det_dim_constants.py +2 -2
  31. dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
  32. dodal/devices/detector/detector.py +5 -5
  33. dodal/devices/detector/detector_motion.py +38 -31
  34. dodal/devices/eiger.py +11 -15
  35. dodal/devices/eiger_odin.py +9 -10
  36. dodal/devices/fast_grid_scan.py +4 -3
  37. dodal/devices/fluorescence_detector_motion.py +13 -4
  38. dodal/devices/focusing_mirror.py +4 -4
  39. dodal/devices/hutch_shutter.py +4 -4
  40. dodal/devices/i22/dcm.py +4 -3
  41. dodal/devices/i22/fswitch.py +4 -4
  42. dodal/devices/i22/nxsas.py +23 -32
  43. dodal/devices/i24/pmac.py +47 -8
  44. dodal/devices/ipin.py +7 -4
  45. dodal/devices/linkam3.py +11 -5
  46. dodal/devices/logging_ophyd_device.py +1 -1
  47. dodal/devices/motors.py +31 -5
  48. dodal/devices/oav/grid_overlay.py +1 -0
  49. dodal/devices/oav/microns_for_zoom_levels.json +1 -1
  50. dodal/devices/oav/oav_detector.py +2 -1
  51. dodal/devices/oav/oav_parameters.py +18 -10
  52. dodal/devices/oav/oav_to_redis_forwarder.py +100 -0
  53. dodal/devices/oav/pin_image_recognition/__init__.py +6 -6
  54. dodal/devices/oav/pin_image_recognition/utils.py +5 -6
  55. dodal/devices/oav/utils.py +2 -2
  56. dodal/devices/p99/__init__.py +0 -0
  57. dodal/devices/p99/sample_stage.py +43 -0
  58. dodal/devices/robot.py +30 -18
  59. dodal/devices/scintillator.py +8 -5
  60. dodal/devices/smargon.py +3 -3
  61. dodal/devices/status.py +2 -31
  62. dodal/devices/tetramm.py +4 -4
  63. dodal/devices/thawer.py +5 -3
  64. dodal/devices/undulator_dcm.py +6 -8
  65. dodal/devices/util/adjuster_plans.py +2 -2
  66. dodal/devices/util/epics_util.py +5 -7
  67. dodal/devices/util/lookup_tables.py +2 -3
  68. dodal/devices/util/save_panda.py +87 -0
  69. dodal/devices/util/test_utils.py +17 -0
  70. dodal/devices/webcam.py +3 -3
  71. dodal/devices/xbpm_feedback.py +0 -23
  72. dodal/devices/zebra.py +10 -10
  73. dodal/devices/zebra_controlled_shutter.py +3 -3
  74. dodal/devices/zocalo/zocalo_interaction.py +10 -2
  75. dodal/devices/zocalo/zocalo_results.py +31 -18
  76. dodal/log.py +14 -5
  77. dodal/plans/data_session_metadata.py +1 -0
  78. dodal/plans/motor_util_plans.py +117 -0
  79. dodal/utils.py +65 -22
  80. dls_dodal-1.29.4.dist-info/RECORD +0 -125
  81. dls_dodal-1.29.4.dist-info/entry_points.txt +0 -2
  82. dodal/devices/qbpm1.py +0 -8
  83. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/LICENSE +0 -0
  84. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/top_level.txt +0 -0
  85. /dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +0 -0
@@ -1,15 +1,16 @@
1
1
  import asyncio
2
- from collections import OrderedDict, namedtuple
2
+ from collections import namedtuple
3
3
  from dataclasses import asdict, dataclass
4
4
  from enum import Enum
5
5
 
6
6
  from bluesky.protocols import Movable, Reading
7
- from ophyd_async.core import AsyncStatus, SignalR, StandardReadable
7
+ from event_model.documents.event_descriptor import DataKey
8
+ from ophyd_async.core import AsyncStatus, HintedSignal, SignalR, StandardReadable
8
9
  from ophyd_async.core.soft_signal_backend import SoftConverter, SoftSignalBackend
9
10
 
11
+ from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
10
12
  from dodal.devices.aperture import Aperture
11
13
  from dodal.devices.scatterguard import Scatterguard
12
- from dodal.log import LOGGER
13
14
 
14
15
 
15
16
  class InvalidApertureMove(Exception):
@@ -39,14 +40,10 @@ class ApertureScatterguardTolerances:
39
40
 
40
41
  @dataclass
41
42
  class SingleAperturePosition:
42
- # Default values are needed as ophyd_async sim does not respect initial_values of
43
- # soft signal backends see https://github.com/bluesky/ophyd-async/issues/266
44
- name: str = ""
45
- GDA_name: str = ""
46
- radius_microns: float | None = None
47
- location: ApertureFiveDimensionalLocation = ApertureFiveDimensionalLocation(
48
- 0, 0, 0, 0, 0
49
- )
43
+ name: str
44
+ GDA_name: str
45
+ radius_microns: float | None
46
+ location: ApertureFiveDimensionalLocation
50
47
 
51
48
 
52
49
  # Use StrEnum once we stop python 3.10 support
@@ -64,7 +61,7 @@ def position_from_params(
64
61
  name: str,
65
62
  GDA_name: AperturePositionGDANames,
66
63
  radius_microns: float | None,
67
- params: dict,
64
+ params: GDABeamlineParameters,
68
65
  ) -> SingleAperturePosition:
69
66
  return SingleAperturePosition(
70
67
  name,
@@ -80,7 +77,9 @@ def position_from_params(
80
77
  )
81
78
 
82
79
 
83
- def tolerances_from_params(params: dict) -> ApertureScatterguardTolerances:
80
+ def load_tolerances_from_beamline_params(
81
+ params: GDABeamlineParameters,
82
+ ) -> ApertureScatterguardTolerances:
84
83
  return ApertureScatterguardTolerances(
85
84
  ap_x=params["miniap_x_tolerance"],
86
85
  ap_y=params["miniap_y_tolerance"],
@@ -90,76 +89,50 @@ def tolerances_from_params(params: dict) -> ApertureScatterguardTolerances:
90
89
  )
91
90
 
92
91
 
93
- @dataclass
94
- class AperturePositions:
95
- """Holds the motor positions needed to select a particular aperture size. This class should be instantiated with definitions for its sizes
96
- using from_gda_beamline_params"""
97
-
98
- LARGE: SingleAperturePosition
99
- MEDIUM: SingleAperturePosition
100
- SMALL: SingleAperturePosition
101
- ROBOT_LOAD: SingleAperturePosition
102
-
103
- tolerances: ApertureScatterguardTolerances
104
-
105
- UNKNOWN = SingleAperturePosition(
106
- "Unknown", "UNKNOWN", None, ApertureFiveDimensionalLocation(0, 0, 0, 0, 0)
107
- )
108
-
109
- @classmethod
110
- def from_gda_beamline_params(cls, params):
111
- return cls(
112
- LARGE=position_from_params(
113
- "Large", AperturePositionGDANames.LARGE_APERTURE, 100, params
114
- ),
115
- MEDIUM=position_from_params(
116
- "Medium", AperturePositionGDANames.MEDIUM_APERTURE, 50, params
117
- ),
118
- SMALL=position_from_params(
119
- "Small", AperturePositionGDANames.SMALL_APERTURE, 20, params
120
- ),
121
- ROBOT_LOAD=position_from_params(
122
- "Robot load", AperturePositionGDANames.ROBOT_LOAD, None, params
123
- ),
124
- tolerances=tolerances_from_params(params),
125
- )
92
+ class AperturePosition(Enum):
93
+ ROBOT_LOAD = 0
94
+ SMALL = 1
95
+ MEDIUM = 2
96
+ LARGE = 3
126
97
 
127
- def get_position_from_gda_aperture_name(
128
- self, gda_aperture_name: AperturePositionGDANames
129
- ) -> SingleAperturePosition:
130
- apertures = [ap for ap in self.as_list() if ap.GDA_name == gda_aperture_name]
131
- if not apertures:
132
- raise ValueError(
133
- f"Tried to convert unknown aperture name {gda_aperture_name} to a SingleAperturePosition"
134
- )
135
- else:
136
- return apertures[0]
137
98
 
138
- def as_list(self) -> list[SingleAperturePosition]:
139
- return [
140
- self.LARGE,
141
- self.MEDIUM,
142
- self.SMALL,
143
- self.ROBOT_LOAD,
144
- ]
99
+ def load_positions_from_beamline_parameters(
100
+ params: GDABeamlineParameters,
101
+ ) -> dict[AperturePosition, SingleAperturePosition]:
102
+ return {
103
+ AperturePosition.ROBOT_LOAD: position_from_params(
104
+ "Robot load", AperturePositionGDANames.ROBOT_LOAD, None, params
105
+ ),
106
+ AperturePosition.SMALL: position_from_params(
107
+ "Small", AperturePositionGDANames.SMALL_APERTURE, 20, params
108
+ ),
109
+ AperturePosition.MEDIUM: position_from_params(
110
+ "Medium", AperturePositionGDANames.MEDIUM_APERTURE, 50, params
111
+ ),
112
+ AperturePosition.LARGE: position_from_params(
113
+ "Large", AperturePositionGDANames.LARGE_APERTURE, 100, params
114
+ ),
115
+ }
145
116
 
146
117
 
147
118
  class ApertureScatterguard(StandardReadable, Movable):
148
- def __init__(self, prefix: str = "", name: str = "") -> None:
149
- self.aperture = Aperture(prefix + "-MO-MAPT-01:")
150
- self.scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
151
- self.aperture_positions: AperturePositions | None = None
152
- self.TOLERANCE_STEPS = 3 # Number of MRES steps
119
+ def __init__(
120
+ self,
121
+ loaded_positions: dict[AperturePosition, SingleAperturePosition],
122
+ tolerances: ApertureScatterguardTolerances,
123
+ prefix: str = "",
124
+ name: str = "",
125
+ ) -> None:
126
+ self._aperture = Aperture(prefix + "-MO-MAPT-01:")
127
+ self._scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
128
+ self._loaded_positions = loaded_positions
129
+ self._tolerances = tolerances
153
130
  aperture_backend = SoftSignalBackend(
154
- SingleAperturePosition, AperturePositions.UNKNOWN
131
+ SingleAperturePosition, self._loaded_positions[AperturePosition.ROBOT_LOAD]
155
132
  )
156
133
  aperture_backend.converter = self.ApertureConverter()
157
134
  self.selected_aperture = self.SelectedAperture(backend=aperture_backend)
158
- self.set_readable_signals(
159
- read=[
160
- self.selected_aperture,
161
- ]
162
- )
135
+ self.add_readables([self.selected_aperture], wrapper=HintedSignal)
163
136
  super().__init__(name)
164
137
 
165
138
  class ApertureConverter(SoftConverter):
@@ -176,43 +149,47 @@ class ApertureScatterguard(StandardReadable, Movable):
176
149
  class SelectedAperture(SignalR):
177
150
  async def read(self, *args, **kwargs):
178
151
  assert isinstance(self.parent, ApertureScatterguard)
179
- await self._backend.put(await self.parent._get_current_aperture_position())
152
+ assert self._backend
153
+ await self._backend.put(await self.parent.get_current_aperture_position())
180
154
  return {self.name: await self._backend.get_reading()}
181
155
 
182
- async def describe(self):
183
- return OrderedDict(
184
- [
185
- (
186
- self._name,
187
- {
188
- "source": self._backend.source(self._name), # type: ignore
189
- "dtype": "array",
190
- "shape": [
191
- -1,
192
- ], # TODO describe properly - see https://github.com/DiamondLightSource/dodal/issues/253
193
- },
194
- ),
195
- ],
196
- )
156
+ async def describe(self) -> dict[str, DataKey]:
157
+ return {
158
+ self._name: DataKey(
159
+ dtype="array",
160
+ shape=[
161
+ -1,
162
+ ], # TODO describe properly - see https://github.com/DiamondLightSource/dodal/issues/253,
163
+ source=self._backend.source(self._name), # type: ignore
164
+ )
165
+ }
197
166
 
198
- def load_aperture_positions(self, positions: AperturePositions):
199
- LOGGER.info(f"{self.name} loaded in {positions}")
200
- self.aperture_positions = positions
167
+ def get_position_from_gda_aperture_name(
168
+ self, gda_aperture_name: AperturePositionGDANames
169
+ ) -> AperturePosition:
170
+ for aperture, detail in self._loaded_positions.items():
171
+ if detail.GDA_name == gda_aperture_name:
172
+ return aperture
173
+ raise ValueError(
174
+ f"Tried to convert unknown aperture name {gda_aperture_name} to a SingleAperturePosition"
175
+ )
201
176
 
202
- def set(self, pos: SingleAperturePosition) -> AsyncStatus:
203
- assert isinstance(self.aperture_positions, AperturePositions)
204
- if pos not in self.aperture_positions.as_list():
205
- raise InvalidApertureMove(f"Unknown aperture: {pos}")
177
+ def get_gda_name_for_position(self, position: AperturePosition) -> str:
178
+ detailed_position = self._loaded_positions[position]
179
+ return detailed_position.GDA_name
206
180
 
207
- return AsyncStatus(self._safe_move_within_datacollection_range(pos.location))
181
+ @AsyncStatus.wrap
182
+ async def set(self, value: AperturePosition):
183
+ position = self._loaded_positions[value]
184
+ await self._safe_move_within_datacollection_range(position.location)
208
185
 
209
186
  def _get_motor_list(self):
210
187
  return [
211
- self.aperture.x,
212
- self.aperture.y,
213
- self.aperture.z,
214
- self.scatterguard.x,
215
- self.scatterguard.y,
188
+ self._aperture.x,
189
+ self._aperture.y,
190
+ self._aperture.z,
191
+ self._scatterguard.x,
192
+ self._scatterguard.y,
216
193
  ]
217
194
 
218
195
  @AsyncStatus.wrap
@@ -223,31 +200,32 @@ class ApertureScatterguard(StandardReadable, Movable):
223
200
  aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = positions
224
201
 
225
202
  await asyncio.gather(
226
- self.aperture.x.set(aperture_x),
227
- self.aperture.y.set(aperture_y),
228
- self.aperture.z.set(aperture_z),
229
- self.scatterguard.x.set(scatterguard_x),
230
- self.scatterguard.y.set(scatterguard_y),
203
+ self._aperture.x.set(aperture_x),
204
+ self._aperture.y.set(aperture_y),
205
+ self._aperture.z.set(aperture_z),
206
+ self._scatterguard.x.set(scatterguard_x),
207
+ self._scatterguard.y.set(scatterguard_y),
231
208
  )
232
209
 
233
- async def _get_current_aperture_position(self) -> SingleAperturePosition:
210
+ async def get_current_aperture_position(self) -> SingleAperturePosition:
234
211
  """
235
212
  Returns the current aperture position using readback values
236
213
  for SMALL, MEDIUM, LARGE. ROBOT_LOAD position defined when
237
214
  mini aperture y <= ROBOT_LOAD.location.aperture_y + tolerance.
238
215
  If no position is found then raises InvalidApertureMove.
239
216
  """
240
- assert isinstance(self.aperture_positions, AperturePositions)
241
- current_ap_y = await self.aperture.y.user_readback.get_value(cached=False)
242
- robot_load_ap_y = self.aperture_positions.ROBOT_LOAD.location.aperture_y
243
- if await self.aperture.large.get_value(cached=False) == 1:
244
- return self.aperture_positions.LARGE
245
- elif await self.aperture.medium.get_value(cached=False) == 1:
246
- return self.aperture_positions.MEDIUM
247
- elif await self.aperture.small.get_value(cached=False) == 1:
248
- return self.aperture_positions.SMALL
249
- elif current_ap_y <= robot_load_ap_y + self.aperture_positions.tolerances.ap_y:
250
- return self.aperture_positions.ROBOT_LOAD
217
+ current_ap_y = await self._aperture.y.user_readback.get_value(cached=False)
218
+ robot_load_ap_y = self._loaded_positions[
219
+ AperturePosition.ROBOT_LOAD
220
+ ].location.aperture_y
221
+ if await self._aperture.large.get_value(cached=False) == 1:
222
+ return self._loaded_positions[AperturePosition.LARGE]
223
+ elif await self._aperture.medium.get_value(cached=False) == 1:
224
+ return self._loaded_positions[AperturePosition.MEDIUM]
225
+ elif await self._aperture.small.get_value(cached=False) == 1:
226
+ return self._loaded_positions[AperturePosition.SMALL]
227
+ elif current_ap_y <= robot_load_ap_y + self._tolerances.ap_y:
228
+ return self._loaded_positions[AperturePosition.ROBOT_LOAD]
251
229
 
252
230
  raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
253
231
 
@@ -259,46 +237,46 @@ class ApertureScatterguard(StandardReadable, Movable):
259
237
  See https://github.com/DiamondLightSource/hyperion/wiki/Aperture-Scatterguard-Collisions
260
238
  for why this is required.
261
239
  """
262
- assert self.aperture_positions is not None
240
+ assert self._loaded_positions is not None
263
241
  # unpacking the position
264
242
  aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = pos
265
243
 
266
- ap_z_in_position = await self.aperture.z.motor_done_move.get_value()
244
+ ap_z_in_position = await self._aperture.z.motor_done_move.get_value()
267
245
  if not ap_z_in_position:
268
246
  raise InvalidApertureMove(
269
247
  "ApertureScatterguard z is still moving. Wait for it to finish "
270
248
  "before triggering another move."
271
249
  )
272
250
 
273
- current_ap_z = await self.aperture.z.user_readback.get_value()
251
+ current_ap_z = await self._aperture.z.user_readback.get_value()
274
252
  diff_on_z = abs(current_ap_z - aperture_z)
275
- if diff_on_z > self.aperture_positions.tolerances.ap_z:
253
+ if diff_on_z > self._tolerances.ap_z:
276
254
  raise InvalidApertureMove(
277
255
  "ApertureScatterguard safe move is not yet defined for positions "
278
256
  "outside of LARGE, MEDIUM, SMALL, ROBOT_LOAD. "
279
- f"Current aperture z ({current_ap_z}), outside of tolerance ({self.aperture_positions.tolerances.ap_z}) from target ({aperture_z})."
257
+ f"Current aperture z ({current_ap_z}), outside of tolerance ({self._tolerances.ap_z}) from target ({aperture_z})."
280
258
  )
281
259
 
282
- current_ap_y = await self.aperture.y.user_readback.get_value()
260
+ current_ap_y = await self._aperture.y.user_readback.get_value()
283
261
 
284
262
  if aperture_y > current_ap_y:
285
263
  await asyncio.gather(
286
- self.scatterguard.x.set(scatterguard_x),
287
- self.scatterguard.y.set(scatterguard_y),
264
+ self._scatterguard.x.set(scatterguard_x),
265
+ self._scatterguard.y.set(scatterguard_y),
288
266
  )
289
267
  await asyncio.gather(
290
- self.aperture.x.set(aperture_x),
291
- self.aperture.y.set(aperture_y),
292
- self.aperture.z.set(aperture_z),
268
+ self._aperture.x.set(aperture_x),
269
+ self._aperture.y.set(aperture_y),
270
+ self._aperture.z.set(aperture_z),
293
271
  )
294
272
  return
295
273
  await asyncio.gather(
296
- self.aperture.x.set(aperture_x),
297
- self.aperture.y.set(aperture_y),
298
- self.aperture.z.set(aperture_z),
274
+ self._aperture.x.set(aperture_x),
275
+ self._aperture.y.set(aperture_y),
276
+ self._aperture.z.set(aperture_z),
299
277
  )
300
278
 
301
279
  await asyncio.gather(
302
- self.scatterguard.x.set(scatterguard_x),
303
- self.scatterguard.y.set(scatterguard_y),
280
+ self._scatterguard.x.set(scatterguard_x),
281
+ self._scatterguard.y.set(scatterguard_y),
304
282
  )
@@ -1,7 +1,7 @@
1
- from typing import Any, Mapping
1
+ from collections.abc import Mapping
2
+ from typing import Any
2
3
 
3
- from ophyd import Component as Cpt
4
- from ophyd import DetectorBase, EpicsSignal, Signal
4
+ from ophyd import EpicsSignal, Signal
5
5
  from ophyd.areadetector.base import ADComponent as Cpt
6
6
  from ophyd.areadetector.detectors import DetectorBase
7
7
 
@@ -60,7 +60,7 @@ class AdAravisDetector(SingleTriggerV33, DetectorBase):
60
60
  signal.put_complete = True
61
61
  self.cam.acquire.put_complete = True
62
62
 
63
- def stage(self, *args, **kwargs):
63
+ def stage(self, *args, **kwargs) -> list[object]:
64
64
  # We have to manually set the acquire period bcause the EPICS driver
65
65
  # doesn't do it for us. If acquire time is a staged signal, we use the
66
66
  # stage value to calculate the acquire period, otherwise we perform
@@ -69,13 +69,15 @@ class AdAravisDetector(SingleTriggerV33, DetectorBase):
69
69
  acquire_time = self.stage_sigs[self.cam.acquire_time]
70
70
  else:
71
71
  acquire_time = self.cam.acquire_time.get()
72
- self.stage_sigs[self.cam.acquire_period] = acquire_time + _ACQUIRE_BUFFER_PERIOD
72
+ self.stage_sigs[self.cam.acquire_period] = (
73
+ float(acquire_time) + _ACQUIRE_BUFFER_PERIOD
74
+ )
73
75
 
74
76
  # Ensure detector warmed up
75
77
  self._prime_hdf()
76
78
 
77
79
  # Now calling the super method should set the acquire period
78
- super(AdAravisDetector, self).stage(*args, **kwargs)
80
+ return super().stage(*args, **kwargs)
79
81
 
80
82
  def _prime_hdf(self) -> None:
81
83
  """
@@ -1,4 +1,3 @@
1
- from ophyd import Component as Cpt
2
1
  from ophyd.areadetector.base import ADComponent as Cpt
3
2
  from ophyd.areadetector.detectors import DetectorBase
4
3
 
@@ -33,7 +32,7 @@ class AdSimDetector(SingleTriggerV33, DetectorBase):
33
32
  **self.stage_sigs, # type: ignore
34
33
  }
35
34
 
36
- def stage(self, *args, **kwargs):
35
+ def stage(self, *args, **kwargs) -> list[object]:
37
36
  # We have to manually set the acquire period bcause the EPICS driver
38
37
  # doesn't do it for us. If acquire time is a staged signal, we use the
39
38
  # stage value to calculate the acquire period, otherwise we perform
@@ -45,4 +44,4 @@ class AdSimDetector(SingleTriggerV33, DetectorBase):
45
44
  self.stage_sigs[self.cam.acquire_period] = acquire_time
46
45
 
47
46
  # Now calling the super method should set the acquire period
48
- super(AdSimDetector, self).stage(*args, **kwargs)
47
+ return super().stage(*args, **kwargs)
@@ -1,7 +1,7 @@
1
1
  import time as ttime
2
2
 
3
3
  from ophyd import Component as Cpt
4
- from ophyd import EpicsSignal, EpicsSignalRO, Staged
4
+ from ophyd import DetectorBase, Device, EpicsSignal, EpicsSignalRO, Staged
5
5
  from ophyd.areadetector import ADTriggerStatus, TriggerBase
6
6
  from ophyd.areadetector.cam import AreaDetectorCam
7
7
  from ophyd.areadetector.filestore_mixins import FileStoreHDF5, FileStoreIterativeWrite
@@ -14,6 +14,8 @@ class SingleTriggerV33(TriggerBase):
14
14
  def __init__(self, *args, image_name=None, **kwargs):
15
15
  super().__init__(*args, **kwargs)
16
16
  if image_name is None:
17
+ # Ensure that this mixin is part of valid device with name
18
+ assert isinstance(self, Device)
17
19
  image_name = "_".join([self.name, "image"])
18
20
  self._image_name = image_name
19
21
 
@@ -29,10 +31,12 @@ class SingleTriggerV33(TriggerBase):
29
31
 
30
32
  def _acq_done(*args, **kwargs):
31
33
  # TODO sort out if anything useful in here
32
- self._status._finished()
34
+ self._status._finished() # noqa: SLF001
33
35
 
34
36
  self._acquisition_signal.put(1, use_complete=True, callback=_acq_done)
35
- self.dispatch(self._image_name, ttime.time())
37
+ # Ensure that this mixin is part of valid Detector with generate_datum
38
+ assert isinstance(self, DetectorBase)
39
+ self.generate_datum(self._image_name, ttime.time())
36
40
  return self._status
37
41
 
38
42
 
@@ -54,15 +58,18 @@ class SynchronisedAdDriverBase(AreaDetectorCam):
54
58
 
55
59
  def ensure_nonblocking(self):
56
60
  self.stage_sigs["wait_for_plugins"] = "Yes"
57
- for c in self.parent.component_names:
58
- cpt = getattr(self.parent, c)
59
- if cpt is self:
60
- continue
61
- if hasattr(cpt, "ensure_nonblocking"):
62
- cpt.ensure_nonblocking()
63
-
64
-
65
- class Hdf5Writer(HDF5Plugin, FileStoreHDF5, FileStoreIterativeWrite):
61
+ if self.parent is not None:
62
+ for c in self.parent.component_names:
63
+ cpt = getattr(self.parent, c)
64
+ if cpt is self:
65
+ continue
66
+ if hasattr(cpt, "ensure_nonblocking"):
67
+ cpt.ensure_nonblocking()
68
+
69
+
70
+ # ophyd code to be removed, only used for adim
71
+ # https://github.com/DiamondLightSource/dodal/issues/404
72
+ class Hdf5Writer(HDF5Plugin, FileStoreHDF5, FileStoreIterativeWrite): # type: ignore
66
73
  """ """
67
74
 
68
75
  pool_max_buffers = None
@@ -70,4 +77,5 @@ class Hdf5Writer(HDF5Plugin, FileStoreHDF5, FileStoreIterativeWrite):
70
77
  file_number_write = None
71
78
 
72
79
  def get_frames_per_point(self):
80
+ assert isinstance(self.parent, DetectorBase)
73
81
  return self.parent.cam.num_images.get()
@@ -1,9 +1,21 @@
1
- from ophyd import Component as Cpt
2
- from ophyd import Device, EpicsSignal, EpicsSignalRO
1
+ from enum import Enum
3
2
 
3
+ from ophyd_async.core import StandardReadable
4
+ from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
4
5
 
5
- class Cryo(Device):
6
- course = Cpt(EpicsSignal, "-EA-CJET-01:COARSE:CTRL")
7
- fine = Cpt(EpicsSignal, "-EA-CJET-01:FINE:CTRL")
8
- temp = Cpt(EpicsSignalRO, "-EA-CSTRM-01:TEMP")
9
- backpress = Cpt(EpicsSignalRO, "-EA-CSTRM-01:BACKPRESS")
6
+
7
+ class InOut(str, Enum):
8
+ IN = "In"
9
+ OUT = "Out"
10
+
11
+
12
+ class CryoStream(StandardReadable):
13
+ def __init__(self, prefix: str, name: str = ""):
14
+ self.course = epics_signal_rw(InOut, f"{prefix}-EA-CJET-01:COARSE:CTRL")
15
+ self.fine = epics_signal_rw(InOut, f"{prefix}-EA-CJET-01:FINE:CTRL")
16
+ self.temperature_k = epics_signal_r(float, f"{prefix}-EA-CSTRM-01:TEMP")
17
+ self.back_pressure_bar = epics_signal_r(
18
+ float, f"{prefix}-EA-CSTRM-01:BACKPRESS"
19
+ )
20
+
21
+ super().__init__(name)
@@ -1,2 +1,13 @@
1
- # export GDA detector names for convenience
2
- from dodal.devices.detector.detector import * # noqa: F403
1
+ from dodal.devices.detector.det_dim_constants import EIGER2_X_16M_SIZE
2
+ from dodal.devices.detector.detector import (
3
+ DetectorDistanceToBeamXYConverter,
4
+ DetectorParams,
5
+ TriggerMode,
6
+ )
7
+
8
+ __all__ = [
9
+ "DetectorParams",
10
+ "EIGER2_X_16M_SIZE",
11
+ "TriggerMode",
12
+ "DetectorDistanceToBeamXYConverter",
13
+ ]
@@ -1,4 +1,4 @@
1
- from typing import Dict, Generic, TypeVar
1
+ from typing import Generic, TypeVar
2
2
 
3
3
  from pydantic.dataclasses import dataclass
4
4
 
@@ -11,7 +11,7 @@ class DetectorSize(Generic[T]):
11
11
  height: T
12
12
 
13
13
 
14
- ALL_DETECTORS: Dict[str, "DetectorSizeConstants"] = {}
14
+ ALL_DETECTORS: dict[str, "DetectorSizeConstants"] = {}
15
15
 
16
16
 
17
17
  @dataclass
@@ -47,7 +47,7 @@ class DetectorDistanceToBeamXYConverter:
47
47
 
48
48
  def parse_table(self) -> list:
49
49
  rows = loadtxt(self.lookup_file, delimiter=" ", comments=["#", "Units"])
50
- columns = list(zip(*rows))
50
+ columns = list(zip(*rows, strict=False))
51
51
 
52
52
  return columns
53
53
 
@@ -1,5 +1,5 @@
1
1
  from enum import Enum, auto
2
- from typing import Any, Tuple
2
+ from typing import Any
3
3
 
4
4
  from pydantic import BaseModel, root_validator, validator
5
5
 
@@ -41,8 +41,8 @@ class DetectorParams(BaseModel):
41
41
  det_dist_to_beam_converter_path: str
42
42
  trigger_mode: TriggerMode = TriggerMode.SET_FRAMES
43
43
  detector_size_constants: DetectorSizeConstants = EIGER2_X_16M_SIZE
44
- beam_xy_converter: DetectorDistanceToBeamXYConverter
45
- run_number: int
44
+ beam_xy_converter: DetectorDistanceToBeamXYConverter = None # type: ignore # Filled in by validator
45
+ run_number: int = None # type: ignore # Filled in by validator
46
46
  enable_dev_shm: bool = (
47
47
  False # Remove in https://github.com/DiamondLightSource/hyperion/issues/1395
48
48
  )
@@ -80,7 +80,7 @@ class DetectorParams(BaseModel):
80
80
  directory += "/"
81
81
  return directory
82
82
 
83
- def get_beam_position_mm(self, detector_distance: float) -> Tuple[float, float]:
83
+ def get_beam_position_mm(self, detector_distance: float) -> tuple[float, float]:
84
84
  x_beam_mm = self.beam_xy_converter.get_beam_xy_from_det_dist(
85
85
  detector_distance, Axis.X_AXIS
86
86
  )
@@ -105,7 +105,7 @@ class DetectorParams(BaseModel):
105
105
  roi_size = self.detector_size_constants.roi_size_pixels
106
106
  return roi_size if self.use_roi_mode else full_size
107
107
 
108
- def get_beam_position_pixels(self, detector_distance: float) -> Tuple[float, float]:
108
+ def get_beam_position_pixels(self, detector_distance: float) -> tuple[float, float]:
109
109
  full_size_pixels = self.detector_size_constants.det_size_pixels
110
110
  roi_size_pixels = self.get_detector_size_pizels()
111
111