dls-dodal 1.46.0__py3-none-any.whl → 1.48.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.
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/METADATA +2 -2
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/RECORD +74 -63
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +0 -1
- dodal/beamlines/aithre.py +6 -0
- dodal/beamlines/b01_1.py +1 -1
- dodal/beamlines/b07.py +2 -6
- dodal/beamlines/b07_1.py +1 -3
- dodal/beamlines/i03.py +33 -21
- dodal/beamlines/i04.py +65 -26
- dodal/beamlines/i09.py +1 -3
- dodal/beamlines/i09_1.py +1 -3
- dodal/beamlines/i18.py +1 -1
- dodal/beamlines/i19_1.py +9 -6
- dodal/beamlines/i23.py +17 -1
- dodal/beamlines/i24.py +5 -5
- dodal/beamlines/p38.py +1 -1
- dodal/beamlines/p60.py +2 -6
- dodal/beamlines/p99.py +48 -4
- dodal/common/beamlines/beamline_parameters.py +3 -30
- dodal/common/data_util.py +4 -0
- dodal/devices/aithre_lasershaping/goniometer.py +36 -2
- dodal/devices/aithre_lasershaping/laser_robot.py +27 -0
- dodal/devices/aperturescatterguard.py +47 -47
- dodal/devices/current_amplifiers/struck_scaler_counter.py +1 -1
- dodal/devices/diamond_filter.py +5 -17
- dodal/devices/eiger.py +1 -1
- dodal/devices/electron_analyser/__init__.py +18 -0
- dodal/devices/electron_analyser/abstract/__init__.py +22 -0
- dodal/devices/electron_analyser/abstract/base_detector.py +223 -0
- dodal/devices/electron_analyser/abstract/base_driver_io.py +230 -0
- dodal/devices/electron_analyser/{abstract_region.py → abstract/base_region.py} +3 -9
- dodal/devices/electron_analyser/specs/__init__.py +10 -0
- dodal/devices/electron_analyser/specs/detector.py +13 -0
- dodal/devices/electron_analyser/specs/driver_io.py +89 -0
- dodal/devices/electron_analyser/{specs_region.py → specs/region.py} +1 -1
- dodal/devices/electron_analyser/types.py +6 -0
- dodal/devices/electron_analyser/util.py +13 -0
- dodal/devices/electron_analyser/vgscienta/__init__.py +11 -0
- dodal/devices/electron_analyser/vgscienta/detector.py +22 -0
- dodal/devices/electron_analyser/vgscienta/driver_io.py +67 -0
- dodal/devices/electron_analyser/{vgscienta_region.py → vgscienta/region.py} +1 -2
- dodal/devices/fast_grid_scan.py +7 -9
- dodal/devices/i03/__init__.py +3 -0
- dodal/devices/i04/__init__.py +3 -0
- dodal/devices/i04/constants.py +9 -0
- dodal/devices/i04/murko_results.py +192 -0
- dodal/devices/i10/diagnostics.py +9 -61
- dodal/devices/i18/diode.py +37 -4
- dodal/devices/i24/focus_mirrors.py +9 -13
- dodal/devices/i24/pilatus_metadata.py +9 -9
- dodal/devices/i24/pmac.py +19 -14
- dodal/devices/{i03 → mx_phase1}/beamstop.py +26 -15
- dodal/devices/oav/oav_calculations.py +2 -2
- dodal/devices/oav/oav_detector.py +80 -32
- dodal/devices/oav/oav_parameters.py +46 -16
- dodal/devices/oav/oav_to_redis_forwarder.py +2 -2
- dodal/devices/oav/utils.py +2 -2
- dodal/devices/p99/andor2_point.py +41 -0
- dodal/devices/positioner.py +49 -0
- dodal/devices/robot.py +20 -1
- dodal/devices/smargon.py +43 -4
- dodal/devices/tetramm.py +5 -2
- dodal/devices/util/adjuster_plans.py +1 -1
- dodal/devices/zebra/zebra.py +8 -0
- dodal/devices/zebra/zebra_constants_mapping.py +1 -1
- dodal/devices/zocalo/__init__.py +0 -3
- dodal/devices/zocalo/zocalo_results.py +6 -32
- dodal/log.py +14 -14
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +167 -0
- dodal/common/signal_utils.py +0 -88
- dodal/devices/electron_analyser/abstract_analyser_io.py +0 -47
- dodal/devices/electron_analyser/specs_analyser_io.py +0 -19
- dodal/devices/electron_analyser/vgscienta_analyser_io.py +0 -26
- dodal/devices/logging_ophyd_device.py +0 -17
- dodal/plan_stubs/electron_analyser/__init__.py +0 -0
- dodal/plan_stubs/electron_analyser/configure_controller.py +0 -80
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pickle
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from bluesky.protocols import Stageable, Triggerable
|
|
8
|
+
from ophyd_async.core import (
|
|
9
|
+
AsyncStatus,
|
|
10
|
+
StandardReadable,
|
|
11
|
+
soft_signal_r_and_setter,
|
|
12
|
+
soft_signal_rw,
|
|
13
|
+
)
|
|
14
|
+
from redis.asyncio import StrictRedis
|
|
15
|
+
|
|
16
|
+
from dodal.devices.i04.constants import RedisConstants
|
|
17
|
+
from dodal.devices.oav.oav_calculations import (
|
|
18
|
+
calculate_beam_distance,
|
|
19
|
+
)
|
|
20
|
+
from dodal.log import LOGGER
|
|
21
|
+
|
|
22
|
+
NO_MURKO_RESULT = (-1, -1)
|
|
23
|
+
|
|
24
|
+
MurkoResult = dict
|
|
25
|
+
FullMurkoResults = dict[str, list[MurkoResult]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MurkoMetadata(TypedDict):
|
|
29
|
+
zoom_percentage: float
|
|
30
|
+
microns_per_x_pixel: float
|
|
31
|
+
microns_per_y_pixel: float
|
|
32
|
+
beam_centre_i: int
|
|
33
|
+
beam_centre_j: int
|
|
34
|
+
sample_id: str
|
|
35
|
+
omega_angle: float
|
|
36
|
+
uuid: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Coord(Enum):
|
|
40
|
+
x = 0
|
|
41
|
+
y = 1
|
|
42
|
+
z = 2
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
46
|
+
"""Device that takes crystal centre values from Murko and uses them to set the
|
|
47
|
+
x, y, z coordinate of the sample to be in line with the beam centre.
|
|
48
|
+
The most_likely_click[1] value from Murko corresponds with the x coordinate of the
|
|
49
|
+
sample. The most_likely_click[0] value from Murko corresponds with a component of
|
|
50
|
+
the y and z coordinates of the sample, depending on the omega angle, as the sample
|
|
51
|
+
is rotated around the x axis.
|
|
52
|
+
|
|
53
|
+
Given a most_likely_click value at a certain omega angle θ:
|
|
54
|
+
most_likely_click[1] = x
|
|
55
|
+
most_likely_click[0] = cos(θ)y - sin(θ)z
|
|
56
|
+
|
|
57
|
+
A value for x can be found by averaging all most_likely_click[1] values, and
|
|
58
|
+
solutions for y and z can be calculated using numpy's linear algebra library.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
TIMEOUT_S = 2
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
redis_host=RedisConstants.REDIS_HOST,
|
|
66
|
+
redis_password=RedisConstants.REDIS_PASSWORD,
|
|
67
|
+
redis_db=RedisConstants.MURKO_REDIS_DB,
|
|
68
|
+
name="",
|
|
69
|
+
stop_angle=350,
|
|
70
|
+
):
|
|
71
|
+
self.redis_client = StrictRedis(
|
|
72
|
+
host=redis_host,
|
|
73
|
+
password=redis_password,
|
|
74
|
+
db=redis_db,
|
|
75
|
+
)
|
|
76
|
+
self.pubsub = self.redis_client.pubsub()
|
|
77
|
+
self._last_omega = 0
|
|
78
|
+
self.sample_id = soft_signal_rw(str) # Should get from redis
|
|
79
|
+
self.stop_angle = stop_angle
|
|
80
|
+
self.x_dists_mm = []
|
|
81
|
+
self.y_dists_mm = []
|
|
82
|
+
self.omegas = []
|
|
83
|
+
|
|
84
|
+
with self.add_children_as_readables():
|
|
85
|
+
# Diffs from current x/y/z
|
|
86
|
+
self.x_mm, self._x_mm_setter = soft_signal_r_and_setter(float)
|
|
87
|
+
self.y_mm, self._y_mm_setter = soft_signal_r_and_setter(float)
|
|
88
|
+
self.z_mm, self._z_mm_setter = soft_signal_r_and_setter(float)
|
|
89
|
+
super().__init__(name=name)
|
|
90
|
+
|
|
91
|
+
@AsyncStatus.wrap
|
|
92
|
+
async def stage(self):
|
|
93
|
+
await self.pubsub.subscribe("murko-results")
|
|
94
|
+
self._x_mm_setter(0)
|
|
95
|
+
self._y_mm_setter(0)
|
|
96
|
+
self._z_mm_setter(0)
|
|
97
|
+
|
|
98
|
+
@AsyncStatus.wrap
|
|
99
|
+
async def unstage(self):
|
|
100
|
+
await self.pubsub.unsubscribe()
|
|
101
|
+
|
|
102
|
+
@AsyncStatus.wrap
|
|
103
|
+
async def trigger(self):
|
|
104
|
+
# Wait for results
|
|
105
|
+
sample_id = await self.sample_id.get_value()
|
|
106
|
+
while self._last_omega < self.stop_angle:
|
|
107
|
+
# waits here for next batch to be received
|
|
108
|
+
message = await self.pubsub.get_message(timeout=self.TIMEOUT_S)
|
|
109
|
+
if message is None: # No more messages to process
|
|
110
|
+
break
|
|
111
|
+
await self.process_batch(message, sample_id)
|
|
112
|
+
|
|
113
|
+
for i in range(len(self.omegas)):
|
|
114
|
+
LOGGER.debug(
|
|
115
|
+
f"omega: {round(self.omegas[i], 2)}, x: {round(self.x_dists_mm[i], 2)}, y: {round(self.y_dists_mm[i], 2)}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
LOGGER.info(f"Using average of x beam distances: {self.x_dists_mm}")
|
|
119
|
+
avg_x = float(np.mean(self.x_dists_mm))
|
|
120
|
+
LOGGER.info(f"Finding least square y and z from y distances: {self.y_dists_mm}")
|
|
121
|
+
best_y, best_z = get_yz_least_squares(self.y_dists_mm, self.omegas)
|
|
122
|
+
# x, y, z are relative to beam centre. Need to move negative these values to get centred.
|
|
123
|
+
self._x_mm_setter(-avg_x)
|
|
124
|
+
self._y_mm_setter(-best_y)
|
|
125
|
+
self._z_mm_setter(-best_z)
|
|
126
|
+
|
|
127
|
+
async def process_batch(self, message: dict | None, sample_id: str):
|
|
128
|
+
if message and message["type"] == "message":
|
|
129
|
+
batch_results: list[dict] = pickle.loads(message["data"])
|
|
130
|
+
for results in batch_results:
|
|
131
|
+
for uuid, result in results.items():
|
|
132
|
+
if metadata_str := await self.redis_client.hget( # type: ignore
|
|
133
|
+
f"murko:{sample_id}:metadata", uuid
|
|
134
|
+
):
|
|
135
|
+
LOGGER.info(
|
|
136
|
+
f"Found metadata for uuid {uuid}, processing result"
|
|
137
|
+
)
|
|
138
|
+
self.process_result(
|
|
139
|
+
result, MurkoMetadata(json.loads(metadata_str))
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
LOGGER.info(f"Found no metadata for uuid {uuid}")
|
|
143
|
+
|
|
144
|
+
def process_result(self, result: dict, metadata: MurkoMetadata):
|
|
145
|
+
"""Uses the 'most_likely_click' coordinates from Murko to calculate the
|
|
146
|
+
horizontal and vertical distances from the beam centre, and store these values
|
|
147
|
+
as well as the omega angle the image was taken at.
|
|
148
|
+
"""
|
|
149
|
+
omega = metadata["omega_angle"]
|
|
150
|
+
coords = result["most_likely_click"] # As proportion from top, left of image
|
|
151
|
+
LOGGER.info(f"Got most_likely_click: {coords} at angle {omega}")
|
|
152
|
+
if (
|
|
153
|
+
tuple(coords) == NO_MURKO_RESULT
|
|
154
|
+
): # See https://github.com/MartinSavko/murko/issues/9
|
|
155
|
+
LOGGER.info("Murko didn't produce a result, moving on")
|
|
156
|
+
else:
|
|
157
|
+
shape = result["original_shape"] # Dimensions of image in pixels
|
|
158
|
+
# Murko returns coords as y, x
|
|
159
|
+
centre_px = (coords[1] * shape[1], coords[0] * shape[0])
|
|
160
|
+
|
|
161
|
+
beam_dist_px = calculate_beam_distance(
|
|
162
|
+
(metadata["beam_centre_i"], metadata["beam_centre_j"]),
|
|
163
|
+
centre_px[0],
|
|
164
|
+
centre_px[1],
|
|
165
|
+
)
|
|
166
|
+
self.x_dists_mm.append(
|
|
167
|
+
beam_dist_px[0] * metadata["microns_per_x_pixel"] / 1000
|
|
168
|
+
)
|
|
169
|
+
self.y_dists_mm.append(
|
|
170
|
+
beam_dist_px[1] * metadata["microns_per_y_pixel"] / 1000
|
|
171
|
+
)
|
|
172
|
+
self.omegas.append(omega)
|
|
173
|
+
self._last_omega = omega
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def get_yz_least_squares(vertical_dists: list, omegas: list) -> tuple[float, float]:
|
|
177
|
+
"""Get the least squares solution for y and z from the vertical distances and omega angles.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
v_dists (list): List of vertical distances from beam centre. Any units
|
|
181
|
+
omegas (list): List of omega angles in degrees.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
tuple[float, float]: y, z distances from centre, in whichever units
|
|
185
|
+
v_dists came as.
|
|
186
|
+
"""
|
|
187
|
+
thetas = np.radians(omegas)
|
|
188
|
+
matrix = np.column_stack([np.cos(thetas), -np.sin(thetas)])
|
|
189
|
+
|
|
190
|
+
yz, residuals, rank, s = np.linalg.lstsq(matrix, vertical_dists, rcond=None)
|
|
191
|
+
y, z = yz
|
|
192
|
+
return y, z
|
dodal/devices/i10/diagnostics.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
from bluesky.protocols import Movable
|
|
2
1
|
from ophyd_async.core import (
|
|
3
|
-
AsyncStatus,
|
|
4
2
|
Device,
|
|
5
3
|
StandardReadable,
|
|
6
4
|
StrictEnum,
|
|
@@ -13,7 +11,6 @@ from ophyd_async.epics.core import (
|
|
|
13
11
|
epics_signal_r,
|
|
14
12
|
epics_signal_rw,
|
|
15
13
|
)
|
|
16
|
-
from ophyd_async.epics.motor import Motor
|
|
17
14
|
|
|
18
15
|
from dodal.devices.current_amplifiers import (
|
|
19
16
|
CurrentAmpDet,
|
|
@@ -23,6 +20,7 @@ from dodal.devices.current_amplifiers import (
|
|
|
23
20
|
FemtoDDPCA,
|
|
24
21
|
StruckScaler,
|
|
25
22
|
)
|
|
23
|
+
from dodal.devices.positioner import create_positioner
|
|
26
24
|
|
|
27
25
|
|
|
28
26
|
class D3Position(StrictEnum):
|
|
@@ -70,36 +68,6 @@ class InOutReadBackTable(StrictEnum):
|
|
|
70
68
|
OUT_OF_BEAM = "Out of Beam"
|
|
71
69
|
|
|
72
70
|
|
|
73
|
-
class Positioner(StandardReadable, Movable):
|
|
74
|
-
"""1D stage with a enum table to select positions."""
|
|
75
|
-
|
|
76
|
-
def __init__(
|
|
77
|
-
self,
|
|
78
|
-
prefix: str,
|
|
79
|
-
positioner_enum: type[StrictEnum],
|
|
80
|
-
positioner_suffix: str = "",
|
|
81
|
-
Positioner_pv_suffix: str = ":MP:SELECT",
|
|
82
|
-
name: str = "",
|
|
83
|
-
) -> None:
|
|
84
|
-
self._stage_motion = Motor(prefix=prefix + positioner_suffix)
|
|
85
|
-
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
86
|
-
self.stage_position = epics_signal_rw(
|
|
87
|
-
positioner_enum,
|
|
88
|
-
read_pv=prefix + positioner_suffix + Positioner_pv_suffix,
|
|
89
|
-
)
|
|
90
|
-
super().__init__(name=name)
|
|
91
|
-
self.positioner_enum = positioner_enum
|
|
92
|
-
|
|
93
|
-
@AsyncStatus.wrap
|
|
94
|
-
async def set(self, value: StrictEnum) -> None:
|
|
95
|
-
if value in self.positioner_enum:
|
|
96
|
-
await self.stage_position.set(value=value)
|
|
97
|
-
else:
|
|
98
|
-
raise ValueError(
|
|
99
|
-
f"{value} is not an allow position. Position must be: {self.positioner_enum}"
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
71
|
class I10PneumaticStage(StandardReadable):
|
|
104
72
|
"""Pneumatic stage only has two real positions in or out.
|
|
105
73
|
Use for fluorescent screen which can be insert into the x-ray beam.
|
|
@@ -154,16 +122,13 @@ class FullDiagnostic(Device):
|
|
|
154
122
|
self,
|
|
155
123
|
prefix: str,
|
|
156
124
|
positioner_enum: type[StrictEnum],
|
|
157
|
-
positioner_suffix: str
|
|
158
|
-
Positioner_pv_suffix: str = ":MP:SELECT",
|
|
125
|
+
positioner_suffix: str,
|
|
159
126
|
cam_infix: str = "DCAM:",
|
|
160
127
|
name: str = "",
|
|
161
128
|
) -> None:
|
|
162
|
-
self.positioner =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
positioner_suffix=positioner_suffix,
|
|
166
|
-
Positioner_pv_suffix=Positioner_pv_suffix,
|
|
129
|
+
self.positioner = create_positioner(
|
|
130
|
+
positioner_enum,
|
|
131
|
+
prefix + positioner_suffix,
|
|
167
132
|
)
|
|
168
133
|
self.screen = ScreenCam(
|
|
169
134
|
prefix,
|
|
@@ -185,28 +150,11 @@ class I10Diagnostic(Device):
|
|
|
185
150
|
positioner_suffix="DET:X",
|
|
186
151
|
)
|
|
187
152
|
self.d4 = ScreenCam(prefix=prefix + "PHDGN-04:")
|
|
188
|
-
self.d5 =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
self.d5A = Positioner(
|
|
195
|
-
prefix=prefix + "PHDGN-06:",
|
|
196
|
-
positioner_enum=D5APosition,
|
|
197
|
-
positioner_suffix="DET:X",
|
|
198
|
-
)
|
|
153
|
+
self.d5 = create_positioner(D5Position, f"{prefix}IONC-01:Y")
|
|
154
|
+
self.d5A = create_positioner(D5APosition, f"{prefix}PHDGN-06:DET:X")
|
|
155
|
+
self.d6 = FullDiagnostic(f"{prefix}PHDGN-05:", D6Position, "DET:X")
|
|
156
|
+
self.d7 = create_positioner(D7Position, f"{prefix}PHDGN-07:Y")
|
|
199
157
|
|
|
200
|
-
self.d6 = FullDiagnostic(
|
|
201
|
-
prefix=prefix + "PHDGN-05:",
|
|
202
|
-
positioner_enum=D6Position,
|
|
203
|
-
positioner_suffix="DET:X",
|
|
204
|
-
)
|
|
205
|
-
self.d7 = Positioner(
|
|
206
|
-
prefix=prefix + "PHDGN-07:",
|
|
207
|
-
positioner_enum=D7Position,
|
|
208
|
-
positioner_suffix="Y",
|
|
209
|
-
)
|
|
210
158
|
super().__init__(name)
|
|
211
159
|
|
|
212
160
|
|
dodal/devices/i18/diode.py
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
-
from ophyd_async.core import
|
|
2
|
-
StandardReadable,
|
|
3
|
-
)
|
|
1
|
+
from ophyd_async.core import StandardReadable, StrictEnum
|
|
4
2
|
from ophyd_async.epics.core import epics_signal_r
|
|
5
3
|
|
|
4
|
+
from dodal.devices.positioner import create_positioner
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FilterAValues(StrictEnum):
|
|
8
|
+
"""Maps from a short usable name to the string name in EPICS"""
|
|
9
|
+
|
|
10
|
+
AL_2MM = "2 mm Al"
|
|
11
|
+
AL_1_5MM = "1.5 mm Al"
|
|
12
|
+
AL_1_25MM = "1.25 mm Al"
|
|
13
|
+
AL_0_8MM = "0.8 mm Al"
|
|
14
|
+
AL_0_55MM = "0.55 mm Al"
|
|
15
|
+
AL_0_5MM = "0.5 mm Al"
|
|
16
|
+
AL_0_3MM = "0.3 mm Al"
|
|
17
|
+
AL_0_25MM = "0.25 mm Al"
|
|
18
|
+
AL_0_15MM = "0.15 mm Al"
|
|
19
|
+
AL_0_1MM = "0.1 mm Al"
|
|
20
|
+
AL_0_05MM = "0.05 mm Al"
|
|
21
|
+
AL_0_025MM = "0.025 mm Al"
|
|
22
|
+
AL_GAP = "Gap"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FilterBValues(StrictEnum):
|
|
26
|
+
DIAMOND_THIN = "Diamond thin"
|
|
27
|
+
DIAMOND_THICK = "Diamond thick"
|
|
28
|
+
NI_DRAIN = "ni drain"
|
|
29
|
+
AU_DRAIN = "au drain"
|
|
30
|
+
AL_DRAIN = "al drain"
|
|
31
|
+
GAP = "Gap"
|
|
32
|
+
IN_LINE_DIODE = "in line diode"
|
|
33
|
+
|
|
6
34
|
|
|
7
35
|
class Diode(StandardReadable):
|
|
8
36
|
def __init__(
|
|
@@ -10,8 +38,13 @@ class Diode(StandardReadable):
|
|
|
10
38
|
prefix: str,
|
|
11
39
|
name: str = "",
|
|
12
40
|
):
|
|
13
|
-
self._prefix = prefix
|
|
14
41
|
with self.add_children_as_readables():
|
|
15
42
|
self.signal = epics_signal_r(float, prefix + "B:DIODE:I")
|
|
43
|
+
self.positioner_a = create_positioner(
|
|
44
|
+
FilterAValues, prefix + "A:MP", positioner_pv_suffix=":SELECT"
|
|
45
|
+
) # more complex, will be fixed on Tuesday 20.05.2025
|
|
46
|
+
self.positioner_b = create_positioner(
|
|
47
|
+
FilterBValues, prefix + "B:MP", positioner_pv_suffix=":SELECT"
|
|
48
|
+
)
|
|
16
49
|
|
|
17
50
|
super().__init__(name=name)
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
from ophyd_async.core import StandardReadable, StrictEnum
|
|
1
|
+
from ophyd_async.core import StandardReadable, StrictEnum, derived_signal_r
|
|
2
2
|
from ophyd_async.epics.core import epics_signal_rw
|
|
3
3
|
|
|
4
|
-
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
5
|
-
|
|
6
4
|
|
|
7
5
|
class HFocusMode(StrictEnum):
|
|
8
6
|
FOCUS_10 = "HMFMfocus10"
|
|
@@ -40,21 +38,19 @@ class FocusMirrorsMode(StandardReadable):
|
|
|
40
38
|
self.vertical = epics_signal_rw(VFocusMode, prefix + "G0:TARGETAPPLY")
|
|
41
39
|
|
|
42
40
|
with self.add_children_as_readables():
|
|
43
|
-
self.beam_size_x =
|
|
44
|
-
|
|
41
|
+
self.beam_size_x = derived_signal_r(
|
|
42
|
+
self._get_beam_size_x, horizontal=self.horizontal, derived_units="um"
|
|
45
43
|
)
|
|
46
|
-
self.beam_size_y =
|
|
47
|
-
|
|
44
|
+
self.beam_size_y = derived_signal_r(
|
|
45
|
+
self._get_beam_size_y, vertical=self.vertical, derived_units="um"
|
|
48
46
|
)
|
|
49
47
|
|
|
50
48
|
super().__init__(name)
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
beam_x = BEAM_SIZES[h_mode.removeprefix("HMFM")][0]
|
|
50
|
+
def _get_beam_size_x(self, horizontal: HFocusMode) -> int:
|
|
51
|
+
beam_x = BEAM_SIZES[horizontal.removeprefix("HMFM")][0]
|
|
55
52
|
return beam_x
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
beam_y = BEAM_SIZES[v_mode.removeprefix("VMFM")][1]
|
|
54
|
+
def _get_beam_size_y(self, vertical: VFocusMode) -> int:
|
|
55
|
+
beam_y = BEAM_SIZES[vertical.removeprefix("VMFM")][1]
|
|
60
56
|
return beam_y
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
-
from ophyd_async.core import StandardReadable
|
|
5
|
+
from ophyd_async.core import StandardReadable, derived_signal_r
|
|
6
6
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
7
7
|
|
|
8
|
-
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
9
|
-
|
|
10
8
|
|
|
11
9
|
class PilatusMetadata(StandardReadable):
|
|
12
10
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
@@ -14,21 +12,23 @@ class PilatusMetadata(StandardReadable):
|
|
|
14
12
|
self.template = epics_signal_r(str, prefix + "cam1:FileTemplate_RBV")
|
|
15
13
|
self.filenumber = epics_signal_r(int, prefix + "cam1:FileNumber_RBV")
|
|
16
14
|
with self.add_children_as_readables():
|
|
17
|
-
self.filename_template =
|
|
18
|
-
|
|
15
|
+
self.filename_template = derived_signal_r(
|
|
16
|
+
self._get_full_filename_template,
|
|
17
|
+
filename=self.filename,
|
|
18
|
+
filename_template=self.template,
|
|
19
|
+
file_number=self.filenumber,
|
|
19
20
|
)
|
|
20
21
|
super().__init__(name)
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
def _get_full_filename_template(
|
|
24
|
+
self, filename: str, filename_template: str, file_number: int
|
|
25
|
+
) -> str:
|
|
23
26
|
"""
|
|
24
27
|
Get the template file path by querying the detector PVs.
|
|
25
28
|
Mirror the construction that the PPU does.
|
|
26
29
|
|
|
27
30
|
Returns: A template string, with the image numbers replaced with '#'
|
|
28
31
|
"""
|
|
29
|
-
filename = await self.filename.get_value()
|
|
30
|
-
filename_template = await self.template.get_value()
|
|
31
|
-
file_number = await self.filenumber.get_value()
|
|
32
32
|
# Exploit fact that passing negative numbers will put the - before the 0's
|
|
33
33
|
expected_filename = str(
|
|
34
34
|
filename_template % (filename, f"{file_number:05d}_", -9)
|
dodal/devices/i24/pmac.py
CHANGED
|
@@ -10,6 +10,7 @@ from ophyd_async.core import (
|
|
|
10
10
|
SignalR,
|
|
11
11
|
SignalRW,
|
|
12
12
|
StandardReadable,
|
|
13
|
+
observe_signals_value,
|
|
13
14
|
soft_signal_rw,
|
|
14
15
|
wait_for_value,
|
|
15
16
|
)
|
|
@@ -120,15 +121,16 @@ class ProgramRunner(Device, Flyable):
|
|
|
120
121
|
self,
|
|
121
122
|
pmac_str_sig: SignalRW,
|
|
122
123
|
status_sig: SignalR,
|
|
124
|
+
counter_sig: SignalR,
|
|
123
125
|
prog_num_sig: SignalRW,
|
|
124
|
-
|
|
126
|
+
counter_time_sig: SignalRW,
|
|
125
127
|
name: str = "",
|
|
126
128
|
) -> None:
|
|
127
129
|
self._signal_ref = Reference(pmac_str_sig)
|
|
128
130
|
self._status_ref = Reference(status_sig)
|
|
131
|
+
self._counter_ref = Reference(counter_sig)
|
|
129
132
|
self._prog_num_ref = Reference(prog_num_sig)
|
|
130
|
-
|
|
131
|
-
self._collection_time_ref = Reference(collection_time_sig)
|
|
133
|
+
self._counter_time_ref = Reference(counter_time_sig)
|
|
132
134
|
|
|
133
135
|
super().__init__(name)
|
|
134
136
|
|
|
@@ -151,16 +153,18 @@ class ProgramRunner(Device, Flyable):
|
|
|
151
153
|
|
|
152
154
|
@AsyncStatus.wrap
|
|
153
155
|
async def complete(self):
|
|
154
|
-
"""Stop collecting when the scan status PV goes to 0
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
complete_time (float): total time required by the collection to \
|
|
158
|
-
finish correctly.
|
|
156
|
+
"""Stop collecting when the scan status PV goes to 0 or when counter PV hasn't \
|
|
157
|
+
updated for 30 seconds.
|
|
159
158
|
"""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
self._status_ref(),
|
|
163
|
-
|
|
159
|
+
counter_time = await self._counter_time_ref().get_value()
|
|
160
|
+
async for signal, value in observe_signals_value(
|
|
161
|
+
self._status_ref(),
|
|
162
|
+
self._counter_ref(),
|
|
163
|
+
timeout=counter_time,
|
|
164
|
+
):
|
|
165
|
+
if signal is self._status_ref():
|
|
166
|
+
if value == ScanState.DONE:
|
|
167
|
+
break
|
|
164
168
|
|
|
165
169
|
|
|
166
170
|
class ProgramAbort(Triggerable):
|
|
@@ -217,13 +221,14 @@ class PMAC(StandardReadable):
|
|
|
217
221
|
# A couple of soft signals for running a collection: program number to send to
|
|
218
222
|
# the PMAC_STRING and expected collection time.
|
|
219
223
|
self.program_number = soft_signal_rw(int)
|
|
220
|
-
self.
|
|
224
|
+
self.counter_time = soft_signal_rw(float, initial_value=30.0, units="s")
|
|
221
225
|
|
|
222
226
|
self.run_program = ProgramRunner(
|
|
223
227
|
self.pmac_string,
|
|
224
228
|
self.scanstatus,
|
|
229
|
+
self.counter,
|
|
225
230
|
self.program_number,
|
|
226
|
-
self.
|
|
231
|
+
self.counter_time,
|
|
227
232
|
)
|
|
228
233
|
self.abort_program = ProgramAbort(self.pmac_string, self.scanstatus)
|
|
229
234
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import asyncio
|
|
2
2
|
from math import isclose
|
|
3
3
|
|
|
4
|
-
from ophyd_async.core import
|
|
4
|
+
from ophyd_async.core import (
|
|
5
|
+
StandardReadable,
|
|
6
|
+
StrictEnum,
|
|
7
|
+
derived_signal_rw,
|
|
8
|
+
)
|
|
5
9
|
from ophyd_async.epics.motor import Motor
|
|
6
10
|
|
|
7
11
|
from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
|
|
8
|
-
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class BeamstopPositions(StrictEnum):
|
|
@@ -33,14 +36,13 @@ class BeamstopPositions(StrictEnum):
|
|
|
33
36
|
|
|
34
37
|
class Beamstop(StandardReadable):
|
|
35
38
|
"""
|
|
36
|
-
Beamstop for I03.
|
|
39
|
+
Beamstop for I03 and I04.
|
|
37
40
|
|
|
38
41
|
Attributes:
|
|
39
42
|
x: beamstop x position in mm
|
|
40
43
|
y: beamstop y position in mm
|
|
41
44
|
z: beamstop z position in mm
|
|
42
|
-
selected_pos: Get the current position of the beamstop as an enum.
|
|
43
|
-
is read-only.
|
|
45
|
+
selected_pos: Get or set the current position of the beamstop as an enum.
|
|
44
46
|
"""
|
|
45
47
|
|
|
46
48
|
def __init__(
|
|
@@ -53,10 +55,13 @@ class Beamstop(StandardReadable):
|
|
|
53
55
|
self.x_mm = Motor(prefix + "X")
|
|
54
56
|
self.y_mm = Motor(prefix + "Y")
|
|
55
57
|
self.z_mm = Motor(prefix + "Z")
|
|
56
|
-
self.selected_pos =
|
|
57
|
-
|
|
58
|
+
self.selected_pos = derived_signal_rw(
|
|
59
|
+
self._get_selected_position,
|
|
60
|
+
self._set_selected_position,
|
|
61
|
+
x=self.x_mm,
|
|
62
|
+
y=self.y_mm,
|
|
63
|
+
z=self.z_mm,
|
|
58
64
|
)
|
|
59
|
-
|
|
60
65
|
self._in_beam_xyz_mm = [
|
|
61
66
|
float(beamline_parameters[f"in_beam_{axis}_STANDARD"])
|
|
62
67
|
for axis in ("x", "y", "z")
|
|
@@ -68,12 +73,8 @@ class Beamstop(StandardReadable):
|
|
|
68
73
|
|
|
69
74
|
super().__init__(name)
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
current_pos =
|
|
73
|
-
self.x_mm.user_readback.get_value(),
|
|
74
|
-
self.y_mm.user_readback.get_value(),
|
|
75
|
-
self.z_mm.user_readback.get_value(),
|
|
76
|
-
)
|
|
76
|
+
def _get_selected_position(self, x: float, y: float, z: float) -> BeamstopPositions:
|
|
77
|
+
current_pos = [x, y, z]
|
|
77
78
|
if all(
|
|
78
79
|
isclose(axis_pos, axis_in_beam, abs_tol=axis_tolerance)
|
|
79
80
|
for axis_pos, axis_in_beam, axis_tolerance in zip(
|
|
@@ -83,3 +84,13 @@ class Beamstop(StandardReadable):
|
|
|
83
84
|
return BeamstopPositions.DATA_COLLECTION
|
|
84
85
|
else:
|
|
85
86
|
return BeamstopPositions.UNKNOWN
|
|
87
|
+
|
|
88
|
+
async def _set_selected_position(self, position: BeamstopPositions) -> None:
|
|
89
|
+
if position == BeamstopPositions.DATA_COLLECTION:
|
|
90
|
+
await asyncio.gather(
|
|
91
|
+
self.x_mm.set(self._in_beam_xyz_mm[0]),
|
|
92
|
+
self.y_mm.set(self._in_beam_xyz_mm[1]),
|
|
93
|
+
self.z_mm.set(self._in_beam_xyz_mm[2]),
|
|
94
|
+
)
|
|
95
|
+
elif position == BeamstopPositions.UNKNOWN:
|
|
96
|
+
raise ValueError(f"Cannot set beamstop to position {position}")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
def
|
|
4
|
+
def camera_coordinates_to_xyz_mm(
|
|
5
5
|
horizontal: float,
|
|
6
6
|
vertical: float,
|
|
7
7
|
omega: float,
|
|
@@ -9,7 +9,7 @@ def camera_coordinates_to_xyz(
|
|
|
9
9
|
microns_per_j_pixel: float,
|
|
10
10
|
) -> np.ndarray:
|
|
11
11
|
"""
|
|
12
|
-
Converts from (horizontal,vertical) pixel measurements from the OAV camera into to (x, y, z) motor coordinates in
|
|
12
|
+
Converts from (horizontal,vertical) pixel measurements from the OAV camera into to (x, y, z) motor coordinates in millimetres.
|
|
13
13
|
For an overview of the coordinate system for I03 see https://github.com/DiamondLightSource/hyperion/wiki/Gridscan-Coordinate-System.
|
|
14
14
|
|
|
15
15
|
Args:
|