dls-dodal 1.30.0__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 (65) hide show
  1. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/METADATA +4 -4
  2. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/RECORD +64 -62
  3. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamline_specific_utils/i03.py +1 -4
  6. dodal/beamlines/__init__.py +4 -0
  7. dodal/beamlines/i03.py +8 -8
  8. dodal/beamlines/i04.py +10 -9
  9. dodal/beamlines/i13_1.py +7 -7
  10. dodal/beamlines/i22.py +18 -18
  11. dodal/beamlines/p38.py +14 -14
  12. dodal/beamlines/p45.py +11 -11
  13. dodal/beamlines/training_rig.py +64 -0
  14. dodal/common/beamlines/beamline_parameters.py +5 -4
  15. dodal/common/beamlines/beamline_utils.py +9 -9
  16. dodal/common/types.py +4 -2
  17. dodal/common/udc_directory_provider.py +29 -22
  18. dodal/common/visit.py +59 -60
  19. dodal/devices/CTAB.py +1 -1
  20. dodal/devices/aperture.py +1 -1
  21. dodal/devices/aperturescatterguard.py +140 -188
  22. dodal/devices/areadetector/plugins/MJPG.py +2 -1
  23. dodal/devices/backlight.py +12 -1
  24. dodal/devices/dcm.py +1 -1
  25. dodal/devices/detector/detector.py +31 -30
  26. dodal/devices/detector/detector_motion.py +1 -1
  27. dodal/devices/fast_grid_scan.py +14 -24
  28. dodal/devices/focusing_mirror.py +2 -2
  29. dodal/devices/i22/dcm.py +1 -1
  30. dodal/devices/i22/fswitch.py +6 -2
  31. dodal/devices/i22/nxsas.py +32 -11
  32. dodal/devices/i24/aperture.py +1 -1
  33. dodal/devices/i24/beamstop.py +1 -1
  34. dodal/devices/i24/dcm.py +1 -1
  35. dodal/devices/i24/i24_detector_motion.py +1 -1
  36. dodal/devices/i24/pmac.py +24 -8
  37. dodal/devices/linkam3.py +1 -1
  38. dodal/devices/motors.py +1 -1
  39. dodal/devices/oav/oav_to_redis_forwarder.py +46 -17
  40. dodal/devices/robot.py +1 -2
  41. dodal/devices/scatterguard.py +1 -1
  42. dodal/devices/scintillator.py +1 -1
  43. dodal/devices/slits.py +1 -1
  44. dodal/devices/smargon.py +1 -1
  45. dodal/devices/tetramm.py +20 -16
  46. dodal/devices/training_rig/__init__.py +0 -0
  47. dodal/devices/training_rig/sample_stage.py +10 -0
  48. dodal/devices/turbo_slit.py +1 -1
  49. dodal/devices/undulator.py +1 -1
  50. dodal/devices/util/adjuster_plans.py +1 -1
  51. dodal/devices/util/save_panda.py +1 -1
  52. dodal/devices/util/test_utils.py +1 -1
  53. dodal/devices/xbpm_feedback.py +1 -2
  54. dodal/devices/xspress3/xspress3.py +1 -1
  55. dodal/devices/zebra.py +5 -0
  56. dodal/devices/zebra_controlled_shutter.py +24 -9
  57. dodal/devices/zocalo/zocalo_results.py +6 -2
  58. dodal/log.py +32 -10
  59. dodal/plans/check_topup.py +65 -10
  60. dodal/plans/data_session_metadata.py +8 -10
  61. dodal/plans/motor_util_plans.py +1 -1
  62. dodal/devices/beamstop.py +0 -8
  63. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/LICENSE +0 -0
  64. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/entry_points.txt +0 -0
  65. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,16 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
- from collections import namedtuple
3
- from dataclasses import asdict, dataclass
4
4
  from enum import Enum
5
5
 
6
- from bluesky.protocols import Movable, Reading
7
- from event_model.documents.event_descriptor import DataKey
8
- from ophyd_async.core import AsyncStatus, HintedSignal, SignalR, StandardReadable
9
- 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
10
14
 
11
15
  from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
12
16
  from dodal.devices.aperture import Aperture
@@ -17,100 +21,76 @@ class InvalidApertureMove(Exception):
17
21
  pass
18
22
 
19
23
 
