dls-dodal 1.67.0__py3-none-any.whl → 1.69.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.67.0.dist-info → dls_dodal-1.69.0.dist-info}/METADATA +2 -32
- {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/RECORD +79 -71
- dodal/_version.py +2 -2
- dodal/beamlines/adsim.py +30 -23
- dodal/beamlines/b07.py +1 -1
- dodal/beamlines/b07_1.py +1 -1
- dodal/beamlines/i02_1.py +14 -42
- dodal/beamlines/i02_2.py +5 -11
- dodal/beamlines/i03.py +4 -1
- dodal/beamlines/i03_supervisor.py +19 -0
- dodal/beamlines/i04.py +74 -179
- dodal/beamlines/i05.py +9 -1
- dodal/beamlines/i06.py +1 -1
- dodal/beamlines/i06_1.py +24 -0
- dodal/beamlines/i09.py +53 -9
- dodal/beamlines/i09_1.py +9 -1
- dodal/beamlines/i09_2.py +7 -6
- dodal/beamlines/i10_optics.py +1 -1
- dodal/beamlines/i16.py +34 -0
- dodal/beamlines/i17.py +1 -1
- dodal/beamlines/i20_1.py +14 -0
- dodal/beamlines/i21.py +71 -4
- dodal/beamlines/i23.py +19 -25
- dodal/beamlines/i24.py +55 -105
- dodal/beamlines/p60.py +12 -2
- dodal/common/__init__.py +2 -1
- dodal/common/maths.py +80 -0
- dodal/devices/eiger.py +44 -23
- dodal/devices/electron_analyser/__init__.py +0 -33
- dodal/devices/electron_analyser/base/__init__.py +58 -0
- dodal/devices/electron_analyser/base/base_controller.py +84 -0
- dodal/devices/electron_analyser/base/base_detector.py +214 -0
- dodal/devices/electron_analyser/{abstract → base}/base_driver_io.py +23 -42
- dodal/devices/electron_analyser/{enums.py → base/base_enums.py} +0 -5
- dodal/devices/electron_analyser/{abstract → base}/base_region.py +48 -11
- dodal/devices/electron_analyser/{util.py → base/base_util.py} +1 -1
- dodal/devices/electron_analyser/{energy_sources.py → base/energy_sources.py} +27 -26
- dodal/devices/electron_analyser/specs/__init__.py +4 -4
- dodal/devices/electron_analyser/specs/specs_detector.py +47 -0
- dodal/devices/electron_analyser/specs/{driver_io.py → specs_driver_io.py} +23 -26
- dodal/devices/electron_analyser/specs/{region.py → specs_region.py} +4 -3
- dodal/devices/electron_analyser/vgscienta/__init__.py +4 -4
- dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +53 -0
- dodal/devices/electron_analyser/vgscienta/{driver_io.py → vgscienta_driver_io.py} +25 -31
- dodal/devices/electron_analyser/vgscienta/{region.py → vgscienta_region.py} +6 -6
- dodal/devices/fast_shutter.py +108 -25
- dodal/devices/i04/beam_centre.py +84 -0
- dodal/devices/i04/max_pixel.py +4 -17
- dodal/devices/i04/murko_results.py +18 -3
- dodal/devices/i09_2_shared/i09_apple2.py +0 -72
- dodal/devices/i10/i10_apple2.py +7 -7
- dodal/devices/i17/i17_apple2.py +6 -6
- dodal/devices/i21/__init__.py +3 -1
- dodal/devices/i24/commissioning_jungfrau.py +9 -10
- dodal/devices/insertion_device/__init__.py +62 -0
- dodal/devices/insertion_device/apple2_controller.py +380 -0
- dodal/devices/insertion_device/apple2_undulator.py +152 -481
- dodal/devices/insertion_device/energy.py +88 -0
- dodal/devices/insertion_device/energy_motor_lookup.py +1 -1
- dodal/devices/insertion_device/enum.py +17 -0
- dodal/devices/insertion_device/lookup_table_models.py +66 -36
- dodal/devices/insertion_device/polarisation.py +36 -0
- dodal/devices/oav/oav_detector.py +66 -1
- dodal/devices/oav/utils.py +17 -0
- dodal/devices/robot.py +35 -18
- dodal/devices/selectable_source.py +38 -0
- dodal/devices/zebra/zebra.py +15 -0
- dodal/devices/zebra/zebra_constants_mapping.py +1 -0
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +0 -1
- dodal/testing/__init__.py +0 -0
- dodal/testing/electron_analyser/device_factory.py +4 -4
- dodal/testing/fixtures/devices/apple2.py +1 -1
- dodal/testing/fixtures/run_engine.py +4 -0
- dodal/devices/electron_analyser/abstract/__init__.py +0 -25
- dodal/devices/electron_analyser/abstract/base_detector.py +0 -63
- dodal/devices/electron_analyser/abstract/types.py +0 -12
- dodal/devices/electron_analyser/detector.py +0 -143
- dodal/devices/electron_analyser/specs/detector.py +0 -34
- dodal/devices/electron_analyser/types.py +0 -57
- dodal/devices/electron_analyser/vgscienta/detector.py +0 -48
- {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/electron_analyser/specs/{enums.py → specs_enums.py} +0 -0
- /dodal/devices/electron_analyser/vgscienta/{enums.py → vgscienta_enums.py} +0 -0
dodal/common/maths.py
CHANGED
|
@@ -48,3 +48,83 @@ def in_micros(t: float) -> int:
|
|
|
48
48
|
if t < 0:
|
|
49
49
|
raise ValueError(f"Expected a positive time in seconds, got {t!r}")
|
|
50
50
|
return int(np.ceil(t * 1e6))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Rectangle2D:
|
|
54
|
+
"""
|
|
55
|
+
A 2D rectangle defined by two opposite corners.
|
|
56
|
+
|
|
57
|
+
This class represents a rectangle in 2D space using two points: (x1, y1) and (x2, y2).
|
|
58
|
+
It provides methods to query rectangle properties and check point containment.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
x1 (float): The x-coordinate of the first corner.
|
|
62
|
+
y1 (float): The y-coordinate of the first corner.
|
|
63
|
+
x2 (float): The x-coordinate of the second corner.
|
|
64
|
+
y2 (float): The y-coordinate of the second corner.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, x1: float, y1: float, x2: float, y2: float):
|
|
68
|
+
"""
|
|
69
|
+
Initialize a Rectangle2D with two corner points.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
x1 (float): The x-coordinate of the first corner.
|
|
73
|
+
y1 (float): The y-coordinate of the first corner.
|
|
74
|
+
x2 (float): The x-coordinate of the second corner.
|
|
75
|
+
y2 (float): The y-coordinate of the second corner.
|
|
76
|
+
"""
|
|
77
|
+
self.x1, self.y1 = x1, y1
|
|
78
|
+
self.x2, self.y2 = x2, y2
|
|
79
|
+
|
|
80
|
+
def get_max_x(self) -> float:
|
|
81
|
+
"""
|
|
82
|
+
Get the maximum x-coordinate of the rectangle.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
float: The larger of the two x-coordinates (x1, x2).
|
|
86
|
+
"""
|
|
87
|
+
return max(self.x1, self.x2)
|
|
88
|
+
|
|
89
|
+
def get_min_x(self) -> float:
|
|
90
|
+
"""
|
|
91
|
+
Get the minimum x-coordinate of the rectangle.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
float: The smaller of the two x-coordinates (x1, x2).
|
|
95
|
+
"""
|
|
96
|
+
return min(self.x1, self.x2)
|
|
97
|
+
|
|
98
|
+
def get_max_y(self) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Get the maximum y-coordinate of the rectangle.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
float: The larger of the two y-coordinates (y1, y2).
|
|
104
|
+
"""
|
|
105
|
+
return max(self.y1, self.y2)
|
|
106
|
+
|
|
107
|
+
def get_min_y(self) -> float:
|
|
108
|
+
"""
|
|
109
|
+
Get the minimum y-coordinate of the rectangle.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
float: The smaller of the two y-coordinates (y1, y2).
|
|
113
|
+
"""
|
|
114
|
+
return min(self.y1, self.y2)
|
|
115
|
+
|
|
116
|
+
def contains(self, x: float, y: float) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Check if a point is contained within the rectangle.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
x (float): The x-coordinate of the point.
|
|
122
|
+
y (float): The y-coordinate of the point.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
bool: True if the point is within the rectangle bounds, False otherwise.
|
|
126
|
+
"""
|
|
127
|
+
return (
|
|
128
|
+
self.get_min_x() <= x <= self.get_max_x()
|
|
129
|
+
and self.get_min_y() <= y <= self.get_max_y()
|
|
130
|
+
)
|
dodal/devices/eiger.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# type: ignore # Eiger will soon be ophyd-async https://github.com/DiamondLightSource/dodal/issues/700
|
|
2
1
|
from dataclasses import dataclass
|
|
3
2
|
from enum import Enum
|
|
4
3
|
|
|
5
4
|
from bluesky.protocols import Stageable
|
|
6
5
|
from ophyd import Component, Device, EpicsSignalRO, Signal
|
|
7
6
|
from ophyd.areadetector.cam import EigerDetectorCam
|
|
8
|
-
from ophyd.
|
|
7
|
+
from ophyd.signal import AttributeSignal
|
|
8
|
+
from ophyd.status import AndStatus, Status, StatusBase, SubscriptionStatus
|
|
9
9
|
|
|
10
10
|
from dodal.devices.detector import DetectorParams, TriggerMode
|
|
11
11
|
from dodal.devices.eiger_odin import EigerOdin
|
|
@@ -56,7 +56,6 @@ class EigerDetector(Device, Stageable):
|
|
|
56
56
|
|
|
57
57
|
stale_params = Component(EpicsSignalRO, "CAM:StaleParameters_RBV")
|
|
58
58
|
bit_depth = Component(EpicsSignalRO, "CAM:BitDepthImage_RBV")
|
|
59
|
-
|
|
60
59
|
filewriters_finished: StatusBase
|
|
61
60
|
|
|
62
61
|
detector_params: DetectorParams | None = None
|
|
@@ -64,9 +63,24 @@ class EigerDetector(Device, Stageable):
|
|
|
64
63
|
arming_status = Status()
|
|
65
64
|
arming_status.set_finished()
|
|
66
65
|
|
|
67
|
-
def __init__(
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
beamline: str = "i03",
|
|
69
|
+
ispyb_detector_id: int | None = None,
|
|
70
|
+
*args,
|
|
71
|
+
**kwargs,
|
|
72
|
+
):
|
|
68
73
|
super().__init__(*args, **kwargs)
|
|
69
74
|
self.beamline = beamline
|
|
75
|
+
|
|
76
|
+
self.detector_id = ispyb_detector_id
|
|
77
|
+
self.ispyb_detector_id = AttributeSignal(
|
|
78
|
+
attr="detector_id",
|
|
79
|
+
parent=self,
|
|
80
|
+
name="eiger-ispyb_detector_id",
|
|
81
|
+
write_access=False,
|
|
82
|
+
)
|
|
83
|
+
|
|
70
84
|
# using i03 timeouts as default
|
|
71
85
|
self.timeouts = AVAILABLE_TIMEOUTS.get(beamline, AVAILABLE_TIMEOUTS["i03"])
|
|
72
86
|
self.disarming_status = None
|
|
@@ -75,10 +89,11 @@ class EigerDetector(Device, Stageable):
|
|
|
75
89
|
def with_params(
|
|
76
90
|
cls,
|
|
77
91
|
params: DetectorParams,
|
|
78
|
-
name: str = "EigerDetector",
|
|
79
92
|
beamline: str = "i03",
|
|
93
|
+
ispyb_detector_id: int | None = None,
|
|
94
|
+
name: str = "EigerDetector",
|
|
80
95
|
):
|
|
81
|
-
det = cls(name=name, beamline=beamline)
|
|
96
|
+
det = cls(name=name, beamline=beamline, ispyb_detector_id=ispyb_detector_id)
|
|
82
97
|
det.set_detector_parameters(params)
|
|
83
98
|
return det
|
|
84
99
|
|
|
@@ -123,7 +138,7 @@ class EigerDetector(Device, Stageable):
|
|
|
123
138
|
LOGGER.info("Waiting for arming to finish")
|
|
124
139
|
self.arming_status.wait(self.timeouts.arming_timeout)
|
|
125
140
|
|
|
126
|
-
def stage(self):
|
|
141
|
+
def stage(self): # pyright: ignore[reportIncompatibleMethodOverride]
|
|
127
142
|
self.wait_on_arming_if_started()
|
|
128
143
|
if not self.is_armed():
|
|
129
144
|
LOGGER.info("Eiger not armed, arming")
|
|
@@ -132,6 +147,7 @@ class EigerDetector(Device, Stageable):
|
|
|
132
147
|
|
|
133
148
|
def stop_odin_when_all_frames_collected(self):
|
|
134
149
|
LOGGER.info("Waiting on all frames")
|
|
150
|
+
assert self.detector_params
|
|
135
151
|
try:
|
|
136
152
|
await_value(
|
|
137
153
|
self.odin.file_writer.num_captured,
|
|
@@ -141,7 +157,7 @@ class EigerDetector(Device, Stageable):
|
|
|
141
157
|
LOGGER.info("Stopping Odin")
|
|
142
158
|
self.odin.stop().wait(self.timeouts.odin_stop_timeout)
|
|
143
159
|
|
|
144
|
-
def unstage(self) -> bool:
|
|
160
|
+
def unstage(self) -> bool: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
145
161
|
assert self.detector_params is not None
|
|
146
162
|
try:
|
|
147
163
|
self.disarming_status = Status()
|
|
@@ -166,7 +182,7 @@ class EigerDetector(Device, Stageable):
|
|
|
166
182
|
self.disarming_status.set_finished()
|
|
167
183
|
return status_ok
|
|
168
184
|
|
|
169
|
-
def stop(self, *args):
|
|
185
|
+
def stop(self, *args): # pyright: ignore[reportIncompatibleMethodOverride]
|
|
170
186
|
"""Emergency stop the device, mainly used to clean up after error."""
|
|
171
187
|
LOGGER.info("Eiger stop() called - cleaning up...")
|
|
172
188
|
if self.disarming_status and not self.disarming_status.done:
|
|
@@ -241,7 +257,8 @@ class EigerDetector(Device, Stageable):
|
|
|
241
257
|
1, timeout=self.timeouts.general_status_timeout
|
|
242
258
|
)
|
|
243
259
|
status &= self.cam.image_mode.set(
|
|
244
|
-
self.cam.ImageMode.MULTIPLE,
|
|
260
|
+
self.cam.ImageMode.MULTIPLE, # pyright: ignore[reportAttributeAccessIssue]
|
|
261
|
+
timeout=self.timeouts.general_status_timeout,
|
|
245
262
|
)
|
|
246
263
|
status &= self.cam.trigger_mode.set(
|
|
247
264
|
InternalEigerTriggerMode.EXTERNAL_SERIES.value,
|
|
@@ -285,25 +302,24 @@ class EigerDetector(Device, Stageable):
|
|
|
285
302
|
beam_x_pixels, beam_y_pixels = self.detector_params.get_beam_position_pixels(
|
|
286
303
|
self.detector_params.detector_distance
|
|
287
304
|
)
|
|
288
|
-
|
|
305
|
+
self.cam.beam_center_x.set(
|
|
289
306
|
beam_x_pixels, timeout=self.timeouts.general_status_timeout
|
|
290
|
-
)
|
|
291
|
-
|
|
307
|
+
).wait(timeout=self.timeouts.general_status_timeout)
|
|
308
|
+
self.cam.beam_center_y.set(
|
|
292
309
|
beam_y_pixels, timeout=self.timeouts.general_status_timeout
|
|
293
|
-
)
|
|
294
|
-
|
|
310
|
+
).wait(timeout=self.timeouts.general_status_timeout)
|
|
311
|
+
self.cam.det_distance.set(
|
|
295
312
|
self.detector_params.detector_distance,
|
|
296
313
|
timeout=self.timeouts.general_status_timeout,
|
|
297
|
-
)
|
|
298
|
-
|
|
314
|
+
).wait(timeout=self.timeouts.general_status_timeout)
|
|
315
|
+
self.cam.omega_start.set(
|
|
299
316
|
self.detector_params.omega_start,
|
|
300
317
|
timeout=self.timeouts.general_status_timeout,
|
|
301
|
-
)
|
|
302
|
-
status
|
|
318
|
+
).wait(timeout=self.timeouts.general_status_timeout)
|
|
319
|
+
status = self.cam.omega_incr.set(
|
|
303
320
|
self.detector_params.omega_increment,
|
|
304
321
|
timeout=self.timeouts.general_status_timeout,
|
|
305
322
|
)
|
|
306
|
-
|
|
307
323
|
return status
|
|
308
324
|
|
|
309
325
|
def set_detector_threshold(self, energy: float, tolerance: float = 0.1) -> Status:
|
|
@@ -315,7 +331,7 @@ class EigerDetector(Device, Stageable):
|
|
|
315
331
|
this tolerance it is not set again. Defaults to 0.1eV.
|
|
316
332
|
"""
|
|
317
333
|
|
|
318
|
-
current_energy = self.cam.photon_energy.get()
|
|
334
|
+
current_energy = float(self.cam.photon_energy.get())
|
|
319
335
|
if abs(current_energy - energy) > tolerance:
|
|
320
336
|
LOGGER.info(f"Setting detector threshold to {energy}")
|
|
321
337
|
return self.cam.photon_energy.set(
|
|
@@ -398,7 +414,7 @@ class EigerDetector(Device, Stageable):
|
|
|
398
414
|
def disarm_detector(self):
|
|
399
415
|
self.cam.acquire.set(0).wait(self.timeouts.general_status_timeout)
|
|
400
416
|
|
|
401
|
-
def wait_for_stale_params(self) ->
|
|
417
|
+
def wait_for_stale_params(self) -> SubscriptionStatus:
|
|
402
418
|
LOGGER.info("Eiger arming: Waiting for stale params...")
|
|
403
419
|
return await_value(self.stale_params, 0, 60)
|
|
404
420
|
|
|
@@ -412,6 +428,11 @@ class EigerDetector(Device, Stageable):
|
|
|
412
428
|
detector_params: DetectorParams = self.detector_params
|
|
413
429
|
if detector_params.use_roi_mode:
|
|
414
430
|
functions_to_do_arm.append(self.enable_roi_mode)
|
|
431
|
+
threshold_energy = (
|
|
432
|
+
detector_params.expected_energy_ev
|
|
433
|
+
if detector_params.expected_energy_ev
|
|
434
|
+
else float(self.cam.photon_energy.get())
|
|
435
|
+
)
|
|
415
436
|
|
|
416
437
|
arming_sequence_funcs = [
|
|
417
438
|
# If a beam dump occurs after arming the eiger but prior to eiger staging,
|
|
@@ -419,7 +440,7 @@ class EigerDetector(Device, Stageable):
|
|
|
419
440
|
# if this previously completed successfully we must reset the odin first
|
|
420
441
|
self.odin.stop,
|
|
421
442
|
lambda: self.change_dev_shm(detector_params.enable_dev_shm),
|
|
422
|
-
lambda: self.set_detector_threshold(
|
|
443
|
+
lambda: self.set_detector_threshold(threshold_energy),
|
|
423
444
|
self.set_cam_pvs,
|
|
424
445
|
self.set_odin_number_of_frame_chunks,
|
|
425
446
|
self.set_odin_pvs,
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from .detector import (
|
|
2
|
-
ElectronAnalyserDetector,
|
|
3
|
-
ElectronAnalyserRegionDetector,
|
|
4
|
-
TElectronAnalyserDetector,
|
|
5
|
-
TElectronAnalyserRegionDetector,
|
|
6
|
-
)
|
|
7
|
-
from .energy_sources import DualEnergySource, EnergySource
|
|
8
|
-
from .enums import EnergyMode, SelectedSource
|
|
9
|
-
from .types import (
|
|
10
|
-
ElectronAnalyserDetectorImpl,
|
|
11
|
-
ElectronAnalyserDriverImpl,
|
|
12
|
-
GenericElectronAnalyserDetector,
|
|
13
|
-
GenericElectronAnalyserRegionDetector,
|
|
14
|
-
)
|
|
15
|
-
from .util import to_binding_energy, to_kinetic_energy
|
|
16
|
-
|
|
17
|
-
__all__ = [
|
|
18
|
-
"to_binding_energy",
|
|
19
|
-
"to_kinetic_energy",
|
|
20
|
-
"DualEnergySource",
|
|
21
|
-
"SelectedSource",
|
|
22
|
-
"EnergySource",
|
|
23
|
-
"EnergyMode",
|
|
24
|
-
"SelectedSource",
|
|
25
|
-
"ElectronAnalyserDetector",
|
|
26
|
-
"ElectronAnalyserDetectorImpl",
|
|
27
|
-
"ElectronAnalyserDriverImpl",
|
|
28
|
-
"TElectronAnalyserDetector",
|
|
29
|
-
"ElectronAnalyserRegionDetector",
|
|
30
|
-
"TElectronAnalyserRegionDetector",
|
|
31
|
-
"GenericElectronAnalyserDetector",
|
|
32
|
-
"GenericElectronAnalyserRegionDetector",
|
|
33
|
-
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from .base_controller import (
|
|
2
|
+
ElectronAnalyserController,
|
|
3
|
+
GenericElectronAnalyserController,
|
|
4
|
+
)
|
|
5
|
+
from .base_detector import (
|
|
6
|
+
BaseElectronAnalyserDetector,
|
|
7
|
+
ElectronAnalyserDetector,
|
|
8
|
+
ElectronAnalyserRegionDetector,
|
|
9
|
+
GenericBaseElectronAnalyserDetector,
|
|
10
|
+
GenericElectronAnalyserDetector,
|
|
11
|
+
GenericElectronAnalyserRegionDetector,
|
|
12
|
+
)
|
|
13
|
+
from .base_driver_io import (
|
|
14
|
+
AbstractAnalyserDriverIO,
|
|
15
|
+
GenericAnalyserDriverIO,
|
|
16
|
+
TAbstractAnalyserDriverIO,
|
|
17
|
+
)
|
|
18
|
+
from .base_enums import EnergyMode
|
|
19
|
+
from .base_region import (
|
|
20
|
+
AbstractBaseRegion,
|
|
21
|
+
AbstractBaseSequence,
|
|
22
|
+
GenericRegion,
|
|
23
|
+
GenericSequence,
|
|
24
|
+
TAbstractBaseRegion,
|
|
25
|
+
TAbstractBaseSequence,
|
|
26
|
+
TAcquisitionMode,
|
|
27
|
+
TLensMode,
|
|
28
|
+
)
|
|
29
|
+
from .base_util import to_binding_energy, to_kinetic_energy
|
|
30
|
+
from .energy_sources import AbstractEnergySource, DualEnergySource, EnergySource
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"ElectronAnalyserController",
|
|
34
|
+
"GenericElectronAnalyserController",
|
|
35
|
+
"BaseElectronAnalyserDetector",
|
|
36
|
+
"ElectronAnalyserDetector",
|
|
37
|
+
"ElectronAnalyserRegionDetector",
|
|
38
|
+
"GenericBaseElectronAnalyserDetector",
|
|
39
|
+
"GenericElectronAnalyserDetector",
|
|
40
|
+
"GenericElectronAnalyserRegionDetector",
|
|
41
|
+
"AbstractAnalyserDriverIO",
|
|
42
|
+
"GenericAnalyserDriverIO",
|
|
43
|
+
"TAbstractAnalyserDriverIO",
|
|
44
|
+
"EnergyMode",
|
|
45
|
+
"AbstractBaseRegion",
|
|
46
|
+
"AbstractBaseSequence",
|
|
47
|
+
"GenericRegion",
|
|
48
|
+
"GenericSequence",
|
|
49
|
+
"TAbstractBaseRegion",
|
|
50
|
+
"TAbstractBaseSequence",
|
|
51
|
+
"TAcquisitionMode",
|
|
52
|
+
"TLensMode",
|
|
53
|
+
"to_binding_energy",
|
|
54
|
+
"to_kinetic_energy",
|
|
55
|
+
"AbstractEnergySource",
|
|
56
|
+
"DualEnergySource",
|
|
57
|
+
"EnergySource",
|
|
58
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from typing import Generic, TypeVar
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import TriggerInfo
|
|
4
|
+
from ophyd_async.epics.adcore import ADImageMode
|
|
5
|
+
|
|
6
|
+
from dodal.devices.controllers import ConstantDeadTimeController
|
|
7
|
+
from dodal.devices.electron_analyser.base.base_driver_io import (
|
|
8
|
+
GenericAnalyserDriverIO,
|
|
9
|
+
TAbstractAnalyserDriverIO,
|
|
10
|
+
)
|
|
11
|
+
from dodal.devices.electron_analyser.base.base_region import (
|
|
12
|
+
GenericRegion,
|
|
13
|
+
TAbstractBaseRegion,
|
|
14
|
+
)
|
|
15
|
+
from dodal.devices.electron_analyser.base.energy_sources import AbstractEnergySource
|
|
16
|
+
from dodal.devices.fast_shutter import FastShutter
|
|
17
|
+
from dodal.devices.selectable_source import SourceSelector
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ElectronAnalyserController(
|
|
21
|
+
ConstantDeadTimeController[TAbstractAnalyserDriverIO],
|
|
22
|
+
Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Specialised controller for the electron analysers to provide additional setup logic
|
|
26
|
+
such as selecting the energy source to use from requested region and giving the
|
|
27
|
+
driver the correct region parameters.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
driver: TAbstractAnalyserDriverIO,
|
|
33
|
+
energy_source: AbstractEnergySource,
|
|
34
|
+
shutter: FastShutter | None = None,
|
|
35
|
+
source_selector: SourceSelector | None = None,
|
|
36
|
+
deadtime: float = 0,
|
|
37
|
+
image_mode: ADImageMode = ADImageMode.SINGLE,
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Parameters:
|
|
41
|
+
driver: The electron analyser driver to wrap around that holds the PV's.
|
|
42
|
+
energy_source: Device that holds the excitation energy and ability to switch
|
|
43
|
+
between sources.
|
|
44
|
+
deadtime: For a given exposure, what is the safest minimum time between
|
|
45
|
+
exposures that can be determined without reading signals.
|
|
46
|
+
image_mode: The image mode to configure the driver with before measuring.
|
|
47
|
+
"""
|
|
48
|
+
self.energy_source = energy_source
|
|
49
|
+
self.shutter = shutter
|
|
50
|
+
self.source_selector = source_selector
|
|
51
|
+
super().__init__(driver, deadtime, image_mode)
|
|
52
|
+
|
|
53
|
+
async def setup_with_region(self, region: TAbstractBaseRegion) -> None:
|
|
54
|
+
"""Logic to set the driver with a region."""
|
|
55
|
+
if self.source_selector is not None:
|
|
56
|
+
await self.source_selector.set(region.excitation_energy_source)
|
|
57
|
+
|
|
58
|
+
# Should this be moved to a VGScientController only?
|
|
59
|
+
if self.shutter is not None:
|
|
60
|
+
await self.shutter.set(self.shutter.close_state)
|
|
61
|
+
|
|
62
|
+
excitation_energy = await self.energy_source.energy.get_value()
|
|
63
|
+
epics_region = region.prepare_for_epics(excitation_energy)
|
|
64
|
+
await self.driver.set(epics_region)
|
|
65
|
+
|
|
66
|
+
async def prepare(self, trigger_info: TriggerInfo) -> None:
|
|
67
|
+
"""Do all necessary steps to prepare the detector for triggers."""
|
|
68
|
+
# Let the driver know the excitation energy before measuring for binding energy
|
|
69
|
+
# axis calculation.
|
|
70
|
+
excitation_energy = await self.energy_source.energy.get_value()
|
|
71
|
+
await self.driver.cached_excitation_energy.set(excitation_energy)
|
|
72
|
+
|
|
73
|
+
if self.shutter is not None:
|
|
74
|
+
await self.shutter.set(self.shutter.open_state)
|
|
75
|
+
|
|
76
|
+
await super().prepare(trigger_info)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
GenericElectronAnalyserController = ElectronAnalyserController[
|
|
80
|
+
GenericAnalyserDriverIO, GenericRegion
|
|
81
|
+
]
|
|
82
|
+
TElectronAnalyserController = TypeVar(
|
|
83
|
+
"TElectronAnalyserController", bound=ElectronAnalyserController
|
|
84
|
+
)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from typing import Generic, TypeVar
|
|
2
|
+
|
|
3
|
+
from bluesky.protocols import Reading, Stageable, Triggerable
|
|
4
|
+
from event_model import DataKey
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
AsyncConfigurable,
|
|
7
|
+
AsyncReadable,
|
|
8
|
+
AsyncStatus,
|
|
9
|
+
Device,
|
|
10
|
+
TriggerInfo,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from dodal.common.data_util import load_json_file_to_class
|
|
14
|
+
from dodal.devices.electron_analyser.base.base_controller import (
|
|
15
|
+
ElectronAnalyserController,
|
|
16
|
+
)
|
|
17
|
+
from dodal.devices.electron_analyser.base.base_driver_io import (
|
|
18
|
+
GenericAnalyserDriverIO,
|
|
19
|
+
TAbstractAnalyserDriverIO,
|
|
20
|
+
)
|
|
21
|
+
from dodal.devices.electron_analyser.base.base_region import (
|
|
22
|
+
GenericRegion,
|
|
23
|
+
GenericSequence,
|
|
24
|
+
TAbstractBaseRegion,
|
|
25
|
+
TAbstractBaseSequence,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BaseElectronAnalyserDetector(
|
|
30
|
+
Device,
|
|
31
|
+
Triggerable,
|
|
32
|
+
AsyncReadable,
|
|
33
|
+
AsyncConfigurable,
|
|
34
|
+
Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Detector for data acquisition of electron analyser. Can only acquire using settings
|
|
38
|
+
already configured for the device.
|
|
39
|
+
|
|
40
|
+
If possible, this should be changed to inherit from a StandardDetector. Currently,
|
|
41
|
+
StandardDetector forces you to use a file writer which doesn't apply here.
|
|
42
|
+
See issue https://github.com/bluesky/ophyd-async/issues/888
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
controller: ElectronAnalyserController[
|
|
48
|
+
TAbstractAnalyserDriverIO, TAbstractBaseRegion
|
|
49
|
+
],
|
|
50
|
+
name: str = "",
|
|
51
|
+
):
|
|
52
|
+
self._controller = controller
|
|
53
|
+
super().__init__(name)
|
|
54
|
+
|
|
55
|
+
@AsyncStatus.wrap
|
|
56
|
+
async def set(self, region: TAbstractBaseRegion) -> None:
|
|
57
|
+
await self._controller.setup_with_region(region)
|
|
58
|
+
|
|
59
|
+
@AsyncStatus.wrap
|
|
60
|
+
async def trigger(self) -> None:
|
|
61
|
+
await self._controller.prepare(TriggerInfo())
|
|
62
|
+
await self._controller.arm()
|
|
63
|
+
await self._controller.wait_for_idle()
|
|
64
|
+
|
|
65
|
+
async def read(self) -> dict[str, Reading]:
|
|
66
|
+
return await self._controller.driver.read()
|
|
67
|
+
|
|
68
|
+
async def describe(self) -> dict[str, DataKey]:
|
|
69
|
+
data = await self._controller.driver.describe()
|
|
70
|
+
# Correct the shape for image
|
|
71
|
+
prefix = self._controller.driver.name + "-"
|
|
72
|
+
energy_size = len(await self._controller.driver.energy_axis.get_value())
|
|
73
|
+
angle_size = len(await self._controller.driver.angle_axis.get_value())
|
|
74
|
+
data[prefix + "image"]["shape"] = [angle_size, energy_size]
|
|
75
|
+
return data
|
|
76
|
+
|
|
77
|
+
async def read_configuration(self) -> dict[str, Reading]:
|
|
78
|
+
return await self._controller.driver.read_configuration()
|
|
79
|
+
|
|
80
|
+
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
81
|
+
return await self._controller.driver.describe_configuration()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
GenericBaseElectronAnalyserDetector = BaseElectronAnalyserDetector[
|
|
85
|
+
GenericAnalyserDriverIO, GenericRegion
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ElectronAnalyserRegionDetector(
|
|
90
|
+
BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
|
|
91
|
+
Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
Extends electron analyser detector to configure specific region settings before data
|
|
95
|
+
acquisition. It is designed to only exist inside a plan.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
controller: ElectronAnalyserController[
|
|
101
|
+
TAbstractAnalyserDriverIO, TAbstractBaseRegion
|
|
102
|
+
],
|
|
103
|
+
region: TAbstractBaseRegion,
|
|
104
|
+
name: str = "",
|
|
105
|
+
):
|
|
106
|
+
self.region = region
|
|
107
|
+
super().__init__(controller, name)
|
|
108
|
+
|
|
109
|
+
@AsyncStatus.wrap
|
|
110
|
+
async def trigger(self) -> None:
|
|
111
|
+
# Configure region parameters on the driver first before data collection.
|
|
112
|
+
await self.set(self.region)
|
|
113
|
+
await super().trigger()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
GenericElectronAnalyserRegionDetector = ElectronAnalyserRegionDetector[
|
|
117
|
+
GenericAnalyserDriverIO, GenericRegion
|
|
118
|
+
]
|
|
119
|
+
TElectronAnalyserRegionDetector = TypeVar(
|
|
120
|
+
"TElectronAnalyserRegionDetector",
|
|
121
|
+
bound=ElectronAnalyserRegionDetector,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ElectronAnalyserDetector(
|
|
126
|
+
BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
|
|
127
|
+
Stageable,
|
|
128
|
+
Generic[TAbstractBaseSequence, TAbstractAnalyserDriverIO, TAbstractBaseRegion],
|
|
129
|
+
):
|
|
130
|
+
"""
|
|
131
|
+
Electron analyser detector with the additional functionality to load a sequence file
|
|
132
|
+
and create a list of temporary ElectronAnalyserRegionDetector objects. These will
|
|
133
|
+
setup configured region settings before data acquisition.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
sequence_class: type[TAbstractBaseSequence],
|
|
139
|
+
controller: ElectronAnalyserController[
|
|
140
|
+
TAbstractAnalyserDriverIO, TAbstractBaseRegion
|
|
141
|
+
],
|
|
142
|
+
name: str = "",
|
|
143
|
+
):
|
|
144
|
+
self._sequence_class = sequence_class
|
|
145
|
+
super().__init__(controller, name)
|
|
146
|
+
|
|
147
|
+
@AsyncStatus.wrap
|
|
148
|
+
async def stage(self) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Prepare the detector for use by ensuring it is idle and ready.
|
|
151
|
+
|
|
152
|
+
This method asynchronously stages the detector by first disarming the controller
|
|
153
|
+
to ensure the detector is not actively acquiring data, then invokes the driver's
|
|
154
|
+
stage procedure. This ensures the detector is in a known, ready state
|
|
155
|
+
before use.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
Any exceptions raised by the driver's stage or controller's disarm methods.
|
|
159
|
+
"""
|
|
160
|
+
await self._controller.disarm()
|
|
161
|
+
|
|
162
|
+
@AsyncStatus.wrap
|
|
163
|
+
async def unstage(self) -> None:
|
|
164
|
+
"""Disarm the detector."""
|
|
165
|
+
await self._controller.disarm()
|
|
166
|
+
|
|
167
|
+
def load_sequence(self, filename: str) -> TAbstractBaseSequence:
|
|
168
|
+
"""
|
|
169
|
+
Load the sequence data from a provided json file into a sequence class.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
filename: Path to the sequence file containing the region data.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Pydantic model representing the sequence file.
|
|
176
|
+
"""
|
|
177
|
+
return load_json_file_to_class(self._sequence_class, filename)
|
|
178
|
+
|
|
179
|
+
def create_region_detector_list(
|
|
180
|
+
self, filename: str, enabled_only=True
|
|
181
|
+
) -> list[
|
|
182
|
+
ElectronAnalyserRegionDetector[TAbstractAnalyserDriverIO, TAbstractBaseRegion]
|
|
183
|
+
]:
|
|
184
|
+
"""
|
|
185
|
+
Create a list of detectors equal to the number of regions in a sequence file.
|
|
186
|
+
Each detector is responsible for setting up a specific region.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
filename: Path to the sequence file containing the region data.
|
|
190
|
+
enabled_only: If true, only include the region if enabled is True.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
List of ElectronAnalyserRegionDetector, equal to the number of regions in
|
|
194
|
+
the sequence file.
|
|
195
|
+
"""
|
|
196
|
+
seq = self.load_sequence(filename)
|
|
197
|
+
regions: list[TAbstractBaseRegion] = (
|
|
198
|
+
seq.get_enabled_regions() if enabled_only else seq.regions
|
|
199
|
+
)
|
|
200
|
+
return [
|
|
201
|
+
ElectronAnalyserRegionDetector[
|
|
202
|
+
TAbstractAnalyserDriverIO, TAbstractBaseRegion
|
|
203
|
+
](self._controller, r, self.name + "_" + r.name)
|
|
204
|
+
for r in regions
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
GenericElectronAnalyserDetector = ElectronAnalyserDetector[
|
|
209
|
+
GenericSequence, GenericAnalyserDriverIO, GenericRegion
|
|
210
|
+
]
|
|
211
|
+
TElectronAnalyserDetector = TypeVar(
|
|
212
|
+
"TElectronAnalyserDetector",
|
|
213
|
+
bound=ElectronAnalyserDetector,
|
|
214
|
+
)
|