dls-dodal 1.65.0__py3-none-any.whl → 1.66.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 (59) hide show
  1. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/RECORD +56 -50
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/aithre.py +21 -2
  5. dodal/beamlines/i03.py +23 -3
  6. dodal/beamlines/i04.py +18 -3
  7. dodal/beamlines/i05.py +28 -1
  8. dodal/beamlines/i06.py +62 -0
  9. dodal/beamlines/i07.py +20 -0
  10. dodal/beamlines/i09_1.py +7 -2
  11. dodal/beamlines/i10_optics.py +18 -8
  12. dodal/beamlines/i18.py +3 -3
  13. dodal/beamlines/i22.py +3 -3
  14. dodal/beamlines/p38.py +3 -3
  15. dodal/devices/aithre_lasershaping/goniometer.py +26 -9
  16. dodal/devices/aperturescatterguard.py +3 -2
  17. dodal/devices/apple2_undulator.py +89 -44
  18. dodal/devices/areadetector/plugins/mjpg.py +10 -3
  19. dodal/devices/beamsize/__init__.py +0 -0
  20. dodal/devices/beamsize/beamsize.py +6 -0
  21. dodal/devices/detector/det_resolution.py +4 -2
  22. dodal/devices/fast_grid_scan.py +14 -2
  23. dodal/devices/i03/beamsize.py +35 -0
  24. dodal/devices/i03/constants.py +7 -0
  25. dodal/devices/i03/undulator_dcm.py +2 -2
  26. dodal/devices/i04/beamsize.py +45 -0
  27. dodal/devices/i04/murko_results.py +36 -26
  28. dodal/devices/i04/transfocator.py +23 -29
  29. dodal/devices/i07/id.py +38 -0
  30. dodal/devices/i09_1_shared/__init__.py +6 -2
  31. dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
  32. dodal/devices/i10/i10_apple2.py +22 -316
  33. dodal/devices/i17/i17_apple2.py +7 -4
  34. dodal/devices/ipin.py +20 -2
  35. dodal/devices/motors.py +19 -3
  36. dodal/devices/mx_phase1/beamstop.py +31 -12
  37. dodal/devices/oav/oav_calculations.py +9 -4
  38. dodal/devices/oav/oav_detector.py +65 -7
  39. dodal/devices/oav/oav_parameters.py +3 -1
  40. dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
  41. dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
  42. dodal/devices/oav/pin_image_recognition/utils.py +23 -1
  43. dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
  44. dodal/devices/oav/utils.py +16 -6
  45. dodal/devices/robot.py +17 -7
  46. dodal/devices/scintillator.py +36 -14
  47. dodal/devices/smargon.py +2 -3
  48. dodal/devices/thawer.py +7 -45
  49. dodal/devices/undulator.py +152 -68
  50. dodal/devices/util/lookup_tables_apple2.py +390 -0
  51. dodal/plans/load_panda_yaml.py +9 -0
  52. dodal/plans/verify_undulator_gap.py +2 -2
  53. dodal/beamline_specific_utils/i03.py +0 -17
  54. dodal/testing/__init__.py +0 -3
  55. dodal/testing/setup.py +0 -67
  56. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/WHEEL +0 -0
  57. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/entry_points.txt +0 -0
  58. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/licenses/LICENSE +0 -0
  59. {dls_dodal-1.65.0.dist-info → dls_dodal-1.66.0.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,8 @@ note:
6
6
  idd == id1, idu == id2.
7
7
  """
8
8
 
9
+ from pathlib import Path
10
+
9
11
  from daq_config_server.client import ConfigServer
10
12
 
11
13
  from dodal.common.beamlines.beamline_utils import device_factory
@@ -34,6 +36,12 @@ from dodal.devices.i10.i10_apple2 import (
34
36
  from dodal.devices.i10.i10_setting_data import I10Grating
35
37
  from dodal.devices.pgm import PlaneGratingMonochromator
36
38
  from dodal.devices.synchrotron import Synchrotron
39
+ from dodal.devices.util.lookup_tables_apple2 import (
40
+ DEFAULT_GAP_FILE,
41
+ DEFAULT_PHASE_FILE,
42
+ EnergyMotorLookup,
43
+ LookupTableConfig,
44
+ )
37
45
  from dodal.log import set_beamline as set_log_beamline
38
46
  from dodal.utils import BeamlinePrefix, get_beamline_name
39
47
 
@@ -115,12 +123,13 @@ def idd() -> I10Apple2:
115
123
  @device_factory()
116
124
  def idd_controller() -> I10Apple2Controller:
117
125
  """I10 downstream insertion device controller."""
118
- return I10Apple2Controller(
119
- apple2=idd(),
120
- lookuptable_dir=LOOK_UPTABLE_DIR,
121
- source=("Source", "idd"),
126
+ idd_energy_motor_lut = EnergyMotorLookup(
122
127
  config_client=I10_CONF_CLIENT,
128
+ lut_config=LookupTableConfig(source=("Source", "idd")),
129
+ gap_path=Path(LOOK_UPTABLE_DIR, DEFAULT_GAP_FILE),
130
+ phase_path=Path(LOOK_UPTABLE_DIR, DEFAULT_PHASE_FILE),
123
131
  )
132
+ return I10Apple2Controller(apple2=idd(), energy_motor_lut=idd_energy_motor_lut)
124
133
 
125
134
 
126
135
  @device_factory()
@@ -179,12 +188,13 @@ def idu() -> I10Apple2:
179
188
  @device_factory()
180
189
  def idu_controller() -> I10Apple2Controller:
181
190
  """I10 upstream insertion device controller."""
182
- return I10Apple2Controller(
183
- apple2=idu(),
184
- lookuptable_dir=LOOK_UPTABLE_DIR,
185
- source=("Source", "idu"),
191
+ idu_energy_motor_lut = EnergyMotorLookup(
186
192
  config_client=I10_CONF_CLIENT,
193
+ lut_config=LookupTableConfig(source=("Source", "idu")),
194
+ gap_path=Path(LOOK_UPTABLE_DIR, DEFAULT_GAP_FILE),
195
+ phase_path=Path(LOOK_UPTABLE_DIR, DEFAULT_PHASE_FILE),
187
196
  )
197
+ return I10Apple2Controller(apple2=idd(), energy_motor_lut=idu_energy_motor_lut)
188
198
 
189
199
 
190
200
  @device_factory()
dodal/beamlines/i18.py CHANGED
@@ -23,7 +23,7 @@ from dodal.devices.motors import XYStage, XYZThetaStage
23
23
  from dodal.devices.slits import Slits
24
24
  from dodal.devices.synchrotron import Synchrotron
25
25
  from dodal.devices.tetramm import TetrammDetector
26
- from dodal.devices.undulator import Undulator
26
+ from dodal.devices.undulator import UndulatorInKeV
27
27
  from dodal.log import set_beamline as set_log_beamline
28
28
  from dodal.utils import BeamlinePrefix, get_beamline_name
29
29
 
@@ -53,8 +53,8 @@ def synchrotron() -> Synchrotron:
53
53
 
54
54
 
55
55
  @device_factory()
56
- def undulator() -> Undulator:
57
- return Undulator(f"{PREFIX.insertion_prefix}-MO-SERVC-01:")
56
+ def undulator() -> UndulatorInKeV:
57
+ return UndulatorInKeV(f"{PREFIX.insertion_prefix}-MO-SERVC-01:")
58
58
 
59
59
 
60
60
  # See https://github.com/DiamondLightSource/dodal/issues/1180
dodal/beamlines/i22.py CHANGED
@@ -23,7 +23,7 @@ from dodal.devices.motors import XYPitchStage
23
23
  from dodal.devices.slits import Slits
24
24
  from dodal.devices.synchrotron import Synchrotron
25
25
  from dodal.devices.tetramm import TetrammDetector
26
- from dodal.devices.undulator import Undulator
26
+ from dodal.devices.undulator import UndulatorInKeV
27
27
  from dodal.devices.watsonmarlow323_pump import WatsonMarlow323Pump
28
28
  from dodal.log import set_beamline as set_log_beamline
29
29
  from dodal.utils import BeamlinePrefix, get_beamline_name
@@ -160,8 +160,8 @@ def dcm() -> DCM:
160
160
 
161
161
 
162
162
  @device_factory()
163
- def undulator() -> Undulator:
164
- return Undulator(
163
+ def undulator() -> UndulatorInKeV:
164
+ return UndulatorInKeV(
165
165
  prefix=f"{PREFIX.insertion_prefix}-MO-SERVC-01:",
166
166
  id_gap_lookup_table_path="/dls_sw/i22/software/daq_configuration/lookup/BeamLine_Undulator_toGap.txt",
167
167
  poles=80,
dodal/beamlines/p38.py CHANGED
@@ -22,7 +22,7 @@ from dodal.devices.linkam3 import Linkam3
22
22
  from dodal.devices.pressure_jump_cell import PressureJumpCell
23
23
  from dodal.devices.slits import Slits
24
24
  from dodal.devices.tetramm import TetrammDetector
25
- from dodal.devices.undulator import Undulator
25
+ from dodal.devices.undulator import UndulatorInKeV
26
26
  from dodal.devices.watsonmarlow323_pump import WatsonMarlow323Pump
27
27
  from dodal.log import set_beamline as set_log_beamline
28
28
  from dodal.utils import BeamlinePrefix, get_beamline_name
@@ -157,8 +157,8 @@ def dcm() -> DCM:
157
157
 
158
158
 
159
159
  @device_factory(mock=True)
160
- def undulator() -> Undulator:
161
- return Undulator(
160
+ def undulator() -> UndulatorInKeV:
161
+ return UndulatorInKeV(
162
162
  f"{PREFIX.insertion_prefix}-MO-SERVC-01:",
163
163
  poles=80,
164
164
  length=2.0,
@@ -1,9 +1,9 @@
1
1
  from ophyd_async.epics.motor import Motor
2
2
 
3
- from dodal.devices.motors import XYZStage, create_axis_perp_to_rotation
3
+ from dodal.devices.motors import XYZOmegaStage, create_axis_perp_to_rotation
4
4
 
5
5
 
6
- class Goniometer(XYZStage):
6
+ class Goniometer(XYZOmegaStage):
7
7
  """The Aithre lab goniometer and the XYZ stage it sits on.
8
8
 
9
9
  `x`, `y` and `z` control the axes of the positioner at the base, while `sampy` and
@@ -15,11 +15,28 @@ class Goniometer(XYZStage):
15
15
  regardless of the current rotation.
16
16
  """
17
17
 
18
- def __init__(self, prefix: str, name: str = "") -> None:
19
- self.sampy = Motor(prefix + "SAMPY")
20
- self.sampz = Motor(prefix + "SAMPZ")
21
- self.omega = Motor(prefix + "OMEGA")
22
- self.vertical_position = create_axis_perp_to_rotation(
23
- self.omega, self.sampz, self.sampy
18
+ def __init__(
19
+ self,
20
+ prefix: str,
21
+ name: str = "",
22
+ x_infix: str = "X",
23
+ y_infix: str = "Y",
24
+ z_infix: str = "Z",
25
+ omega_infix: str = "OMEGA",
26
+ sampy_infix: str = "SAMPY",
27
+ sampz_infix: str = "SAMPZ",
28
+ ) -> None:
29
+ super().__init__(
30
+ prefix=prefix,
31
+ name=name,
32
+ x_infix=x_infix,
33
+ y_infix=y_infix,
34
+ z_infix=z_infix,
35
+ omega_infix=omega_infix,
24
36
  )
25
- super().__init__(name)
37
+ with self.add_children_as_readables():
38
+ self.sampy = Motor(prefix + sampy_infix)
39
+ self.sampz = Motor(prefix + sampz_infix)
40
+ self.vertical_position = create_axis_perp_to_rotation(
41
+ self.omega, self.sampz, self.sampy
42
+ )
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ from math import inf
4
5
 
5
6
  from bluesky.protocols import Preparable
6
7
  from ophyd_async.core import (
@@ -112,7 +113,7 @@ def load_positions_from_beamline_parameters(
112
113
  ) -> dict[ApertureValue, AperturePosition]:
113
114
  return {
114
115
  ApertureValue.OUT_OF_BEAM: AperturePosition.from_gda_params(
115
- _GDAParamApertureValue.ROBOT_LOAD, 0, params
116
+ _GDAParamApertureValue.ROBOT_LOAD, inf, params
116
117
  ),
117
118
  ApertureValue.SMALL: AperturePosition.from_gda_params(
118
119
  _GDAParamApertureValue.SMALL, 20, params
@@ -124,7 +125,7 @@ def load_positions_from_beamline_parameters(
124
125
  _GDAParamApertureValue.LARGE, 100, params
125
126
  ),
126
127
  ApertureValue.PARKED: AperturePosition.from_gda_params(
127
- _GDAParamApertureValue.MANUAL_LOAD, 0, params
128
+ _GDAParamApertureValue.MANUAL_LOAD, inf, params
128
129
  ),
129
130
  }
130
131
 
@@ -23,11 +23,13 @@ from ophyd_async.core import (
23
23
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
24
24
  from ophyd_async.epics.motor import Motor
25
25
 
26
+ from dodal.common.enums import EnabledDisabledUpper
26
27
  from dodal.log import LOGGER
27
28
 
28
29
  T = TypeVar("T")
29
30
 
30
31
  DEFAULT_MOTOR_MIN_TIMEOUT = 10
32
+ MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
31
33
 
32
34
 
33
35
  class UndulatorGateStatus(StrictEnum):
@@ -36,16 +38,24 @@ class UndulatorGateStatus(StrictEnum):
36
38
 
37
39
 
38
40
  @dataclass
39
- class Apple2PhasesVal:
41
+ class Apple2LockedPhasesVal:
40
42
  top_outer: str
41
- top_inner: str
42
43
  btm_inner: str
44
+
45
+
46
+ @dataclass
47
+ class Apple2PhasesVal(Apple2LockedPhasesVal):
48
+ top_inner: str
43
49
  btm_outer: str
44
50
 
45
51
 
46
52
  @dataclass
47
- class Apple2Val(Apple2PhasesVal):
53
+ class Apple2Val:
48
54
  gap: str
55
+ phase: Apple2LockedPhasesVal | Apple2PhasesVal
56
+
57
+ def extract_phase_val(self):
58
+ return self.phase
49
59
 
50
60
 
51
61
  class Pol(StrictEnum):
@@ -80,10 +90,7 @@ class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
80
90
  def __init__(self, set_move: SignalW, prefix: str, name: str = ""):
81
91
  # Gate keeper open when move is requested, closed when move is completed
82
92
  self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
83
-
84
- split_pv = prefix.split("-")
85
- fault_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
86
- self.fault = epics_signal_r(float, fault_pv)
93
+ self.status = epics_signal_r(EnabledDisabledUpper, prefix + "IDBLENA")
87
94
  self.set_move = set_move
88
95
  super().__init__(name)
89
96
 
@@ -91,14 +98,14 @@ class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
91
98
  async def set(self, value: T) -> None:
92
99
  LOGGER.info(f"Setting {self.name} to {value}")
93
100
  await self.raise_if_cannot_move()
94
- await self._set_demand_positions(value)
101
+ await self.set_demand_positions(value)
95
102
  timeout = await self.get_timeout()
96
103
  LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
97
104
  await self.set_move.set(value=1, timeout=timeout)
98
105
  await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
99
106
 
100
107
  @abc.abstractmethod
101
- async def _set_demand_positions(self, value: T) -> None:
108
+ async def set_demand_positions(self, value: T) -> None:
102
109
  """Set the demand positions on the device without actually hitting move."""
103
110
 
104
111
  @abc.abstractmethod
@@ -106,8 +113,8 @@ class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
106
113
  """Get the timeout for the move based on an estimate of how long it will take."""
107
114
 
108
115
  async def raise_if_cannot_move(self) -> None:
109
- if await self.fault.get_value() != 0:
110
- raise RuntimeError(f"{self.name} is in fault state")
116
+ if await self.status.get_value() is not EnabledDisabledUpper.ENABLED:
117
+ raise RuntimeError(f"{self.name} is DISABLED and cannot move.")
111
118
  if await self.gate.get_value() == UndulatorGateStatus.OPEN:
112
119
  raise RuntimeError(f"{self.name} is already in motion.")
113
120
 
@@ -158,7 +165,7 @@ class UndulatorGap(SafeUndulatorMover[float]):
158
165
  self.user_readback = epics_signal_r(float, prefix + "CURRGAPD")
159
166
  super().__init__(self.set_move, prefix, name)
160
167
 
161
- async def _set_demand_positions(self, value: float) -> None:
168
+ async def set_demand_positions(self, value: float) -> None:
162
169
  await self.user_setpoint.set(str(value))
163
170
 
164
171
  async def get_timeout(self) -> float:
@@ -206,49 +213,39 @@ class UndulatorPhaseMotor(StandardReadable):
206
213
  super().__init__(name=name)
207
214
 
208
215
 
209
- class UndulatorPhaseAxes(SafeUndulatorMover[Apple2PhasesVal]):
210
- """
211
- A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
212
- e.g. top_outer == Q1
213
- top_inner == Q2
214
- btm_inner == q3
215
- btm_outer == q4
216
+ Apple2PhaseValType = TypeVar("Apple2PhaseValType", bound=Apple2LockedPhasesVal)
216
217
 
217
- """
218
+
219
+ class UndulatorLockedPhaseAxes(SafeUndulatorMover[Apple2PhaseValType]):
220
+ """Two phase Motor to make up the locked id phase motion."""
218
221
 
219
222
  def __init__(
220
223
  self,
221
224
  prefix: str,
222
225
  top_outer: str,
223
- top_inner: str,
224
226
  btm_inner: str,
225
- btm_outer: str,
226
227
  name: str = "",
227
228
  ):
228
229
  # Gap demand set point and readback
229
230
  with self.add_children_as_readables():
230
231
  self.top_outer = UndulatorPhaseMotor(prefix=prefix, infix=top_outer)
231
- self.top_inner = UndulatorPhaseMotor(prefix=prefix, infix=top_inner)
232
232
  self.btm_inner = UndulatorPhaseMotor(prefix=prefix, infix=btm_inner)
233
- self.btm_outer = UndulatorPhaseMotor(prefix=prefix, infix=btm_outer)
234
233
  # Nothing move until this is set to 1 and it will return to 0 when done.
235
234
  self.set_move = epics_signal_rw(int, f"{prefix}BL{top_outer}" + "MOVE")
236
-
235
+ self.axes = [self.top_outer, self.btm_inner]
237
236
  super().__init__(self.set_move, prefix, name)
238
237
 
239
- async def _set_demand_positions(self, value: Apple2PhasesVal) -> None:
238
+ async def set_demand_positions(self, value: Apple2PhaseValType) -> None:
240
239
  await asyncio.gather(
241
240
  self.top_outer.user_setpoint.set(value=value.top_outer),
242
- self.top_inner.user_setpoint.set(value=value.top_inner),
243
241
  self.btm_inner.user_setpoint.set(value=value.btm_inner),
244
- self.btm_outer.user_setpoint.set(value=value.btm_outer),
245
242
  )
246
243
 
247
244
  async def get_timeout(self) -> float:
248
245
  """
249
246
  Get all four motor speed, current positions and target positions to calculate required timeout.
250
247
  """
251
- axes = [self.top_outer, self.top_inner, self.btm_inner, self.btm_outer]
248
+
252
249
  timeouts = await asyncio.gather(
253
250
  *[
254
251
  estimate_motor_timeout(
@@ -256,7 +253,7 @@ class UndulatorPhaseAxes(SafeUndulatorMover[Apple2PhasesVal]):
256
253
  axis.user_readback,
257
254
  axis.velocity,
258
255
  )
259
- for axis in axes
256
+ for axis in self.axes
260
257
  ]
261
258
  )
262
259
  """A 2.0 multiplier is required to prevent premature motor timeouts in phase
@@ -266,6 +263,42 @@ class UndulatorPhaseAxes(SafeUndulatorMover[Apple2PhasesVal]):
266
263
  return np.max(timeouts) * 2.0
267
264
 
268
265
 
266
+ class UndulatorPhaseAxes(UndulatorLockedPhaseAxes[Apple2PhasesVal]):
267
+ """
268
+ A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
269
+ e.g. top_outer == Q1
270
+ top_inner == Q2
271
+ btm_inner == q3
272
+ btm_outer == q4
273
+
274
+ """
275
+
276
+ def __init__(
277
+ self,
278
+ prefix: str,
279
+ top_outer: str,
280
+ top_inner: str,
281
+ btm_inner: str,
282
+ btm_outer: str,
283
+ name: str = "",
284
+ ):
285
+ # Gap demand set point and readback
286
+ with self.add_children_as_readables():
287
+ self.top_inner = UndulatorPhaseMotor(prefix=prefix, infix=top_inner)
288
+ self.btm_outer = UndulatorPhaseMotor(prefix=prefix, infix=btm_outer)
289
+
290
+ super().__init__(prefix, top_outer=top_outer, btm_inner=btm_inner, name=name)
291
+ self.axes.extend([self.top_inner, self.btm_outer])
292
+
293
+ async def set_demand_positions(self, value: Apple2PhasesVal) -> None:
294
+ await asyncio.gather(
295
+ self.top_outer.user_setpoint.set(value=value.top_outer),
296
+ self.top_inner.user_setpoint.set(value=value.top_inner),
297
+ self.btm_inner.user_setpoint.set(value=value.btm_inner),
298
+ self.btm_outer.user_setpoint.set(value=value.btm_outer),
299
+ )
300
+
301
+
269
302
  class UndulatorJawPhase(SafeUndulatorMover[float]):
270
303
  """
271
304
  A JawPhase movable, this is use for moving the jaw phase which is use to control the
@@ -287,7 +320,7 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
287
320
 
288
321
  super().__init__(self.set_move, prefix, name)
289
322
 
290
- async def _set_demand_positions(self, value: float) -> None:
323
+ async def set_demand_positions(self, value: float) -> None:
291
324
  await self.jaw_phase.user_setpoint.set(value=str(value))
292
325
 
293
326
  async def get_timeout(self) -> float:
@@ -301,7 +334,10 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
301
334
  )
302
335
 
303
336
 
304
- class Apple2(StandardReadable, Movable):
337
+ PhaseAxesType = TypeVar("PhaseAxesType", bound=UndulatorLockedPhaseAxes)
338
+
339
+
340
+ class Apple2(StandardReadable, Movable[Apple2Val], Generic[PhaseAxesType]):
305
341
  """
306
342
  Device representing the combined motor controls for an Apple2 undulator.
307
343
 
@@ -313,7 +349,7 @@ class Apple2(StandardReadable, Movable):
313
349
  The undulator phase axes device, consisting of four phase motors.
314
350
  """
315
351
 
316
- def __init__(self, id_gap: UndulatorGap, id_phase: UndulatorPhaseAxes, name=""):
352
+ def __init__(self, id_gap: UndulatorGap, id_phase: PhaseAxesType, name=""):
317
353
  """
318
354
  Parameters
319
355
  ----------
@@ -339,12 +375,12 @@ class Apple2(StandardReadable, Movable):
339
375
 
340
376
  # Only need to check gap as the phase motors share both fault and gate with gap.
341
377
  await self.gap().raise_if_cannot_move()
378
+
342
379
  await asyncio.gather(
343
- self.phase().top_outer.user_setpoint.set(value=id_motor_values.top_outer),
344
- self.phase().top_inner.user_setpoint.set(value=id_motor_values.top_inner),
345
- self.phase().btm_inner.user_setpoint.set(value=id_motor_values.btm_inner),
346
- self.phase().btm_outer.user_setpoint.set(value=id_motor_values.btm_outer),
347
- self.gap().user_setpoint.set(value=id_motor_values.gap),
380
+ self.phase().set_demand_positions(
381
+ value=id_motor_values.extract_phase_val()
382
+ ),
383
+ self.gap().set_demand_positions(value=float(id_motor_values.gap)),
348
384
  )
349
385
  timeout = np.max(
350
386
  await asyncio.gather(self.gap().get_timeout(), self.phase().get_timeout())
@@ -367,7 +403,7 @@ class EnergyMotorConvertor(Protocol):
367
403
  ...
368
404
 
369
405
 
370
- Apple2Type = TypeVar("Apple2Type", bound="Apple2")
406
+ Apple2Type = TypeVar("Apple2Type", bound=Apple2)
371
407
 
372
408
 
373
409
  class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
@@ -444,16 +480,25 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
444
480
  self.polarisation_setpoint, self._polarisation_setpoint_set = (
445
481
  soft_signal_r_and_setter(Pol)
446
482
  )
483
+ # check if undulator phase is unlocked.
484
+ if isinstance(self.apple2().phase(), UndulatorPhaseAxes):
485
+ top_inner = self.apple2().phase().top_inner.user_readback
486
+ btm_outer = self.apple2().phase().btm_outer.user_readback
487
+ else:
488
+ # If locked phase axes make the locked phase 0.
489
+ top_inner = btm_outer = soft_signal_rw(float, initial_value=0.0)
490
+
447
491
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
448
492
  # Hardware backed read/write for polarisation.
493
+
449
494
  self.polarisation = derived_signal_rw(
450
495
  raw_to_derived=self._read_pol,
451
496
  set_derived=self._set_pol,
452
497
  pol=self.polarisation_setpoint,
453
498
  top_outer=self.apple2().phase().top_outer.user_readback,
454
- top_inner=self.apple2().phase().top_inner.user_readback,
499
+ top_inner=top_inner,
455
500
  btm_inner=self.apple2().phase().btm_inner.user_readback,
456
- btm_outer=self.apple2().phase().btm_outer.user_readback,
501
+ btm_outer=btm_outer,
457
502
  gap=self.apple2().gap().user_readback,
458
503
  )
459
504
  super().__init__(name)
@@ -501,7 +546,7 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
501
546
  ) -> None:
502
547
  # This changes the pol setpoint and then changes polarisation via set energy.
503
548
  self._polarisation_setpoint_set(value)
504
- await self.energy.set(await self.energy.get_value())
549
+ await self.energy.set(await self.energy.get_value(), timeout=MAXIMUM_MOVE_TIME)
505
550
 
506
551
  def _read_pol(
507
552
  self,
@@ -681,7 +726,7 @@ class InsertionDeviceEnergy(InsertionDeviceEnergyBase):
681
726
 
682
727
  @AsyncStatus.wrap
683
728
  async def set(self, energy: float) -> None:
684
- await self.energy().set(energy)
729
+ await self.energy().set(energy, timeout=MAXIMUM_MOVE_TIME)
685
730
 
686
731
 
687
732
  class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
@@ -696,7 +741,7 @@ class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
696
741
 
697
742
  @AsyncStatus.wrap
698
743
  async def set(self, pol: Pol) -> None:
699
- await self.polarisation().set(pol)
744
+ await self.polarisation().set(pol, timeout=MAXIMUM_MOVE_TIME)
700
745
 
701
746
  async def locate(self) -> Location[Pol]:
702
747
  """Return the current polarisation"""
@@ -28,11 +28,18 @@ class MJPG(StandardReadable, Triggerable, ABC):
28
28
  latest image from the stream to the `post_processing` method for child classes to handle.
29
29
  """
30
30
 
31
- def __init__(self, prefix: str, name: str = "") -> None:
31
+ def __init__(
32
+ self,
33
+ prefix: str,
34
+ name: str = "",
35
+ x_size_pv: str = "ArraySize1_RBV",
36
+ y_size_pv: str = "ArraySize2_RBV",
37
+ ) -> None:
32
38
  self.url = epics_signal_rw(str, prefix + "JPG_URL_RBV")
39
+ self.video_url = epics_signal_rw(str, prefix + "MJPG_URL_RBV")
33
40
 
34
- self.x_size = epics_signal_r(int, prefix + "ArraySize1_RBV")
35
- self.y_size = epics_signal_r(int, prefix + "ArraySize2_RBV")
41
+ self.x_size = epics_signal_r(int, prefix + x_size_pv)
42
+ self.y_size = epics_signal_r(int, prefix + y_size_pv)
36
43
 
37
44
  with self.add_children_as_readables():
38
45
  self.filename = soft_signal_rw(str)
File without changes
@@ -0,0 +1,6 @@
1
+ from ophyd_async.core import SignalR, StandardReadable
2
+
3
+
4
+ class BeamsizeBase(StandardReadable):
5
+ x_um: SignalR[float]
6
+ y_um: SignalR[float]
@@ -10,14 +10,16 @@ def _get_detector_radius_mm(detector_params: DetectorParams):
10
10
  return 0.5 * _get_detector_max_size_mm(detector_params)
11
11
 
12
12
 
13
- def _get_detector_max_size_mm(detector_params):
13
+ def _get_detector_max_size_mm(detector_params: DetectorParams):
14
14
  return max(
15
15
  detector_params.detector_size_constants.det_dimension.width,
16
16
  detector_params.detector_size_constants.det_dimension.height,
17
17
  )
18
18
 
19
19
 
20
- def _get_beam_xy_accounting_for_roi(detector_params, det_distance_mm):
20
+ def _get_beam_xy_accounting_for_roi(
21
+ detector_params: DetectorParams, det_distance_mm: float
22
+ ):
21
23
  beam_x = detector_params.beam_xy_converter.get_beam_xy_from_det_dist(
22
24
  det_distance_mm, Axis.X_AXIS
23
25
  )
@@ -1,5 +1,7 @@
1
1
  import asyncio
2
2
  from abc import ABC, abstractmethod
3
+ from functools import partial
4
+ from math import isclose
3
5
  from typing import Generic, TypeVar
4
6
 
5
7
  import numpy as np
@@ -307,13 +309,23 @@ class FastGridScanCommon(
307
309
  set_statuses = []
308
310
 
309
311
  LOGGER.info("Applying gridscan parameters...")
312
+
310
313
  # Create arguments for bps.mv
311
314
  for key, signal in self._movable_params.items():
312
315
  param_value = value.__dict__[key]
313
- set_statuses.append(await set_and_wait_for_value(signal, param_value)) # type: ignore
316
+
317
+ matcher = partial(isclose, param_value, abs_tol=0.001)
318
+
319
+ set_statuses.append(
320
+ set_and_wait_for_value(
321
+ signal, # type: ignore
322
+ param_value,
323
+ match_value=matcher,
324
+ )
325
+ )
314
326
 
315
327
  # Counter should always start at 0
316
- set_statuses.append(await set_and_wait_for_value(self.position_counter, 0))
328
+ set_statuses.append(set_and_wait_for_value(self.position_counter, 0))
317
329
 
318
330
  LOGGER.info("Gridscan parameters applied, waiting for sets to complete...")
319
331
 
@@ -0,0 +1,35 @@
1
+ from ophyd_async.core import Reference, derived_signal_r
2
+
3
+ from dodal.devices.aperturescatterguard import ApertureScatterguard
4
+ from dodal.devices.beamsize.beamsize import BeamsizeBase
5
+ from dodal.devices.i03.constants import BeamsizeConstants
6
+
7
+
8
+ class Beamsize(BeamsizeBase):
9
+ def __init__(self, aperture_scatterguard: ApertureScatterguard, name=""):
10
+ super().__init__(name=name)
11
+ self._aperture_scatterguard_ref = Reference(aperture_scatterguard)
12
+
13
+ with self.add_children_as_readables():
14
+ self.x_um = derived_signal_r(
15
+ self._get_beamsize_x,
16
+ aperture_radius=self._aperture_scatterguard_ref().radius,
17
+ derived_units="µm",
18
+ )
19
+ self.y_um = derived_signal_r(
20
+ self._get_beamsize_y,
21
+ aperture_radius=self._aperture_scatterguard_ref().radius,
22
+ derived_units="µm",
23
+ )
24
+
25
+ def _get_beamsize_x(
26
+ self,
27
+ aperture_radius: float,
28
+ ) -> float:
29
+ return min(aperture_radius, BeamsizeConstants.BEAM_WIDTH_UM)
30
+
31
+ def _get_beamsize_y(
32
+ self,
33
+ aperture_radius: float,
34
+ ) -> float:
35
+ return min(aperture_radius, BeamsizeConstants.BEAM_HEIGHT_UM)
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class BeamsizeConstants:
6
+ BEAM_WIDTH_UM = 80.0
7
+ BEAM_HEIGHT_UM = 20.0
@@ -5,7 +5,7 @@ from ophyd_async.core import AsyncStatus, Reference, StandardReadable
5
5
 
6
6
  from dodal.common.beamlines.beamline_parameters import get_beamline_parameters
7
7
  from dodal.devices.i03.dcm import DCM
8
- from dodal.devices.undulator import Undulator
8
+ from dodal.devices.undulator import UndulatorInKeV
9
9
  from dodal.log import LOGGER
10
10
 
11
11
  ENERGY_TIMEOUT_S: float = 30.0
@@ -31,7 +31,7 @@ class UndulatorDCM(StandardReadable, Movable[float]):
31
31
 
32
32
  def __init__(
33
33
  self,
34
- undulator: Undulator,
34
+ undulator: UndulatorInKeV,
35
35
  dcm: DCM,
36
36
  daq_configuration_path: str,
37
37
  name: str = "",