20
- ApertureFiveDimensionalLocation = namedtuple(
21
- "ApertureFiveDimensionalLocation",
22
- [
23
- "aperture_x",
24
- "aperture_y",
25
- "aperture_z",
26
- "scatterguard_x",
27
- "scatterguard_y",
28
- ],
29
- )
30
-
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
+ )
31
41
 
32
- @dataclass
33
- class ApertureScatterguardTolerances:
34
- ap_x: float
35
- ap_y: float
36
- ap_z: float
37
- sg_x: float
38
- sg_y: float
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
+ )
39
53
 
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,
67
+ )
40
68
 
41
- @dataclass
42
- class SingleAperturePosition:
43
- name: str
44
- GDA_name: str
45
- radius_microns: float | None
46
- location: ApertureFiveDimensionalLocation
47
69
 
70
+ class ApertureValue(str, Enum):
71
+ """Maps from a short usable name to the value name in the GDA Beamline parameters"""
48
72
 
49
- # Use StrEnum once we stop python 3.10 support
50
- class AperturePositionGDANames(str, Enum):
51
- LARGE_APERTURE = "LARGE_APERTURE"
52
- MEDIUM_APERTURE = "MEDIUM_APERTURE"
53
- SMALL_APERTURE = "SMALL_APERTURE"
54
73
  ROBOT_LOAD = "ROBOT_LOAD"
55
-
56
- def __str__(self):
57
- return str(self.value)
58
-
59
-
60
- def position_from_params(
61
- name: str,
62
- GDA_name: AperturePositionGDANames,
63
- radius_microns: float | None,
64
- params: GDABeamlineParameters,
65
- ) -> SingleAperturePosition:
66
- return SingleAperturePosition(
67
- name,
68
- GDA_name,
69
- radius_microns,
70
- ApertureFiveDimensionalLocation(
71
- params[f"miniap_x_{GDA_name}"],
72
- params[f"miniap_y_{GDA_name}"],
73
- params[f"miniap_z_{GDA_name}"],
74
- params[f"sg_x_{GDA_name}"],
75
- params[f"sg_y_{GDA_name}"],
76
- ),
77
- )
78
-
79
-
80
- def load_tolerances_from_beamline_params(
81
- params: GDABeamlineParameters,
82
- ) -> ApertureScatterguardTolerances:
83
- return ApertureScatterguardTolerances(
84
- ap_x=params["miniap_x_tolerance"],
85
- ap_y=params["miniap_y_tolerance"],
86
- ap_z=params["miniap_z_tolerance"],
87
- sg_x=params["sg_x_tolerance"],
88
- sg_y=params["sg_y_tolerance"],
89
- )
90
-
91
-
92
- class AperturePosition(Enum):
93
- ROBOT_LOAD = 0
94
- SMALL = 1
95
- MEDIUM = 2
96
- LARGE = 3
74
+ SMALL = "SMALL_APERTURE"
75
+ MEDIUM = "MEDIUM_APERTURE"
76
+ LARGE = "LARGE_APERTURE"
97
77
 
98
78
 
