dls-dodal 1.29.4__py3-none-any.whl → 1.31.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 (103) hide show
  1. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/METADATA +29 -44
  2. dls_dodal-1.31.0.dist-info/RECORD +134 -0
  3. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/WHEEL +1 -1
  4. dls_dodal-1.31.0.dist-info/entry_points.txt +3 -0
  5. dodal/__init__.py +1 -4
  6. dodal/_version.py +2 -2
  7. dodal/beamline_specific_utils/i03.py +1 -4
  8. dodal/beamlines/__init__.py +7 -1
  9. dodal/beamlines/i03.py +34 -29
  10. dodal/beamlines/i04.py +39 -16
  11. dodal/beamlines/i13_1.py +66 -0
  12. dodal/beamlines/i22.py +22 -22
  13. dodal/beamlines/i24.py +1 -1
  14. dodal/beamlines/p38.py +21 -21
  15. dodal/beamlines/p45.py +18 -16
  16. dodal/beamlines/p99.py +61 -0
  17. dodal/beamlines/training_rig.py +64 -0
  18. dodal/cli.py +6 -3
  19. dodal/common/beamlines/beamline_parameters.py +7 -6
  20. dodal/common/beamlines/beamline_utils.py +15 -14
  21. dodal/common/maths.py +1 -3
  22. dodal/common/types.py +6 -5
  23. dodal/common/udc_directory_provider.py +39 -21
  24. dodal/common/visit.py +60 -62
  25. dodal/devices/CTAB.py +22 -17
  26. dodal/devices/aperture.py +1 -1
  27. dodal/devices/aperturescatterguard.py +139 -209
  28. dodal/devices/areadetector/adaravis.py +8 -6
  29. dodal/devices/areadetector/adsim.py +2 -3
  30. dodal/devices/areadetector/adutils.py +20 -12
  31. dodal/devices/areadetector/plugins/MJPG.py +2 -1
  32. dodal/devices/backlight.py +12 -1
  33. dodal/devices/cryostream.py +19 -7
  34. dodal/devices/dcm.py +1 -1
  35. dodal/devices/detector/__init__.py +13 -2
  36. dodal/devices/detector/det_dim_constants.py +2 -2
  37. dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
  38. dodal/devices/detector/detector.py +33 -32
  39. dodal/devices/detector/detector_motion.py +38 -31
  40. dodal/devices/eiger.py +11 -15
  41. dodal/devices/eiger_odin.py +9 -10
  42. dodal/devices/fast_grid_scan.py +18 -27
  43. dodal/devices/fluorescence_detector_motion.py +13 -4
  44. dodal/devices/focusing_mirror.py +6 -6
  45. dodal/devices/hutch_shutter.py +4 -4
  46. dodal/devices/i22/dcm.py +5 -4
  47. dodal/devices/i22/fswitch.py +10 -6
  48. dodal/devices/i22/nxsas.py +55 -43
  49. dodal/devices/i24/aperture.py +1 -1
  50. dodal/devices/i24/beamstop.py +1 -1
  51. dodal/devices/i24/dcm.py +1 -1
  52. dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +1 -1
  53. dodal/devices/i24/pmac.py +67 -12
  54. dodal/devices/ipin.py +7 -4
  55. dodal/devices/linkam3.py +12 -6
  56. dodal/devices/logging_ophyd_device.py +1 -1
  57. dodal/devices/motors.py +32 -6
  58. dodal/devices/oav/grid_overlay.py +1 -0
  59. dodal/devices/oav/microns_for_zoom_levels.json +1 -1
  60. dodal/devices/oav/oav_detector.py +2 -1
  61. dodal/devices/oav/oav_parameters.py +18 -10
  62. dodal/devices/oav/oav_to_redis_forwarder.py +129 -0
  63. dodal/devices/oav/pin_image_recognition/__init__.py +6 -6
  64. dodal/devices/oav/pin_image_recognition/utils.py +5 -6
  65. dodal/devices/oav/utils.py +2 -2
  66. dodal/devices/p99/__init__.py +0 -0
  67. dodal/devices/p99/sample_stage.py +43 -0
  68. dodal/devices/robot.py +31 -20
  69. dodal/devices/scatterguard.py +1 -1
  70. dodal/devices/scintillator.py +8 -5
  71. dodal/devices/slits.py +1 -1
  72. dodal/devices/smargon.py +4 -4
  73. dodal/devices/status.py +2 -31
  74. dodal/devices/tetramm.py +23 -19
  75. dodal/devices/thawer.py +5 -3
  76. dodal/devices/training_rig/__init__.py +0 -0
  77. dodal/devices/training_rig/sample_stage.py +10 -0
  78. dodal/devices/turbo_slit.py +1 -1
  79. dodal/devices/undulator.py +1 -1
  80. dodal/devices/undulator_dcm.py +6 -8
  81. dodal/devices/util/adjuster_plans.py +3 -3
  82. dodal/devices/util/epics_util.py +5 -7
  83. dodal/devices/util/lookup_tables.py +2 -3
  84. dodal/devices/util/save_panda.py +87 -0
  85. dodal/devices/util/test_utils.py +17 -0
  86. dodal/devices/webcam.py +3 -3
  87. dodal/devices/xbpm_feedback.py +1 -25
  88. dodal/devices/xspress3/xspress3.py +1 -1
  89. dodal/devices/zebra.py +15 -10
  90. dodal/devices/zebra_controlled_shutter.py +26 -11
  91. dodal/devices/zocalo/zocalo_interaction.py +10 -2
  92. dodal/devices/zocalo/zocalo_results.py +36 -19
  93. dodal/log.py +46 -15
  94. dodal/plans/check_topup.py +65 -10
  95. dodal/plans/data_session_metadata.py +8 -9
  96. dodal/plans/motor_util_plans.py +117 -0
  97. dodal/utils.py +65 -22
  98. dls_dodal-1.29.4.dist-info/RECORD +0 -125
  99. dls_dodal-1.29.4.dist-info/entry_points.txt +0 -2
  100. dodal/devices/beamstop.py +0 -8
  101. dodal/devices/qbpm1.py +0 -8
  102. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/LICENSE +0 -0
  103. {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/top_level.txt +0 -0
@@ -1,226 +1,147 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
- from collections import OrderedDict, namedtuple
3
- from dataclasses import asdict, dataclass
4
4
  from enum import Enum
5
5
 
6
- from bluesky.protocols import Movable, Reading
7
- from ophyd_async.core import AsyncStatus, SignalR, StandardReadable
8
- from ophyd_async.core.soft_signal_backend import SoftConverter, SoftSignalBackend
6
+ from bluesky.protocols import Movable
7
+ from ophyd_async.core import (
8
+ AsyncStatus,
9
+ HintedSignal,
10
+ StandardReadable,
11
+ soft_signal_rw,
12
+ )
13
+ from pydantic import BaseModel, Field
9
14
 
15
+ from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
10
16
  from dodal.devices.aperture import Aperture
11
17
  from dodal.devices.scatterguard import Scatterguard
12
- from dodal.log import LOGGER
13
18
 
14
19
 
15
20
  class InvalidApertureMove(Exception):
16
21
  pass
17
22
 
18
23
 
19
- ApertureFiveDimensionalLocation = namedtuple(
20
- "ApertureFiveDimensionalLocation",
21
- [
22
- "aperture_x",
23
- "aperture_y",
24
- "aperture_z",
25
- "scatterguard_x",
26
- "scatterguard_y",
27
- ],
28
- )
29
-
24
+ class AperturePosition(BaseModel):
25
+ aperture_x: float
26
+ aperture_y: float
27
+ aperture_z: float
28
+ scatterguard_x: float
29
+ scatterguard_y: float
30
+ radius: float | None = Field(json_schema_extra={"units": "µm"}, default=None)
31
+
32
+ @property
33
+ def values(self) -> tuple[float, float, float, float, float]:
34
+ return (
35
+ self.aperture_x,
36
+ self.aperture_y,
37
+ self.aperture_z,
38
+ self.scatterguard_x,
39
+ self.scatterguard_y,
40
+ )
30
41
 
31
- @dataclass
32
- class ApertureScatterguardTolerances:
33
- ap_x: float
34
- ap_y: float
35
- ap_z: float
36
- sg_x: float
37
- sg_y: float
38
-
39
-
40
- @dataclass
41
- 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
- )
50
-
51
-
52
- # Use StrEnum once we stop python 3.10 support
53
- class AperturePositionGDANames(str, Enum):
54
- LARGE_APERTURE = "LARGE_APERTURE"
55
- MEDIUM_APERTURE = "MEDIUM_APERTURE"
56
- SMALL_APERTURE = "SMALL_APERTURE"
57
- ROBOT_LOAD = "ROBOT_LOAD"
42
+ @staticmethod
43
+ def tolerances_from_gda_params(
44
+ params: GDABeamlineParameters,
45
+ ) -> AperturePosition:
46
+ return AperturePosition(
47
+ aperture_x=params["miniap_x_tolerance"],
48
+ aperture_y=params["miniap_y_tolerance"],
49
+ aperture_z=params["miniap_z_tolerance"],
50
+ scatterguard_x=params["sg_x_tolerance"],
51
+ scatterguard_y=params["sg_y_tolerance"],
52
+ )
58
53
 
