dls-dodal 1.45.0__py3-none-any.whl → 1.47.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.45.0.dist-info → dls_dodal-1.47.0.dist-info}/METADATA +2 -2
- {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/RECORD +76 -64
- {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +0 -1
- dodal/beamlines/b07.py +2 -6
- dodal/beamlines/b07_1.py +1 -3
- dodal/beamlines/i03.py +16 -19
- dodal/beamlines/i04.py +49 -17
- dodal/beamlines/i09.py +1 -3
- dodal/beamlines/i09_1.py +1 -3
- dodal/beamlines/i18.py +7 -4
- dodal/beamlines/i22.py +3 -3
- dodal/beamlines/i23.py +75 -4
- dodal/beamlines/p38.py +4 -4
- dodal/beamlines/p60.py +2 -6
- dodal/beamlines/p99.py +48 -4
- dodal/common/beamlines/beamline_parameters.py +1 -2
- dodal/common/beamlines/beamline_utils.py +5 -0
- dodal/common/data_util.py +4 -0
- dodal/devices/aperturescatterguard.py +47 -47
- dodal/devices/common_dcm.py +77 -0
- 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 +8 -0
- dodal/devices/electron_analyser/abstract/__init__.py +28 -0
- dodal/devices/electron_analyser/abstract/base_detector.py +210 -0
- dodal/devices/electron_analyser/abstract/base_driver_io.py +121 -0
- dodal/devices/electron_analyser/{abstract_region.py → abstract/base_region.py} +2 -9
- dodal/devices/electron_analyser/specs/__init__.py +11 -0
- dodal/devices/electron_analyser/specs/detector.py +29 -0
- dodal/devices/electron_analyser/specs/driver_io.py +64 -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 +12 -0
- dodal/devices/electron_analyser/vgscienta/detector.py +36 -0
- dodal/devices/electron_analyser/vgscienta/driver_io.py +39 -0
- dodal/devices/electron_analyser/{vgscienta_region.py → vgscienta/region.py} +1 -1
- dodal/devices/fast_grid_scan.py +7 -9
- dodal/devices/i03/__init__.py +3 -0
- dodal/devices/{dcm.py → i03/dcm.py} +8 -12
- dodal/devices/{undulator_dcm.py → i03/undulator_dcm.py} +6 -4
- dodal/devices/i04/__init__.py +3 -0
- dodal/devices/i04/constants.py +9 -0
- dodal/devices/i04/murko_results.py +195 -0
- dodal/devices/i10/diagnostics.py +9 -61
- dodal/devices/i13_1/merlin.py +3 -4
- dodal/devices/i13_1/merlin_controller.py +1 -1
- dodal/devices/i22/dcm.py +10 -12
- dodal/devices/i24/dcm.py +8 -17
- 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 +6 -12
- dodal/devices/oav/oav_calculations.py +2 -2
- dodal/devices/oav/oav_detector.py +32 -22
- dodal/devices/oav/utils.py +2 -2
- dodal/devices/p99/andor2_point.py +41 -0
- dodal/devices/positioner.py +49 -0
- dodal/devices/tetramm.py +8 -6
- dodal/devices/turbo_slit.py +2 -2
- dodal/devices/util/adjuster_plans.py +1 -1
- dodal/devices/zebra/zebra.py +4 -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/plan_stubs/data_session.py +10 -1
- dodal/plan_stubs/electron_analyser/__init__.py +3 -0
- dodal/plan_stubs/electron_analyser/{configure_controller.py → configure_driver.py} +30 -18
- dodal/plans/verify_undulator_gap.py +2 -2
- 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
- {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
-
from ophyd_async.core import Array1D,
|
|
2
|
+
from ophyd_async.core import Array1D, soft_signal_r_and_setter
|
|
3
3
|
from ophyd_async.epics.core import epics_signal_r
|
|
4
4
|
from ophyd_async.epics.motor import Motor
|
|
5
5
|
|
|
@@ -8,9 +8,14 @@ from dodal.common.crystal_metadata import (
|
|
|
8
8
|
MaterialsEnum,
|
|
9
9
|
make_crystal_metadata_from_material,
|
|
10
10
|
)
|
|
11
|
+
from dodal.devices.common_dcm import (
|
|
12
|
+
BaseDCM,
|
|
13
|
+
PitchAndRollCrystal,
|
|
14
|
+
StationaryCrystal,
|
|
15
|
+
)
|
|
11
16
|
|
|
12
17
|
|
|
13
|
-
class DCM(
|
|
18
|
+
class DCM(BaseDCM[PitchAndRollCrystal, StationaryCrystal]):
|
|
14
19
|
"""
|
|
15
20
|
A double crystal monochromator (DCM), used to select the energy of the beam.
|
|
16
21
|
|
|
@@ -30,13 +35,7 @@ class DCM(StandardReadable):
|
|
|
30
35
|
MaterialsEnum.Si, (1, 1, 1)
|
|
31
36
|
)
|
|
32
37
|
with self.add_children_as_readables():
|
|
33
|
-
self.bragg_in_degrees = Motor(prefix + "BRAGG")
|
|
34
|
-
self.roll_in_mrad = Motor(prefix + "ROLL")
|
|
35
|
-
self.offset_in_mm = Motor(prefix + "OFFSET")
|
|
36
38
|
self.perp_in_mm = Motor(prefix + "PERP")
|
|
37
|
-
self.energy_in_kev = Motor(prefix + "ENERGY")
|
|
38
|
-
self.pitch_in_mrad = Motor(prefix + "PITCH")
|
|
39
|
-
self.wavelength = Motor(prefix + "WAVELENGTH")
|
|
40
39
|
|
|
41
40
|
# temperatures
|
|
42
41
|
self.xtal1_temp = epics_signal_r(float, prefix + "TEMP1")
|
|
@@ -58,7 +57,4 @@ class DCM(StandardReadable):
|
|
|
58
57
|
Array1D[np.uint64],
|
|
59
58
|
initial_value=reflection_array,
|
|
60
59
|
)
|
|
61
|
-
|
|
62
|
-
float, prefix + "DSPACING:RBV"
|
|
63
|
-
)
|
|
64
|
-
super().__init__(name)
|
|
60
|
+
super().__init__(prefix, PitchAndRollCrystal, StationaryCrystal, name)
|
|
@@ -4,10 +4,9 @@ from bluesky.protocols import Movable
|
|
|
4
4
|
from ophyd_async.core import AsyncStatus, Reference, StandardReadable
|
|
5
5
|
|
|
6
6
|
from dodal.common.beamlines.beamline_parameters import get_beamline_parameters
|
|
7
|
-
|
|
8
|
-
from
|
|
9
|
-
from .
|
|
10
|
-
from .undulator import Undulator
|
|
7
|
+
from dodal.devices.i03.dcm import DCM
|
|
8
|
+
from dodal.devices.undulator import Undulator
|
|
9
|
+
from dodal.log import LOGGER
|
|
11
10
|
|
|
12
11
|
ENERGY_TIMEOUT_S: float = 30.0
|
|
13
12
|
|
|
@@ -23,6 +22,9 @@ class UndulatorDCM(StandardReadable, Movable[float]):
|
|
|
23
22
|
Calling unulator_dcm.set(energy) will move the DCM motor, perform a table lookup
|
|
24
23
|
and move the Undulator gap motor if needed. So the set method can be thought of as
|
|
25
24
|
a comprehensive way to set beam energy.
|
|
25
|
+
|
|
26
|
+
This class will be removed in the future. Use the separate Undulator and DCM devices
|
|
27
|
+
instead. See https://github.com/DiamondLightSource/dodal/issues/1092
|
|
26
28
|
"""
|
|
27
29
|
|
|
28
30
|
def __init__(
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class RedisConstants:
|
|
7
|
+
REDIS_HOST = os.environ.get("VALKEY_PROD_SVC_SERVICE_HOST", "test_redis")
|
|
8
|
+
REDIS_PASSWORD = os.environ.get("VALKEY_PASSWORD", "test_redis_password")
|
|
9
|
+
MURKO_REDIS_DB = 7
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pickle
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from bluesky.protocols import Stageable, Triggerable
|
|
9
|
+
from ophyd_async.core import (
|
|
10
|
+
AsyncStatus,
|
|
11
|
+
StandardReadable,
|
|
12
|
+
soft_signal_r_and_setter,
|
|
13
|
+
soft_signal_rw,
|
|
14
|
+
)
|
|
15
|
+
from redis.asyncio import StrictRedis
|
|
16
|
+
|
|
17
|
+
from dodal.devices.i04.constants import RedisConstants
|
|
18
|
+
from dodal.devices.oav.oav_calculations import (
|
|
19
|
+
calculate_beam_distance,
|
|
20
|
+
camera_coordinates_to_xyz_mm,
|
|
21
|
+
)
|
|
22
|
+
from dodal.log import LOGGER
|
|
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 coords 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
|
+
(x, z) coords can be read at 90°, and (x, y) at 180° (or the closest omega angle to
|
|
49
|
+
90° and 180°). The average of the x values at these angles is taken, and sin(omega)z
|
|
50
|
+
and cosine(omega)y are taken to account for the rotation. This value is used to
|
|
51
|
+
calculate a number of mm the sample needs to move to be in line with the beam centre.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
TIMEOUT_S = 2
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
redis_host=RedisConstants.REDIS_HOST,
|
|
59
|
+
redis_password=RedisConstants.REDIS_PASSWORD,
|
|
60
|
+
redis_db=RedisConstants.MURKO_REDIS_DB,
|
|
61
|
+
name="",
|
|
62
|
+
):
|
|
63
|
+
self.redis_client = StrictRedis(
|
|
64
|
+
host=redis_host,
|
|
65
|
+
password=redis_password,
|
|
66
|
+
db=redis_db,
|
|
67
|
+
)
|
|
68
|
+
self.pubsub = self.redis_client.pubsub()
|
|
69
|
+
self._last_omega = 0
|
|
70
|
+
self._last_result = None
|
|
71
|
+
self.sample_id = soft_signal_rw(str) # Should get from redis
|
|
72
|
+
self.coords = {"x": {}, "y": {}, "z": {}}
|
|
73
|
+
self.search_angles = OrderedDict(
|
|
74
|
+
[ # Angles to search and dimensions to gather at each angle
|
|
75
|
+
(90, ("x", "z")),
|
|
76
|
+
(180, ("x", "y")),
|
|
77
|
+
(270, ()), # Stop searching here
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
self.angles_to_search = list(self.search_angles.keys())
|
|
81
|
+
|
|
82
|
+
with self.add_children_as_readables():
|
|
83
|
+
# Diffs from current x/y/z
|
|
84
|
+
self.x_mm, self._x_mm_setter = soft_signal_r_and_setter(float)
|
|
85
|
+
self.y_mm, self._y_mm_setter = soft_signal_r_and_setter(float)
|
|
86
|
+
self.z_mm, self._z_mm_setter = soft_signal_r_and_setter(float)
|
|
87
|
+
super().__init__(name=name)
|
|
88
|
+
|
|
89
|
+
@AsyncStatus.wrap
|
|
90
|
+
async def stage(self):
|
|
91
|
+
await self.pubsub.subscribe("murko-results")
|
|
92
|
+
self._x_mm_setter(0)
|
|
93
|
+
self._y_mm_setter(0)
|
|
94
|
+
self._z_mm_setter(0)
|
|
95
|
+
|
|
96
|
+
@AsyncStatus.wrap
|
|
97
|
+
async def unstage(self):
|
|
98
|
+
await self.pubsub.unsubscribe()
|
|
99
|
+
|
|
100
|
+
@AsyncStatus.wrap
|
|
101
|
+
async def trigger(self):
|
|
102
|
+
# Wait for results
|
|
103
|
+
sample_id = await self.sample_id.get_value()
|
|
104
|
+
final_message = None
|
|
105
|
+
while self.angles_to_search:
|
|
106
|
+
# waits here for next batch to be recieved
|
|
107
|
+
message = await self.pubsub.get_message(timeout=self.TIMEOUT_S)
|
|
108
|
+
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
|
+
break
|
|
113
|
+
await self.process_batch(message, sample_id)
|
|
114
|
+
final_message = message
|
|
115
|
+
x_values = list(self.coords["x"].values())
|
|
116
|
+
y_values = list(self.coords["y"].values())
|
|
117
|
+
z_values = list(self.coords["z"].values())
|
|
118
|
+
assert x_values, "No x values"
|
|
119
|
+
assert z_values, "No z values"
|
|
120
|
+
assert y_values, "No y values"
|
|
121
|
+
self._x_mm_setter(float(np.mean(x_values)))
|
|
122
|
+
self._y_mm_setter(float(np.mean(y_values)))
|
|
123
|
+
self._z_mm_setter(float(np.mean(z_values)))
|
|
124
|
+
|
|
125
|
+
async def process_batch(self, message: dict | None, sample_id: str):
|
|
126
|
+
if message and message["type"] == "message":
|
|
127
|
+
batch_results = pickle.loads(message["data"])
|
|
128
|
+
for results in batch_results:
|
|
129
|
+
LOGGER.info(f"Got {results} from redis")
|
|
130
|
+
for uuid, result in results.items():
|
|
131
|
+
metadata_str = await self.redis_client.hget( # type: ignore
|
|
132
|
+
f"murko:{sample_id}:metadata", uuid
|
|
133
|
+
)
|
|
134
|
+
if metadata_str and self.angles_to_search:
|
|
135
|
+
self.process_result(result, uuid, metadata_str)
|
|
136
|
+
|
|
137
|
+
def process_result(
|
|
138
|
+
self, result: dict, uuid: int, metadata_str: str
|
|
139
|
+
) -> float | None:
|
|
140
|
+
metadata = MurkoMetadata(json.loads(metadata_str))
|
|
141
|
+
omega_angle = metadata["omega_angle"]
|
|
142
|
+
LOGGER.info(f"Got angle {omega_angle}")
|
|
143
|
+
# Find closest to next search angle
|
|
144
|
+
movement = self.get_coords_if_at_angle(metadata, result, omega_angle)
|
|
145
|
+
if movement is not None:
|
|
146
|
+
LOGGER.info(f"Using result {uuid}, {metadata_str}, {result}")
|
|
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.
|
|
159
|
+
"""
|
|
160
|
+
search_angle = self.angles_to_search[0]
|
|
161
|
+
LOGGER.info(f"Compare {omega}, {search_angle}, {self._last_omega}")
|
|
162
|
+
if ( # if last omega is closest
|
|
163
|
+
abs(omega - search_angle) >= abs(self._last_omega - search_angle)
|
|
164
|
+
and self._last_result is not None
|
|
165
|
+
):
|
|
166
|
+
closest_result = self._last_result
|
|
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
|
|
171
|
+
else:
|
|
172
|
+
return None
|
|
173
|
+
coords = closest_result[
|
|
174
|
+
"most_likely_click"
|
|
175
|
+
] # As proportion from top, left of image
|
|
176
|
+
shape = closest_result["original_shape"] # Dimensions of image in pixels
|
|
177
|
+
# Murko returns coords as y, x
|
|
178
|
+
centre_px = (coords[1] * shape[1], coords[0] * shape[0])
|
|
179
|
+
LOGGER.info(
|
|
180
|
+
f"Using image taken at {closest_omega}, which found xtal at {centre_px}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
beam_dist_px = calculate_beam_distance(
|
|
184
|
+
(metadata["beam_centre_i"], metadata["beam_centre_j"]),
|
|
185
|
+
centre_px[0],
|
|
186
|
+
centre_px[1],
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return camera_coordinates_to_xyz_mm(
|
|
190
|
+
beam_dist_px[0],
|
|
191
|
+
beam_dist_px[1],
|
|
192
|
+
closest_omega,
|
|
193
|
+
metadata["microns_per_x_pixel"],
|
|
194
|
+
metadata["microns_per_y_pixel"],
|
|
195
|
+
)
|
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/i13_1/merlin.py
CHANGED
|
@@ -23,10 +23,9 @@ class Merlin(StandardDetector):
|
|
|
23
23
|
super().__init__(
|
|
24
24
|
MerlinController(self.drv),
|
|
25
25
|
adcore.ADHDFWriter(
|
|
26
|
-
self.hdf,
|
|
27
|
-
path_provider,
|
|
28
|
-
|
|
29
|
-
adcore.ADBaseDatasetDescriber(self.drv),
|
|
26
|
+
fileio=self.hdf,
|
|
27
|
+
path_provider=path_provider,
|
|
28
|
+
dataset_describer=adcore.ADBaseDatasetDescriber(self.drv),
|
|
30
29
|
),
|
|
31
30
|
config_sigs=(self.drv.acquire_period, self.drv.acquire_time),
|
|
32
31
|
name=name,
|
|
@@ -37,7 +37,7 @@ class MerlinController(ADBaseController):
|
|
|
37
37
|
DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
|
|
38
38
|
)
|
|
39
39
|
await asyncio.gather(
|
|
40
|
-
self.driver.num_images.set(trigger_info.
|
|
40
|
+
self.driver.num_images.set(trigger_info.total_number_of_exposures),
|
|
41
41
|
self.driver.image_mode.set(ADImageMode.MULTIPLE),
|
|
42
42
|
)
|
|
43
43
|
|
dodal/devices/i22/dcm.py
CHANGED
|
@@ -5,7 +5,6 @@ from bluesky.protocols import Reading
|
|
|
5
5
|
from event_model.documents.event_descriptor import DataKey
|
|
6
6
|
from ophyd_async.core import (
|
|
7
7
|
Array1D,
|
|
8
|
-
StandardReadable,
|
|
9
8
|
StandardReadableFormat,
|
|
10
9
|
soft_signal_r_and_setter,
|
|
11
10
|
)
|
|
@@ -13,13 +12,18 @@ from ophyd_async.epics.core import epics_signal_r
|
|
|
13
12
|
from ophyd_async.epics.motor import Motor
|
|
14
13
|
|
|
15
14
|
from dodal.common.crystal_metadata import CrystalMetadata
|
|
15
|
+
from dodal.devices.common_dcm import (
|
|
16
|
+
BaseDCM,
|
|
17
|
+
PitchAndRollCrystal,
|
|
18
|
+
RollCrystal,
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
# Conversion constant for energy and wavelength, taken from the X-Ray data booklet
|
|
18
22
|
# Converts between energy in KeV and wavelength in angstrom
|
|
19
23
|
_CONVERSION_CONSTANT = 12.3984
|
|
20
24
|
|
|
21
25
|
|
|
22
|
-
class
|
|
26
|
+
class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
|
|
23
27
|
"""
|
|
24
28
|
A double crystal monochromator (DCM), used to select the energy of the beam.
|
|
25
29
|
|
|
@@ -39,13 +43,7 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
39
43
|
) -> None:
|
|
40
44
|
with self.add_children_as_readables():
|
|
41
45
|
# Positionable Parameters
|
|
42
|
-
self.bragg = Motor(prefix + "BRAGG")
|
|
43
|
-
self.offset = Motor(prefix + "OFFSET")
|
|
44
46
|
self.perp = Motor(prefix + "PERP")
|
|
45
|
-
self.energy = Motor(prefix + "ENERGY")
|
|
46
|
-
self.crystal_1_roll = Motor(prefix + "XTAL1:ROLL")
|
|
47
|
-
self.crystal_2_roll = Motor(prefix + "XTAL2:ROLL")
|
|
48
|
-
self.crystal_2_pitch = Motor(prefix + "XTAL2:PITCH")
|
|
49
47
|
|
|
50
48
|
# Temperatures
|
|
51
49
|
self.backplate_temp = epics_signal_r(float, temperature_prefix + "PT100-7")
|
|
@@ -93,12 +91,12 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
93
91
|
units=crystal_2_metadata.d_spacing[1],
|
|
94
92
|
)
|
|
95
93
|
|
|
96
|
-
super().__init__(name)
|
|
94
|
+
super().__init__(prefix, RollCrystal, PitchAndRollCrystal, name)
|
|
97
95
|
|
|
98
96
|
async def describe(self) -> dict[str, DataKey]:
|
|
99
97
|
default_describe = await super().describe()
|
|
100
98
|
return {
|
|
101
|
-
f"{self.name}-
|
|
99
|
+
f"{self.name}-wavelength_in_a": DataKey(
|
|
102
100
|
dtype="number",
|
|
103
101
|
shape=[],
|
|
104
102
|
source=self.name,
|
|
@@ -109,7 +107,7 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
109
107
|
|
|
110
108
|
async def read(self) -> dict[str, Reading]:
|
|
111
109
|
default_reading = await super().read()
|
|
112
|
-
energy: float = default_reading[f"{self.name}-
|
|
110
|
+
energy: float = default_reading[f"{self.name}-energy_in_kev"]["value"]
|
|
113
111
|
if energy > 0.0:
|
|
114
112
|
wavelength = _CONVERSION_CONSTANT / energy
|
|
115
113
|
else:
|
|
@@ -117,7 +115,7 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
117
115
|
|
|
118
116
|
return {
|
|
119
117
|
**default_reading,
|
|
120
|
-
f"{self.name}-
|
|
118
|
+
f"{self.name}-wavelength_in_a": Reading(
|
|
121
119
|
value=wavelength,
|
|
122
120
|
timestamp=time.time(),
|
|
123
121
|
),
|
dodal/devices/i24/dcm.py
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
from ophyd_async.core import StandardReadable
|
|
2
1
|
from ophyd_async.epics.core import epics_signal_r
|
|
3
|
-
from ophyd_async.epics.motor import Motor
|
|
4
2
|
|
|
3
|
+
from dodal.devices.common_dcm import (
|
|
4
|
+
BaseDCM,
|
|
5
|
+
PitchAndRollCrystal,
|
|
6
|
+
RollCrystal,
|
|
7
|
+
)
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
|
|
10
|
+
class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
|
|
7
11
|
"""
|
|
8
12
|
A double crystal monocromator device, used to select the beam energy.
|
|
9
13
|
"""
|
|
10
14
|
|
|
11
15
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
12
16
|
with self.add_children_as_readables():
|
|
13
|
-
# Motors
|
|
14
|
-
self.bragg_in_degrees = Motor(prefix + "-MO-DCM-01:BRAGG")
|
|
15
|
-
self.x_translation_in_mm = Motor(prefix + "-MO-DCM-01:X")
|
|
16
|
-
self.offset_in_mm = Motor(prefix + "-MO-DCM-01:OFFSET")
|
|
17
|
-
self.gap_in_mm = Motor(prefix + "-MO-DCM-01:GAP")
|
|
18
|
-
self.energy_in_kev = Motor(prefix + "-MO-DCM-01:ENERGY")
|
|
19
|
-
self.xtal1_roll = Motor(prefix + "-MO-DCM-01:XTAL1:ROLL")
|
|
20
|
-
self.xtal2_roll = Motor(prefix + "-MO-DCM-01:XTAL2:ROLL")
|
|
21
|
-
self.xtal2_pitch = Motor(prefix + "-MO-DCM-01:XTAL2:PITCH")
|
|
22
|
-
|
|
23
|
-
# Wavelength is calculated in epics from the energy
|
|
24
|
-
self.wavelength_in_a = epics_signal_r(float, prefix + "-MO-DCM-01:LAMBDA")
|
|
25
|
-
|
|
26
17
|
# Temperatures
|
|
27
18
|
self.xtal1_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-1")
|
|
28
19
|
self.xtal1_heater_temp = epics_signal_r(
|
|
@@ -39,4 +30,4 @@ class DCM(StandardReadable):
|
|
|
39
30
|
self.b1_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-7")
|
|
40
31
|
self.gap_temp = epics_signal_r(float, prefix + "-DI-DCM-01:TC-1")
|
|
41
32
|
|
|
42
|
-
super().__init__(name)
|
|
33
|
+
super().__init__(prefix + "-MO-DCM-01:", RollCrystal, PitchAndRollCrystal, 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)
|