99
79
  def load_positions_from_beamline_parameters(
100
80
  params: GDABeamlineParameters,
101
- ) -> dict[AperturePosition, SingleAperturePosition]:
81
+ ) -> dict[ApertureValue, AperturePosition]:
102
82
  return {
103
- AperturePosition.ROBOT_LOAD: position_from_params(
104
- "Robot load", AperturePositionGDANames.ROBOT_LOAD, None, params
83
+ ApertureValue.ROBOT_LOAD: AperturePosition.from_gda_params(
84
+ ApertureValue.ROBOT_LOAD, None, params
105
85
  ),
106
- AperturePosition.SMALL: position_from_params(
107
- "Small", AperturePositionGDANames.SMALL_APERTURE, 20, params
86
+ ApertureValue.SMALL: AperturePosition.from_gda_params(
87
+ ApertureValue.SMALL, 20, params
108
88
  ),
109
- AperturePosition.MEDIUM: position_from_params(
110
- "Medium", AperturePositionGDANames.MEDIUM_APERTURE, 50, params
89
+ ApertureValue.MEDIUM: AperturePosition.from_gda_params(
90
+ ApertureValue.MEDIUM, 50, params
111
91
  ),
112
- AperturePosition.LARGE: position_from_params(
113
- "Large", AperturePositionGDANames.LARGE_APERTURE, 100, params
92
+ ApertureValue.LARGE: AperturePosition.from_gda_params(
93
+ ApertureValue.LARGE, 100, params
114
94
  ),
115
95
  }
116
96
 
@@ -118,119 +98,86 @@ def load_positions_from_beamline_parameters(
118
98
  class ApertureScatterguard(StandardReadable, Movable):
119
99
  def __init__(
120
100
  self,
121
- loaded_positions: dict[AperturePosition, SingleAperturePosition],
122
- tolerances: ApertureScatterguardTolerances,
101
+ loaded_positions: dict[ApertureValue, AperturePosition],
102
+ tolerances: AperturePosition,
123
103
  prefix: str = "",
124
104
  name: str = "",
125
105
  ) -> None:
126
- self._aperture = Aperture(prefix + "-MO-MAPT-01:")
127
- self._scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
106
+ self.aperture = Aperture(prefix + "-MO-MAPT-01:")
107
+ self.scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
108
+ self.radius = soft_signal_rw(float, units="µm")
128
109
  self._loaded_positions = loaded_positions
129
110
  self._tolerances = tolerances
130
- aperture_backend = SoftSignalBackend(
131
- SingleAperturePosition, self._loaded_positions[AperturePosition.ROBOT_LOAD]
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
+ ],
132
120
  )
133
- aperture_backend.converter = self.ApertureConverter()
134
- self.selected_aperture = self.SelectedAperture(backend=aperture_backend)
135
- self.add_readables([self.selected_aperture], wrapper=HintedSignal)
136
- super().__init__(name)
137
-
138
- class ApertureConverter(SoftConverter):
139
- # Ophyd-async #311 should add a default converter for dataclasses to do this
140
- def reading(
141
- self, value: SingleAperturePosition, timestamp: float, severity: int
142
- ) -> Reading:
143
- return Reading(
144
- value=asdict(value),
145
- timestamp=timestamp,
146
- alarm_severity=-1 if severity > 2 else severity,
147
- )
121
+ with self.add_children_as_readables(HintedSignal):
122
+ self.selected_aperture = soft_signal_rw(ApertureValue)
148
123
 
149
- class SelectedAperture(SignalR):
150
- async def read(self, *args, **kwargs):
151
- assert isinstance(self.parent, ApertureScatterguard)
152
- assert self._backend
153
- await self._backend.put(await self.parent.get_current_aperture_position())
154
- return {self.name: await self._backend.get_reading()}
155
-
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
- }
124
+ super().__init__(name)
166
125
 
167
126
  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
- )
176
-
177
- def get_gda_name_for_position(self, position: AperturePosition) -> str:
178
- detailed_position = self._loaded_positions[position]
179
- return detailed_position.GDA_name
127
+ self, gda_aperture_name: str
128
+ ) -> ApertureValue:
129
+ return ApertureValue(gda_aperture_name)
180
130
 
181
131
  @AsyncStatus.wrap
182
- async def set(self, value: AperturePosition):
132
+ async def set(self, value: ApertureValue):
183
133
  position = self._loaded_positions[value]
184
- await self._safe_move_within_datacollection_range(position.location)
185
-
186
- def _get_motor_list(self):
187
- return [
188
- self._aperture.x,
189
- self._aperture.y,
190
- self._aperture.z,
191
- self._scatterguard.x,
192
- self._scatterguard.y,
193
- ]
134
+ await self._safe_move_within_datacollection_range(position, value)
194
135
 
195
136
  @AsyncStatus.wrap
196
- async def _set_raw_unsafe(self, positions: ApertureFiveDimensionalLocation):
137
+ async def _set_raw_unsafe(self, position: AperturePosition):
197
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)
198
141
 
199
- # unpacking the position
200
- 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
+ )
201
145
 
202
146
  await asyncio.gather(
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),
147
+ self.aperture.x.set(aperture_x),
148
+ self.aperture.y.set(aperture_y),
149
+ self.aperture.z.set(aperture_z),
150
+ self.scatterguard.x.set(scatterguard_x),
151
+ self.scatterguard.y.set(scatterguard_y),
208
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
209
158
 