59
- def __str__(self):
60
- return str(self.value)
61
-
62
-
63
- def position_from_params(
64
- name: str,
65
- GDA_name: AperturePositionGDANames,
66
- radius_microns: float | None,
67
- params: dict,
68
- ) -> SingleAperturePosition:
69
- return SingleAperturePosition(
70
- name,
71
- GDA_name,
72
- radius_microns,
73
- ApertureFiveDimensionalLocation(
74
- params[f"miniap_x_{GDA_name}"],
75
- params[f"miniap_y_{GDA_name}"],
76
- params[f"miniap_z_{GDA_name}"],
77
- params[f"sg_x_{GDA_name}"],
78
- params[f"sg_y_{GDA_name}"],
79
- ),
80
- )
81
-
82
-
83
- def tolerances_from_params(params: dict) -> ApertureScatterguardTolerances:
84
- return ApertureScatterguardTolerances(
85
- ap_x=params["miniap_x_tolerance"],
86
- ap_y=params["miniap_y_tolerance"],
87
- ap_z=params["miniap_z_tolerance"],
88
- sg_x=params["sg_x_tolerance"],
89
- sg_y=params["sg_y_tolerance"],
90
- )
91
-
92
-
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),
54
+ @staticmethod
55
+ def from_gda_params(
56
+ name: ApertureValue,
57
+ radius: float | None,
58
+ params: GDABeamlineParameters,
59
+ ) -> AperturePosition:
60
+ return AperturePosition(
61
+ aperture_x=params[f"miniap_x_{name.value}"],
62
+ aperture_y=params[f"miniap_y_{name.value}"],
63
+ aperture_z=params[f"miniap_z_{name.value}"],
64
+ scatterguard_x=params[f"sg_x_{name.value}"],
65
+ scatterguard_y=params[f"sg_y_{name.value}"],
66
+ radius=radius,
125
67
  )
