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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .base_detector import (
|
|
2
|
+
AbstractAnalyserDriverIO,
|
|
3
|
+
AbstractElectronAnalyserDetector,
|
|
4
|
+
AbstractElectronAnalyserRegionDetector,
|
|
5
|
+
TAbstractElectronAnalyserDetector,
|
|
6
|
+
TAbstractElectronAnalyserRegionDetector,
|
|
7
|
+
)
|
|
8
|
+
from .base_driver_io import AbstractAnalyserDriverIO, TAbstractAnalyserDriverIO
|
|
9
|
+
from .base_region import (
|
|
10
|
+
AbstractBaseRegion,
|
|
11
|
+
AbstractBaseSequence,
|
|
12
|
+
TAbstractBaseRegion,
|
|
13
|
+
TAbstractBaseSequence,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AbstractBaseRegion",
|
|
18
|
+
"AbstractBaseSequence",
|
|
19
|
+
"TAbstractBaseRegion",
|
|
20
|
+
"TAbstractBaseSequence",
|
|
21
|
+
"AbstractAnalyserDriverIO",
|
|
22
|
+
"AbstractElectronAnalyserDetector",
|
|
23
|
+
"AbstractElectronAnalyserRegionDetector",
|
|
24
|
+
"TAbstractElectronAnalyserDetector",
|
|
25
|
+
"TAbstractElectronAnalyserRegionDetector",
|
|
26
|
+
"AbstractAnalyserDriverIO",
|
|
27
|
+
"TAbstractAnalyserDriverIO",
|
|
28
|
+
]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
from bluesky.protocols import (
|
|
6
|
+
Reading,
|
|
7
|
+
Stageable,
|
|
8
|
+
Triggerable,
|
|
9
|
+
)
|
|
10
|
+
from event_model import DataKey
|
|
11
|
+
from ophyd_async.core import (
|
|
12
|
+
AsyncStatus,
|
|
13
|
+
Device,
|
|
14
|
+
Reference,
|
|
15
|
+
)
|
|
16
|
+
from ophyd_async.core._protocol import AsyncConfigurable, AsyncReadable
|
|
17
|
+
from ophyd_async.epics.adcore import (
|
|
18
|
+
ADBaseController,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from dodal.common.data_util import load_json_file_to_class
|
|
22
|
+
from dodal.devices.electron_analyser.abstract.base_driver_io import (
|
|
23
|
+
AbstractAnalyserDriverIO,
|
|
24
|
+
TAbstractAnalyserDriverIO,
|
|
25
|
+
)
|
|
26
|
+
from dodal.devices.electron_analyser.abstract.base_region import (
|
|
27
|
+
TAbstractBaseRegion,
|
|
28
|
+
TAbstractBaseSequence,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AnalyserController(ADBaseController[AbstractAnalyserDriverIO]):
|
|
33
|
+
def get_deadtime(self, exposure: float | None) -> float:
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BaseElectronAnalyserDetector(
|
|
38
|
+
Device,
|
|
39
|
+
Stageable,
|
|
40
|
+
Triggerable,
|
|
41
|
+
AsyncReadable,
|
|
42
|
+
AsyncConfigurable,
|
|
43
|
+
Generic[TAbstractAnalyserDriverIO],
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Detector for data acquisition of electron analyser. Can only acquire using settings
|
|
47
|
+
already configured for the device.
|
|
48
|
+
|
|
49
|
+
If possible, this should be changed to inheirt from a StandardDetector. Currently,
|
|
50
|
+
StandardDetector forces you to use a file writer which doesn't apply here.
|
|
51
|
+
See issue https://github.com/bluesky/ophyd-async/issues/888
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
name: str,
|
|
57
|
+
driver: TAbstractAnalyserDriverIO,
|
|
58
|
+
):
|
|
59
|
+
self.controller: AnalyserController = AnalyserController(driver=driver)
|
|
60
|
+
super().__init__(name)
|
|
61
|
+
|
|
62
|
+
@AsyncStatus.wrap
|
|
63
|
+
async def trigger(self) -> None:
|
|
64
|
+
await self.controller.arm()
|
|
65
|
+
await self.controller.wait_for_idle()
|
|
66
|
+
|
|
67
|
+
@AsyncStatus.wrap
|
|
68
|
+
async def stage(self) -> None:
|
|
69
|
+
"""Make sure the detector is idle and ready to be used."""
|
|
70
|
+
await asyncio.gather(self.controller.disarm())
|
|
71
|
+
|
|
72
|
+
@AsyncStatus.wrap
|
|
73
|
+
async def unstage(self) -> None:
|
|
74
|
+
"""Disarm the detector."""
|
|
75
|
+
await asyncio.gather(self.controller.disarm())
|
|
76
|
+
|
|
77
|
+
async def read(self) -> dict[str, Reading]:
|
|
78
|
+
return await self.driver.read()
|
|
79
|
+
|
|
80
|
+
async def describe(self) -> dict[str, DataKey]:
|
|
81
|
+
data = await self.driver.describe()
|
|
82
|
+
# Correct the shape for image
|
|
83
|
+
prefix = self.driver.name + "-"
|
|
84
|
+
energy_size = len(await self.driver.energy_axis.get_value())
|
|
85
|
+
angle_size = len(await self.driver.angle_axis.get_value())
|
|
86
|
+
data[prefix + "image"]["shape"] = [angle_size, energy_size]
|
|
87
|
+
return data
|
|
88
|
+
|
|
89
|
+
async def read_configuration(self) -> dict[str, Reading]:
|
|
90
|
+
return await self.driver.read_configuration()
|
|
91
|
+
|
|
92
|
+
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
93
|
+
return await self.driver.describe_configuration()
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def driver(self) -> TAbstractAnalyserDriverIO:
|
|
98
|
+
"""
|
|
99
|
+
Define property for the driver. Some implementations will store this as a
|
|
100
|
+
reference so it doesn't run into errors with conflicting parents.
|
|
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
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self, name: str, driver: TAbstractAnalyserDriverIO, region: TAbstractBaseRegion
|
|
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.
|
|
137
|
+
"""
|
|
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
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
Array1D,
|
|
7
|
+
SignalR,
|
|
8
|
+
StandardReadable,
|
|
9
|
+
StandardReadableFormat,
|
|
10
|
+
derived_signal_r,
|
|
11
|
+
soft_signal_rw,
|
|
12
|
+
)
|
|
13
|
+
from ophyd_async.epics.adcore import ADBaseIO
|
|
14
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
15
|
+
|
|
16
|
+
from dodal.devices.electron_analyser.types import EnergyMode
|
|
17
|
+
from dodal.devices.electron_analyser.util import to_binding_energy
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AbstractAnalyserDriverIO(ABC, StandardReadable, ADBaseIO):
|
|
21
|
+
"""
|
|
22
|
+
Generic device to configure electron analyser with new region settings.
|
|
23
|
+
Electron analysers should inherit from this class for further specialisation.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
27
|
+
with self.add_children_as_readables():
|
|
28
|
+
self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE")
|
|
29
|
+
self.spectrum = epics_signal_r(Array1D[np.float64], prefix + "INT_SPECTRUM")
|
|
30
|
+
self.total_intensity = derived_signal_r(
|
|
31
|
+
self._calculate_total_intensity, spectrum=self.spectrum
|
|
32
|
+
)
|
|
33
|
+
self.excitation_energy = soft_signal_rw(float, initial_value=0, units="eV")
|
|
34
|
+
|
|
35
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
36
|
+
# Used for setting up region data acquisition.
|
|
37
|
+
self.region_name = soft_signal_rw(str, initial_value="null")
|
|
38
|
+
self.energy_mode = soft_signal_rw(
|
|
39
|
+
EnergyMode, initial_value=EnergyMode.KINETIC
|
|
40
|
+
)
|
|
41
|
+
self.low_energy = epics_signal_rw(float, prefix + "LOW_ENERGY")
|
|
42
|
+
self.high_energy = epics_signal_rw(float, prefix + "HIGH_ENERGY")
|
|
43
|
+
self.slices = epics_signal_rw(int, prefix + "SLICES")
|
|
44
|
+
self.lens_mode = epics_signal_rw(str, prefix + "LENS_MODE")
|
|
45
|
+
self.pass_energy = epics_signal_rw(
|
|
46
|
+
self.pass_energy_type, prefix + "PASS_ENERGY"
|
|
47
|
+
)
|
|
48
|
+
self.energy_step = epics_signal_rw(float, prefix + "STEP_SIZE")
|
|
49
|
+
self.iterations = epics_signal_rw(int, prefix + "NumExposures")
|
|
50
|
+
self.acquisition_mode = epics_signal_rw(str, prefix + "ACQ_MODE")
|
|
51
|
+
|
|
52
|
+
# Read once per scan after data acquired
|
|
53
|
+
self.energy_axis = self._create_energy_axis_signal(prefix)
|
|
54
|
+
self.binding_energy_axis = derived_signal_r(
|
|
55
|
+
self._calculate_binding_energy_axis,
|
|
56
|
+
"eV",
|
|
57
|
+
energy_axis=self.energy_axis,
|
|
58
|
+
excitation_energy=self.excitation_energy,
|
|
59
|
+
energy_mode=self.energy_mode,
|
|
60
|
+
)
|
|
61
|
+
self.angle_axis = self._create_angle_axis_signal(prefix)
|
|
62
|
+
self.step_time = epics_signal_r(float, prefix + "AcquireTime")
|
|
63
|
+
self.total_steps = epics_signal_r(int, prefix + "TOTAL_POINTS_RBV")
|
|
64
|
+
self.total_time = derived_signal_r(
|
|
65
|
+
self._calculate_total_time,
|
|
66
|
+
"s",
|
|
67
|
+
total_steps=self.total_steps,
|
|
68
|
+
step_time=self.step_time,
|
|
69
|
+
iterations=self.iterations,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
super().__init__(prefix=prefix, name=name)
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
76
|
+
"""
|
|
77
|
+
The signal that defines the angle axis. Depends on analyser model.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
82
|
+
"""
|
|
83
|
+
The signal that defines the energy axis. Depends on analyser model.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def _calculate_binding_energy_axis(
|
|
87
|
+
self,
|
|
88
|
+
energy_axis: Array1D[np.float64],
|
|
89
|
+
excitation_energy: float,
|
|
90
|
+
energy_mode: EnergyMode,
|
|
91
|
+
) -> Array1D[np.float64]:
|
|
92
|
+
is_binding = energy_mode == EnergyMode.BINDING
|
|
93
|
+
return np.array(
|
|
94
|
+
[
|
|
95
|
+
to_binding_energy(i_energy_axis, EnergyMode.KINETIC, excitation_energy)
|
|
96
|
+
if is_binding
|
|
97
|
+
else i_energy_axis
|
|
98
|
+
for i_energy_axis in energy_axis
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _calculate_total_time(
|
|
103
|
+
self, total_steps: int, step_time: float, iterations: int
|
|
104
|
+
) -> float:
|
|
105
|
+
return total_steps * step_time * iterations
|
|
106
|
+
|
|
107
|
+
def _calculate_total_intensity(self, spectrum: Array1D[np.float64]) -> float:
|
|
108
|
+
return float(np.sum(spectrum, dtype=np.float64))
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
@abstractmethod
|
|
112
|
+
def pass_energy_type(self) -> type:
|
|
113
|
+
"""
|
|
114
|
+
Return the type the pass_energy should be. Each one is unfortunately different
|
|
115
|
+
for the underlying analyser software and cannot be changed on epics side.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
TAbstractAnalyserDriverIO = TypeVar(
|
|
120
|
+
"TAbstractAnalyserDriverIO", bound=AbstractAnalyserDriverIO
|
|
121
|
+
)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from abc import ABC
|
|
3
3
|
from collections.abc import Callable
|
|
4
|
-
from enum import Enum
|
|
5
4
|
from typing import Generic, TypeVar
|
|
6
5
|
|
|
7
6
|
from pydantic import BaseModel, Field, model_validator
|
|
8
7
|
|
|
8
|
+
from dodal.devices.electron_analyser.types import EnergyMode
|
|
9
|
+
|
|
9
10
|
|
|
10
11
|
def java_to_python_case(java_str: str) -> str:
|
|
11
12
|
"""
|
|
@@ -42,11 +43,6 @@ def energy_mode_validation(data: dict) -> dict:
|
|
|
42
43
|
return data
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
class EnergyMode(str, Enum):
|
|
46
|
-
KINETIC = "Kinetic"
|
|
47
|
-
BINDING = "Binding"
|
|
48
|
-
|
|
49
|
-
|
|
50
46
|
class AbstractBaseRegion(ABC, JavaToPythonModel):
|
|
51
47
|
"""
|
|
52
48
|
Generic region model that holds the data. Specialised region models should inherit
|
|
@@ -73,9 +69,6 @@ class AbstractBaseRegion(ABC, JavaToPythonModel):
|
|
|
73
69
|
def is_kinetic_energy(self) -> bool:
|
|
74
70
|
return self.energy_mode == EnergyMode.KINETIC
|
|
75
71
|
|
|
76
|
-
def to_kinetic_energy(self, value: float, excitation_energy: float) -> float:
|
|
77
|
-
return value if self.is_binding_energy() else excitation_energy - value
|
|
78
|
-
|
|
79
72
|
@model_validator(mode="before")
|
|
80
73
|
@classmethod
|
|
81
74
|
def before_validation(cls, data: dict) -> dict:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .detector import SpecsDetector, SpecsRegionDetector
|
|
2
|
+
from .driver_io import SpecsAnalyserDriverIO
|
|
3
|
+
from .region import SpecsRegion, SpecsSequence
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"SpecsDetector",
|
|
7
|
+
"SpecsRegionDetector",
|
|
8
|
+
"SpecsAnalyserDriverIO",
|
|
9
|
+
"SpecsRegion",
|
|
10
|
+
"SpecsSequence",
|
|
11
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dodal.devices.electron_analyser.abstract.base_detector import (
|
|
2
|
+
AbstractElectronAnalyserDetector,
|
|
3
|
+
AbstractElectronAnalyserRegionDetector,
|
|
4
|
+
)
|
|
5
|
+
from dodal.devices.electron_analyser.specs.driver_io import SpecsAnalyserDriverIO
|
|
6
|
+
from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSequence
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SpecsRegionDetector(
|
|
10
|
+
AbstractElectronAnalyserRegionDetector[SpecsAnalyserDriverIO, SpecsRegion]
|
|
11
|
+
):
|
|
12
|
+
def configure_region(self):
|
|
13
|
+
# ToDo - Need to move configure plans to here and rewrite tests
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SpecsDetector(
|
|
18
|
+
AbstractElectronAnalyserDetector[SpecsAnalyserDriverIO, SpecsSequence, SpecsRegion]
|
|
19
|
+
):
|
|
20
|
+
def __init__(self, prefix: str, name: str):
|
|
21
|
+
super().__init__(prefix, name, SpecsSequence)
|
|
22
|
+
|
|
23
|
+
def _create_driver(self, prefix: str) -> SpecsAnalyserDriverIO:
|
|
24
|
+
return SpecsAnalyserDriverIO(prefix, "driver")
|
|
25
|
+
|
|
26
|
+
def _create_region_detector(
|
|
27
|
+
self, driver: SpecsAnalyserDriverIO, region: SpecsRegion
|
|
28
|
+
) -> SpecsRegionDetector:
|
|
29
|
+
return SpecsRegionDetector(self.name, driver, region)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ophyd_async.core import Array1D, SignalR, StandardReadableFormat, derived_signal_r
|
|
3
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
4
|
+
|
|
5
|
+
from dodal.devices.electron_analyser.abstract.base_driver_io import (
|
|
6
|
+
AbstractAnalyserDriverIO,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO):
|
|
11
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
12
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
13
|
+
# Used for setting up region data acquisition.
|
|
14
|
+
self.psu_mode = epics_signal_rw(str, prefix + "SCAN_RANGE")
|
|
15
|
+
self.snapshot_values = epics_signal_rw(int, prefix + "VALUES")
|
|
16
|
+
self.centre_energy = epics_signal_rw(float, prefix + "KINETIC_ENERGY")
|
|
17
|
+
|
|
18
|
+
# Used to read detector data after acqusition.
|
|
19
|
+
self.min_angle_axis = epics_signal_r(float, prefix + "Y_MIN_RBV")
|
|
20
|
+
self.max_angle_axis = epics_signal_r(float, prefix + "Y_MAX_RBV")
|
|
21
|
+
|
|
22
|
+
super().__init__(prefix, name)
|
|
23
|
+
|
|
24
|
+
def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
25
|
+
angle_axis = derived_signal_r(
|
|
26
|
+
self._calculate_angle_axis,
|
|
27
|
+
min_angle=self.min_angle_axis,
|
|
28
|
+
max_angle=self.max_angle_axis,
|
|
29
|
+
slices=self.slices,
|
|
30
|
+
)
|
|
31
|
+
return angle_axis
|
|
32
|
+
|
|
33
|
+
def _calculate_angle_axis(
|
|
34
|
+
self, min_angle: float, max_angle: float, slices: int
|
|
35
|
+
) -> Array1D[np.float64]:
|
|
36
|
+
# SPECS returns the extreme edges of the range, not the centre of the pixels
|
|
37
|
+
width = (max_angle - min_angle) / slices
|
|
38
|
+
offset = width / 2
|
|
39
|
+
|
|
40
|
+
axis = np.array([min_angle + offset + i * width for i in range(slices)])
|
|
41
|
+
return axis
|
|
42
|
+
|
|
43
|
+
def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
44
|
+
energy_axis = derived_signal_r(
|
|
45
|
+
self._calculate_energy_axis,
|
|
46
|
+
"eV",
|
|
47
|
+
min_energy=self.low_energy,
|
|
48
|
+
max_energy=self.high_energy,
|
|
49
|
+
total_points_iterations=self.slices,
|
|
50
|
+
)
|
|
51
|
+
return energy_axis
|
|
52
|
+
|
|
53
|
+
def _calculate_energy_axis(
|
|
54
|
+
self, min_energy: float, max_energy: float, total_points_iterations: int
|
|
55
|
+
) -> Array1D[np.float64]:
|
|
56
|
+
# Note: Don't use the energy step because of the case where the step doesn't
|
|
57
|
+
# exactly fill the range
|
|
58
|
+
step = (max_energy - min_energy) / (total_points_iterations - 1)
|
|
59
|
+
axis = np.array([min_energy + i * step for i in range(total_points_iterations)])
|
|
60
|
+
return axis
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def pass_energy_type(self) -> type:
|
|
64
|
+
return float
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from dodal.devices.electron_analyser.types import EnergyMode
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def to_kinetic_energy(
|
|
5
|
+
value: float, value_mode: EnergyMode, excitation_energy: float
|
|
6
|
+
) -> float:
|
|
7
|
+
return value if value_mode == EnergyMode.KINETIC else excitation_energy - value
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def to_binding_energy(
|
|
11
|
+
value: float, value_mode: EnergyMode, excitation_energy: float
|
|
12
|
+
) -> float:
|
|
13
|
+
return value if value_mode == EnergyMode.BINDING else excitation_energy - value
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .detector import VGScientaDetector, VGScientaRegionDetector
|
|
2
|
+
from .driver_io import VGScientaAnalyserDriverIO
|
|
3
|
+
from .region import VGScientaExcitationEnergySource, VGScientaRegion, VGScientaSequence
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"VGScientaDetector",
|
|
7
|
+
"VGScientaRegionDetector",
|
|
8
|
+
"VGScientaAnalyserDriverIO",
|
|
9
|
+
"VGScientaExcitationEnergySource",
|
|
10
|
+
"VGScientaRegion",
|
|
11
|
+
"VGScientaSequence",
|
|
12
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dodal.devices.electron_analyser.abstract.base_detector import (
|
|
2
|
+
AbstractElectronAnalyserDetector,
|
|
3
|
+
AbstractElectronAnalyserRegionDetector,
|
|
4
|
+
)
|
|
5
|
+
from dodal.devices.electron_analyser.vgscienta.driver_io import (
|
|
6
|
+
VGScientaAnalyserDriverIO,
|
|
7
|
+
)
|
|
8
|
+
from dodal.devices.electron_analyser.vgscienta.region import (
|
|
9
|
+
VGScientaRegion,
|
|
10
|
+
VGScientaSequence,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VGScientaRegionDetector(
|
|
15
|
+
AbstractElectronAnalyserRegionDetector[VGScientaAnalyserDriverIO, VGScientaRegion]
|
|
16
|
+
):
|
|
17
|
+
def configure_region(self):
|
|
18
|
+
# ToDo - Need to move configure plans to here and rewrite tests
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VGScientaDetector(
|
|
23
|
+
AbstractElectronAnalyserDetector[
|
|
24
|
+
VGScientaAnalyserDriverIO, VGScientaSequence, VGScientaRegion
|
|
25
|
+
]
|
|
26
|
+
):
|
|
27
|
+
def __init__(self, prefix: str, name: str):
|
|
28
|
+
super().__init__(prefix, name, VGScientaSequence)
|
|
29
|
+
|
|
30
|
+
def _create_driver(self, prefix: str) -> VGScientaAnalyserDriverIO:
|
|
31
|
+
return VGScientaAnalyserDriverIO(prefix, "driver")
|
|
32
|
+
|
|
33
|
+
def _create_region_detector(
|
|
34
|
+
self, driver: VGScientaAnalyserDriverIO, region: VGScientaRegion
|
|
35
|
+
) -> VGScientaRegionDetector:
|
|
36
|
+
return VGScientaRegionDetector(self.name, driver, region)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ophyd_async.core import Array1D, SignalR, StandardReadableFormat, soft_signal_rw
|
|
3
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
4
|
+
|
|
5
|
+
from dodal.devices.electron_analyser.abstract.base_driver_io import (
|
|
6
|
+
AbstractAnalyserDriverIO,
|
|
7
|
+
)
|
|
8
|
+
from dodal.devices.electron_analyser.vgscienta.region import (
|
|
9
|
+
DetectorMode,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO):
|
|
14
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
15
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
16
|
+
self.excitation_energy_source = soft_signal_rw(str, initial_value=None)
|
|
17
|
+
# Used for setting up region data acquisition.
|
|
18
|
+
self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
|
|
19
|
+
self.first_x_channel = epics_signal_rw(int, prefix + "MinX")
|
|
20
|
+
self.first_y_channel = epics_signal_rw(int, prefix + "MinY")
|
|
21
|
+
self.x_channel_size = epics_signal_rw(int, prefix + "SizeX")
|
|
22
|
+
self.y_channel_size = epics_signal_rw(int, prefix + "SizeY")
|
|
23
|
+
self.detector_mode = epics_signal_rw(DetectorMode, prefix + "DETECTOR_MODE")
|
|
24
|
+
|
|
25
|
+
with self.add_children_as_readables():
|
|
26
|
+
# Used to read detector data after acqusition.
|
|
27
|
+
self.external_io = epics_signal_r(Array1D[np.float64], prefix + "EXTIO")
|
|
28
|
+
|
|
29
|
+
super().__init__(prefix, name)
|
|
30
|
+
|
|
31
|
+
def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
32
|
+
return epics_signal_r(Array1D[np.float64], prefix + "X_SCALE_RBV")
|
|
33
|
+
|
|
34
|
+
def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
35
|
+
return epics_signal_r(Array1D[np.float64], prefix + "Y_SCALE_RBV")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def pass_energy_type(self) -> type:
|
|
39
|
+
return str
|
|
@@ -4,7 +4,7 @@ from enum import Enum
|
|
|
4
4
|
from ophyd_async.core import StrictEnum
|
|
5
5
|
from pydantic import Field
|
|
6
6
|
|
|
7
|
-
from dodal.devices.electron_analyser.
|
|
7
|
+
from dodal.devices.electron_analyser.abstract.base_region import (
|
|
8
8
|
AbstractBaseRegion,
|
|
9
9
|
AbstractBaseSequence,
|
|
10
10
|
JavaToPythonModel,
|
dodal/devices/fast_grid_scan.py
CHANGED
|
@@ -12,6 +12,7 @@ from ophyd_async.core import (
|
|
|
12
12
|
Signal,
|
|
13
13
|
SignalRW,
|
|
14
14
|
StandardReadable,
|
|
15
|
+
derived_signal_r,
|
|
15
16
|
wait_for_value,
|
|
16
17
|
)
|
|
17
18
|
from ophyd_async.epics.core import (
|
|
@@ -23,7 +24,6 @@ from ophyd_async.epics.core import (
|
|
|
23
24
|
from pydantic import field_validator
|
|
24
25
|
from pydantic.dataclasses import dataclass
|
|
25
26
|
|
|
26
|
-
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
27
27
|
from dodal.log import LOGGER
|
|
28
28
|
from dodal.parameters.experiment_parameter_base import AbstractExperimentWithBeamParams
|
|
29
29
|
|
|
@@ -203,8 +203,11 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
|
203
203
|
self.stop_cmd = epics_signal_x(f"{prefix}STOP.PROC")
|
|
204
204
|
self.status = epics_signal_r(int, f"{prefix}SCAN_STATUS")
|
|
205
205
|
|
|
206
|
-
self.expected_images =
|
|
207
|
-
|
|
206
|
+
self.expected_images = derived_signal_r(
|
|
207
|
+
self._calculate_expected_images,
|
|
208
|
+
x=self.x_steps,
|
|
209
|
+
y=self.y_steps,
|
|
210
|
+
z=self.z_steps,
|
|
208
211
|
)
|
|
209
212
|
|
|
210
213
|
self.motion_program = MotionProgram(smargon_prefix)
|
|
@@ -231,12 +234,7 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
|
|
|
231
234
|
}
|
|
232
235
|
super().__init__(name)
|
|
233
236
|
|
|
234
|
-
|
|
235
|
-
x, y, z = await asyncio.gather(
|
|
236
|
-
self.x_steps.get_value(),
|
|
237
|
-
self.y_steps.get_value(),
|
|
238
|
-
self.z_steps.get_value(),
|
|
239
|
-
)
|
|
237
|
+
def _calculate_expected_images(self, x: float, y: float, z: float) -> float:
|
|
240
238
|
LOGGER.info(f"Reading num of images found {x, y, z} images in each axis")
|
|
241
239
|
first_grid = x * y
|
|
242
240
|
second_grid = x * z
|
dodal/devices/i03/__init__.py
CHANGED