210
- async def get_current_aperture_position(self) -> SingleAperturePosition:
159
+ async def get_current_aperture_position(self) -> ApertureValue:
211
160
  """
212
161
  Returns the current aperture position using readback values
213
162
  for SMALL, MEDIUM, LARGE. ROBOT_LOAD position defined when
214
163
  mini aperture y <= ROBOT_LOAD.location.aperture_y + tolerance.
215
164
  If no position is found then raises InvalidApertureMove.
216
165
  """
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]
166
+ current_ap_y = await self.aperture.y.user_readback.get_value(cached=False)
167
+ robot_load_ap_y = self._loaded_positions[ApertureValue.ROBOT_LOAD].aperture_y
168
+ if await self.aperture.large.get_value(cached=False) == 1:
169
+ return ApertureValue.LARGE
170
+ elif await self.aperture.medium.get_value(cached=False) == 1:
171
+ return ApertureValue.MEDIUM
172
+ elif await self.aperture.small.get_value(cached=False) == 1:
173
+ return ApertureValue.SMALL
174
+ elif current_ap_y <= robot_load_ap_y + self._tolerances.aperture_y:
175
+ return ApertureValue.ROBOT_LOAD
229
176
 
230
177
  raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
231
178
 
232
179
  async def _safe_move_within_datacollection_range(
233
- self, pos: ApertureFiveDimensionalLocation
180
+ self, position: AperturePosition, value: ApertureValue
234
181
  ):
235
182
  """
236
183
  Move the aperture and scatterguard combo safely to a new position.
@@ -238,45 +185,50 @@ class ApertureScatterguard(StandardReadable, Movable):
238
185
  for why this is required.
239
186
  """
240
187
  assert self._loaded_positions is not None
241
- # unpacking the position
242
- aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = pos
243
188
 
244
- ap_z_in_position = await self._aperture.z.motor_done_move.get_value()
189
+ ap_z_in_position = await self.aperture.z.motor_done_move.get_value()
245
190
  if not ap_z_in_position:
246
191
  raise InvalidApertureMove(
247
192
  "ApertureScatterguard z is still moving. Wait for it to finish "
248
193
  "before triggering another move."
249
194
  )
250
195
 
251
- current_ap_z = await self._aperture.z.user_readback.get_value()
252
- diff_on_z = abs(current_ap_z - aperture_z)
253
- if diff_on_z > self._tolerances.ap_z:
196
+ current_ap_z = await self.aperture.z.user_readback.get_value()
197
+ diff_on_z = abs(current_ap_z - position.aperture_z)
198
+ if diff_on_z > self._tolerances.aperture_z:
254
199
  raise InvalidApertureMove(
255
200
  "ApertureScatterguard safe move is not yet defined for positions "
256
201
  "outside of LARGE, MEDIUM, SMALL, ROBOT_LOAD. "
257
- f"Current aperture z ({current_ap_z}), outside of tolerance ({self._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})."
258
203
  )
259
204
 
260
- current_ap_y = await self._aperture.y.user_readback.get_value()
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
+ )
261
212
 
262
- if aperture_y > current_ap_y:
213
+ if position.aperture_y > current_ap_y:
263
214
  await asyncio.gather(
264
- self._scatterguard.x.set(scatterguard_x),
265
- self._scatterguard.y.set(scatterguard_y),
215
+ self.scatterguard.x.set(scatterguard_x),
216
+ self.scatterguard.y.set(scatterguard_y),
266
217
  )
267
218
  await asyncio.gather(
268
- self._aperture.x.set(aperture_x),
269
- self._aperture.y.set(aperture_y),
270
- self._aperture.z.set(aperture_z),
219
+ self.aperture.x.set(aperture_x),
220
+ self.aperture.y.set(aperture_y),
221
+ self.aperture.z.set(aperture_z),
271
222
  )
272
223
  return
273
224
  await asyncio.gather(
274
- self._aperture.x.set(aperture_x),
275
- self._aperture.y.set(aperture_y),
276
- self._aperture.z.set(aperture_z),
225
+ self.aperture.x.set(aperture_x),
226
+ self.aperture.y.set(aperture_y),
227
+ self.aperture.z.set(aperture_z),
277
228
  )
278
229
 
279
230
  await asyncio.gather(
280
- self._scatterguard.x.set(scatterguard_x),
281
- self._scatterguard.y.set(scatterguard_y),
231
+ self.scatterguard.x.set(scatterguard_x),
232
+ self.scatterguard.y.set(scatterguard_y),
282
233
  )
234
+ await self.selected_aperture.set(value)
@@ -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)
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,7 +1,7 @@
1
1
  from enum import Enum, auto