126
68
 
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
69
 
138
- def as_list(self) -> list[SingleAperturePosition]:
139
- return [
140
- self.LARGE,
141
- self.MEDIUM,
142
- self.SMALL,
143
- self.ROBOT_LOAD,
144
- ]
70
+ class ApertureValue(str, Enum):
71
+ """Maps from a short usable name to the value name in the GDA Beamline parameters"""
72
+
73
+ ROBOT_LOAD = "ROBOT_LOAD"
74
+ SMALL = "SMALL_APERTURE"
75
+ MEDIUM = "MEDIUM_APERTURE"
76
+ LARGE = "LARGE_APERTURE"
77
+
78
+
79
+ def load_positions_from_beamline_parameters(
80
+ params: GDABeamlineParameters,
81
+ ) -> dict[ApertureValue, AperturePosition]:
82
+ return {
83
+ ApertureValue.ROBOT_LOAD: AperturePosition.from_gda_params(
84
+ ApertureValue.ROBOT_LOAD, None, params
85
+ ),
86
+ ApertureValue.SMALL: AperturePosition.from_gda_params(
87
+ ApertureValue.SMALL, 20, params
88
+ ),
89
+ ApertureValue.MEDIUM: AperturePosition.from_gda_params(
90
+ ApertureValue.MEDIUM, 50, params
91
+ ),
92
+ ApertureValue.LARGE: AperturePosition.from_gda_params(
93
+ ApertureValue.LARGE, 100, params
94
+ ),
95
+ }
145
96
 
