dls-dodal 1.35.0__py3-none-any.whl → 1.36.1a0__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 (46) hide show
  1. {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/METADATA +33 -31
  2. {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/RECORD +46 -37
  3. {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/b01_1.py +16 -31
  6. dodal/beamlines/i22.py +124 -265
  7. dodal/beamlines/i24.py +56 -7
  8. dodal/beamlines/p38.py +16 -1
  9. dodal/beamlines/p99.py +22 -53
  10. dodal/beamlines/training_rig.py +16 -26
  11. dodal/cli.py +54 -8
  12. dodal/common/beamlines/beamline_utils.py +32 -2
  13. dodal/common/beamlines/device_helpers.py +2 -0
  14. dodal/devices/aperture.py +7 -0
  15. dodal/devices/aperturescatterguard.py +195 -79
  16. dodal/devices/dcm.py +5 -4
  17. dodal/devices/eiger.py +88 -49
  18. dodal/devices/fast_grid_scan.py +21 -46
  19. dodal/devices/focusing_mirror.py +8 -3
  20. dodal/devices/i24/beam_center.py +12 -0
  21. dodal/devices/i24/focus_mirrors.py +60 -0
  22. dodal/devices/i24/pilatus_metadata.py +44 -0
  23. dodal/devices/linkam3.py +1 -1
  24. dodal/devices/motors.py +14 -10
  25. dodal/devices/oav/oav_detector.py +2 -2
  26. dodal/devices/oav/pin_image_recognition/__init__.py +4 -5
  27. dodal/devices/oav/utils.py +1 -0
  28. dodal/devices/p99/sample_stage.py +12 -16
  29. dodal/devices/pressure_jump_cell.py +299 -0
  30. dodal/devices/robot.py +1 -1
  31. dodal/devices/tetramm.py +1 -1
  32. dodal/devices/undulator.py +4 -1
  33. dodal/devices/undulator_dcm.py +3 -19
  34. dodal/devices/zocalo/zocalo_results.py +7 -7
  35. dodal/plan_stubs/__init__.py +0 -0
  36. dodal/{plans/data_session_metadata.py → plan_stubs/data_session.py} +2 -2
  37. dodal/{plans/motor_util_plans.py → plan_stubs/motor_utils.py} +2 -2
  38. dodal/plan_stubs/wrapped.py +150 -0
  39. dodal/plans/__init__.py +4 -0
  40. dodal/plans/scanspec.py +66 -0
  41. dodal/plans/wrapped.py +57 -0
  42. dodal/utils.py +151 -2
  43. {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/LICENSE +0 -0
  44. {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/entry_points.txt +0 -0
  45. {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/top_level.txt +0 -0
  46. /dodal/{plans → plan_stubs}/check_topup.py +0 -0
dodal/beamlines/p99.py CHANGED
@@ -1,61 +1,30 @@
1
- from dodal.common.beamlines.beamline_utils import device_instantiation, set_beamline
1
+ from dodal.common.beamlines.beamline_utils import device_factory, set_beamline
2
2
  from dodal.devices.motors import XYZPositioner
3
3
  from dodal.devices.p99.sample_stage import FilterMotor, SampleAngleStage
4
4
  from dodal.log import set_beamline as set_log_beamline
5
- from dodal.utils import get_beamline_name
5
+ from dodal.utils import BeamlinePrefix, get_beamline_name
6
6
 
7
- BL = get_beamline_name("BL99P")
7
+ BL = get_beamline_name("p99")
8
+ PREFIX = BeamlinePrefix(BL)
8
9
  set_log_beamline(BL)
9
10
  set_beamline(BL)
10
11
 
11
12
 
12
- def sample_angle_stage(
13
- wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
14
- ) -> SampleAngleStage:
15
- """Sample stage for p99"""
16
-
17
- return device_instantiation(
18
- SampleAngleStage,
19
- prefix="-MO-STAGE-01:",
20
- name="sample_angle_stage",
21
- wait=wait_for_connection,
22
- fake=fake_with_ophyd_sim,
23
- )
24
-
25
-
26
- def sample_stage_filer(
27
- wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
28
- ) -> FilterMotor:
29
- """Sample stage for p99"""
30
-
31
- return device_instantiation(
32
- FilterMotor,
33
- prefix="-MO-STAGE-02:MP:SELECT",
34
- name="sample_stage_filer",
35
- wait=wait_for_connection,
36
- fake=fake_with_ophyd_sim,
37
- )
38
-
39
-
40
- def sample_xyz_stage(
41
- wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
42
- ) -> XYZPositioner:
43
- return device_instantiation(
44
- FilterMotor,
45
- prefix="-MO-STAGE-02:",
46
- name="sample_xyz_stage",
47
- wait=wait_for_connection,
48
- fake=fake_with_ophyd_sim,
49
- )
50
-
51
-
52
- def sample_lab_xyz_stage(
53
- wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
54
- ) -> XYZPositioner:
55
- return device_instantiation(
56
- FilterMotor,
57
- prefix="-MO-STAGE-02:LAB:",
58
- name="sample_lab_xyz_stage",
59
- wait=wait_for_connection,
60
- fake=fake_with_ophyd_sim,
61
- )
13
+ @device_factory()
14
+ def angle_stage() -> SampleAngleStage:
15
+ return SampleAngleStage(f"{PREFIX.beamline_prefix}-MO-STAGE-01:")
16
+
17
+
18
+ @device_factory()
19
+ def filter() -> FilterMotor:
20
+ return FilterMotor(f"{PREFIX.beamline_prefix}-MO-STAGE-02:MP:SELECT")
21
+
22
+
23
+ @device_factory()
24
+ def sample_stage() -> XYZPositioner:
25
+ return XYZPositioner(f"{PREFIX.beamline_prefix}-MO-STAGE-02:")
26
+
27
+
28
+ @device_factory()
29
+ def lab_stage() -> XYZPositioner:
30
+ return XYZPositioner(f"{PREFIX.beamline_prefix}-MO-STAGE-02:LAB:")
@@ -3,15 +3,16 @@ from pathlib import Path
3
3
  from ophyd_async.epics.adaravis import AravisDetector
4
4
 
5
5
  from dodal.common.beamlines.beamline_utils import (
6
- device_instantiation,
6
+ device_factory,
7
7
  get_path_provider,
8
8
  set_path_provider,
9
9
  )
10
10
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
11
+ from dodal.common.beamlines.device_helpers import HDF5_PREFIX
11
12
  from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider
12
13
  from dodal.devices.training_rig.sample_stage import TrainingRigSampleStage
13
14
  from dodal.log import set_beamline as set_log_beamline
14
- from dodal.utils import get_beamline_name
15
+ from dodal.utils import BeamlinePrefix, get_beamline_name
15
16
 
16
17
  #
17
18
  # HTSS Training Rig
@@ -20,11 +21,12 @@ from dodal.utils import get_beamline_name
20
21
  # simple motors, a GigE camera and a PandA.
21
22
  # Since there are multiple rigs whose PVs are identical aside from the prefix,
22
23
  # this module can be used for any rig. It should fill in the prefix automatically
23
- # if the ${BEAMLINE} environment variable is correctly set. It currently defaults
24
- # to p47.
24
+ # if the ${BEAMLINE} environment variable is correctly set, else defaulting
25
+ # to p46, which is known to be in good working order.
25
26
  #
26
27
 
27
- BL = get_beamline_name("p47")
28
+ BL = get_beamline_name("p46")
29
+ PREFIX = BeamlinePrefix(BL)
28
30
  set_log_beamline(BL)
29
31
  set_utils_beamline(BL)
30
32
 
@@ -37,28 +39,16 @@ set_path_provider(
37
39
  )
38
40
 
39
41
 
40
- def sample_stage(
41
- wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
42
- ) -> TrainingRigSampleStage:
43
- return device_instantiation(
44
- TrainingRigSampleStage,
45
- "sample_stage",
46
- "-MO-MAP-01:STAGE:",
47
- wait_for_connection,
48
- fake_with_ophyd_sim,
49
- )
42
+ @device_factory()
43
+ def sample_stage() -> TrainingRigSampleStage:
44
+ return TrainingRigSampleStage(f"{PREFIX.beamline_prefix}-MO-MAP-01:STAGE:")
50
45
 
51
46
 
52
- def det(
53
- wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
54
- ) -> AravisDetector:
55
- return device_instantiation(
56
- AravisDetector,
57
- "det",
58
- "-EA-DET-01:",
59
- wait_for_connection,
60
- fake_with_ophyd_sim,
61
- drv_suffix="DET:",
62
- hdf_suffix="HDF5:",
47
+ @device_factory()
48
+ def det() -> AravisDetector:
49
+ return AravisDetector(
50
+ f"{PREFIX.beamline_prefix}-EA-DET-01:",
63
51
  path_provider=get_path_provider(),
52
+ drv_suffix="DET:",
53
+ hdf_suffix=HDF5_PREFIX,
64
54
  )
dodal/cli.py CHANGED
@@ -1,11 +1,13 @@
1
1
  import os
2
+ from collections.abc import Mapping
2
3
 
3
4
  import click
4
5
  from bluesky.run_engine import RunEngine
5
6
  from ophyd_async.core import NotConnected
7
+ from ophyd_async.plan_stubs import ensure_connected
6
8
 
7
9
  from dodal.beamlines import all_beamline_names, module_name_for_beamline
8
- from dodal.utils import make_all_devices
10
+ from dodal.utils import AnyDevice, filter_ophyd_devices, make_all_devices
9
11
 
10
12
  from . import __version__
11
13
 
@@ -50,22 +52,66 @@ def connect(beamline: str, all: bool, sim_backend: bool) -> None:
50
52
 
51
53
  # We need to make a RunEngine to allow ophyd-async devices to connect.
52
54
  # See https://blueskyproject.io/ophyd-async/main/explanations/event-loop-choice.html
53
- RunEngine()
55
+ RE = RunEngine(call_returns_result=True)
54
56
 
55
57
  print(f"Attempting connection to {beamline} (using {full_module_path})")
56
- devices, exceptions = make_all_devices(
58
+
59
+ # Force all devices to be lazy (don't connect to PVs on instantiation) and do
60
+ # connection as an extra step, because the alternatives is handling the fact
61
+ # that only some devices may be lazy.
62
+ devices, instance_exceptions = make_all_devices(
57
63
  full_module_path,
58
64
  include_skipped=all,
59
65
  fake_with_ophyd_sim=sim_backend,
66
+ wait_for_connection=False,
60
67
  )
61
- sim_statement = " (sim mode)" if sim_backend else ""
68
+ devices, connect_exceptions = _connect_devices(RE, devices, sim_backend)
62
69
 
63
- print(f"{len(devices)} devices connected{sim_statement}:")
70
+ # Inform user of successful connections
71
+ _report_successful_devices(devices, sim_backend)
72
+
73
+ # If exceptions have occurred, this will print details of the relevant PVs
74
+ exceptions = {**instance_exceptions, **connect_exceptions}
75
+ if len(exceptions) > 0:
76
+ raise NotConnected(exceptions)
77
+
78
+
79
+ def _report_successful_devices(
80
+ devices: Mapping[str, AnyDevice],
81
+ sim_backend: bool,
82
+ ) -> None:
83
+ sim_statement = " (sim mode)" if sim_backend else ""
64
84
  connected_devices = "\n".join(
65
85
  sorted([f"\t{device_name}" for device_name in devices.keys()])
66
86
  )
87
+
88
+ print(f"{len(devices)} devices connected{sim_statement}:")
67
89
  print(connected_devices)
68
90
 
69
- # If exceptions have occurred, this will print details of the relevant PVs
70
- if len(exceptions) > 0:
71
- raise NotConnected(exceptions)
91
+
92
+ def _connect_devices(
93
+ RE: RunEngine,
94
+ devices: Mapping[str, AnyDevice],
95
+ sim_backend: bool,
96
+ ) -> tuple[Mapping[str, AnyDevice], Mapping[str, Exception]]:
97
+ ophyd_devices, ophyd_async_devices = filter_ophyd_devices(devices)
98
+ exceptions = {}
99
+
100
+ # Connect ophyd devices
101
+ for name, device in ophyd_devices.items():
102
+ try:
103
+ device.wait_for_connection()
104
+ except Exception as ex:
105
+ exceptions[name] = ex
106
+
107
+ # Connect ophyd-async devices
108
+ try:
109
+ RE(ensure_connected(*ophyd_async_devices.values(), mock=sim_backend))
110
+ except NotConnected as ex:
111
+ exceptions = {**exceptions, **ex.sub_errors}
112
+
113
+ # Only return the subset of devices that haven't raised an exception
114
+ successful_devices = {
115
+ name: device for name, device in devices.items() if name not in exceptions
116
+ }
117
+ return successful_devices, exceptions
@@ -1,15 +1,23 @@
1
1
  import inspect
2
2
  from collections.abc import Callable
3
- from typing import Final, TypeVar, cast
3
+ from typing import Annotated, Final, TypeVar, cast
4
4
 
5
5
  from bluesky.run_engine import call_in_bluesky_event_loop
6
6
  from ophyd import Device as OphydV1Device
7
7
  from ophyd.sim import make_fake_device
8
+ from ophyd_async.core import DEFAULT_TIMEOUT
8
9
  from ophyd_async.core import Device as OphydV2Device
9
10
  from ophyd_async.core import wait_for_connection as v2_device_wait_for_connection
10
11
 
11
12
  from dodal.common.types import UpdatingPathProvider
12
- from dodal.utils import AnyDevice, BeamlinePrefix, skip_device
13
+ from dodal.utils import (
14
+ AnyDevice,
15
+ BeamlinePrefix,
16
+ D,
17
+ DeviceInitializationController,
18
+ SkipType,
19
+ skip_device,
20
+ )
13
21
 
14
22
  DEFAULT_CONNECTION_TIMEOUT: Final[float] = 5.0
15
23
 
@@ -124,6 +132,28 @@ def device_instantiation(
124
132
  return device_instance
125
133
 
126
134
 
135
+ def device_factory(
136
+ *,
137
+ use_factory_name: Annotated[bool, "Use factory name as name of device"] = True,
138
+ timeout: Annotated[float, "Timeout for connecting to the device"] = DEFAULT_TIMEOUT,
139
+ mock: Annotated[bool, "Use Signals with mock backends for device"] = False,
140
+ skip: Annotated[
141
+ SkipType,
142
+ "mark the factory to be (conditionally) skipped when beamline is imported by external program",
143
+ ] = False,
144
+ ) -> Callable[[Callable[[], D]], DeviceInitializationController[D]]:
145
+ def decorator(factory: Callable[[], D]) -> DeviceInitializationController[D]:
146
+ return DeviceInitializationController(
147
+ factory,
148
+ use_factory_name,
149
+ timeout,
150
+ mock,
151
+ skip,
152
+ )
153
+
154
+ return decorator
155
+
156
+
127
157
  def set_path_provider(provider: UpdatingPathProvider):
128
158
  global PATH_PROVIDER
129
159
 
@@ -2,6 +2,8 @@ from dodal.common.beamlines.beamline_utils import device_instantiation
2
2
  from dodal.devices.slits import Slits
3
3
  from dodal.utils import skip_device
4
4
 
5
+ HDF5_PREFIX = "HDF5:"
6
+
5
7
 
6
8
  @skip_device()
7
9
  def numbered_slits(
dodal/devices/aperture.py CHANGED
@@ -12,3 +12,10 @@ class Aperture(StandardReadable):
12
12
  self.medium = epics_signal_r(float, prefix + "Y:MEDIUM_CALC")
13
13
  self.large = epics_signal_r(float, prefix + "Y:LARGE_CALC")
14
14
  super().__init__(name)
15
+
16
+ async def in_position(self):
17
+ return (
18
+ await self.small.get_value()
19
+ or await self.medium.get_value()
20
+ or await self.large.get_value()
21
+ )
@@ -1,14 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ from collections.abc import Callable, Coroutine
5
+ from typing import Any
4
6
 
5
- from bluesky.protocols import Movable
7
+ from bluesky.protocols import Movable, Triggerable
6
8
  from ophyd_async.core import (
7
9
  AsyncStatus,
10
+ Reference,
8
11
  StandardReadable,
9
12
  StandardReadableFormat,
10
13
  StrictEnum,
11
14
  )
15
+ from ophyd_async.epics.motor import Motor
12
16
  from pydantic import BaseModel, Field
13
17
 
14
18
  from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
@@ -107,7 +111,157 @@ def load_positions_from_beamline_parameters(
107
111
  }
108
112
 
109
113
 
114
+ async def _safe_move_whilst_in_beam(
115
+ aperture: Aperture,
116
+ scatterguard: Scatterguard,
117
+ position: AperturePosition,
118
+ aperture_z_tolerance: float,
119
+ ):
120
+ """
121
+ Move the aperture and scatterguard combo safely to a new position.
122
+ See https://github.com/DiamondLightSource/hyperion/wiki/Aperture-Scatterguard-Collisions
123
+ for why this is required. TLDR is that we have a collision at the top of y so we need
124
+ to make sure we move the assembly down before we move the scatterguard up.
125
+
126
+ We also check that the assembly has been moved into the correct z position
127
+ previously. If we try and move whilst in the incorrect Z position we will collide
128
+ with the table.
129
+ """
130
+ ap_z_in_position = await aperture.z.motor_done_move.get_value()
131
+ if not ap_z_in_position:
132
+ raise InvalidApertureMove(
133
+ "ApertureScatterguard z is still moving. Wait for it to finish "
134
+ "before triggering another move."
135
+ )
136
+
137
+ current_ap_z = await aperture.z.user_readback.get_value()
138
+ diff_on_z = abs(current_ap_z - position.aperture_z)
139
+ if diff_on_z > aperture_z_tolerance:
140
+ raise InvalidApertureMove(
141
+ f"Current aperture z ({current_ap_z}), outside of tolerance ({aperture_z_tolerance}) from target ({position.aperture_z})."
142
+ )
143
+
144
+ current_ap_y = await aperture.y.user_readback.get_value()
145
+
146
+ aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = position.values
147
+
148
+ if aperture_y > current_ap_y:
149
+ # Assembly needs to move up so move the scatterguard down first
150
+ await asyncio.gather(
151
+ scatterguard.x.set(scatterguard_x),
152
+ scatterguard.y.set(scatterguard_y),
153
+ )
154
+ await asyncio.gather(
155
+ aperture.x.set(aperture_x),
156
+ aperture.y.set(aperture_y),
157
+ aperture.z.set(aperture_z),
158
+ )
159
+ else:
160
+ await asyncio.gather(
161
+ aperture.x.set(aperture_x),
162
+ aperture.y.set(aperture_y),
163
+ aperture.z.set(aperture_z),
164
+ )
165
+
166
+ await asyncio.gather(
167
+ scatterguard.x.set(scatterguard_x),
168
+ scatterguard.y.set(scatterguard_y),
169
+ )
170
+
171
+
172
+ class ApertureSelector(StandardReadable, Movable):
173
+ """Allows for moving all axes other than Y into the correct position, this means
174
+ that we can set up the aperture while it is out of the beam then move it in later."""
175
+
176
+ def __init__(
177
+ self,
178
+ aperture: Aperture,
179
+ scatterguard: Scatterguard,
180
+ out_of_beam: Callable[[], Coroutine[Any, Any, bool]],
181
+ loaded_positions: dict[ApertureValue, AperturePosition],
182
+ aperture_z_tolerance: float,
183
+ ):
184
+ self.aperture = Reference(aperture)
185
+ self.scatterguard = Reference(scatterguard)
186
+ self.loaded_positions = loaded_positions
187
+ self.get_is_out_of_beam = out_of_beam
188
+ self.aperture_z_tolerance = aperture_z_tolerance
189
+ super().__init__()
190
+
191
+ @AsyncStatus.wrap
192
+ async def set(self, value: ApertureValue):
193
+ """Moves the assembly to the position for the specified aperture, whilst keeping
194
+ it out of the beam if it already is so.
195
+
196
+ Moving the assembly whilst out of the beam has no collision risk so we can just
197
+ move all the motors together.
198
+ """
199
+ if await self.get_is_out_of_beam():
200
+ aperture_x, _, aperture_z, scatterguard_x, scatterguard_y = (
201
+ self.loaded_positions[value].values
202
+ )
203
+
204
+ await asyncio.gather(
205
+ self.aperture().x.set(aperture_x),
206
+ self.aperture().z.set(aperture_z),
207
+ self.scatterguard().x.set(scatterguard_x),
208
+ self.scatterguard().y.set(scatterguard_y),
209
+ )
210
+ else:
211
+ await _safe_move_whilst_in_beam(
212
+ self.aperture(),
213
+ self.scatterguard(),
214
+ self.loaded_positions[value],
215
+ self.aperture_z_tolerance,
216
+ )
217
+
218
+
219
+ class OutTrigger(StandardReadable, Triggerable):
220
+ """Allows for moving just the Y stage of the assembly out of the beam."""
221
+
222
+ def __init__(
223
+ self,
224
+ aperture_y: Motor,
225
+ out_y: float,
226
+ ):
227
+ self.aperture_y = Reference(aperture_y)
228
+ self.out_y = out_y
229
+ super().__init__()
230
+
231
+ @AsyncStatus.wrap
232
+ async def trigger(self):
233
+ """Moves the assembly out of the beam."""
234
+ await self.aperture_y().set(self.out_y)
235
+
236
+
110
237
  class ApertureScatterguard(StandardReadable, Movable):
238
+ """Move the aperture and scatterguard assembly in a safe way. There are two ways to
239
+ interact with the device depending on if you want simplicity or move flexibility.
240
+
241
+ The simple interface is using:
242
+
243
+ await aperture_scatterguard.set(ApertureValue.LARGE)
244
+
245
+ This will move the assembly so that the large aperture is in the beam, regardless
246
+ of where the assembly currently is.
247
+
248
+ However, the aperture Y axis is faster than the others. In some cases we may want to
249
+ move the assembly out of the beam with this axis without moving others:
250
+
251
+ await aperture_scatterguard.move_out.trigger()
252
+
253
+ We may then want to keep the assembly out of the beam whilst asynchronously preparing
254
+ the other axes for the aperture that's to follow:
255
+
256
+ await aperture_scatterguard.aperture_outside_beam.set(ApertureValue.LARGE)
257
+
258
+ Then, at a later time, move back into the beam:
259
+
260
+ await aperture_scatterguard.set(ApertureValue.LARGE)
261
+
262
+ This move will now be faster as only the y is left to move.
263
+ """
264
+
111
265
  def __init__(
112
266
  self,
113
267
  loaded_positions: dict[ApertureValue, AperturePosition],
@@ -115,8 +269,8 @@ class ApertureScatterguard(StandardReadable, Movable):
115
269
  prefix: str = "",
116
270
  name: str = "",
117
271
  ) -> None:
118
- self.aperture = Aperture(prefix + "-MO-MAPT-01:")
119
- self.scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
272
+ self._aperture = Aperture(prefix + "-MO-MAPT-01:")
273
+ self._scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
120
274
  self.radius = create_hardware_backed_soft_signal(
121
275
  float, self._get_current_radius, units="µm"
122
276
  )
@@ -124,30 +278,43 @@ class ApertureScatterguard(StandardReadable, Movable):
124
278
  self._tolerances = tolerances
125
279
  self.add_readables(
126
280
  [
127
- self.aperture.x.user_readback,
128
- self.aperture.y.user_readback,
129
- self.aperture.z.user_readback,
130
- self.scatterguard.x.user_readback,
131
- self.scatterguard.y.user_readback,
281
+ self._aperture.x.user_readback,
282
+ self._aperture.y.user_readback,
283
+ self._aperture.z.user_readback,
284
+ self._scatterguard.x.user_readback,
285
+ self._scatterguard.y.user_readback,
132
286
  self.radius,
133
287
  ],
134
288
  )
289
+
135
290
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
136
291
  self.selected_aperture = create_hardware_backed_soft_signal(
137
292
  ApertureValue, self._get_current_aperture_position
138
293
  )
139
294
 
140
- super().__init__(name)
295
+ # Setting this will select the aperture but not move it into beam
296
+ self.aperture_outside_beam = ApertureSelector(
297
+ self._aperture,
298
+ self._scatterguard,
299
+ self._is_out_of_beam,
300
+ self._loaded_positions,
301
+ self._tolerances.aperture_z,
302
+ )
303
+
304
+ # Setting this will just move the assembly out of the beam
305
+ self.move_out = OutTrigger(
306
+ self._aperture.y, loaded_positions[ApertureValue.ROBOT_LOAD].aperture_y
307
+ )
141
308
 
142
- def get_position_from_gda_aperture_name(
143
- self, gda_aperture_name: str
144
- ) -> ApertureValue:
145
- return ApertureValue(gda_aperture_name)
309
+ super().__init__(name)
146
310
 
147
311
  @AsyncStatus.wrap
148
312
  async def set(self, value: ApertureValue):
313
+ """This set will move the aperture into the beam or move to robot load"""
149
314
  position = self._loaded_positions[value]
150
- await self._safe_move_within_datacollection_range(position, value)
315
+ await _safe_move_whilst_in_beam(
316
+ self._aperture, self._scatterguard, position, self._tolerances.aperture_z
317
+ )
151
318
 
152
319
  @AsyncStatus.wrap
153
320
  async def _set_raw_unsafe(self, position: AperturePosition):
@@ -157,13 +324,18 @@ class ApertureScatterguard(StandardReadable, Movable):
157
324
  )
158
325
 
159
326
  await asyncio.gather(
160
- self.aperture.x.set(aperture_x),
161
- self.aperture.y.set(aperture_y),
162
- self.aperture.z.set(aperture_z),
163
- self.scatterguard.x.set(scatterguard_x),
164
- self.scatterguard.y.set(scatterguard_y),
327
+ self._aperture.x.set(aperture_x),
328
+ self._aperture.y.set(aperture_y),
329
+ self._aperture.z.set(aperture_z),
330
+ self._scatterguard.x.set(scatterguard_x),
331
+ self._scatterguard.y.set(scatterguard_y),
165
332
  )
166
333
 
334
+ async def _is_out_of_beam(self) -> bool:
335
+ current_ap_y = await self._aperture.y.user_readback.get_value()
336
+ robot_load_ap_y = self._loaded_positions[ApertureValue.ROBOT_LOAD].aperture_y
337
+ return current_ap_y <= robot_load_ap_y + self._tolerances.aperture_y
338
+
167
339
  async def _get_current_aperture_position(self) -> ApertureValue:
168
340
  """
169
341
  Returns the current aperture position using readback values
@@ -171,15 +343,13 @@ class ApertureScatterguard(StandardReadable, Movable):
171
343
  mini aperture y <= ROBOT_LOAD.location.aperture_y + tolerance.
172
344
  If no position is found then raises InvalidApertureMove.
173
345
  """
174
- current_ap_y = await self.aperture.y.user_readback.get_value(cached=False)
175
- robot_load_ap_y = self._loaded_positions[ApertureValue.ROBOT_LOAD].aperture_y
176
- if await self.aperture.large.get_value(cached=False) == 1:
346
+ if await self._aperture.large.get_value(cached=False) == 1:
177
347
  return ApertureValue.LARGE
178
- elif await self.aperture.medium.get_value(cached=False) == 1:
348
+ elif await self._aperture.medium.get_value(cached=False) == 1:
179
349
  return ApertureValue.MEDIUM
180
- elif await self.aperture.small.get_value(cached=False) == 1:
350
+ elif await self._aperture.small.get_value(cached=False) == 1:
181
351
  return ApertureValue.SMALL
182
- elif current_ap_y <= robot_load_ap_y + self._tolerances.aperture_y:
352
+ elif await self._is_out_of_beam():
183
353
  return ApertureValue.ROBOT_LOAD
184
354
 
185
355
  raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
@@ -187,57 +357,3 @@ class ApertureScatterguard(StandardReadable, Movable):
187
357
  async def _get_current_radius(self) -> float:
188
358
  current_value = await self._get_current_aperture_position()
189
359
  return self._loaded_positions[current_value].radius
190
-
191
- async def _safe_move_within_datacollection_range(
192
- self, position: AperturePosition, value: ApertureValue
193
- ):
194
- """
195
- Move the aperture and scatterguard combo safely to a new position.
196
- See https://github.com/DiamondLightSource/hyperion/wiki/Aperture-Scatterguard-Collisions
197
- for why this is required.
198
- """
199
- assert self._loaded_positions is not None
200
-
201
- ap_z_in_position = await self.aperture.z.motor_done_move.get_value()
202
- if not ap_z_in_position:
203
- raise InvalidApertureMove(
204
- "ApertureScatterguard z is still moving. Wait for it to finish "
205
- "before triggering another move."
206
- )
207
-
208
- current_ap_z = await self.aperture.z.user_readback.get_value()
209
- diff_on_z = abs(current_ap_z - position.aperture_z)
210
- if diff_on_z > self._tolerances.aperture_z:
211
- raise InvalidApertureMove(
212
- "ApertureScatterguard safe move is not yet defined for positions "
213
- "outside of LARGE, MEDIUM, SMALL, ROBOT_LOAD. "
214
- f"Current aperture z ({current_ap_z}), outside of tolerance ({self._tolerances.aperture_z}) from target ({position.aperture_z})."
215
- )
216
-
217
- current_ap_y = await self.aperture.y.user_readback.get_value()
218
-
219
- aperture_x, aperture_y, aperture_z, scatterguard_x, scatterguard_y = (
220
- position.values
221
- )
222
-
223
- if position.aperture_y > current_ap_y:
224
- await asyncio.gather(
225
- self.scatterguard.x.set(scatterguard_x),
226
- self.scatterguard.y.set(scatterguard_y),
227
- )
228
- await asyncio.gather(
229
- self.aperture.x.set(aperture_x),
230
- self.aperture.y.set(aperture_y),
231
- self.aperture.z.set(aperture_z),
232
- )
233
- else:
234
- await asyncio.gather(
235
- self.aperture.x.set(aperture_x),
236
- self.aperture.y.set(aperture_y),
237
- self.aperture.z.set(aperture_z),
238
- )
239
-
240
- await asyncio.gather(
241
- self.scatterguard.x.set(scatterguard_x),
242
- self.scatterguard.y.set(scatterguard_y),
243
- )
dodal/devices/dcm.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import numpy as np
2
- from numpy.typing import NDArray
3
- from ophyd_async.core import StandardReadable, soft_signal_r_and_setter
2
+ from ophyd_async.core import Array1D, StandardReadable, soft_signal_r_and_setter
4
3
  from ophyd_async.epics.core import epics_signal_r
5
4
  from ophyd_async.epics.motor import Motor
6
5
 
@@ -56,8 +55,10 @@ class DCM(StandardReadable):
56
55
  )
57
56
  reflection_array = np.array(cm.reflection)
58
57
  self.crystal_metadata_reflection, _ = soft_signal_r_and_setter(
59
- NDArray[np.uint64],
58
+ Array1D[np.uint64],
60
59
  initial_value=reflection_array,
61
60
  )
62
- self.crystal_metadata_d_spacing = epics_signal_r(float, "DSPACING:RBV")
61
+ self.crystal_metadata_d_spacing = epics_signal_r(
62
+ float, prefix + "DSPACING:RBV"
63
+ )
63
64
  super().__init__(name)