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.
- {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/METADATA +33 -31
- {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/RECORD +46 -37
- {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/b01_1.py +16 -31
- dodal/beamlines/i22.py +124 -265
- dodal/beamlines/i24.py +56 -7
- dodal/beamlines/p38.py +16 -1
- dodal/beamlines/p99.py +22 -53
- dodal/beamlines/training_rig.py +16 -26
- dodal/cli.py +54 -8
- dodal/common/beamlines/beamline_utils.py +32 -2
- dodal/common/beamlines/device_helpers.py +2 -0
- dodal/devices/aperture.py +7 -0
- dodal/devices/aperturescatterguard.py +195 -79
- dodal/devices/dcm.py +5 -4
- dodal/devices/eiger.py +88 -49
- dodal/devices/fast_grid_scan.py +21 -46
- dodal/devices/focusing_mirror.py +8 -3
- dodal/devices/i24/beam_center.py +12 -0
- dodal/devices/i24/focus_mirrors.py +60 -0
- dodal/devices/i24/pilatus_metadata.py +44 -0
- dodal/devices/linkam3.py +1 -1
- dodal/devices/motors.py +14 -10
- dodal/devices/oav/oav_detector.py +2 -2
- dodal/devices/oav/pin_image_recognition/__init__.py +4 -5
- dodal/devices/oav/utils.py +1 -0
- dodal/devices/p99/sample_stage.py +12 -16
- dodal/devices/pressure_jump_cell.py +299 -0
- dodal/devices/robot.py +1 -1
- dodal/devices/tetramm.py +1 -1
- dodal/devices/undulator.py +4 -1
- dodal/devices/undulator_dcm.py +3 -19
- dodal/devices/zocalo/zocalo_results.py +7 -7
- dodal/plan_stubs/__init__.py +0 -0
- dodal/{plans/data_session_metadata.py → plan_stubs/data_session.py} +2 -2
- dodal/{plans/motor_util_plans.py → plan_stubs/motor_utils.py} +2 -2
- dodal/plan_stubs/wrapped.py +150 -0
- dodal/plans/__init__.py +4 -0
- dodal/plans/scanspec.py +66 -0
- dodal/plans/wrapped.py +57 -0
- dodal/utils.py +151 -2
- {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.35.0.dist-info → dls_dodal-1.36.1a0.dist-info}/top_level.txt +0 -0
- /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
|
|
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("
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
) ->
|
|
29
|
-
|
|
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:")
|
dodal/beamlines/training_rig.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
24
|
-
# to
|
|
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("
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
devices, connect_exceptions = _connect_devices(RE, devices, sim_backend)
|
|
62
69
|
|
|
63
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
|
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.
|
|
119
|
-
self.
|
|
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.
|
|
128
|
-
self.
|
|
129
|
-
self.
|
|
130
|
-
self.
|
|
131
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
161
|
-
self.
|
|
162
|
-
self.
|
|
163
|
-
self.
|
|
164
|
-
self.
|
|
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
|
-
|
|
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.
|
|
348
|
+
elif await self._aperture.medium.get_value(cached=False) == 1:
|
|
179
349
|
return ApertureValue.MEDIUM
|
|
180
|
-
elif await self.
|
|
350
|
+
elif await self._aperture.small.get_value(cached=False) == 1:
|
|
181
351
|
return ApertureValue.SMALL
|
|
182
|
-
elif
|
|
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
|
|
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
|
-
|
|
58
|
+
Array1D[np.uint64],
|
|
60
59
|
initial_value=reflection_array,
|
|
61
60
|
)
|
|
62
|
-
self.crystal_metadata_d_spacing = epics_signal_r(
|
|
61
|
+
self.crystal_metadata_d_spacing = epics_signal_r(
|
|
62
|
+
float, prefix + "DSPACING:RBV"
|
|
63
|
+
)
|
|
63
64
|
super().__init__(name)
|