146
97
 
147
98
  class ApertureScatterguard(StandardReadable, Movable):
148
- def __init__(self, prefix: str = "", name: str = "") -> None:
99
+ def __init__(
100
+ self,
101
+ loaded_positions: dict[ApertureValue, AperturePosition],
102
+ tolerances: AperturePosition,
103
+ prefix: str = "",
104
+ name: str = "",
105
+ ) -> None:
149
106
  self.aperture = Aperture(prefix + "-MO-MAPT-01:")
150
107
  self.scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
151
- self.aperture_positions: AperturePositions | None = None
152
- self.TOLERANCE_STEPS = 3 # Number of MRES steps
153
- aperture_backend = SoftSignalBackend(
154
- SingleAperturePosition, AperturePositions.UNKNOWN
108
+ self.radius = soft_signal_rw(float, units="µm")
109
+ self._loaded_positions = loaded_positions
110
+ self._tolerances = tolerances
111
+ self.add_readables(
112
+ [
113
+ self.aperture.x.user_readback,
114
+ self.aperture.y.user_readback,
115
+ self.aperture.z.user_readback,
116
+ self.scatterguard.x.user_readback,
117
+ self.scatterguard.y.user_readback,
118
+ self.radius,
119
+ ],
155
120
  )
156
- aperture_backend.converter = self.ApertureConverter()
157
- self.selected_aperture = self.SelectedAperture(backend=aperture_backend)
158
- self.set_readable_signals(
159
- read=[
160
- self.selected_aperture,
161
- ]
162
- )
163
- super().__init__(name)
164
-
165
- class ApertureConverter(SoftConverter):
166
- # Ophyd-async #311 should add a default converter for dataclasses to do this
167
- def reading(
168
- self, value: SingleAperturePosition, timestamp: float, severity: int
169
- ) -> Reading:
170
- return Reading(
171
- value=asdict(value),
172
- timestamp=timestamp,
173
- alarm_severity=-1 if severity > 2 else severity,
174
- )
175
-
176
- class SelectedAperture(SignalR):
177
- async def read(self, *args, **kwargs):
178
- assert isinstance(self.parent, ApertureScatterguard)
179
- await self._backend.put(await self.parent._get_current_aperture_position())
180
- return {self.name: await self._backend.get_reading()}
181
-
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
- )
121
+ with self.add_children_as_readables(HintedSignal):
122
+ self.selected_aperture = soft_signal_rw(ApertureValue)
197
123
 
198
- def load_aperture_positions(self, positions: AperturePositions):
199
- LOGGER.info(f"{self.name} loaded in {positions}")
200
- self.aperture_positions = positions
201
-
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}")
124
+ super().__init__(name)
206
125
 
207
- return AsyncStatus(self._safe_move_within_datacollection_range(pos.location))
126
+ def get_position_from_gda_aperture_name(
127
+ self, gda_aperture_name: str
128
+ ) -> ApertureValue:
129
+ return ApertureValue(gda_aperture_name)
208
130
 
