dls-dodal 1.46.0__py3-none-any.whl → 1.48.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.46.0.dist-info → dls_dodal-1.48.0.dist-info}/METADATA +2 -2
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/RECORD +74 -63
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +0 -1
- dodal/beamlines/aithre.py +6 -0
- dodal/beamlines/b01_1.py +1 -1
- dodal/beamlines/b07.py +2 -6
- dodal/beamlines/b07_1.py +1 -3
- dodal/beamlines/i03.py +33 -21
- dodal/beamlines/i04.py +65 -26
- dodal/beamlines/i09.py +1 -3
- dodal/beamlines/i09_1.py +1 -3
- dodal/beamlines/i18.py +1 -1
- dodal/beamlines/i19_1.py +9 -6
- dodal/beamlines/i23.py +17 -1
- dodal/beamlines/i24.py +5 -5
- dodal/beamlines/p38.py +1 -1
- dodal/beamlines/p60.py +2 -6
- dodal/beamlines/p99.py +48 -4
- dodal/common/beamlines/beamline_parameters.py +3 -30
- dodal/common/data_util.py +4 -0
- dodal/devices/aithre_lasershaping/goniometer.py +36 -2
- dodal/devices/aithre_lasershaping/laser_robot.py +27 -0
- dodal/devices/aperturescatterguard.py +47 -47
- 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 +18 -0
- dodal/devices/electron_analyser/abstract/__init__.py +22 -0
- dodal/devices/electron_analyser/abstract/base_detector.py +223 -0
- dodal/devices/electron_analyser/abstract/base_driver_io.py +230 -0
- dodal/devices/electron_analyser/{abstract_region.py → abstract/base_region.py} +3 -9
- dodal/devices/electron_analyser/specs/__init__.py +10 -0
- dodal/devices/electron_analyser/specs/detector.py +13 -0
- dodal/devices/electron_analyser/specs/driver_io.py +89 -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 +11 -0
- dodal/devices/electron_analyser/vgscienta/detector.py +22 -0
- dodal/devices/electron_analyser/vgscienta/driver_io.py +67 -0
- dodal/devices/electron_analyser/{vgscienta_region.py → vgscienta/region.py} +1 -2
- dodal/devices/fast_grid_scan.py +7 -9
- dodal/devices/i03/__init__.py +3 -0
- dodal/devices/i04/__init__.py +3 -0
- dodal/devices/i04/constants.py +9 -0
- dodal/devices/i04/murko_results.py +192 -0
- dodal/devices/i10/diagnostics.py +9 -61
- dodal/devices/i18/diode.py +37 -4
- 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 +26 -15
- dodal/devices/oav/oav_calculations.py +2 -2
- dodal/devices/oav/oav_detector.py +80 -32
- dodal/devices/oav/oav_parameters.py +46 -16
- dodal/devices/oav/oav_to_redis_forwarder.py +2 -2
- dodal/devices/oav/utils.py +2 -2
- dodal/devices/p99/andor2_point.py +41 -0
- dodal/devices/positioner.py +49 -0
- dodal/devices/robot.py +20 -1
- dodal/devices/smargon.py +43 -4
- dodal/devices/tetramm.py +5 -2
- dodal/devices/util/adjuster_plans.py +1 -1
- dodal/devices/zebra/zebra.py +8 -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/plans/configure_arm_trigger_and_disarm_detector.py +167 -0
- 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
- dodal/plan_stubs/electron_analyser/__init__.py +0 -0
- dodal/plan_stubs/electron_analyser/configure_controller.py +0 -80
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from bluesky.protocols import Movable, Preparable
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
Array1D,
|
|
9
|
+
AsyncStatus,
|
|
10
|
+
SignalR,
|
|
11
|
+
StandardReadable,
|
|
12
|
+
StandardReadableFormat,
|
|
13
|
+
derived_signal_r,
|
|
14
|
+
soft_signal_rw,
|
|
15
|
+
)
|
|
16
|
+
from ophyd_async.epics.adcore import ADBaseIO
|
|
17
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
18
|
+
from ophyd_async.epics.motor import Motor
|
|
19
|
+
|
|
20
|
+
from dodal.devices.electron_analyser.abstract.base_region import (
|
|
21
|
+
TAbstractBaseRegion,
|
|
22
|
+
)
|
|
23
|
+
from dodal.devices.electron_analyser.types import EnergyMode
|
|
24
|
+
from dodal.devices.electron_analyser.util import to_binding_energy, to_kinetic_energy
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AbstractAnalyserDriverIO(
|
|
28
|
+
ABC,
|
|
29
|
+
StandardReadable,
|
|
30
|
+
ADBaseIO,
|
|
31
|
+
Preparable,
|
|
32
|
+
Movable[TAbstractBaseRegion],
|
|
33
|
+
Generic[TAbstractBaseRegion],
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Generic device to configure electron analyser with new region settings.
|
|
37
|
+
Electron analysers should inherit from this class for further specialisation.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
41
|
+
with self.add_children_as_readables():
|
|
42
|
+
self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE")
|
|
43
|
+
self.spectrum = epics_signal_r(Array1D[np.float64], prefix + "INT_SPECTRUM")
|
|
44
|
+
self.total_intensity = derived_signal_r(
|
|
45
|
+
self._calculate_total_intensity, spectrum=self.spectrum
|
|
46
|
+
)
|
|
47
|
+
self.excitation_energy = soft_signal_rw(float, initial_value=0, units="eV")
|
|
48
|
+
|
|
49
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
50
|
+
# Used for setting up region data acquisition.
|
|
51
|
+
self.region_name = soft_signal_rw(str, initial_value="null")
|
|
52
|
+
self.energy_mode = soft_signal_rw(
|
|
53
|
+
EnergyMode, initial_value=EnergyMode.KINETIC
|
|
54
|
+
)
|
|
55
|
+
self.low_energy = epics_signal_rw(float, prefix + "LOW_ENERGY")
|
|
56
|
+
self.high_energy = epics_signal_rw(float, prefix + "HIGH_ENERGY")
|
|
57
|
+
self.slices = epics_signal_rw(int, prefix + "SLICES")
|
|
58
|
+
self.lens_mode = epics_signal_rw(str, prefix + "LENS_MODE")
|
|
59
|
+
self.pass_energy = epics_signal_rw(
|
|
60
|
+
self.pass_energy_type, prefix + "PASS_ENERGY"
|
|
61
|
+
)
|
|
62
|
+
self.energy_step = epics_signal_rw(float, prefix + "STEP_SIZE")
|
|
63
|
+
self.iterations = epics_signal_rw(int, prefix + "NumExposures")
|
|
64
|
+
self.acquisition_mode = epics_signal_rw(str, prefix + "ACQ_MODE")
|
|
65
|
+
self.excitation_energy_source = soft_signal_rw(str, initial_value="")
|
|
66
|
+
|
|
67
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
68
|
+
# Read once per scan after data acquired
|
|
69
|
+
self.energy_axis = self._create_energy_axis_signal(prefix)
|
|
70
|
+
self.binding_energy_axis = derived_signal_r(
|
|
71
|
+
self._calculate_binding_energy_axis,
|
|
72
|
+
"eV",
|
|
73
|
+
energy_axis=self.energy_axis,
|
|
74
|
+
excitation_energy=self.excitation_energy,
|
|
75
|
+
energy_mode=self.energy_mode,
|
|
76
|
+
)
|
|
77
|
+
self.angle_axis = self._create_angle_axis_signal(prefix)
|
|
78
|
+
self.step_time = epics_signal_r(float, prefix + "AcquireTime")
|
|
79
|
+
self.total_steps = epics_signal_r(int, prefix + "TOTAL_POINTS_RBV")
|
|
80
|
+
self.total_time = derived_signal_r(
|
|
81
|
+
self._calculate_total_time,
|
|
82
|
+
"s",
|
|
83
|
+
total_steps=self.total_steps,
|
|
84
|
+
step_time=self.step_time,
|
|
85
|
+
iterations=self.iterations,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
super().__init__(prefix=prefix, name=name)
|
|
89
|
+
|
|
90
|
+
@AsyncStatus.wrap
|
|
91
|
+
async def prepare(self, value: Motor):
|
|
92
|
+
"""
|
|
93
|
+
Prepare the driver for a region by passing in the energy source motor selected
|
|
94
|
+
by a region.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
value: The motor that contains the information on the current excitation
|
|
98
|
+
energy. Needed to prepare region for epics to accuratly calculate
|
|
99
|
+
kinetic energy for an energy scan when in binding energy mode.
|
|
100
|
+
"""
|
|
101
|
+
energy_source = value
|
|
102
|
+
excitation_energy_value = await energy_source.user_readback.get_value() # eV
|
|
103
|
+
excitation_energy_source_name = energy_source.name
|
|
104
|
+
|
|
105
|
+
await asyncio.gather(
|
|
106
|
+
self.excitation_energy.set(excitation_energy_value),
|
|
107
|
+
self.excitation_energy_source.set(excitation_energy_source_name),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@AsyncStatus.wrap
|
|
111
|
+
async def set(self, region: TAbstractBaseRegion):
|
|
112
|
+
"""
|
|
113
|
+
This should encompass all core region logic which is common to every electron
|
|
114
|
+
analyser for setting up the driver.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
region: Contains the parameters to setup the driver for a scan.
|
|
118
|
+
"""
|
|
119
|
+
pass_energy_type = self.pass_energy_type
|
|
120
|
+
pass_energy = pass_energy_type(region.pass_energy)
|
|
121
|
+
|
|
122
|
+
excitation_energy = await self.excitation_energy.get_value()
|
|
123
|
+
low_energy = to_kinetic_energy(
|
|
124
|
+
region.low_energy, region.energy_mode, excitation_energy
|
|
125
|
+
)
|
|
126
|
+
high_energy = to_kinetic_energy(
|
|
127
|
+
region.high_energy, region.energy_mode, excitation_energy
|
|
128
|
+
)
|
|
129
|
+
await asyncio.gather(
|
|
130
|
+
self.region_name.set(region.name),
|
|
131
|
+
self.energy_mode.set(region.energy_mode),
|
|
132
|
+
self.low_energy.set(low_energy),
|
|
133
|
+
self.high_energy.set(high_energy),
|
|
134
|
+
self.slices.set(region.slices),
|
|
135
|
+
self.lens_mode.set(region.lens_mode),
|
|
136
|
+
self.pass_energy.set(pass_energy),
|
|
137
|
+
self.iterations.set(region.iterations),
|
|
138
|
+
self.acquisition_mode.set(region.acquisition_mode),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
143
|
+
"""
|
|
144
|
+
The signal that defines the angle axis. Depends on analyser model.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
prefix: PV string used for connecting to angle axis.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Signal that can give us angle axis array data.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
@abstractmethod
|
|
154
|
+
def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
155
|
+
"""
|
|
156
|
+
The signal that defines the energy axis. Depends on analyser model.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
prefix: PV string used for connecting to energy axis.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Signal that can give us energy axis array data.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def _calculate_binding_energy_axis(
|
|
166
|
+
self,
|
|
167
|
+
energy_axis: Array1D[np.float64],
|
|
168
|
+
excitation_energy: float,
|
|
169
|
+
energy_mode: EnergyMode,
|
|
170
|
+
) -> Array1D[np.float64]:
|
|
171
|
+
"""
|
|
172
|
+
Calculate the binding energy axis to calibrate the spectra data. Function for a
|
|
173
|
+
derived signal.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
energy_axis: Array data of the original energy_axis from epics.
|
|
177
|
+
excitation_energy: The excitation energy value used for the scan of this
|
|
178
|
+
region.
|
|
179
|
+
energy_mode: The energy_mode of the region that was used for the scan
|
|
180
|
+
of this region.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Array that is the correct axis for the spectra data.
|
|
184
|
+
"""
|
|
185
|
+
is_binding = energy_mode == EnergyMode.BINDING
|
|
186
|
+
return np.array(
|
|
187
|
+
[
|
|
188
|
+
to_binding_energy(i_energy_axis, EnergyMode.KINETIC, excitation_energy)
|
|
189
|
+
if is_binding
|
|
190
|
+
else i_energy_axis
|
|
191
|
+
for i_energy_axis in energy_axis
|
|
192
|
+
]
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _calculate_total_time(
|
|
196
|
+
self, total_steps: int, step_time: float, iterations: int
|
|
197
|
+
) -> float:
|
|
198
|
+
"""
|
|
199
|
+
Calulcate the total time the scan takes for this region. Function for a derived
|
|
200
|
+
signal.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
total_steps: Number of steps for the region.
|
|
204
|
+
step_time: Time for each step for the region.
|
|
205
|
+
iterations: The number of iterations the region collected data for.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Calculated total time in seconds.
|
|
209
|
+
"""
|
|
210
|
+
return total_steps * step_time * iterations
|
|
211
|
+
|
|
212
|
+
def _calculate_total_intensity(self, spectrum: Array1D[np.float64]) -> float:
|
|
213
|
+
return float(np.sum(spectrum, dtype=np.float64))
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
@abstractmethod
|
|
217
|
+
def pass_energy_type(self) -> type:
|
|
218
|
+
"""
|
|
219
|
+
Return the type the pass_energy should be. Depends on underlying analyser
|
|
220
|
+
software.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Type the pass energy parameter from a region needs to be cast to so it can
|
|
224
|
+
be set correctly on the signal.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
TAbstractAnalyserDriverIO = TypeVar(
|
|
229
|
+
"TAbstractAnalyserDriverIO", bound=AbstractAnalyserDriverIO
|
|
230
|
+
)
|
|
@@ -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
|
|
@@ -57,6 +53,7 @@ class AbstractBaseRegion(ABC, JavaToPythonModel):
|
|
|
57
53
|
enabled: bool = False
|
|
58
54
|
slices: int = 1
|
|
59
55
|
iterations: int = 1
|
|
56
|
+
excitation_energy_source: str = "source1"
|
|
60
57
|
# These ones we need subclasses to provide default values
|
|
61
58
|
lens_mode: str
|
|
62
59
|
pass_energy: int
|
|
@@ -73,9 +70,6 @@ class AbstractBaseRegion(ABC, JavaToPythonModel):
|
|
|
73
70
|
def is_kinetic_energy(self) -> bool:
|
|
74
71
|
return self.energy_mode == EnergyMode.KINETIC
|
|
75
72
|
|
|
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
73
|
@model_validator(mode="before")
|
|
80
74
|
@classmethod
|
|
81
75
|
def before_validation(cls, data: dict) -> dict:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from dodal.devices.electron_analyser.abstract.base_detector import (
|
|
2
|
+
ElectronAnalyserDetector,
|
|
3
|
+
)
|
|
4
|
+
from dodal.devices.electron_analyser.specs.driver_io import SpecsAnalyserDriverIO
|
|
5
|
+
from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSequence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SpecsDetector(
|
|
9
|
+
ElectronAnalyserDetector[SpecsAnalyserDriverIO, SpecsSequence, SpecsRegion]
|
|
10
|
+
):
|
|
11
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
12
|
+
driver = SpecsAnalyserDriverIO(prefix=prefix)
|
|
13
|
+
super().__init__(prefix, SpecsSequence, driver, name)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ophyd_async.core import (
|
|
5
|
+
Array1D,
|
|
6
|
+
AsyncStatus,
|
|
7
|
+
SignalR,
|
|
8
|
+
StandardReadableFormat,
|
|
9
|
+
derived_signal_r,
|
|
10
|
+
)
|
|
11
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
12
|
+
|
|
13
|
+
from dodal.devices.electron_analyser.abstract.base_driver_io import (
|
|
14
|
+
AbstractAnalyserDriverIO,
|
|
15
|
+
)
|
|
16
|
+
from dodal.devices.electron_analyser.specs.region import SpecsRegion
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO[SpecsRegion]):
|
|
20
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
21
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
22
|
+
# Used for setting up region data acquisition.
|
|
23
|
+
self.psu_mode = epics_signal_rw(str, prefix + "SCAN_RANGE")
|
|
24
|
+
self.snapshot_values = epics_signal_rw(int, prefix + "VALUES")
|
|
25
|
+
self.centre_energy = epics_signal_rw(float, prefix + "KINETIC_ENERGY")
|
|
26
|
+
|
|
27
|
+
# Used to read detector data after acqusition.
|
|
28
|
+
self.min_angle_axis = epics_signal_r(float, prefix + "Y_MIN_RBV")
|
|
29
|
+
self.max_angle_axis = epics_signal_r(float, prefix + "Y_MAX_RBV")
|
|
30
|
+
|
|
31
|
+
super().__init__(prefix, name)
|
|
32
|
+
|
|
33
|
+
@AsyncStatus.wrap
|
|
34
|
+
async def set(self, region: SpecsRegion):
|
|
35
|
+
await super().set(region)
|
|
36
|
+
|
|
37
|
+
await asyncio.gather(
|
|
38
|
+
self.snapshot_values.set(region.values),
|
|
39
|
+
self.psu_mode.set(region.psu_mode),
|
|
40
|
+
)
|
|
41
|
+
# ToDo - This needs to be changed to an Enum
|
|
42
|
+
# https://github.com/DiamondLightSource/dodal/issues/1258
|
|
43
|
+
if region.acquisition_mode == "Fixed Transmission":
|
|
44
|
+
await self.centre_energy.set(region.centre_energy)
|
|
45
|
+
|
|
46
|
+
if self.acquisition_mode == "Fixed Energy":
|
|
47
|
+
await self.energy_step.set(region.energy_step)
|
|
48
|
+
|
|
49
|
+
def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
50
|
+
angle_axis = derived_signal_r(
|
|
51
|
+
self._calculate_angle_axis,
|
|
52
|
+
min_angle=self.min_angle_axis,
|
|
53
|
+
max_angle=self.max_angle_axis,
|
|
54
|
+
slices=self.slices,
|
|
55
|
+
)
|
|
56
|
+
return angle_axis
|
|
57
|
+
|
|
58
|
+
def _calculate_angle_axis(
|
|
59
|
+
self, min_angle: float, max_angle: float, slices: int
|
|
60
|
+
) -> Array1D[np.float64]:
|
|
61
|
+
# SPECS returns the extreme edges of the range, not the centre of the pixels
|
|
62
|
+
width = (max_angle - min_angle) / slices
|
|
63
|
+
offset = width / 2
|
|
64
|
+
|
|
65
|
+
axis = np.array([min_angle + offset + i * width for i in range(slices)])
|
|
66
|
+
return axis
|
|
67
|
+
|
|
68
|
+
def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
69
|
+
energy_axis = derived_signal_r(
|
|
70
|
+
self._calculate_energy_axis,
|
|
71
|
+
"eV",
|
|
72
|
+
min_energy=self.low_energy,
|
|
73
|
+
max_energy=self.high_energy,
|
|
74
|
+
total_points_iterations=self.slices,
|
|
75
|
+
)
|
|
76
|
+
return energy_axis
|
|
77
|
+
|
|
78
|
+
def _calculate_energy_axis(
|
|
79
|
+
self, min_energy: float, max_energy: float, total_points_iterations: int
|
|
80
|
+
) -> Array1D[np.float64]:
|
|
81
|
+
# Note: Don't use the energy step because of the case where the step doesn't
|
|
82
|
+
# exactly fill the range
|
|
83
|
+
step = (max_energy - min_energy) / (total_points_iterations - 1)
|
|
84
|
+
axis = np.array([min_energy + i * step for i in range(total_points_iterations)])
|
|
85
|
+
return axis
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def pass_energy_type(self) -> type:
|
|
89
|
+
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,11 @@
|
|
|
1
|
+
from .detector import VGScientaDetector
|
|
2
|
+
from .driver_io import VGScientaAnalyserDriverIO
|
|
3
|
+
from .region import VGScientaExcitationEnergySource, VGScientaRegion, VGScientaSequence
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"VGScientaDetector",
|
|
7
|
+
"VGScientaAnalyserDriverIO",
|
|
8
|
+
"VGScientaExcitationEnergySource",
|
|
9
|
+
"VGScientaRegion",
|
|
10
|
+
"VGScientaSequence",
|
|
11
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from dodal.devices.electron_analyser.abstract.base_detector import (
|
|
2
|
+
ElectronAnalyserDetector,
|
|
3
|
+
)
|
|
4
|
+
from dodal.devices.electron_analyser.vgscienta.driver_io import (
|
|
5
|
+
VGScientaAnalyserDriverIO,
|
|
6
|
+
)
|
|
7
|
+
from dodal.devices.electron_analyser.vgscienta.region import (
|
|
8
|
+
VGScientaRegion,
|
|
9
|
+
VGScientaSequence,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VGScientaDetector(
|
|
14
|
+
ElectronAnalyserDetector[
|
|
15
|
+
VGScientaAnalyserDriverIO,
|
|
16
|
+
VGScientaSequence,
|
|
17
|
+
VGScientaRegion,
|
|
18
|
+
]
|
|
19
|
+
):
|
|
20
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
21
|
+
driver = VGScientaAnalyserDriverIO(prefix)
|
|
22
|
+
super().__init__(prefix, VGScientaSequence, driver, name)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ophyd_async.core import (
|
|
5
|
+
Array1D,
|
|
6
|
+
AsyncStatus,
|
|
7
|
+
SignalR,
|
|
8
|
+
StandardReadableFormat,
|
|
9
|
+
)
|
|
10
|
+
from ophyd_async.epics.adcore import ADImageMode
|
|
11
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
12
|
+
|
|
13
|
+
from dodal.devices.electron_analyser.abstract.base_driver_io import (
|
|
14
|
+
AbstractAnalyserDriverIO,
|
|
15
|
+
)
|
|
16
|
+
from dodal.devices.electron_analyser.util import to_kinetic_energy
|
|
17
|
+
from dodal.devices.electron_analyser.vgscienta.region import (
|
|
18
|
+
DetectorMode,
|
|
19
|
+
VGScientaRegion,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO[VGScientaRegion]):
|
|
24
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
25
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
26
|
+
# Used for setting up region data acquisition.
|
|
27
|
+
self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
|
|
28
|
+
self.first_x_channel = epics_signal_rw(int, prefix + "MinX")
|
|
29
|
+
self.first_y_channel = epics_signal_rw(int, prefix + "MinY")
|
|
30
|
+
self.x_channel_size = epics_signal_rw(int, prefix + "SizeX")
|
|
31
|
+
self.y_channel_size = epics_signal_rw(int, prefix + "SizeY")
|
|
32
|
+
self.detector_mode = epics_signal_rw(DetectorMode, prefix + "DETECTOR_MODE")
|
|
33
|
+
|
|
34
|
+
with self.add_children_as_readables():
|
|
35
|
+
# Used to read detector data after acqusition.
|
|
36
|
+
self.external_io = epics_signal_r(Array1D[np.float64], prefix + "EXTIO")
|
|
37
|
+
|
|
38
|
+
super().__init__(prefix, name)
|
|
39
|
+
|
|
40
|
+
@AsyncStatus.wrap
|
|
41
|
+
async def set(self, region: VGScientaRegion):
|
|
42
|
+
await super().set(region)
|
|
43
|
+
|
|
44
|
+
excitation_energy = await self.excitation_energy.get_value()
|
|
45
|
+
centre_energy = to_kinetic_energy(
|
|
46
|
+
region.fix_energy, region.energy_mode, excitation_energy
|
|
47
|
+
)
|
|
48
|
+
await asyncio.gather(
|
|
49
|
+
self.centre_energy.set(centre_energy),
|
|
50
|
+
self.energy_step.set(region.energy_step),
|
|
51
|
+
self.first_x_channel.set(region.first_x_channel),
|
|
52
|
+
self.first_y_channel.set(region.first_y_channel),
|
|
53
|
+
self.x_channel_size.set(region.x_channel_size()),
|
|
54
|
+
self.y_channel_size.set(region.y_channel_size()),
|
|
55
|
+
self.detector_mode.set(region.detector_mode),
|
|
56
|
+
self.image_mode.set(ADImageMode.SINGLE),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
60
|
+
return epics_signal_r(Array1D[np.float64], prefix + "X_SCALE_RBV")
|
|
61
|
+
|
|
62
|
+
def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
63
|
+
return epics_signal_r(Array1D[np.float64], prefix + "Y_SCALE_RBV")
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def pass_energy_type(self) -> type:
|
|
67
|
+
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,
|
|
@@ -40,7 +40,6 @@ class VGScientaRegion(AbstractBaseRegion):
|
|
|
40
40
|
energy_step: float = Field(default=200.0)
|
|
41
41
|
# Specific to this class
|
|
42
42
|
id: str = Field(default=str(uuid.uuid4()), alias="region_id")
|
|
43
|
-
excitation_energy_source: str = "source1"
|
|
44
43
|
fix_energy: float = 9.0
|
|
45
44
|
total_steps: float = 13.0
|
|
46
45
|
total_time: float = 13.0
|
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: int, y: int, z: int) -> int:
|
|
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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class RedisConstants:
|
|
7
|
+
REDIS_HOST = os.environ.get("VALKEY_PROD_SVC_SERVICE_HOST", "test_redis")
|
|
8
|
+
REDIS_PASSWORD = os.environ.get("VALKEY_PASSWORD", "test_redis_password")
|
|
9
|
+
MURKO_REDIS_DB = 7
|