dls-dodal 1.47.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.47.0.dist-info → dls_dodal-1.48.0.dist-info}/METADATA +2 -2
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/RECORD +40 -40
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +6 -0
- dodal/beamlines/b01_1.py +1 -1
- dodal/beamlines/i03.py +21 -6
- dodal/beamlines/i04.py +17 -10
- dodal/beamlines/i18.py +1 -1
- dodal/beamlines/i19_1.py +9 -6
- dodal/beamlines/i24.py +5 -5
- dodal/common/beamlines/beamline_parameters.py +2 -28
- dodal/devices/aithre_lasershaping/goniometer.py +36 -2
- dodal/devices/aithre_lasershaping/laser_robot.py +27 -0
- dodal/devices/electron_analyser/__init__.py +10 -0
- dodal/devices/electron_analyser/abstract/__init__.py +0 -6
- dodal/devices/electron_analyser/abstract/base_detector.py +69 -56
- dodal/devices/electron_analyser/abstract/base_driver_io.py +114 -5
- dodal/devices/electron_analyser/abstract/base_region.py +1 -0
- dodal/devices/electron_analyser/specs/__init__.py +1 -2
- dodal/devices/electron_analyser/specs/detector.py +5 -21
- dodal/devices/electron_analyser/specs/driver_io.py +27 -2
- dodal/devices/electron_analyser/vgscienta/__init__.py +1 -2
- dodal/devices/electron_analyser/vgscienta/detector.py +8 -22
- dodal/devices/electron_analyser/vgscienta/driver_io.py +31 -3
- dodal/devices/electron_analyser/vgscienta/region.py +0 -1
- dodal/devices/fast_grid_scan.py +1 -1
- dodal/devices/i04/murko_results.py +93 -96
- dodal/devices/i18/diode.py +37 -4
- dodal/devices/mx_phase1/beamstop.py +23 -6
- dodal/devices/oav/oav_detector.py +61 -23
- dodal/devices/oav/oav_parameters.py +46 -16
- dodal/devices/oav/oav_to_redis_forwarder.py +2 -2
- dodal/devices/robot.py +20 -1
- dodal/devices/smargon.py +43 -4
- dodal/devices/zebra/zebra.py +8 -0
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +167 -0
- dodal/plan_stubs/electron_analyser/__init__.py +0 -3
- dodal/plan_stubs/electron_analyser/configure_driver.py +0 -92
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.48.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import pickle
|
|
3
|
-
from collections import OrderedDict
|
|
4
3
|
from enum import Enum
|
|
5
4
|
from typing import TypedDict
|
|
6
5
|
|
|
@@ -17,10 +16,11 @@ from redis.asyncio import StrictRedis
|
|
|
17
16
|
from dodal.devices.i04.constants import RedisConstants
|
|
18
17
|
from dodal.devices.oav.oav_calculations import (
|
|
19
18
|
calculate_beam_distance,
|
|
20
|
-
camera_coordinates_to_xyz_mm,
|
|
21
19
|
)
|
|
22
20
|
from dodal.log import LOGGER
|
|
23
21
|
|
|
22
|
+
NO_MURKO_RESULT = (-1, -1)
|
|
23
|
+
|
|
24
24
|
MurkoResult = dict
|
|
25
25
|
FullMurkoResults = dict[str, list[MurkoResult]]
|
|
26
26
|
|
|
@@ -43,12 +43,19 @@ class Coord(Enum):
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
46
|
-
"""Device that takes crystal centre
|
|
46
|
+
"""Device that takes crystal centre values from Murko and uses them to set the
|
|
47
47
|
x, y, z coordinate of the sample to be in line with the beam centre.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
52
59
|
"""
|
|
53
60
|
|
|
54
61
|
TIMEOUT_S = 2
|
|
@@ -59,6 +66,7 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
59
66
|
redis_password=RedisConstants.REDIS_PASSWORD,
|
|
60
67
|
redis_db=RedisConstants.MURKO_REDIS_DB,
|
|
61
68
|
name="",
|
|
69
|
+
stop_angle=350,
|
|
62
70
|
):
|
|
63
71
|
self.redis_client = StrictRedis(
|
|
64
72
|
host=redis_host,
|
|
@@ -67,17 +75,11 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
67
75
|
)
|
|
68
76
|
self.pubsub = self.redis_client.pubsub()
|
|
69
77
|
self._last_omega = 0
|
|
70
|
-
self._last_result = None
|
|
71
78
|
self.sample_id = soft_signal_rw(str) # Should get from redis
|
|
72
|
-
self.
|
|
73
|
-
self.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
(180, ("x", "y")),
|
|
77
|
-
(270, ()), # Stop searching here
|
|
78
|
-
]
|
|
79
|
-
)
|
|
80
|
-
self.angles_to_search = list(self.search_angles.keys())
|
|
79
|
+
self.stop_angle = stop_angle
|
|
80
|
+
self.x_dists_mm = []
|
|
81
|
+
self.y_dists_mm = []
|
|
82
|
+
self.omegas = []
|
|
81
83
|
|
|
82
84
|
with self.add_children_as_readables():
|
|
83
85
|
# Diffs from current x/y/z
|
|
@@ -101,95 +103,90 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
101
103
|
async def trigger(self):
|
|
102
104
|
# Wait for results
|
|
103
105
|
sample_id = await self.sample_id.get_value()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# waits here for next batch to be recieved
|
|
106
|
+
while self._last_omega < self.stop_angle:
|
|
107
|
+
# waits here for next batch to be received
|
|
107
108
|
message = await self.pubsub.get_message(timeout=self.TIMEOUT_S)
|
|
108
109
|
if message is None: # No more messages to process
|
|
109
|
-
await self.process_batch(
|
|
110
|
-
final_message, sample_id
|
|
111
|
-
) # Process final message again
|
|
112
110
|
break
|
|
113
111
|
await self.process_batch(message, sample_id)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
self.
|
|
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)
|
|
124
126
|
|
|
125
127
|
async def process_batch(self, message: dict | None, sample_id: str):
|
|
126
128
|
if message and message["type"] == "message":
|
|
127
|
-
batch_results = pickle.loads(message["data"])
|
|
129
|
+
batch_results: list[dict] = pickle.loads(message["data"])
|
|
128
130
|
for results in batch_results:
|
|
129
|
-
LOGGER.info(f"Got {results} from redis")
|
|
130
131
|
for uuid, result in results.items():
|
|
131
|
-
metadata_str
|
|
132
|
+
if metadata_str := await self.redis_client.hget( # type: ignore
|
|
132
133
|
f"murko:{sample_id}:metadata", uuid
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
search_angle = self.angles_to_search.pop(0)
|
|
148
|
-
for coord in self.search_angles[search_angle]:
|
|
149
|
-
self.coords[coord][omega_angle] = movement[Coord[coord].value]
|
|
150
|
-
LOGGER.info(f"Found {coord} at {movement}, angle = {omega_angle}")
|
|
151
|
-
self._last_omega = omega_angle
|
|
152
|
-
self._last_result = result
|
|
153
|
-
|
|
154
|
-
def get_coords_if_at_angle(
|
|
155
|
-
self, metadata: MurkoMetadata, result: MurkoResult, omega: float
|
|
156
|
-
) -> np.ndarray | None:
|
|
157
|
-
"""Gets the 'most_likely_click' coordinates from Murko if omega or the last
|
|
158
|
-
omega are the closest angle to the search angle. Otherwise returns None.
|
|
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.
|
|
159
148
|
"""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
):
|
|
166
|
-
|
|
167
|
-
closest_omega = self._last_omega
|
|
168
|
-
elif omega - search_angle >= 0: # if this omega is closest
|
|
169
|
-
closest_result = result
|
|
170
|
-
closest_omega = omega
|
|
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")
|
|
171
156
|
else:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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)])
|
|
188
189
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
closest_omega,
|
|
193
|
-
metadata["microns_per_x_pixel"],
|
|
194
|
-
metadata["microns_per_y_pixel"],
|
|
195
|
-
)
|
|
190
|
+
yz, residuals, rank, s = np.linalg.lstsq(matrix, vertical_dists, rcond=None)
|
|
191
|
+
y, z = yz
|
|
192
|
+
return y, z
|
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,6 +1,11 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from math import isclose
|
|
2
3
|
|
|
3
|
-
from ophyd_async.core import
|
|
4
|
+
from ophyd_async.core import (
|
|
5
|
+
StandardReadable,
|
|
6
|
+
StrictEnum,
|
|
7
|
+
derived_signal_rw,
|
|
8
|
+
)
|
|
4
9
|
from ophyd_async.epics.motor import Motor
|
|
5
10
|
|
|
6
11
|
from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
|
|
@@ -37,8 +42,7 @@ class Beamstop(StandardReadable):
|
|
|
37
42
|
x: beamstop x position in mm
|
|
38
43
|
y: beamstop y position in mm
|
|
39
44
|
z: beamstop z position in mm
|
|
40
|
-
selected_pos: Get the current position of the beamstop as an enum.
|
|
41
|
-
is read-only.
|
|
45
|
+
selected_pos: Get or set the current position of the beamstop as an enum.
|
|
42
46
|
"""
|
|
43
47
|
|
|
44
48
|
def __init__(
|
|
@@ -51,10 +55,13 @@ class Beamstop(StandardReadable):
|
|
|
51
55
|
self.x_mm = Motor(prefix + "X")
|
|
52
56
|
self.y_mm = Motor(prefix + "Y")
|
|
53
57
|
self.z_mm = Motor(prefix + "Z")
|
|
54
|
-
self.selected_pos =
|
|
55
|
-
self._get_selected_position,
|
|
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,
|
|
56
64
|
)
|
|
57
|
-
|
|
58
65
|
self._in_beam_xyz_mm = [
|
|
59
66
|
float(beamline_parameters[f"in_beam_{axis}_STANDARD"])
|
|
60
67
|
for axis in ("x", "y", "z")
|
|
@@ -77,3 +84,13 @@ class Beamstop(StandardReadable):
|
|
|
77
84
|
return BeamstopPositions.DATA_COLLECTION
|
|
78
85
|
else:
|
|
79
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}")
|
|
@@ -5,14 +5,20 @@ from ophyd_async.core import (
|
|
|
5
5
|
DEFAULT_TIMEOUT,
|
|
6
6
|
AsyncStatus,
|
|
7
7
|
LazyMock,
|
|
8
|
+
SignalR,
|
|
8
9
|
StandardReadable,
|
|
9
10
|
derived_signal_r,
|
|
10
11
|
soft_signal_rw,
|
|
11
12
|
)
|
|
12
|
-
from ophyd_async.epics.core import epics_signal_rw
|
|
13
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
13
14
|
|
|
14
15
|
from dodal.devices.areadetector.plugins.CAM import Cam
|
|
15
|
-
from dodal.devices.oav.oav_parameters import
|
|
16
|
+
from dodal.devices.oav.oav_parameters import (
|
|
17
|
+
DEFAULT_OAV_WINDOW,
|
|
18
|
+
OAVConfig,
|
|
19
|
+
OAVConfigBase,
|
|
20
|
+
OAVConfigBeamCentre,
|
|
21
|
+
)
|
|
16
22
|
from dodal.devices.oav.snapshots.snapshot import Snapshot
|
|
17
23
|
from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
|
|
18
24
|
|
|
@@ -53,7 +59,10 @@ class ZoomController(StandardReadable, Movable[str]):
|
|
|
53
59
|
|
|
54
60
|
|
|
55
61
|
class OAV(StandardReadable):
|
|
56
|
-
|
|
62
|
+
beam_centre_i: SignalR[int]
|
|
63
|
+
beam_centre_j: SignalR[int]
|
|
64
|
+
|
|
65
|
+
def __init__(self, prefix: str, config: OAVConfigBase, name: str = ""):
|
|
57
66
|
self.oav_config = config
|
|
58
67
|
self._prefix = prefix
|
|
59
68
|
self._name = name
|
|
@@ -79,18 +88,6 @@ class OAV(StandardReadable):
|
|
|
79
88
|
size=self.sizes[Coords.Y],
|
|
80
89
|
coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
|
|
81
90
|
)
|
|
82
|
-
self.beam_centre_i = derived_signal_r(
|
|
83
|
-
self._get_beam_position,
|
|
84
|
-
zoom_level=self.zoom_controller.level,
|
|
85
|
-
size=self.sizes[Coords.X],
|
|
86
|
-
coord=soft_signal_rw(datatype=int, initial_value=Coords.X.value),
|
|
87
|
-
)
|
|
88
|
-
self.beam_centre_j = derived_signal_r(
|
|
89
|
-
self._get_beam_position,
|
|
90
|
-
zoom_level=self.zoom_controller.level,
|
|
91
|
-
size=self.sizes[Coords.Y],
|
|
92
|
-
coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
|
|
93
|
-
)
|
|
94
91
|
self.snapshot = Snapshot(
|
|
95
92
|
f"{self._prefix}MJPG:",
|
|
96
93
|
self._name,
|
|
@@ -107,14 +104,6 @@ class OAV(StandardReadable):
|
|
|
107
104
|
value = self.parameters[_zoom].microns_per_pixel[coord]
|
|
108
105
|
return value * DEFAULT_OAV_WINDOW[coord] / size
|
|
109
106
|
|
|
110
|
-
def _get_beam_position(self, zoom_level: str, size: int, coord: int) -> int:
|
|
111
|
-
"""Extracts the beam location in pixels `xCentre` `yCentre`, for a requested \
|
|
112
|
-
zoom level. """
|
|
113
|
-
_zoom = self._read_current_zoom(zoom_level)
|
|
114
|
-
value = self.parameters[_zoom].crosshair[coord]
|
|
115
|
-
|
|
116
|
-
return int(value * size / DEFAULT_OAV_WINDOW[coord])
|
|
117
|
-
|
|
118
107
|
async def connect(
|
|
119
108
|
self,
|
|
120
109
|
mock: bool | LazyMock = False,
|
|
@@ -124,3 +113,52 @@ class OAV(StandardReadable):
|
|
|
124
113
|
self.parameters = self.oav_config.get_parameters()
|
|
125
114
|
|
|
126
115
|
return await super().connect(mock, timeout, force_reconnect)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class OAVBeamCentreFile(OAV):
|
|
119
|
+
"""OAV device that reads its beam centre values from a file. The config parameter
|
|
120
|
+
must be a OAVConfigBeamCentre object, as this contains a filepath to where the beam
|
|
121
|
+
centre values are stored.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, prefix: str, config: OAVConfigBeamCentre, name: str = ""):
|
|
125
|
+
super().__init__(prefix, config, name)
|
|
126
|
+
|
|
127
|
+
with self.add_children_as_readables():
|
|
128
|
+
self.beam_centre_i = derived_signal_r(
|
|
129
|
+
self._get_beam_position,
|
|
130
|
+
zoom_level=self.zoom_controller.level,
|
|
131
|
+
size=self.sizes[Coords.X],
|
|
132
|
+
coord=soft_signal_rw(datatype=int, initial_value=Coords.X.value),
|
|
133
|
+
)
|
|
134
|
+
self.beam_centre_j = derived_signal_r(
|
|
135
|
+
self._get_beam_position,
|
|
136
|
+
zoom_level=self.zoom_controller.level,
|
|
137
|
+
size=self.sizes[Coords.Y],
|
|
138
|
+
coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
|
|
139
|
+
)
|
|
140
|
+
# Set name so that new child signals get correct name
|
|
141
|
+
self.set_name(self.name)
|
|
142
|
+
|
|
143
|
+
def _get_beam_position(self, zoom_level: str, size: int, coord: int) -> int:
|
|
144
|
+
"""Extracts the beam location in pixels `xCentre` `yCentre`, for a requested \
|
|
145
|
+
zoom level. """
|
|
146
|
+
_zoom = self._read_current_zoom(zoom_level)
|
|
147
|
+
value = self.parameters[_zoom].crosshair[coord]
|
|
148
|
+
return int(value * size / DEFAULT_OAV_WINDOW[coord])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class OAVBeamCentrePV(OAV):
|
|
152
|
+
"""OAV device that reads its beam centre values from PVs."""
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self, prefix: str, config: OAVConfig, name: str = "", overlay_channel: int = 1
|
|
156
|
+
):
|
|
157
|
+
with self.add_children_as_readables():
|
|
158
|
+
self.beam_centre_i = epics_signal_r(
|
|
159
|
+
int, prefix + f"OVER:{overlay_channel}:CenterX"
|
|
160
|
+
)
|
|
161
|
+
self.beam_centre_j = epics_signal_r(
|
|
162
|
+
int, prefix + f"OVER:{overlay_channel}:CenterY"
|
|
163
|
+
)
|
|
164
|
+
super().__init__(prefix, config, name)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import xml.etree.ElementTree as et
|
|
3
|
+
from abc import abstractmethod
|
|
3
4
|
from collections import ChainMap
|
|
4
5
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Any
|
|
6
|
+
from typing import Any, Generic, TypeVar
|
|
6
7
|
from xml.etree.ElementTree import Element
|
|
7
8
|
|
|
8
9
|
# GDA currently assumes this aspect ratio for the OAV window size.
|
|
@@ -107,22 +108,19 @@ class OAVParameters:
|
|
|
107
108
|
@dataclass
|
|
108
109
|
class ZoomParams:
|
|
109
110
|
microns_per_pixel: tuple[float, float]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class ZoomParamsCrosshair(ZoomParams):
|
|
110
115
|
crosshair: tuple[int, int]
|
|
111
116
|
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
""" Read the OAV config files and return a dictionary of {'zoom_level': ZoomParams}\
|
|
115
|
-
with information about microns per pixels and crosshairs.
|
|
116
|
-
"""
|
|
118
|
+
ParamType = TypeVar("ParamType", bound="ZoomParams")
|
|
117
119
|
|
|
118
|
-
def __init__(self, zoom_params_file: str, display_config_file: str):
|
|
119
|
-
self.zoom_params = self._get_zoom_params(zoom_params_file)
|
|
120
|
-
self.display_config = self._get_display_config(display_config_file)
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return file_lines
|
|
121
|
+
class OAVConfigBase(Generic[ParamType]):
|
|
122
|
+
def __init__(self, zoom_params_file: str):
|
|
123
|
+
self.zoom_params = self._get_zoom_params(zoom_params_file)
|
|
126
124
|
|
|
127
125
|
def _get_zoom_params(self, zoom_params_file: str):
|
|
128
126
|
tree = et.parse(zoom_params_file)
|
|
@@ -138,6 +136,39 @@ class OAVConfig:
|
|
|
138
136
|
um_per_pix[zoom] = (um_pix_x, um_pix_y)
|
|
139
137
|
return um_per_pix
|
|
140
138
|
|
|
139
|
+
@abstractmethod
|
|
140
|
+
def get_parameters(self) -> dict[str, ParamType]: ...
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class OAVConfig(OAVConfigBase[ZoomParams]):
|
|
144
|
+
def get_parameters(self) -> dict[str, ZoomParams]:
|
|
145
|
+
config = {}
|
|
146
|
+
um_xy = self._read_zoom_params()
|
|
147
|
+
for zoom_key in list(um_xy.keys()):
|
|
148
|
+
config[zoom_key] = ZoomParams(
|
|
149
|
+
microns_per_pixel=um_xy[zoom_key],
|
|
150
|
+
)
|
|
151
|
+
return config
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class OAVConfigBeamCentre(OAVConfigBase[ZoomParamsCrosshair]):
|
|
155
|
+
""" Read the OAV config files and return a dictionary of {'zoom_level': ZoomParams}\
|
|
156
|
+
with information about microns per pixels and crosshairs.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
zoom_params_file: str,
|
|
162
|
+
display_config_file: str,
|
|
163
|
+
):
|
|
164
|
+
self.display_config = self._get_display_config(display_config_file)
|
|
165
|
+
super().__init__(zoom_params_file)
|
|
166
|
+
|
|
167
|
+
def _get_display_config(self, display_config_file: str):
|
|
168
|
+
with open(display_config_file) as f:
|
|
169
|
+
file_lines = f.readlines()
|
|
170
|
+
return file_lines
|
|
171
|
+
|
|
141
172
|
def _read_display_config(self) -> dict:
|
|
142
173
|
crosshairs = {}
|
|
143
174
|
for i in range(len(self.display_config)):
|
|
@@ -148,13 +179,12 @@ class OAVConfig:
|
|
|
148
179
|
crosshairs[zoom] = (x, y)
|
|
149
180
|
return crosshairs
|
|
150
181
|
|
|
151
|
-
def get_parameters(self) -> dict[str,
|
|
182
|
+
def get_parameters(self) -> dict[str, ZoomParamsCrosshair]:
|
|
152
183
|
config = {}
|
|
153
184
|
um_xy = self._read_zoom_params()
|
|
154
185
|
bc_xy = self._read_display_config()
|
|
155
186
|
for zoom_key in list(bc_xy.keys()):
|
|
156
|
-
config[zoom_key] =
|
|
157
|
-
microns_per_pixel=um_xy[zoom_key],
|
|
158
|
-
crosshair=bc_xy[zoom_key],
|
|
187
|
+
config[zoom_key] = ZoomParamsCrosshair(
|
|
188
|
+
microns_per_pixel=um_xy[zoom_key], crosshair=bc_xy[zoom_key]
|
|
159
189
|
)
|
|
160
190
|
return config
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections.abc import Awaitable, Callable
|
|
3
3
|
from datetime import timedelta
|
|
4
|
-
from enum import
|
|
4
|
+
from enum import IntEnum
|
|
5
5
|
from uuid import uuid4
|
|
6
6
|
|
|
7
7
|
from aiohttp import ClientResponse, ClientSession
|
|
@@ -29,7 +29,7 @@ async def get_next_jpeg(response: ClientResponse) -> bytes:
|
|
|
29
29
|
return line + await response.content.readuntil(JPEG_STOP_BYTE)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class Source(
|
|
32
|
+
class Source(IntEnum):
|
|
33
33
|
FULL_SCREEN = 0
|
|
34
34
|
ROI = 1
|
|
35
35
|
|
dodal/devices/robot.py
CHANGED
|
@@ -11,7 +11,12 @@ from ophyd_async.core import (
|
|
|
11
11
|
set_and_wait_for_value,
|
|
12
12
|
wait_for_value,
|
|
13
13
|
)
|
|
14
|
-
from ophyd_async.epics.core import
|
|
14
|
+
from ophyd_async.epics.core import (
|
|
15
|
+
epics_signal_r,
|
|
16
|
+
epics_signal_rw,
|
|
17
|
+
epics_signal_rw_rbv,
|
|
18
|
+
epics_signal_x,
|
|
19
|
+
)
|
|
15
20
|
|
|
16
21
|
from dodal.log import LOGGER
|
|
17
22
|
|
|
@@ -88,6 +93,20 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
|
88
93
|
self.controller_error = ErrorStatus(prefix + "CNTL")
|
|
89
94
|
|
|
90
95
|
self.reset = epics_signal_x(prefix + "RESET.PROC")
|
|
96
|
+
self.stop = epics_signal_x(prefix + "ABORT.PROC")
|
|
97
|
+
self.init = epics_signal_x(prefix + "INIT.PROC")
|
|
98
|
+
self.soak = epics_signal_x(prefix + "SOAK.PROC")
|
|
99
|
+
self.home = epics_signal_x(prefix + "GOHM.PROC")
|
|
100
|
+
self.unload = epics_signal_x(prefix + "UNLD.PROC")
|
|
101
|
+
self.dry = epics_signal_x(prefix + "DRY.PROC")
|
|
102
|
+
self.open = epics_signal_x(prefix + "COLO.PROC")
|
|
103
|
+
self.close = epics_signal_x(prefix + "COLC.PROC")
|
|
104
|
+
self.cryomode_rbv = epics_signal_r(float, prefix + "CRYO_MODE_RBV")
|
|
105
|
+
self.cryomode = epics_signal_rw(str, prefix + "CRYO_MODE_CTRL")
|
|
106
|
+
self.gripper_temp = epics_signal_r(float, prefix + "GRIPPER_TEMP")
|
|
107
|
+
self.dewar_lid_temperature = epics_signal_rw(
|
|
108
|
+
float, prefix + "DW_1_TEMP", prefix + "DW_1_SET_POINT"
|
|
109
|
+
)
|
|
91
110
|
super().__init__(name=name)
|
|
92
111
|
|
|
93
112
|
async def pin_mounted_or_no_pin_found(self):
|