209
- def _get_motor_list(self):
210
- return [
211
- self.aperture.x,
212
- self.aperture.y,
213
- self.aperture.z,
214
- self.scatterguard.x,
215
- self.scatterguard.y,
216
- ]
131
+ @AsyncStatus.wrap
132
+ async def set(self, value: ApertureValue):
133
+ position = self._loaded_positions[value]
134
+ await self._safe_move_within_datacollection_range(position, value)
217
135
 
218
136
  @AsyncStatus.wrap
219
- async def _set_raw_unsafe(self, positions: ApertureFiveDimensionalLocation):
137
+ async def _set_raw_unsafe(self, position: AperturePosition):
220
138
  """Accept the risks and move in an unsafe way. Collisions possible."""
139
+ if position.radius is not None:
140
+ await self.radius.set(position.radius)
221
141
 
222
- # unpacking the position
223
- aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = positions
142
+ aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = (
143
+ position.values
144
+ )
224
145
 
225
146
  await asyncio.gather(
226
147
  self.aperture.x.set(aperture_x),
@@ -229,39 +150,41 @@ class ApertureScatterguard(StandardReadable, Movable):
229
150
  self.scatterguard.x.set(scatterguard_x),
230
151
  self.scatterguard.y.set(scatterguard_y),
231
152
  )
153
+ try:
154
+ value = await self.get_current_aperture_position()
155
+ self.selected_aperture.set(value)
156
+ except InvalidApertureMove:
157
+ self.selected_aperture.set(None) # type: ignore
232
158
 
233
- async def _get_current_aperture_position(self) -> SingleAperturePosition:
159
+ async def get_current_aperture_position(self) -> ApertureValue:
234
160
  """
235
161
  Returns the current aperture position using readback values
236
162
  for SMALL, MEDIUM, LARGE. ROBOT_LOAD position defined when
237
163
  mini aperture y <= ROBOT_LOAD.location.aperture_y + tolerance.
238
164
  If no position is found then raises InvalidApertureMove.
239
165
  """
240
- assert isinstance(self.aperture_positions, AperturePositions)
241
166
  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
167
+ robot_load_ap_y = self._loaded_positions[ApertureValue.ROBOT_LOAD].aperture_y
243
168
  if await self.aperture.large.get_value(cached=False) == 1:
244
- return self.aperture_positions.LARGE
169
+ return ApertureValue.LARGE
245
170
  elif await self.aperture.medium.get_value(cached=False) == 1:
246
- return self.aperture_positions.MEDIUM
171
+ return ApertureValue.MEDIUM
247
172
  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
173
+ return ApertureValue.SMALL
174
+ elif current_ap_y <= robot_load_ap_y + self._tolerances.aperture_y:
175
+ return ApertureValue.ROBOT_LOAD
251
176
 
252
177
  raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
253
178
 
254
179
  async def _safe_move_within_datacollection_range(
255
- self, pos: ApertureFiveDimensionalLocation
180
+ self, position: AperturePosition, value: ApertureValue
256
181
  ):
257
182
  """
258
183
  Move the aperture and scatterguard combo safely to a new position.
259
184
  See https://github.com/DiamondLightSource/hyperion/wiki/Aperture-Scatterguard-Collisions
260
185
  for why this is required.
261
186
  """
262
- assert self.aperture_positions is not None
263
- # unpacking the position
264
- aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = pos
187
+ assert self._loaded_positions is not None
265
188
 
266
189
  ap_z_in_position = await self.aperture.z.motor_done_move.get_value()
267
190
  if not ap_z_in_position:
@@ -271,17 +194,23 @@ class ApertureScatterguard(StandardReadable, Movable):
271
194
  )
272
195
 
273
196
  current_ap_z = await self.aperture.z.user_readback.get_value()
