dls-dodal 1.60.0__py3-none-any.whl → 1.62.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.60.0.dist-info → dls_dodal-1.62.0.dist-info}/METADATA +1 -1
- {dls_dodal-1.60.0.dist-info → dls_dodal-1.62.0.dist-info}/RECORD +45 -30
- dodal/_version.py +2 -2
- dodal/beamlines/i04.py +1 -1
- dodal/beamlines/i19_2.py +10 -0
- dodal/devices/apple2_undulator.py +85 -52
- dodal/devices/areadetector/__init__.py +0 -0
- dodal/devices/areadetector/plugins/__init__.py +0 -0
- dodal/devices/attenuator/__init__.py +0 -0
- dodal/devices/controllers.py +8 -6
- dodal/devices/electron_analyser/abstract/__init__.py +2 -2
- dodal/devices/electron_analyser/abstract/base_detector.py +13 -26
- dodal/devices/electron_analyser/abstract/base_driver_io.py +5 -4
- dodal/devices/electron_analyser/abstract/base_region.py +28 -13
- dodal/devices/electron_analyser/detector.py +19 -31
- dodal/devices/electron_analyser/specs/driver_io.py +0 -1
- dodal/devices/electron_analyser/vgscienta/driver_io.py +0 -1
- dodal/devices/fast_grid_scan.py +111 -32
- dodal/devices/fast_shutter.py +57 -0
- dodal/devices/i02_1/fast_grid_scan.py +1 -1
- dodal/devices/i04/murko_results.py +24 -12
- dodal/devices/i10/i10_apple2.py +15 -15
- dodal/devices/i10/rasor/__init__.py +0 -0
- dodal/devices/i11/__init__.py +0 -0
- dodal/devices/i15/__init__.py +0 -0
- dodal/devices/i15/dcm.py +10 -9
- dodal/devices/i15/focussing_mirror.py +4 -20
- dodal/devices/i15/jack.py +2 -10
- dodal/devices/i15/laue.py +1 -5
- dodal/devices/i15/multilayer_mirror.py +1 -5
- dodal/devices/i15/rail.py +1 -5
- dodal/devices/i18/__init__.py +0 -0
- dodal/devices/i19/mapt_configuration.py +38 -0
- dodal/devices/i19/pin_col_stages.py +170 -0
- dodal/devices/i22/__init__.py +0 -0
- dodal/devices/i24/commissioning_jungfrau.py +9 -1
- dodal/devices/mx_phase1/__init__.py +0 -0
- dodal/devices/oav/snapshots/__init__.py +0 -0
- dodal/devices/xspress3/__init__.py +0 -0
- dodal/parameters/__init__.py +0 -0
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +27 -5
- {dls_dodal-1.60.0.dist-info → dls_dodal-1.62.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.60.0.dist-info → dls_dodal-1.62.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.60.0.dist-info → dls_dodal-1.62.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.60.0.dist-info → dls_dodal-1.62.0.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from abc import ABC
|
|
3
3
|
from collections.abc import Callable
|
|
4
|
-
from typing import Generic, TypeVar
|
|
4
|
+
from typing import Generic, Self, TypeVar
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field, model_validator
|
|
7
7
|
|
|
@@ -88,28 +88,43 @@ class AbstractBaseRegion(
|
|
|
88
88
|
return self.energy_mode == EnergyMode.KINETIC
|
|
89
89
|
|
|
90
90
|
def switch_energy_mode(
|
|
91
|
-
self, energy_mode: EnergyMode, excitation_energy: float
|
|
92
|
-
) ->
|
|
91
|
+
self, energy_mode: EnergyMode, excitation_energy: float, copy: bool = True
|
|
92
|
+
) -> Self:
|
|
93
93
|
"""
|
|
94
|
-
Switch region to new energy mode: Kinetic or Binding.
|
|
95
|
-
|
|
94
|
+
Switch region with to a new energy mode with a new energy mode: Kinetic or Binding.
|
|
95
|
+
It caculates new values for low_energy, centre_energy, high_energy, via the
|
|
96
|
+
excitation enerrgy. It doesn't calculate anything if the region is already of
|
|
97
|
+
the same energy mode.
|
|
96
98
|
|
|
97
99
|
Parameters:
|
|
98
|
-
energy_mode:
|
|
99
|
-
excitation_energy:
|
|
100
|
-
|
|
100
|
+
energy_mode: Mode you want to switch the region to.
|
|
101
|
+
excitation_energy: Energy conversion for low_energy, centre_energy, and
|
|
102
|
+
high_energy for new energy mode.
|
|
103
|
+
copy: Defaults to True. If true, create a copy of this region for the new
|
|
104
|
+
energy_mode and return it. If False, alter this region for the
|
|
105
|
+
energy_mode and return it self.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Region with selected energy mode and new calculated energy values.
|
|
101
109
|
"""
|
|
110
|
+
switched_r = self.model_copy() if copy else self
|
|
102
111
|
conv = (
|
|
103
112
|
to_binding_energy
|
|
104
113
|
if energy_mode == EnergyMode.BINDING
|
|
105
114
|
else to_kinetic_energy
|
|
106
115
|
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
self.centre_energy, self.energy_mode, excitation_energy
|
|
116
|
+
switched_r.low_energy = conv(
|
|
117
|
+
switched_r.low_energy, switched_r.energy_mode, excitation_energy
|
|
110
118
|
)
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
switched_r.centre_energy = conv(
|
|
120
|
+
switched_r.centre_energy, switched_r.energy_mode, excitation_energy
|
|
121
|
+
)
|
|
122
|
+
switched_r.high_energy = conv(
|
|
123
|
+
switched_r.high_energy, switched_r.energy_mode, excitation_energy
|
|
124
|
+
)
|
|
125
|
+
switched_r.energy_mode = energy_mode
|
|
126
|
+
|
|
127
|
+
return switched_r
|
|
113
128
|
|
|
114
129
|
@model_validator(mode="before")
|
|
115
130
|
@classmethod
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
from typing import Generic, TypeVar
|
|
2
2
|
|
|
3
3
|
from bluesky.protocols import Stageable
|
|
4
|
-
from ophyd_async.core import
|
|
5
|
-
|
|
6
|
-
Reference,
|
|
7
|
-
)
|
|
4
|
+
from ophyd_async.core import AsyncStatus
|
|
5
|
+
from ophyd_async.epics.adcore import ADBaseController
|
|
8
6
|
|
|
9
7
|
from dodal.common.data_util import load_json_file_to_class
|
|
8
|
+
from dodal.devices.controllers import ConstantDeadTimeController
|
|
10
9
|
from dodal.devices.electron_analyser.abstract.base_detector import (
|
|
11
|
-
|
|
10
|
+
BaseElectronAnalyserDetector,
|
|
12
11
|
)
|
|
13
12
|
from dodal.devices.electron_analyser.abstract.base_driver_io import (
|
|
14
13
|
TAbstractAnalyserDriverIO,
|
|
@@ -20,35 +19,27 @@ from dodal.devices.electron_analyser.abstract.base_region import (
|
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class ElectronAnalyserRegionDetector(
|
|
23
|
-
|
|
22
|
+
BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO],
|
|
24
23
|
Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
|
|
25
24
|
):
|
|
26
25
|
"""
|
|
27
26
|
Extends electron analyser detector to configure specific region settings before data
|
|
28
|
-
|
|
29
|
-
is designed to only exist inside a plan.
|
|
27
|
+
acquisition. It is designed to only exist inside a plan.
|
|
30
28
|
"""
|
|
31
29
|
|
|
32
30
|
def __init__(
|
|
33
31
|
self,
|
|
34
|
-
|
|
32
|
+
controller: ADBaseController[TAbstractAnalyserDriverIO],
|
|
35
33
|
region: TAbstractBaseRegion,
|
|
36
34
|
name: str = "",
|
|
37
35
|
):
|
|
38
|
-
self._driver_ref = Reference(driver)
|
|
39
36
|
self.region = region
|
|
40
|
-
super().__init__(
|
|
41
|
-
|
|
42
|
-
@property
|
|
43
|
-
def driver(self) -> TAbstractAnalyserDriverIO:
|
|
44
|
-
# Store as a reference, this implementation will be given a driver so needs to
|
|
45
|
-
# make sure we don't get conflicting parents.
|
|
46
|
-
return self._driver_ref()
|
|
37
|
+
super().__init__(controller, name)
|
|
47
38
|
|
|
48
39
|
@AsyncStatus.wrap
|
|
49
40
|
async def trigger(self) -> None:
|
|
50
41
|
# Configure region parameters on the driver first before data collection.
|
|
51
|
-
await self.driver.set(self.region)
|
|
42
|
+
await self._controller.driver.set(self.region)
|
|
52
43
|
await super().trigger()
|
|
53
44
|
|
|
54
45
|
|
|
@@ -59,7 +50,7 @@ TElectronAnalyserRegionDetector = TypeVar(
|
|
|
59
50
|
|
|
60
51
|
|
|
61
52
|
class ElectronAnalyserDetector(
|
|
62
|
-
|
|
53
|
+
BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO],
|
|
63
54
|
Stageable,
|
|
64
55
|
Generic[
|
|
65
56
|
TAbstractAnalyserDriverIO,
|
|
@@ -79,16 +70,11 @@ class ElectronAnalyserDetector(
|
|
|
79
70
|
driver: TAbstractAnalyserDriverIO,
|
|
80
71
|
name: str = "",
|
|
81
72
|
):
|
|
82
|
-
#
|
|
83
|
-
self.
|
|
73
|
+
# Save driver as direct child so participates with connect()
|
|
74
|
+
self.driver = driver
|
|
84
75
|
self._sequence_class = sequence_class
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def driver(self) -> TAbstractAnalyserDriverIO:
|
|
89
|
-
# This implementation creates the driver and wants this to be the parent so it
|
|
90
|
-
# can be used with connect() method.
|
|
91
|
-
return self._driver
|
|
76
|
+
controller = ConstantDeadTimeController[TAbstractAnalyserDriverIO](driver, 0)
|
|
77
|
+
super().__init__(controller, name)
|
|
92
78
|
|
|
93
79
|
@AsyncStatus.wrap
|
|
94
80
|
async def stage(self) -> None:
|
|
@@ -103,13 +89,13 @@ class ElectronAnalyserDetector(
|
|
|
103
89
|
Raises:
|
|
104
90
|
Any exceptions raised by the driver's stage or controller's disarm methods.
|
|
105
91
|
"""
|
|
106
|
-
await self.
|
|
92
|
+
await self._controller.disarm()
|
|
107
93
|
await self.driver.stage()
|
|
108
94
|
|
|
109
95
|
@AsyncStatus.wrap
|
|
110
96
|
async def unstage(self) -> None:
|
|
111
97
|
"""Disarm the detector."""
|
|
112
|
-
await self.
|
|
98
|
+
await self._controller.disarm()
|
|
113
99
|
await self.driver.unstage()
|
|
114
100
|
|
|
115
101
|
def load_sequence(self, filename: str) -> TAbstractBaseSequence:
|
|
@@ -144,7 +130,9 @@ class ElectronAnalyserDetector(
|
|
|
144
130
|
seq = self.load_sequence(filename)
|
|
145
131
|
regions = seq.get_enabled_regions() if enabled_only else seq.regions
|
|
146
132
|
return [
|
|
147
|
-
ElectronAnalyserRegionDetector(
|
|
133
|
+
ElectronAnalyserRegionDetector(
|
|
134
|
+
self._controller, r, self.name + "_" + r.name
|
|
135
|
+
)
|
|
148
136
|
for r in regions
|
|
149
137
|
]
|
|
150
138
|
|
|
@@ -66,7 +66,6 @@ class SpecsAnalyserDriverIO(
|
|
|
66
66
|
async def _set_region(self, ke_region: SpecsRegion[TLensMode, TPsuMode]):
|
|
67
67
|
await asyncio.gather(
|
|
68
68
|
self.region_name.set(ke_region.name),
|
|
69
|
-
self.energy_mode.set(ke_region.energy_mode),
|
|
70
69
|
self.low_energy.set(ke_region.low_energy),
|
|
71
70
|
self.high_energy.set(ke_region.high_energy),
|
|
72
71
|
self.slices.set(ke_region.slices),
|
|
@@ -74,7 +74,6 @@ class VGScientaAnalyserDriverIO(
|
|
|
74
74
|
async def _set_region(self, ke_region: VGScientaRegion[TLensMode, TPassEnergyEnum]):
|
|
75
75
|
await asyncio.gather(
|
|
76
76
|
self.region_name.set(ke_region.name),
|
|
77
|
-
self.energy_mode.set(ke_region.energy_mode),
|
|
78
77
|
self.low_energy.set(ke_region.low_energy),
|
|
79
78
|
self.centre_energy.set(ke_region.centre_energy),
|
|
80
79
|
self.high_energy.set(ke_region.high_energy),
|
dodal/devices/fast_grid_scan.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from abc import ABC, abstractmethod
|
|
2
3
|
from typing import Generic, TypeVar
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
5
|
-
from bluesky.plan_stubs import
|
|
6
|
-
from bluesky.protocols import Flyable
|
|
6
|
+
from bluesky.plan_stubs import prepare
|
|
7
|
+
from bluesky.protocols import Flyable, Preparable
|
|
7
8
|
from numpy import ndarray
|
|
8
9
|
from ophyd_async.core import (
|
|
9
10
|
AsyncStatus,
|
|
@@ -13,6 +14,7 @@ from ophyd_async.core import (
|
|
|
13
14
|
SignalRW,
|
|
14
15
|
StandardReadable,
|
|
15
16
|
derived_signal_r,
|
|
17
|
+
set_and_wait_for_value,
|
|
16
18
|
soft_signal_r_and_setter,
|
|
17
19
|
wait_for_value,
|
|
18
20
|
)
|
|
@@ -29,6 +31,10 @@ from dodal.log import LOGGER
|
|
|
29
31
|
from dodal.parameters.experiment_parameter_base import AbstractExperimentWithBeamParams
|
|
30
32
|
|
|
31
33
|
|
|
34
|
+
class GridScanInvalidException(RuntimeError):
|
|
35
|
+
"""Raised when the gridscan parameters are not valid."""
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
@dataclass
|
|
33
39
|
class GridAxis:
|
|
34
40
|
start: float
|
|
@@ -144,7 +150,7 @@ class GridScanParamsThreeD(GridScanParamsCommon):
|
|
|
144
150
|
return GridAxis(self.z2_start_mm, self.z_step_size_mm, self.z_steps)
|
|
145
151
|
|
|
146
152
|
|
|
147
|
-
ParamType = TypeVar("ParamType", bound=GridScanParamsCommon
|
|
153
|
+
ParamType = TypeVar("ParamType", bound=GridScanParamsCommon)
|
|
148
154
|
|
|
149
155
|
|
|
150
156
|
class WithDwellTime(BaseModel):
|
|
@@ -190,7 +196,9 @@ class MotionProgram(Device):
|
|
|
190
196
|
self.program_number = soft_signal_r_and_setter(float, -1)[0]
|
|
191
197
|
|
|
192
198
|
|
|
193
|
-
class FastGridScanCommon(
|
|
199
|
+
class FastGridScanCommon(
|
|
200
|
+
StandardReadable, Flyable, ABC, Preparable, Generic[ParamType]
|
|
201
|
+
):
|
|
194
202
|
"""Device containing the minimal signals for a general fast grid scan.
|
|
195
203
|
|
|
196
204
|
When the motion program is started, the goniometer will move in a snake-like grid trajectory,
|
|
@@ -231,8 +239,9 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
|
231
239
|
self.KICKOFF_TIMEOUT: float = 5.0
|
|
232
240
|
|
|
233
241
|
self.COMPLETE_STATUS: float = 60.0
|
|
242
|
+
self.VALIDITY_CHECK_TIMEOUT = 0.5
|
|
234
243
|
|
|
235
|
-
self.
|
|
244
|
+
self._movable_params: dict[str, Signal] = {
|
|
236
245
|
"x_steps": self.x_steps,
|
|
237
246
|
"y_steps": self.y_steps,
|
|
238
247
|
"x_step_size_mm": self.x_step_size,
|
|
@@ -284,6 +293,45 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
|
284
293
|
self, motion_controller_prefix: str
|
|
285
294
|
) -> MotionProgram: ...
|
|
286
295
|
|
|
296
|
+
@AsyncStatus.wrap
|
|
297
|
+
async def prepare(self, value: ParamType):
|
|
298
|
+
"""
|
|
299
|
+
Submit the gridscan parameters to the device for validation prior to
|
|
300
|
+
gridscan kickoff
|
|
301
|
+
Args:
|
|
302
|
+
value: the gridscan parameters
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
GridScanInvalidException: if the gridscan parameters were not valid
|
|
306
|
+
"""
|
|
307
|
+
set_statuses = []
|
|
308
|
+
|
|
309
|
+
LOGGER.info("Applying gridscan parameters...")
|
|
310
|
+
# Create arguments for bps.mv
|
|
311
|
+
for key, signal in self._movable_params.items():
|
|
312
|
+
param_value = value.__dict__[key]
|
|
313
|
+
set_statuses.append(await set_and_wait_for_value(signal, param_value)) # type: ignore
|
|
314
|
+
|
|
315
|
+
# Counter should always start at 0
|
|
316
|
+
set_statuses.append(await set_and_wait_for_value(self.position_counter, 0))
|
|
317
|
+
|
|
318
|
+
LOGGER.info("Gridscan parameters applied, waiting for sets to complete...")
|
|
319
|
+
|
|
320
|
+
# wait for parameter sets to complete
|
|
321
|
+
await asyncio.gather(*set_statuses)
|
|
322
|
+
|
|
323
|
+
LOGGER.info("Sets confirmed, waiting for validity checks to pass...")
|
|
324
|
+
try:
|
|
325
|
+
await wait_for_value(
|
|
326
|
+
self.scan_invalid, 0.0, timeout=self.VALIDITY_CHECK_TIMEOUT
|
|
327
|
+
)
|
|
328
|
+
except TimeoutError as e:
|
|
329
|
+
raise GridScanInvalidException(
|
|
330
|
+
f"Gridscan parameters not validated after {self.VALIDITY_CHECK_TIMEOUT}s"
|
|
331
|
+
) from e
|
|
332
|
+
|
|
333
|
+
LOGGER.info("Gridscan validity confirmed, gridscan is now prepared.")
|
|
334
|
+
|
|
287
335
|
|
|
288
336
|
class FastGridScanThreeD(FastGridScanCommon[ParamType]):
|
|
289
337
|
"""Device for standard 3D FGS.
|
|
@@ -296,23 +344,23 @@ class FastGridScanThreeD(FastGridScanCommon[ParamType]):
|
|
|
296
344
|
Subclasses must implement _create_position_counter.
|
|
297
345
|
"""
|
|
298
346
|
|
|
299
|
-
def __init__(self, prefix: str, name: str = "") -> None:
|
|
300
|
-
full_prefix = prefix +
|
|
347
|
+
def __init__(self, prefix: str, infix: str, name: str = "") -> None:
|
|
348
|
+
full_prefix = prefix + infix
|
|
301
349
|
|
|
302
350
|
# Number of vertical steps during the second grid scan, after the rotation in omega
|
|
303
|
-
self.z_steps = epics_signal_rw_rbv(int, f"{
|
|
304
|
-
self.z_step_size = epics_signal_rw_rbv(float, f"{
|
|
305
|
-
self.z2_start = epics_signal_rw_rbv(float, f"{
|
|
306
|
-
self.y2_start = epics_signal_rw_rbv(float, f"{
|
|
307
|
-
|
|
351
|
+
self.z_steps = epics_signal_rw_rbv(int, f"{full_prefix}Z_NUM_STEPS")
|
|
352
|
+
self.z_step_size = epics_signal_rw_rbv(float, f"{full_prefix}Z_STEP_SIZE")
|
|
353
|
+
self.z2_start = epics_signal_rw_rbv(float, f"{full_prefix}Z2_START")
|
|
354
|
+
self.y2_start = epics_signal_rw_rbv(float, f"{full_prefix}Y2_START")
|
|
355
|
+
# panda does not have x counter
|
|
308
356
|
self.y_counter = epics_signal_r(int, f"{full_prefix}Y_COUNTER")
|
|
309
357
|
|
|
310
358
|
super().__init__(full_prefix, prefix, name)
|
|
311
359
|
|
|
312
|
-
self.
|
|
313
|
-
self.
|
|
314
|
-
self.
|
|
315
|
-
self.
|
|
360
|
+
self._movable_params["z_step_size_mm"] = self.z_step_size
|
|
361
|
+
self._movable_params["z2_start_mm"] = self.z2_start
|
|
362
|
+
self._movable_params["y2_start_mm"] = self.y2_start
|
|
363
|
+
self._movable_params["z_steps"] = self.z_steps
|
|
316
364
|
|
|
317
365
|
def _create_expected_images_signal(self):
|
|
318
366
|
return derived_signal_r(
|
|
@@ -329,7 +377,35 @@ class FastGridScanThreeD(FastGridScanCommon[ParamType]):
|
|
|
329
377
|
return first_grid + second_grid
|
|
330
378
|
|
|
331
379
|
def _create_scan_invalid_signal(self, prefix: str) -> SignalR[float]:
|
|
332
|
-
|
|
380
|
+
self.x_scan_valid = epics_signal_r(float, f"{prefix}X_SCAN_VALID")
|
|
381
|
+
self.y_scan_valid = epics_signal_r(float, f"{prefix}Y_SCAN_VALID")
|
|
382
|
+
self.z_scan_valid = epics_signal_r(float, f"{prefix}Z_SCAN_VALID")
|
|
383
|
+
self.device_scan_invalid = epics_signal_r(float, f"{prefix}SCAN_INVALID")
|
|
384
|
+
|
|
385
|
+
def compute_derived_value(
|
|
386
|
+
x_scan_valid: float,
|
|
387
|
+
y_scan_valid: float,
|
|
388
|
+
z_scan_valid: float,
|
|
389
|
+
device_scan_invalid: float,
|
|
390
|
+
) -> float:
|
|
391
|
+
return (
|
|
392
|
+
1.0
|
|
393
|
+
if not (
|
|
394
|
+
x_scan_valid
|
|
395
|
+
and y_scan_valid
|
|
396
|
+
and z_scan_valid
|
|
397
|
+
and not device_scan_invalid
|
|
398
|
+
)
|
|
399
|
+
else 0.0
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return derived_signal_r(
|
|
403
|
+
compute_derived_value,
|
|
404
|
+
x_scan_valid=self.x_scan_valid,
|
|
405
|
+
y_scan_valid=self.y_scan_valid,
|
|
406
|
+
z_scan_valid=self.z_scan_valid,
|
|
407
|
+
device_scan_invalid=self.device_scan_invalid,
|
|
408
|
+
)
|
|
333
409
|
|
|
334
410
|
def _create_motion_program(self, motion_controller_prefix: str):
|
|
335
411
|
return MotionProgram(motion_controller_prefix)
|
|
@@ -343,11 +419,13 @@ class ZebraFastGridScanThreeD(FastGridScanThreeD[ZebraGridScanParamsThreeD]):
|
|
|
343
419
|
"""
|
|
344
420
|
|
|
345
421
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
346
|
-
|
|
422
|
+
infix = "FGS:"
|
|
423
|
+
full_prefix = prefix + infix
|
|
347
424
|
# Time taken to travel between X steps
|
|
348
425
|
self.dwell_time_ms = epics_signal_rw_rbv(float, f"{full_prefix}DWELL_TIME")
|
|
349
|
-
|
|
350
|
-
|
|
426
|
+
self.x_counter = epics_signal_r(int, f"{full_prefix}X_COUNTER")
|
|
427
|
+
super().__init__(prefix, infix, name)
|
|
428
|
+
self._movable_params["dwell_time_ms"] = self.dwell_time_ms
|
|
351
429
|
|
|
352
430
|
def _create_position_counter(self, prefix: str):
|
|
353
431
|
return epics_signal_rw(
|
|
@@ -363,7 +441,8 @@ class PandAFastGridScan(FastGridScanThreeD[PandAGridScanParams]):
|
|
|
363
441
|
"""
|
|
364
442
|
|
|
365
443
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
366
|
-
|
|
444
|
+
infix = "PGS:"
|
|
445
|
+
full_prefix = prefix + infix
|
|
367
446
|
self.time_between_x_steps_ms = (
|
|
368
447
|
epics_signal_rw_rbv( # Used by motion controller to set goniometer velocity
|
|
369
448
|
float, f"{full_prefix}TIME_BETWEEN_X_STEPS"
|
|
@@ -375,22 +454,22 @@ class PandAFastGridScan(FastGridScanThreeD[PandAGridScanParams]):
|
|
|
375
454
|
self.run_up_distance_mm = epics_signal_rw_rbv(
|
|
376
455
|
float, f"{full_prefix}RUNUP_DISTANCE"
|
|
377
456
|
)
|
|
378
|
-
super().__init__(prefix, name)
|
|
457
|
+
super().__init__(prefix, infix, name)
|
|
379
458
|
|
|
380
|
-
self.
|
|
459
|
+
self._movable_params["run_up_distance_mm"] = self.run_up_distance_mm
|
|
381
460
|
|
|
382
461
|
def _create_position_counter(self, prefix: str):
|
|
383
462
|
return epics_signal_rw(int, f"{prefix}Y_COUNTER")
|
|
384
463
|
|
|
385
464
|
|
|
386
465
|
def set_fast_grid_scan_params(scan: FastGridScanCommon[ParamType], params: ParamType):
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
# Counter should always start at 0
|
|
394
|
-
to_move.extend([scan.position_counter, 0])
|
|
466
|
+
"""
|
|
467
|
+
Apply the fast grid scan parameters to the grid scan device and validate them
|
|
468
|
+
Args:
|
|
469
|
+
scan: The fast grid scan device
|
|
470
|
+
params: The parameters to set
|
|
395
471
|
|
|
396
|
-
|
|
472
|
+
Raises:
|
|
473
|
+
GridScanInvalidException: if the grid scan parameters are not valid
|
|
474
|
+
"""
|
|
475
|
+
yield from prepare(scan, params, wait=True)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
from bluesky.protocols import Movable
|
|
4
|
+
from ophyd_async.core import (
|
|
5
|
+
AsyncStatus,
|
|
6
|
+
EnumTypes,
|
|
7
|
+
StandardReadable,
|
|
8
|
+
)
|
|
9
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
10
|
+
|
|
11
|
+
StrictEnumT = TypeVar("StrictEnumT", bound=EnumTypes)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
|
|
15
|
+
"""
|
|
16
|
+
Basic enum device specialised for a fast shutter with configured open_state and
|
|
17
|
+
close_state so it is generic enough to be used with any device or plan without
|
|
18
|
+
knowing the specific enum to use.
|
|
19
|
+
|
|
20
|
+
For example:
|
|
21
|
+
await shutter.set(shutter.open_state)
|
|
22
|
+
await shutter.set(shutter.close_state)
|
|
23
|
+
OR
|
|
24
|
+
RE(bps.mv(shutter, shutter.open_state))
|
|
25
|
+
RE(bps.mv(shutter, shutter.close_state))
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
prefix: str,
|
|
31
|
+
open_state: StrictEnumT,
|
|
32
|
+
close_state: StrictEnumT,
|
|
33
|
+
name: str = "",
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Arguments:
|
|
37
|
+
prefix: The prefix for the shutter device.
|
|
38
|
+
open_state: The enum value that corresponds with opening the shutter.
|
|
39
|
+
close_state: The enum value that corresponds with closing the shutter.
|
|
40
|
+
"""
|
|
41
|
+
self.open_state = open_state
|
|
42
|
+
self.close_state = close_state
|
|
43
|
+
with self.add_children_as_readables():
|
|
44
|
+
self.state = epics_signal_rw(type(self.open_state), prefix)
|
|
45
|
+
super().__init__(name)
|
|
46
|
+
|
|
47
|
+
@AsyncStatus.wrap
|
|
48
|
+
async def set(self, value: StrictEnumT) -> None:
|
|
49
|
+
await self.state.set(value)
|
|
50
|
+
|
|
51
|
+
async def is_open(self) -> bool:
|
|
52
|
+
"""Checks to see if shutter is currently open"""
|
|
53
|
+
return await self.state.get_value() == self.open_state
|
|
54
|
+
|
|
55
|
+
async def is_closed(self) -> bool:
|
|
56
|
+
"""Checks to see if shutter is currently closed"""
|
|
57
|
+
return await self.state.get_value() == self.close_state
|
|
@@ -35,7 +35,7 @@ class ZebraFastGridScanTwoD(FastGridScanCommon[ZebraGridScanParamsTwoD]):
|
|
|
35
35
|
# See https://github.com/DiamondLightSource/mx-bluesky/issues/1203
|
|
36
36
|
self.dwell_time_ms = epics_signal_rw_rbv(float, f"{full_prefix}EXPOSURE_TIME")
|
|
37
37
|
|
|
38
|
-
self.
|
|
38
|
+
self._movable_params["dwell_time_ms"] = self.dwell_time_ms
|
|
39
39
|
|
|
40
40
|
def _create_expected_images_signal(self):
|
|
41
41
|
return derived_signal_r(
|
|
@@ -32,6 +32,7 @@ class MurkoMetadata(TypedDict):
|
|
|
32
32
|
sample_id: str
|
|
33
33
|
omega_angle: float
|
|
34
34
|
uuid: str
|
|
35
|
+
used_for_centring: bool | None
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class Coord(Enum):
|
|
@@ -47,6 +48,7 @@ class MurkoResult:
|
|
|
47
48
|
y_dist_mm: float
|
|
48
49
|
omega: float
|
|
49
50
|
uuid: str
|
|
51
|
+
metadata: MurkoMetadata
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
class NoResultsFound(ValueError):
|
|
@@ -101,7 +103,7 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
101
103
|
|
|
102
104
|
def _reset(self):
|
|
103
105
|
self._last_omega = 0
|
|
104
|
-
self.
|
|
106
|
+
self._results: list[MurkoResult] = []
|
|
105
107
|
|
|
106
108
|
@AsyncStatus.wrap
|
|
107
109
|
async def stage(self):
|
|
@@ -126,17 +128,17 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
126
128
|
continue
|
|
127
129
|
await self.process_batch(message, sample_id)
|
|
128
130
|
|
|
129
|
-
if not self.
|
|
131
|
+
if not self._results:
|
|
130
132
|
raise NoResultsFound("No results retrieved from Murko")
|
|
131
133
|
|
|
132
|
-
for result in self.
|
|
134
|
+
for result in self._results:
|
|
133
135
|
LOGGER.debug(result)
|
|
134
136
|
|
|
135
|
-
self.filter_outliers()
|
|
137
|
+
filtered_results = self.filter_outliers()
|
|
136
138
|
|
|
137
|
-
x_dists_mm = [result.x_dist_mm for result in
|
|
138
|
-
y_dists_mm = [result.y_dist_mm for result in
|
|
139
|
-
omegas = [result.omega for result in
|
|
139
|
+
x_dists_mm = [result.x_dist_mm for result in filtered_results]
|
|
140
|
+
y_dists_mm = [result.y_dist_mm for result in filtered_results]
|
|
141
|
+
omegas = [result.omega for result in filtered_results]
|
|
140
142
|
|
|
141
143
|
LOGGER.info(f"Using average of x beam distances: {x_dists_mm}")
|
|
142
144
|
avg_x = float(np.mean(x_dists_mm))
|
|
@@ -147,6 +149,11 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
147
149
|
self._y_mm_setter(-best_y)
|
|
148
150
|
self._z_mm_setter(-best_z)
|
|
149
151
|
|
|
152
|
+
for result in self._results:
|
|
153
|
+
await self.redis_client.hset( # type: ignore
|
|
154
|
+
f"murko:{sample_id}:metadata", result.uuid, json.dumps(result.metadata)
|
|
155
|
+
)
|
|
156
|
+
|
|
150
157
|
async def process_batch(self, message: dict | None, sample_id: str):
|
|
151
158
|
if message and message["type"] == "message":
|
|
152
159
|
batch_results: list[dict] = pickle.loads(message["data"])
|
|
@@ -186,13 +193,14 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
186
193
|
centre_px[0],
|
|
187
194
|
centre_px[1],
|
|
188
195
|
)
|
|
189
|
-
self.
|
|
196
|
+
self._results.append(
|
|
190
197
|
MurkoResult(
|
|
191
198
|
centre_px=centre_px,
|
|
192
199
|
x_dist_mm=beam_dist_px[0] * metadata["microns_per_x_pixel"] / 1000,
|
|
193
200
|
y_dist_mm=beam_dist_px[1] * metadata["microns_per_y_pixel"] / 1000,
|
|
194
201
|
omega=omega,
|
|
195
202
|
uuid=metadata["uuid"],
|
|
203
|
+
metadata=metadata,
|
|
196
204
|
)
|
|
197
205
|
)
|
|
198
206
|
self._last_omega = omega
|
|
@@ -203,8 +211,8 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
203
211
|
meaning that by keeping only a percentage of the results with the smallest X we
|
|
204
212
|
remove many of the outliers.
|
|
205
213
|
"""
|
|
206
|
-
LOGGER.info(f"Number of results before filtering: {len(self.
|
|
207
|
-
sorted_results = sorted(self.
|
|
214
|
+
LOGGER.info(f"Number of results before filtering: {len(self._results)}")
|
|
215
|
+
sorted_results = sorted(self._results, key=lambda item: item.centre_px[0])
|
|
208
216
|
|
|
209
217
|
worst_results = [
|
|
210
218
|
r.uuid for r in sorted_results[-self.NUMBER_OF_WRONG_RESULTS_TO_LOG :]
|
|
@@ -214,9 +222,13 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
|
|
|
214
222
|
f"Worst {self.NUMBER_OF_WRONG_RESULTS_TO_LOG} murko results were {worst_results}"
|
|
215
223
|
)
|
|
216
224
|
cutoff = max(1, int(len(sorted_results) * self.PERCENTAGE_TO_USE / 100))
|
|
225
|
+
for i, result in enumerate(sorted_results):
|
|
226
|
+
result.metadata["used_for_centring"] = i < cutoff
|
|
227
|
+
|
|
217
228
|
smallest_x = sorted_results[:cutoff]
|
|
218
|
-
|
|
219
|
-
LOGGER.info(f"Number of results after filtering: {len(
|
|
229
|
+
|
|
230
|
+
LOGGER.info(f"Number of results after filtering: {len(smallest_x)}")
|
|
231
|
+
return smallest_x
|
|
220
232
|
|
|
221
233
|
|
|
222
234
|
def get_yz_least_squares(vertical_dists: list, omegas: list) -> tuple[float, float]:
|
dodal/devices/i10/i10_apple2.py
CHANGED
|
@@ -19,10 +19,9 @@ from ophyd_async.core import (
|
|
|
19
19
|
)
|
|
20
20
|
from pydantic import BaseModel, ConfigDict, RootModel
|
|
21
21
|
|
|
22
|
-
from dodal.
|
|
23
|
-
|
|
24
|
-
from ..apple2_undulator import (
|
|
22
|
+
from dodal.devices.apple2_undulator import (
|
|
25
23
|
Apple2,
|
|
24
|
+
Apple2Motors,
|
|
26
25
|
Apple2Val,
|
|
27
26
|
EnergyMotorConvertor,
|
|
28
27
|
Pol,
|
|
@@ -30,6 +29,8 @@ from ..apple2_undulator import (
|
|
|
30
29
|
UndulatorJawPhase,
|
|
31
30
|
UndulatorPhaseAxes,
|
|
32
31
|
)
|
|
32
|
+
from dodal.log import LOGGER
|
|
33
|
+
|
|
33
34
|
from ..pgm import PGM
|
|
34
35
|
|
|
35
36
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
@@ -359,14 +360,15 @@ class I10Apple2(Apple2):
|
|
|
359
360
|
|
|
360
361
|
with self.add_children_as_readables():
|
|
361
362
|
super().__init__(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
363
|
+
apple2_motors=Apple2Motors(
|
|
364
|
+
id_gap=UndulatorGap(prefix=prefix),
|
|
365
|
+
id_phase=UndulatorPhaseAxes(
|
|
366
|
+
prefix=prefix,
|
|
367
|
+
top_outer="RPQ1",
|
|
368
|
+
top_inner="RPQ2",
|
|
369
|
+
btm_inner="RPQ3",
|
|
370
|
+
btm_outer="RPQ4",
|
|
371
|
+
),
|
|
370
372
|
),
|
|
371
373
|
energy_motor_convertor=energy_motor_convertor,
|
|
372
374
|
name=name,
|
|
@@ -376,8 +378,7 @@ class I10Apple2(Apple2):
|
|
|
376
378
|
move_pv="RPQ1",
|
|
377
379
|
)
|
|
378
380
|
|
|
379
|
-
|
|
380
|
-
async def set(self, value: float) -> None:
|
|
381
|
+
async def _set(self, value: float) -> None:
|
|
381
382
|
"""
|
|
382
383
|
Check polarisation state and use it together with the energy(value)
|
|
383
384
|
to calculate the required gap and phases before setting it.
|
|
@@ -408,11 +409,10 @@ class I10Apple2(Apple2):
|
|
|
408
409
|
)
|
|
409
410
|
|
|
410
411
|
LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
|
|
411
|
-
await self.
|
|
412
|
+
await self.motors.set(id_motor_values=id_set_val)
|
|
412
413
|
if pol != Pol.LA:
|
|
413
414
|
await self.id_jaw_phase.set(0)
|
|
414
415
|
await self.id_jaw_phase.set_move.set(1)
|
|
415
|
-
LOGGER.info(f"Energy set to {value} eV successfully.")
|
|
416
416
|
|
|
417
417
|
|
|
418
418
|
class EnergySetter(StandardReadable, Movable[float]):
|