dls-dodal 1.47.0__py3-none-any.whl → 1.49.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.49.0.dist-info}/METADATA +3 -2
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/RECORD +59 -49
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +21 -0
- dodal/beamlines/b01_1.py +1 -1
- dodal/beamlines/b16.py +65 -0
- dodal/beamlines/b18.py +38 -0
- dodal/beamlines/i03.py +21 -6
- dodal/beamlines/i04.py +17 -10
- dodal/beamlines/i10.py +41 -233
- dodal/beamlines/i18.py +1 -1
- dodal/beamlines/i19_1.py +9 -6
- dodal/beamlines/i24.py +5 -5
- dodal/beamlines/k11.py +35 -0
- dodal/common/beamlines/beamline_parameters.py +2 -28
- dodal/common/beamlines/device_helpers.py +1 -0
- dodal/devices/aithre_lasershaping/goniometer.py +36 -2
- dodal/devices/aithre_lasershaping/laser_robot.py +27 -0
- dodal/devices/apple2_undulator.py +257 -136
- dodal/devices/b16/__init__.py +0 -0
- dodal/devices/b16/detector.py +34 -0
- dodal/devices/bimorph_mirror.py +29 -36
- dodal/devices/electron_analyser/__init__.py +21 -1
- dodal/devices/electron_analyser/abstract/__init__.py +0 -6
- dodal/devices/electron_analyser/abstract/base_detector.py +16 -128
- dodal/devices/electron_analyser/abstract/base_driver_io.py +122 -8
- dodal/devices/electron_analyser/abstract/base_region.py +7 -3
- dodal/devices/electron_analyser/detector.py +141 -0
- dodal/devices/electron_analyser/enums.py +6 -0
- dodal/devices/electron_analyser/specs/__init__.py +3 -2
- dodal/devices/electron_analyser/specs/detector.py +6 -22
- dodal/devices/electron_analyser/specs/driver_io.py +27 -3
- dodal/devices/electron_analyser/specs/enums.py +8 -0
- dodal/devices/electron_analyser/specs/region.py +3 -2
- dodal/devices/electron_analyser/types.py +30 -4
- dodal/devices/electron_analyser/util.py +1 -1
- dodal/devices/electron_analyser/vgscienta/__init__.py +3 -2
- dodal/devices/electron_analyser/vgscienta/detector.py +9 -23
- dodal/devices/electron_analyser/vgscienta/driver_io.py +33 -4
- dodal/devices/electron_analyser/vgscienta/enums.py +19 -0
- dodal/devices/electron_analyser/vgscienta/region.py +7 -23
- dodal/devices/fast_grid_scan.py +1 -1
- dodal/devices/i04/murko_results.py +93 -96
- dodal/devices/i10/__init__.py +0 -0
- dodal/devices/i10/i10_apple2.py +181 -126
- dodal/devices/i18/diode.py +37 -4
- dodal/devices/i22/nxsas.py +1 -1
- dodal/devices/mx_phase1/beamstop.py +23 -6
- dodal/devices/oav/oav_detector.py +101 -25
- 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.49.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/top_level.txt +0 -0
dodal/devices/bimorph_mirror.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from collections.abc import Mapping
|
|
3
2
|
from typing import Annotated as A
|
|
4
3
|
|
|
5
4
|
from bluesky.protocols import Movable
|
|
6
5
|
from ophyd_async.core import (
|
|
7
|
-
DEFAULT_TIMEOUT,
|
|
8
6
|
AsyncStatus,
|
|
9
7
|
DeviceVector,
|
|
10
8
|
SignalR,
|
|
@@ -12,6 +10,7 @@ from ophyd_async.core import (
|
|
|
12
10
|
SignalW,
|
|
13
11
|
StandardReadable,
|
|
14
12
|
StrictEnum,
|
|
13
|
+
set_and_wait_for_other_value,
|
|
15
14
|
wait_for_value,
|
|
16
15
|
)
|
|
17
16
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
@@ -23,6 +22,8 @@ from ophyd_async.epics.core import (
|
|
|
23
22
|
epics_signal_x,
|
|
24
23
|
)
|
|
25
24
|
|
|
25
|
+
DEFAULT_TIMEOUT = 60
|
|
26
|
+
|
|
26
27
|
|
|
27
28
|
class BimorphMirrorOnOff(StrictEnum):
|
|
28
29
|
ON = "ON"
|
|
@@ -41,7 +42,7 @@ class BimorphMirrorStatus(StrictEnum):
|
|
|
41
42
|
ERROR = "Error"
|
|
42
43
|
|
|
43
44
|
|
|
44
|
-
class BimorphMirrorChannel(StandardReadable,
|
|
45
|
+
class BimorphMirrorChannel(StandardReadable, EpicsDevice):
|
|
45
46
|
"""Collection of PVs comprising a single bimorph channel.
|
|
46
47
|
|
|
47
48
|
Attributes:
|
|
@@ -56,23 +57,13 @@ class BimorphMirrorChannel(StandardReadable, Movable[float], EpicsDevice):
|
|
|
56
57
|
status: A[SignalR[BimorphMirrorOnOff], PvSuffix("STATUS"), Format.CONFIG_SIGNAL]
|
|
57
58
|
shift: A[SignalW[float], PvSuffix("SHIFT")]
|
|
58
59
|
|
|
59
|
-
@AsyncStatus.wrap
|
|
60
|
-
async def set(self, value: float):
|
|
61
|
-
"""Sets channel's VOUT to given value.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
value: float to set VOUT to
|
|
65
|
-
"""
|
|
66
|
-
await self.output_voltage.set(value)
|
|
67
|
-
|
|
68
60
|
|
|
69
|
-
class BimorphMirror(StandardReadable, Movable[
|
|
61
|
+
class BimorphMirror(StandardReadable, Movable[list[float]]):
|
|
70
62
|
"""Class to represent CAENels Bimorph Mirrors.
|
|
71
63
|
|
|
72
64
|
Attributes:
|
|
73
65
|
channels: DeviceVector of BimorphMirrorChannel, indexed from 1, for each channel
|
|
74
66
|
enabled: Writeable BimorphOnOff
|
|
75
|
-
commit_target_voltages: Procable signal that writes values in each channel's VTRGT to VOUT
|
|
76
67
|
status: Readable BimorphMirrorStatus Busy/Idle status
|
|
77
68
|
err: Alarm status"""
|
|
78
69
|
|
|
@@ -103,49 +94,51 @@ class BimorphMirror(StandardReadable, Movable[Mapping[int, float]]):
|
|
|
103
94
|
super().__init__(name=name)
|
|
104
95
|
|
|
105
96
|
@AsyncStatus.wrap
|
|
106
|
-
async def set(self, value:
|
|
107
|
-
"""Sets bimorph voltages in
|
|
97
|
+
async def set(self, value: list[float]) -> None:
|
|
98
|
+
"""Sets bimorph voltages in parallel via target voltage and all proc.
|
|
108
99
|
|
|
109
100
|
Args:
|
|
110
|
-
value:
|
|
101
|
+
value: List of float target voltages
|
|
111
102
|
|
|
112
103
|
Raises:
|
|
113
104
|
ValueError: On set to non-existent channel"""
|
|
114
105
|
|
|
115
|
-
if
|
|
106
|
+
if len(value) != len(self.channels):
|
|
116
107
|
raise ValueError(
|
|
117
|
-
f"
|
|
108
|
+
f"Length of value input array does not match number of \
|
|
109
|
+
channels: {len(value)} and {len(self.channels)}"
|
|
118
110
|
)
|
|
119
111
|
|
|
120
|
-
# Write target voltages
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
112
|
+
# Write target voltages in serial
|
|
113
|
+
# Voltages are written in serial as bimorph PSU cannot handle simultaneous sets
|
|
114
|
+
for i, target in enumerate(value):
|
|
115
|
+
await wait_for_value(
|
|
116
|
+
self.status, BimorphMirrorStatus.IDLE, timeout=DEFAULT_TIMEOUT
|
|
117
|
+
)
|
|
118
|
+
await set_and_wait_for_other_value(
|
|
119
|
+
self.channels[i + 1].target_voltage,
|
|
120
|
+
target,
|
|
121
|
+
self.status,
|
|
122
|
+
BimorphMirrorStatus.BUSY,
|
|
123
|
+
)
|
|
127
124
|
|
|
128
125
|
# Trigger set target voltages:
|
|
126
|
+
await wait_for_value(
|
|
127
|
+
self.status, BimorphMirrorStatus.IDLE, timeout=DEFAULT_TIMEOUT
|
|
128
|
+
)
|
|
129
129
|
await self.commit_target_voltages.trigger()
|
|
130
130
|
|
|
131
131
|
# Wait for values to propogate to voltage out rbv:
|
|
132
132
|
await asyncio.gather(
|
|
133
133
|
*[
|
|
134
134
|
wait_for_value(
|
|
135
|
-
self.channels[i].output_voltage,
|
|
136
|
-
|
|
135
|
+
self.channels[i + 1].output_voltage,
|
|
136
|
+
target,
|
|
137
137
|
timeout=DEFAULT_TIMEOUT,
|
|
138
138
|
)
|
|
139
|
-
for i, target in value
|
|
139
|
+
for i, target in enumerate(value)
|
|
140
140
|
],
|
|
141
141
|
wait_for_value(
|
|
142
142
|
self.status, BimorphMirrorStatus.IDLE, timeout=DEFAULT_TIMEOUT
|
|
143
143
|
),
|
|
144
144
|
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def tolerance_func_builder(tolerance: float, target_value: float):
|
|
148
|
-
def is_within_value(x):
|
|
149
|
-
return abs(x - target_value) <= tolerance
|
|
150
|
-
|
|
151
|
-
return is_within_value
|
|
@@ -1,8 +1,28 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .detector import (
|
|
2
|
+
ElectronAnalyserDetector,
|
|
3
|
+
ElectronAnalyserRegionDetector,
|
|
4
|
+
TElectronAnalyserDetector,
|
|
5
|
+
TElectronAnalyserRegionDetector,
|
|
6
|
+
)
|
|
7
|
+
from .enums import EnergyMode
|
|
8
|
+
from .types import (
|
|
9
|
+
ElectronAnalyserDetectorImpl,
|
|
10
|
+
ElectronAnalyserDriverImpl,
|
|
11
|
+
GenericElectronAnalyserDetector,
|
|
12
|
+
GenericElectronAnalyserRegionDetector,
|
|
13
|
+
)
|
|
2
14
|
from .util import to_binding_energy, to_kinetic_energy
|
|
3
15
|
|
|
4
16
|
__all__ = [
|
|
5
17
|
"to_binding_energy",
|
|
6
18
|
"to_kinetic_energy",
|
|
7
19
|
"EnergyMode",
|
|
20
|
+
"ElectronAnalyserDetector",
|
|
21
|
+
"ElectronAnalyserDetectorImpl",
|
|
22
|
+
"ElectronAnalyserDriverImpl",
|
|
23
|
+
"TElectronAnalyserDetector",
|
|
24
|
+
"ElectronAnalyserRegionDetector",
|
|
25
|
+
"TElectronAnalyserRegionDetector",
|
|
26
|
+
"GenericElectronAnalyserDetector",
|
|
27
|
+
"GenericElectronAnalyserRegionDetector",
|
|
8
28
|
]
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
from .base_detector import (
|
|
2
2
|
AbstractAnalyserDriverIO,
|
|
3
3
|
AbstractElectronAnalyserDetector,
|
|
4
|
-
AbstractElectronAnalyserRegionDetector,
|
|
5
|
-
TAbstractElectronAnalyserDetector,
|
|
6
|
-
TAbstractElectronAnalyserRegionDetector,
|
|
7
4
|
)
|
|
8
5
|
from .base_driver_io import AbstractAnalyserDriverIO, TAbstractAnalyserDriverIO
|
|
9
6
|
from .base_region import (
|
|
@@ -20,9 +17,6 @@ __all__ = [
|
|
|
20
17
|
"TAbstractBaseSequence",
|
|
21
18
|
"AbstractAnalyserDriverIO",
|
|
22
19
|
"AbstractElectronAnalyserDetector",
|
|
23
|
-
"AbstractElectronAnalyserRegionDetector",
|
|
24
|
-
"TAbstractElectronAnalyserDetector",
|
|
25
|
-
"TAbstractElectronAnalyserRegionDetector",
|
|
26
20
|
"AbstractAnalyserDriverIO",
|
|
27
21
|
"TAbstractAnalyserDriverIO",
|
|
28
22
|
]
|
|
@@ -1,40 +1,31 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from abc import abstractmethod
|
|
3
|
-
from typing import Generic
|
|
3
|
+
from typing import Generic
|
|
4
4
|
|
|
5
|
-
from bluesky.protocols import
|
|
6
|
-
Reading,
|
|
7
|
-
Stageable,
|
|
8
|
-
Triggerable,
|
|
9
|
-
)
|
|
5
|
+
from bluesky.protocols import Reading, Stageable, Triggerable
|
|
10
6
|
from event_model import DataKey
|
|
11
7
|
from ophyd_async.core import (
|
|
8
|
+
AsyncConfigurable,
|
|
9
|
+
AsyncReadable,
|
|
12
10
|
AsyncStatus,
|
|
13
11
|
Device,
|
|
14
|
-
Reference,
|
|
15
12
|
)
|
|
16
|
-
from ophyd_async.core._protocol import AsyncConfigurable, AsyncReadable
|
|
17
13
|
from ophyd_async.epics.adcore import (
|
|
18
14
|
ADBaseController,
|
|
19
15
|
)
|
|
20
16
|
|
|
21
|
-
from dodal.common.data_util import load_json_file_to_class
|
|
22
17
|
from dodal.devices.electron_analyser.abstract.base_driver_io import (
|
|
23
18
|
AbstractAnalyserDriverIO,
|
|
24
19
|
TAbstractAnalyserDriverIO,
|
|
25
20
|
)
|
|
26
|
-
from dodal.devices.electron_analyser.abstract.base_region import (
|
|
27
|
-
TAbstractBaseRegion,
|
|
28
|
-
TAbstractBaseSequence,
|
|
29
|
-
)
|
|
30
21
|
|
|
31
22
|
|
|
32
|
-
class
|
|
23
|
+
class ElectronAnalyserController(ADBaseController[AbstractAnalyserDriverIO]):
|
|
33
24
|
def get_deadtime(self, exposure: float | None) -> float:
|
|
34
25
|
return 0
|
|
35
26
|
|
|
36
27
|
|
|
37
|
-
class
|
|
28
|
+
class AbstractElectronAnalyserDetector(
|
|
38
29
|
Device,
|
|
39
30
|
Stageable,
|
|
40
31
|
Triggerable,
|
|
@@ -46,17 +37,19 @@ class BaseElectronAnalyserDetector(
|
|
|
46
37
|
Detector for data acquisition of electron analyser. Can only acquire using settings
|
|
47
38
|
already configured for the device.
|
|
48
39
|
|
|
49
|
-
If possible, this should be changed to
|
|
40
|
+
If possible, this should be changed to inherit from a StandardDetector. Currently,
|
|
50
41
|
StandardDetector forces you to use a file writer which doesn't apply here.
|
|
51
42
|
See issue https://github.com/bluesky/ophyd-async/issues/888
|
|
52
43
|
"""
|
|
53
44
|
|
|
54
45
|
def __init__(
|
|
55
46
|
self,
|
|
56
|
-
name: str,
|
|
57
47
|
driver: TAbstractAnalyserDriverIO,
|
|
48
|
+
name: str = "",
|
|
58
49
|
):
|
|
59
|
-
self.controller:
|
|
50
|
+
self.controller: ElectronAnalyserController = ElectronAnalyserController(
|
|
51
|
+
driver=driver
|
|
52
|
+
)
|
|
60
53
|
super().__init__(name)
|
|
61
54
|
|
|
62
55
|
@AsyncStatus.wrap
|
|
@@ -96,115 +89,10 @@ class BaseElectronAnalyserDetector(
|
|
|
96
89
|
@abstractmethod
|
|
97
90
|
def driver(self) -> TAbstractAnalyserDriverIO:
|
|
98
91
|
"""
|
|
99
|
-
Define property for
|
|
100
|
-
reference so it doesn't
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
class AbstractElectronAnalyserRegionDetector(
|
|
105
|
-
BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO],
|
|
106
|
-
Stageable,
|
|
107
|
-
Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
|
|
108
|
-
):
|
|
109
|
-
"""
|
|
110
|
-
Extends electron analyser detector to configure specific region settings before data
|
|
111
|
-
acqusition. This object must be passed in a driver and store it as a reference. It
|
|
112
|
-
is designed to only exist inside a plan.
|
|
113
|
-
"""
|
|
92
|
+
Define common property for all implementations to access the driver. Some
|
|
93
|
+
implementations will store this as a reference so it doesn't have conflicting
|
|
94
|
+
parents.
|
|
114
95
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
):
|
|
118
|
-
self._driver_ref = Reference(driver)
|
|
119
|
-
self.region = region
|
|
120
|
-
super().__init__(name, driver)
|
|
121
|
-
|
|
122
|
-
@property
|
|
123
|
-
def driver(self) -> TAbstractAnalyserDriverIO:
|
|
124
|
-
# Store as a reference, this implementation will be given a driver so needs to
|
|
125
|
-
# make sure we don't get conflicting parents.
|
|
126
|
-
return self._driver_ref()
|
|
127
|
-
|
|
128
|
-
@AsyncStatus.wrap
|
|
129
|
-
async def stage(self) -> None:
|
|
130
|
-
super().stage()
|
|
131
|
-
self.configure_region()
|
|
132
|
-
|
|
133
|
-
@abstractmethod
|
|
134
|
-
def configure_region(self):
|
|
135
|
-
"""
|
|
136
|
-
Setup analyser with configured region.
|
|
96
|
+
Returns:
|
|
97
|
+
instance of the driver.
|
|
137
98
|
"""
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
TAbstractElectronAnalyserRegionDetector = TypeVar(
|
|
141
|
-
"TAbstractElectronAnalyserRegionDetector",
|
|
142
|
-
bound=AbstractElectronAnalyserRegionDetector,
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
class AbstractElectronAnalyserDetector(
|
|
147
|
-
BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO],
|
|
148
|
-
Generic[TAbstractAnalyserDriverIO, TAbstractBaseSequence, TAbstractBaseRegion],
|
|
149
|
-
):
|
|
150
|
-
"""
|
|
151
|
-
Electron analyser detector with the additional functionality to load a sequence file
|
|
152
|
-
and create a list of temporary ElectronAnalyserRegionDetector objects. These will
|
|
153
|
-
setup configured region settings before data acquisition.
|
|
154
|
-
"""
|
|
155
|
-
|
|
156
|
-
def __init__(
|
|
157
|
-
self, prefix: str, name: str, sequence_class: type[TAbstractBaseSequence]
|
|
158
|
-
):
|
|
159
|
-
self._driver = self._create_driver(prefix)
|
|
160
|
-
self._sequence_class = sequence_class
|
|
161
|
-
super().__init__(name, self.driver)
|
|
162
|
-
|
|
163
|
-
@property
|
|
164
|
-
def driver(self) -> TAbstractAnalyserDriverIO:
|
|
165
|
-
# This implementation creates the driver and wants this to be the parent so it
|
|
166
|
-
# can be used with connect() method.
|
|
167
|
-
return self._driver
|
|
168
|
-
|
|
169
|
-
def load_sequence(self, filename: str) -> TAbstractBaseSequence:
|
|
170
|
-
return load_json_file_to_class(self._sequence_class, filename)
|
|
171
|
-
|
|
172
|
-
@abstractmethod
|
|
173
|
-
def _create_driver(self, prefix: str) -> TAbstractAnalyserDriverIO:
|
|
174
|
-
"""
|
|
175
|
-
Define implementation of the driver used for this detector.
|
|
176
|
-
"""
|
|
177
|
-
|
|
178
|
-
@abstractmethod
|
|
179
|
-
def _create_region_detector(
|
|
180
|
-
self, driver: TAbstractAnalyserDriverIO, region: TAbstractBaseRegion
|
|
181
|
-
) -> AbstractElectronAnalyserRegionDetector[
|
|
182
|
-
TAbstractAnalyserDriverIO, TAbstractBaseRegion
|
|
183
|
-
]:
|
|
184
|
-
"""
|
|
185
|
-
Define a way to create a temporary detector object that will always setup a
|
|
186
|
-
specific region before acquiring.
|
|
187
|
-
"""
|
|
188
|
-
|
|
189
|
-
def create_region_detector_list(
|
|
190
|
-
self, filename: str
|
|
191
|
-
) -> list[
|
|
192
|
-
AbstractElectronAnalyserRegionDetector[
|
|
193
|
-
TAbstractAnalyserDriverIO, TAbstractBaseRegion
|
|
194
|
-
]
|
|
195
|
-
]:
|
|
196
|
-
"""
|
|
197
|
-
Create a list of detectors that will setup a specific region from the sequence
|
|
198
|
-
file when used.
|
|
199
|
-
"""
|
|
200
|
-
seq = self.load_sequence(filename)
|
|
201
|
-
return [
|
|
202
|
-
self._create_region_detector(self.driver, r)
|
|
203
|
-
for r in seq.get_enabled_regions()
|
|
204
|
-
]
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
TAbstractElectronAnalyserDetector = TypeVar(
|
|
208
|
-
"TAbstractElectronAnalyserDetector",
|
|
209
|
-
bound=AbstractElectronAnalyserDetector,
|
|
210
|
-
)
|
|
@@ -1,29 +1,46 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import TypeVar
|
|
3
|
+
from typing import Generic, TypeVar
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
6
|
+
from bluesky.protocols import Movable, Preparable
|
|
5
7
|
from ophyd_async.core import (
|
|
6
8
|
Array1D,
|
|
9
|
+
AsyncStatus,
|
|
7
10
|
SignalR,
|
|
8
11
|
StandardReadable,
|
|
9
12
|
StandardReadableFormat,
|
|
13
|
+
StrictEnum,
|
|
10
14
|
derived_signal_r,
|
|
11
15
|
soft_signal_rw,
|
|
12
16
|
)
|
|
13
17
|
from ophyd_async.epics.adcore import ADBaseIO
|
|
14
18
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
19
|
+
from ophyd_async.epics.motor import Motor
|
|
15
20
|
|
|
16
|
-
from dodal.devices.electron_analyser.
|
|
17
|
-
|
|
21
|
+
from dodal.devices.electron_analyser.abstract.base_region import (
|
|
22
|
+
TAbstractBaseRegion,
|
|
23
|
+
)
|
|
24
|
+
from dodal.devices.electron_analyser.enums import EnergyMode
|
|
25
|
+
from dodal.devices.electron_analyser.util import to_binding_energy, to_kinetic_energy
|
|
18
26
|
|
|
19
27
|
|
|
20
|
-
class AbstractAnalyserDriverIO(
|
|
28
|
+
class AbstractAnalyserDriverIO(
|
|
29
|
+
ABC,
|
|
30
|
+
StandardReadable,
|
|
31
|
+
ADBaseIO,
|
|
32
|
+
Preparable,
|
|
33
|
+
Movable[TAbstractBaseRegion],
|
|
34
|
+
Generic[TAbstractBaseRegion],
|
|
35
|
+
):
|
|
21
36
|
"""
|
|
22
37
|
Generic device to configure electron analyser with new region settings.
|
|
23
38
|
Electron analysers should inherit from this class for further specialisation.
|
|
24
39
|
"""
|
|
25
40
|
|
|
26
|
-
def __init__(
|
|
41
|
+
def __init__(
|
|
42
|
+
self, prefix: str, acquisition_mode_type: type[StrictEnum], name: str = ""
|
|
43
|
+
) -> None:
|
|
27
44
|
with self.add_children_as_readables():
|
|
28
45
|
self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE")
|
|
29
46
|
self.spectrum = epics_signal_r(Array1D[np.float64], prefix + "INT_SPECTRUM")
|
|
@@ -47,8 +64,12 @@ class AbstractAnalyserDriverIO(ABC, StandardReadable, ADBaseIO):
|
|
|
47
64
|
)
|
|
48
65
|
self.energy_step = epics_signal_rw(float, prefix + "STEP_SIZE")
|
|
49
66
|
self.iterations = epics_signal_rw(int, prefix + "NumExposures")
|
|
50
|
-
self.acquisition_mode = epics_signal_rw(
|
|
67
|
+
self.acquisition_mode = epics_signal_rw(
|
|
68
|
+
acquisition_mode_type, prefix + "ACQ_MODE"
|
|
69
|
+
)
|
|
70
|
+
self.excitation_energy_source = soft_signal_rw(str, initial_value="")
|
|
51
71
|
|
|
72
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
52
73
|
# Read once per scan after data acquired
|
|
53
74
|
self.energy_axis = self._create_energy_axis_signal(prefix)
|
|
54
75
|
self.binding_energy_axis = derived_signal_r(
|
|
@@ -71,16 +92,79 @@ class AbstractAnalyserDriverIO(ABC, StandardReadable, ADBaseIO):
|
|
|
71
92
|
|
|
72
93
|
super().__init__(prefix=prefix, name=name)
|
|
73
94
|
|
|
95
|
+
@AsyncStatus.wrap
|
|
96
|
+
async def prepare(self, value: Motor):
|
|
97
|
+
"""
|
|
98
|
+
Prepare the driver for a region by passing in the energy source motor selected
|
|
99
|
+
by a region.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
value: The motor that contains the information on the current excitation
|
|
103
|
+
energy. Needed to prepare region for epics to accuratly calculate
|
|
104
|
+
kinetic energy for an energy scan when in binding energy mode.
|
|
105
|
+
"""
|
|
106
|
+
energy_source = value
|
|
107
|
+
excitation_energy_value = await energy_source.user_readback.get_value() # eV
|
|
108
|
+
excitation_energy_source_name = energy_source.name
|
|
109
|
+
|
|
110
|
+
await asyncio.gather(
|
|
111
|
+
self.excitation_energy.set(excitation_energy_value),
|
|
112
|
+
self.excitation_energy_source.set(excitation_energy_source_name),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@AsyncStatus.wrap
|
|
116
|
+
async def set(self, region: TAbstractBaseRegion):
|
|
117
|
+
"""
|
|
118
|
+
This should encompass all core region logic which is common to every electron
|
|
119
|
+
analyser for setting up the driver.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
region: Contains the parameters to setup the driver for a scan.
|
|
123
|
+
"""
|
|
124
|
+
pass_energy_type = self.pass_energy_type
|
|
125
|
+
pass_energy = pass_energy_type(region.pass_energy)
|
|
126
|
+
|
|
127
|
+
excitation_energy = await self.excitation_energy.get_value()
|
|
128
|
+
low_energy = to_kinetic_energy(
|
|
129
|
+
region.low_energy, region.energy_mode, excitation_energy
|
|
130
|
+
)
|
|
131
|
+
high_energy = to_kinetic_energy(
|
|
132
|
+
region.high_energy, region.energy_mode, excitation_energy
|
|
133
|
+
)
|
|
134
|
+
await asyncio.gather(
|
|
135
|
+
self.region_name.set(region.name),
|
|
136
|
+
self.energy_mode.set(region.energy_mode),
|
|
137
|
+
self.low_energy.set(low_energy),
|
|
138
|
+
self.high_energy.set(high_energy),
|
|
139
|
+
self.slices.set(region.slices),
|
|
140
|
+
self.lens_mode.set(region.lens_mode),
|
|
141
|
+
self.pass_energy.set(pass_energy),
|
|
142
|
+
self.iterations.set(region.iterations),
|
|
143
|
+
self.acquisition_mode.set(region.acquisition_mode),
|
|
144
|
+
)
|
|
145
|
+
|
|
74
146
|
@abstractmethod
|
|
75
147
|
def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
76
148
|
"""
|
|
77
149
|
The signal that defines the angle axis. Depends on analyser model.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
prefix: PV string used for connecting to angle axis.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Signal that can give us angle axis array data.
|
|
78
156
|
"""
|
|
79
157
|
|
|
80
158
|
@abstractmethod
|
|
81
159
|
def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
82
160
|
"""
|
|
83
161
|
The signal that defines the energy axis. Depends on analyser model.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
prefix: PV string used for connecting to energy axis.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Signal that can give us energy axis array data.
|
|
84
168
|
"""
|
|
85
169
|
|
|
86
170
|
def _calculate_binding_energy_axis(
|
|
@@ -89,6 +173,20 @@ class AbstractAnalyserDriverIO(ABC, StandardReadable, ADBaseIO):
|
|
|
89
173
|
excitation_energy: float,
|
|
90
174
|
energy_mode: EnergyMode,
|
|
91
175
|
) -> Array1D[np.float64]:
|
|
176
|
+
"""
|
|
177
|
+
Calculate the binding energy axis to calibrate the spectra data. Function for a
|
|
178
|
+
derived signal.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
energy_axis: Array data of the original energy_axis from epics.
|
|
182
|
+
excitation_energy: The excitation energy value used for the scan of this
|
|
183
|
+
region.
|
|
184
|
+
energy_mode: The energy_mode of the region that was used for the scan
|
|
185
|
+
of this region.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Array that is the correct axis for the spectra data.
|
|
189
|
+
"""
|
|
92
190
|
is_binding = energy_mode == EnergyMode.BINDING
|
|
93
191
|
return np.array(
|
|
94
192
|
[
|
|
@@ -102,6 +200,18 @@ class AbstractAnalyserDriverIO(ABC, StandardReadable, ADBaseIO):
|
|
|
102
200
|
def _calculate_total_time(
|
|
103
201
|
self, total_steps: int, step_time: float, iterations: int
|
|
104
202
|
) -> float:
|
|
203
|
+
"""
|
|
204
|
+
Calulcate the total time the scan takes for this region. Function for a derived
|
|
205
|
+
signal.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
total_steps: Number of steps for the region.
|
|
209
|
+
step_time: Time for each step for the region.
|
|
210
|
+
iterations: The number of iterations the region collected data for.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Calculated total time in seconds.
|
|
214
|
+
"""
|
|
105
215
|
return total_steps * step_time * iterations
|
|
106
216
|
|
|
107
217
|
def _calculate_total_intensity(self, spectrum: Array1D[np.float64]) -> float:
|
|
@@ -111,8 +221,12 @@ class AbstractAnalyserDriverIO(ABC, StandardReadable, ADBaseIO):
|
|
|
111
221
|
@abstractmethod
|
|
112
222
|
def pass_energy_type(self) -> type:
|
|
113
223
|
"""
|
|
114
|
-
Return the type the pass_energy should be.
|
|
115
|
-
|
|
224
|
+
Return the type the pass_energy should be. Depends on underlying analyser
|
|
225
|
+
software.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Type the pass energy parameter from a region needs to be cast to so it can
|
|
229
|
+
be set correctly on the signal.
|
|
116
230
|
"""
|
|
117
231
|
|
|
118
232
|
|
|
@@ -3,9 +3,12 @@ from abc import ABC
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import Generic, TypeVar
|
|
5
5
|
|
|
6
|
+
from ophyd_async.core import StrictEnum
|
|
6
7
|
from pydantic import BaseModel, Field, model_validator
|
|
7
8
|
|
|
8
|
-
from dodal.devices.electron_analyser.
|
|
9
|
+
from dodal.devices.electron_analyser.enums import EnergyMode
|
|
10
|
+
|
|
11
|
+
TStrictEnum = TypeVar("TStrictEnum", bound=StrictEnum)
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
def java_to_python_case(java_str: str) -> str:
|
|
@@ -43,7 +46,7 @@ def energy_mode_validation(data: dict) -> dict:
|
|
|
43
46
|
return data
|
|
44
47
|
|
|
45
48
|
|
|
46
|
-
class AbstractBaseRegion(ABC, JavaToPythonModel):
|
|
49
|
+
class AbstractBaseRegion(ABC, JavaToPythonModel, Generic[TStrictEnum]):
|
|
47
50
|
"""
|
|
48
51
|
Generic region model that holds the data. Specialised region models should inherit
|
|
49
52
|
this to extend functionality. All energy units are assumed to be in eV.
|
|
@@ -53,10 +56,11 @@ class AbstractBaseRegion(ABC, JavaToPythonModel):
|
|
|
53
56
|
enabled: bool = False
|
|
54
57
|
slices: int = 1
|
|
55
58
|
iterations: int = 1
|
|
59
|
+
excitation_energy_source: str = "source1"
|
|
56
60
|
# These ones we need subclasses to provide default values
|
|
57
61
|
lens_mode: str
|
|
58
62
|
pass_energy: int
|
|
59
|
-
acquisition_mode:
|
|
63
|
+
acquisition_mode: TStrictEnum
|
|
60
64
|
low_energy: float
|
|
61
65
|
high_energy: float
|
|
62
66
|
step_time: float
|