274
- diff_on_z = abs(current_ap_z - aperture_z)
275
- if diff_on_z > self.aperture_positions.tolerances.ap_z:
197
+ diff_on_z = abs(current_ap_z - position.aperture_z)
198
+ if diff_on_z > self._tolerances.aperture_z:
276
199
  raise InvalidApertureMove(
277
200
  "ApertureScatterguard safe move is not yet defined for positions "
278
201
  "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})."
202
+ f"Current aperture z ({current_ap_z}), outside of tolerance ({self._tolerances.aperture_z}) from target ({position.aperture_z})."
280
203
  )
281
204
 
282
205
  current_ap_y = await self.aperture.y.user_readback.get_value()
206
+ if position.radius is not None:
207
+ await self.radius.set(position.radius)
208
+
209
+ aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = (
210
+ position.values
211
+ )
283
212
 
284
- if aperture_y > current_ap_y:
213
+ if position.aperture_y > current_ap_y:
285
214
  await asyncio.gather(
286
215
  self.scatterguard.x.set(scatterguard_x),
287
216
  self.scatterguard.y.set(scatterguard_y),
@@ -302,3 +231,4 @@ class ApertureScatterguard(StandardReadable, Movable):
302
231
  self.scatterguard.x.set(scatterguard_x),
303
232
  self.scatterguard.y.set(scatterguard_y),
304
233
  )
234
+ await self.selected_aperture.set(value)
@@ -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,6 +1,7 @@
1
1
  import os
2
2
  import threading
3
3
  from abc import ABC, abstractmethod
4
+ from io import BytesIO
4
5
  from pathlib import Path
5
6
 
6
7
  import requests
@@ -70,7 +71,7 @@ class MJPG(Device, ABC):
70
71
  try:
71
72
  response = requests.get(url_str, stream=True)
72
73
  response.raise_for_status()
73
- with Image.open(response.raw) as image:
74
+ with Image.open(BytesIO(response.content)) as image:
74
75
  self.post_processing(image)
75
76
  st.set_finished()
76
77
  except requests.HTTPError as e:
@@ -1,3 +1,4 @@
1
+ from asyncio import sleep
1
2
  from enum import Enum
2
3
 
3
4
  from ophyd_async.core import AsyncStatus, StandardReadable
@@ -17,6 +18,8 @@ class BacklightPosition(str, Enum):
17
18
  class Backlight(StandardReadable):
18
19
  """Simple device to trigger the pneumatic in/out."""
19
20
 
21
+ TIME_TO_MOVE_S = 1 # Tested using a stopwatch on the beamline 09/2024
22
+
20
23
  def __init__(self, prefix: str, name: str = "") -> None:
21
24
  with self.add_children_as_readables():
22
25
  self.power = epics_signal_rw(BacklightPower, prefix + "-EA-BLIT-01:TOGGLE")
@@ -28,9 +31,17 @@ class Backlight(StandardReadable):
28
31
  @AsyncStatus.wrap
29
32
  async def set(self, position: BacklightPosition):
30
33
  """This setter will turn the backlight on when we move it in to the beam and off
31
- when we move it out."""
34
+ when we move it out.
35
+
36
+ Moving the backlight in/out is a pneumatic axis and we have no readback on its
37
+ position so it appears to us to instantly move. In fact it does take some time
38
+ to move completely in/out so we sleep here to simulate this.
39
+ """
40
+ old_position = await self.position.get_value()
32
41
  await self.position.set(position)
33
42
  if position == BacklightPosition.OUT:
34
43
  await self.power.set(BacklightPower.OFF)
35
44
  else:
36
45
  await self.power.set(BacklightPower.ON)
46
+ if old_position != position:
47
+ await sleep(self.TIME_TO_MOVE_S)
@@ -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)
dodal/devices/dcm.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from ophyd_async.core import StandardReadable
2
- from ophyd_async.epics.motion import Motor
2
+ from ophyd_async.epics.motor import Motor
3
3
  from ophyd_async.epics.signal import epics_signal_r
4
4
 
5
5
 
@@ -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