2
- from typing import Any
2
+ from pathlib import Path
3
3
 
4
- from pydantic import BaseModel, root_validator, validator
4
+ from pydantic import BaseModel, Field, field_serializer, field_validator
5
5
 
6
6
  from dodal.devices.detector.det_dim_constants import (
7
7
  EIGER2_X_16M_SIZE,
@@ -28,9 +28,12 @@ class DetectorParams(BaseModel):
28
28
  """Holds parameters for the detector. Provides access to a list of Dectris detector
29
29
  sizes and a converter for distance to beam centre."""
30
30
 
31
+ # https://github.com/pydantic/pydantic/issues/8379
32
+ # Must use model_dump(by_alias=True) if serialising!
33
+
31
34
  expected_energy_ev: float | None = None
32
35
  exposure_time: float
33
- directory: str
36
+ directory: str # : Path https://github.com/DiamondLightSource/dodal/issues/774
34
37
  prefix: str
35
38
  detector_distance: float
36
39
  omega_start: float
@@ -39,46 +42,44 @@ class DetectorParams(BaseModel):
39
42
  num_triggers: int
40
43
  use_roi_mode: bool
41
44
  det_dist_to_beam_converter_path: str
45
+ override_run_number: int | None = Field(default=None, alias="run_number")
42
46
  trigger_mode: TriggerMode = TriggerMode.SET_FRAMES
43
47
  detector_size_constants: DetectorSizeConstants = EIGER2_X_16M_SIZE
44
- beam_xy_converter: DetectorDistanceToBeamXYConverter = None # type: ignore # Filled in by validator
45
- run_number: int = None # type: ignore # Filled in by validator
46
48
  enable_dev_shm: bool = (
47
49
  False # Remove in https://github.com/DiamondLightSource/hyperion/issues/1395
48
50
  )
49
51
 
50
- class Config:
51
- arbitrary_types_allowed = True
52
- json_encoders = {
53
- DetectorDistanceToBeamXYConverter: lambda _: None,
54
- DetectorSizeConstants: lambda d: d.det_type_string,
55
- }
56
-
57
- # should be replaced with model_validator once move to pydantic 2 is complete
58
- @root_validator(pre=True)
59
- def create_beamxy_and_runnumber(cls, values: dict[str, Any]) -> dict[str, Any]:
60
- values["beam_xy_converter"] = DetectorDistanceToBeamXYConverter(
61
- values["det_dist_to_beam_converter_path"]
52
+ @property
53
+ def beam_xy_converter(self) -> DetectorDistanceToBeamXYConverter:
54
+ return DetectorDistanceToBeamXYConverter(self.det_dist_to_beam_converter_path)
55
+
56
+ @property
57
+ def run_number(self) -> int:
58
+ return (
59
+ get_run_number(self.directory, self.prefix)
60
+ if self.override_run_number is None
61
+ else self.override_run_number
62
62
  )
63
- if values.get("run_number") is None:
64
- values["run_number"] = get_run_number(values["directory"], values["prefix"])
65
- return values
66
-
67
- @validator("detector_size_constants", pre=True)
68
- def _parse_detector_size_constants(
69
- cls, det_type: str, values: dict[str, Any]
70
- ) -> DetectorSizeConstants:
63
+
64
+ @field_serializer("detector_size_constants")
65
+ def serialize_detector_size_constants(self, size: DetectorSizeConstants):
66
+ return size.det_type_string
67
+
68
+ @field_validator("detector_size_constants", mode="before")
69
+ @classmethod
70
+ def _parse_detector_size_constants(cls, det_type: str) -> DetectorSizeConstants:
71
71
  return (
72
72
  det_type
73
73
  if isinstance(det_type, DetectorSizeConstants)
74
74
  else constants_from_type(det_type)
75
75
  )
76
76
 
77
- @validator("directory", pre=True)
78
- def _parse_directory(cls, directory: str, values: dict[str, Any]) -> str:
79
- if not directory.endswith("/"):
80
- directory += "/"
81
- return directory
77
+ @field_validator("directory", mode="before")
78
+ @classmethod
79
+ def _parse_directory(cls, directory: str | Path) -> str:
80
+ path = Path(directory)
81
+ assert path.is_dir()
82
+ return str(path)
82
83
 
83
84
  def get_beam_position_mm(self, detector_distance: float) -> tuple[float, float]:
84
85
  x_beam_mm = self.beam_xy_converter.get_beam_xy_from_det_dist(
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
3
  from ophyd_async.core import Device
4
- from ophyd_async.epics.motion import Motor
4
+ from ophyd_async.epics.motor import Motor
5
5
  from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
6
6
 
7
7
 
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, Generic, TypeVar
2
+ from typing import Generic, TypeVar
3
3
 
4
4
  import numpy as np
5
5
  from bluesky.plan_stubs import mv
@@ -20,7 +20,7 @@ from ophyd_async.epics.signal import (
20
20
  epics_signal_rw_rbv,
21
21
  epics_signal_x,
22
22
  )
23
- from pydantic import validator
23
+ from pydantic import field_validator
24
24
  from pydantic.dataclasses import dataclass
25
25
 
26
26
  from dodal.log import LOGGER
@@ -69,9 +69,6 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
69
69
  y2_start: float = 0.1
70
70
  z1_start: float = 0.1
71
71
  z2_start: float = 0.1
72
- x_axis: GridAxis = GridAxis(0, 0, 0)
73
- y_axis: GridAxis = GridAxis(0, 0, 0)
74
- z_axis: GridAxis = GridAxis(0, 0, 0)
75
72
 
76
73
  # Whether to set the stub offsets after centering
77
74
  set_stub_offsets: bool = False
@@ -91,28 +88,20 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
91
88
  "z2_start": self.z2_start,
92
89
  }
93
90
 
94
- class Config:
95
- arbitrary_types_allowed = True
96
- fields = {
97
- "x_axis": {"exclude": True},
98
- "y_axis": {"exclude": True},
99
- "z_axis": {"exclude": True},
100
- }
101
-
102
- @validator("x_axis", always=True)
103
- def _get_x_axis(cls, x_axis: GridAxis, values: dict[str, Any]) -> GridAxis:
104
- return GridAxis(values["x_start"], values["x_step_size"], values["x_steps"])
91
+ @property
92
+ def x_axis(self) -> GridAxis:
93
+ return GridAxis(self.x_start, self.x_step_size, self.x_steps)
105
94
 
106
- @validator("y_axis", always=True)
107
- def _get_y_axis(cls, y_axis: GridAxis, values: dict[str, Any]) -> GridAxis:
108
- return GridAxis(values["y1_start"], values["y_step_size"], values["y_steps"])
95
+ @property
96
+ def y_axis(self) -> GridAxis:
97
+ return GridAxis(self.y1_start, self.y_step_size, self.y_steps)
109
98
 
110
- @validator("z_axis", always=True)
111
- def _get_z_axis(cls, z_axis: GridAxis, values: dict[str, Any]) -> GridAxis:
112
- return GridAxis(values["z2_start"], values["z_step_size"], values["z_steps"])
99
+ @property
100
+ def z_axis(self) -> GridAxis:
101
+ return GridAxis(self.z2_start, self.z_step_size, self.z_steps)
113
102
 
114
103
  def get_num_images(self):
115
- return self.x_steps * self.y_steps + self.x_steps * self.z_steps
104
+ return self.x_steps * (self.y_steps + self.z_steps)
116
105
 
117
106
  @property
118
107
  def is_3d_grid_scan(self):
@@ -155,7 +144,8 @@ class ZebraGridScanParams(GridScanParamsCommon):
155
144
  param_positions["dwell_time_ms"] = self.dwell_time_ms
156
145
  return param_positions
157
146
 
158
- @validator("dwell_time_ms", always=True, check_fields=True)
147
+ @field_validator("dwell_time_ms")
148
+ @classmethod
159
149
  def non_integer_dwell_time(cls, dwell_time_ms: float) -> float:
160
150
  dwell_time_floor_rounded = np.floor(dwell_time_ms)
161
151
  dwell_time_is_close = np.isclose(
@@ -8,9 +8,9 @@ from ophyd_async.core import (
8
8
  HintedSignal,
9
9
  StandardReadable,
10
10
  observe_value,
11
+ soft_signal_r_and_setter,
11
12
  )
12
- from ophyd_async.core.signal import soft_signal_r_and_setter
13
- from ophyd_async.epics.motion import Motor
13
+ from ophyd_async.epics.motor import Motor
14
14
  from ophyd_async.epics.signal import (
15
15
  epics_signal_r,
16
16
  epics_signal_rw,