dls-dodal 1.44.0__py3-none-any.whl → 1.46.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.44.0.dist-info → dls_dodal-1.46.0.dist-info}/METADATA +2 -2
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/RECORD +56 -46
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +2 -0
- dodal/beamlines/b07.py +27 -0
- dodal/beamlines/b07_1.py +25 -0
- dodal/beamlines/i03.py +4 -4
- dodal/beamlines/i04.py +1 -1
- dodal/beamlines/i09.py +25 -0
- dodal/beamlines/i09_1.py +25 -0
- dodal/beamlines/i10.py +19 -35
- dodal/beamlines/i18.py +7 -4
- dodal/beamlines/i19_1.py +2 -1
- dodal/beamlines/i19_2.py +2 -1
- dodal/beamlines/i20_1.py +2 -1
- dodal/beamlines/i22.py +3 -3
- dodal/beamlines/i23.py +67 -2
- dodal/beamlines/p38.py +3 -3
- dodal/beamlines/p60.py +21 -0
- dodal/common/beamlines/beamline_utils.py +5 -0
- dodal/common/visit.py +1 -41
- dodal/devices/common_dcm.py +77 -0
- dodal/devices/detector/det_dist_to_beam_converter.py +16 -23
- dodal/devices/detector/detector.py +2 -1
- dodal/devices/electron_analyser/abstract_analyser_io.py +47 -0
- dodal/devices/electron_analyser/abstract_region.py +112 -0
- dodal/devices/electron_analyser/specs_analyser_io.py +19 -0
- dodal/devices/electron_analyser/specs_region.py +26 -0
- dodal/devices/electron_analyser/vgscienta_analyser_io.py +26 -0
- dodal/devices/electron_analyser/vgscienta_region.py +90 -0
- dodal/devices/{dcm.py → i03/dcm.py} +8 -12
- dodal/devices/{undulator_dcm.py → i03/undulator_dcm.py} +6 -4
- dodal/devices/i10/diagnostics.py +239 -0
- dodal/devices/i10/slits.py +93 -6
- dodal/devices/i13_1/merlin.py +3 -4
- dodal/devices/i13_1/merlin_controller.py +1 -1
- dodal/devices/i19/blueapi_device.py +102 -0
- dodal/devices/i19/shutter.py +5 -43
- dodal/devices/i22/dcm.py +10 -12
- dodal/devices/i24/dcm.py +8 -17
- dodal/devices/motors.py +21 -0
- dodal/devices/tetramm.py +3 -4
- dodal/devices/turbo_slit.py +10 -4
- dodal/devices/undulator.py +9 -7
- dodal/devices/util/adjuster_plans.py +1 -2
- dodal/devices/util/lookup_tables.py +38 -0
- dodal/devices/util/test_utils.py +1 -0
- dodal/devices/zebra/zebra.py +4 -0
- dodal/plan_stubs/data_session.py +10 -1
- dodal/plan_stubs/electron_analyser/configure_controller.py +80 -0
- dodal/plans/verify_undulator_gap.py +2 -2
- dodal/devices/electron_analyser/base_region.py +0 -64
- dodal/devices/electron_analyser/specs/specs_region.py +0 -24
- dodal/devices/electron_analyser/vgscienta/__init__.py +0 -0
- dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +0 -77
- dodal/devices/util/motor_utils.py +0 -6
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.44.0.dist-info → dls_dodal-1.46.0.dist-info}/top_level.txt +0 -0
- /dodal/{devices/electron_analyser/specs → plan_stubs/electron_analyser}/__init__.py +0 -0
dodal/devices/i22/dcm.py
CHANGED
|
@@ -5,7 +5,6 @@ from bluesky.protocols import Reading
|
|
|
5
5
|
from event_model.documents.event_descriptor import DataKey
|
|
6
6
|
from ophyd_async.core import (
|
|
7
7
|
Array1D,
|
|
8
|
-
StandardReadable,
|
|
9
8
|
StandardReadableFormat,
|
|
10
9
|
soft_signal_r_and_setter,
|
|
11
10
|
)
|
|
@@ -13,13 +12,18 @@ from ophyd_async.epics.core import epics_signal_r
|
|
|
13
12
|
from ophyd_async.epics.motor import Motor
|
|
14
13
|
|
|
15
14
|
from dodal.common.crystal_metadata import CrystalMetadata
|
|
15
|
+
from dodal.devices.common_dcm import (
|
|
16
|
+
BaseDCM,
|
|
17
|
+
PitchAndRollCrystal,
|
|
18
|
+
RollCrystal,
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
# Conversion constant for energy and wavelength, taken from the X-Ray data booklet
|
|
18
22
|
# Converts between energy in KeV and wavelength in angstrom
|
|
19
23
|
_CONVERSION_CONSTANT = 12.3984
|
|
20
24
|
|
|
21
25
|
|
|
22
|
-
class
|
|
26
|
+
class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
|
|
23
27
|
"""
|
|
24
28
|
A double crystal monochromator (DCM), used to select the energy of the beam.
|
|
25
29
|
|
|
@@ -39,13 +43,7 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
39
43
|
) -> None:
|
|
40
44
|
with self.add_children_as_readables():
|
|
41
45
|
# Positionable Parameters
|
|
42
|
-
self.bragg = Motor(prefix + "BRAGG")
|
|
43
|
-
self.offset = Motor(prefix + "OFFSET")
|
|
44
46
|
self.perp = Motor(prefix + "PERP")
|
|
45
|
-
self.energy = Motor(prefix + "ENERGY")
|
|
46
|
-
self.crystal_1_roll = Motor(prefix + "XTAL1:ROLL")
|
|
47
|
-
self.crystal_2_roll = Motor(prefix + "XTAL2:ROLL")
|
|
48
|
-
self.crystal_2_pitch = Motor(prefix + "XTAL2:PITCH")
|
|
49
47
|
|
|
50
48
|
# Temperatures
|
|
51
49
|
self.backplate_temp = epics_signal_r(float, temperature_prefix + "PT100-7")
|
|
@@ -93,12 +91,12 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
93
91
|
units=crystal_2_metadata.d_spacing[1],
|
|
94
92
|
)
|
|
95
93
|
|
|
96
|
-
super().__init__(name)
|
|
94
|
+
super().__init__(prefix, RollCrystal, PitchAndRollCrystal, name)
|
|
97
95
|
|
|
98
96
|
async def describe(self) -> dict[str, DataKey]:
|
|
99
97
|
default_describe = await super().describe()
|
|
100
98
|
return {
|
|
101
|
-
f"{self.name}-
|
|
99
|
+
f"{self.name}-wavelength_in_a": DataKey(
|
|
102
100
|
dtype="number",
|
|
103
101
|
shape=[],
|
|
104
102
|
source=self.name,
|
|
@@ -109,7 +107,7 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
109
107
|
|
|
110
108
|
async def read(self) -> dict[str, Reading]:
|
|
111
109
|
default_reading = await super().read()
|
|
112
|
-
energy: float = default_reading[f"{self.name}-
|
|
110
|
+
energy: float = default_reading[f"{self.name}-energy_in_kev"]["value"]
|
|
113
111
|
if energy > 0.0:
|
|
114
112
|
wavelength = _CONVERSION_CONSTANT / energy
|
|
115
113
|
else:
|
|
@@ -117,7 +115,7 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
117
115
|
|
|
118
116
|
return {
|
|
119
117
|
**default_reading,
|
|
120
|
-
f"{self.name}-
|
|
118
|
+
f"{self.name}-wavelength_in_a": Reading(
|
|
121
119
|
value=wavelength,
|
|
122
120
|
timestamp=time.time(),
|
|
123
121
|
),
|
dodal/devices/i24/dcm.py
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
from ophyd_async.core import StandardReadable
|
|
2
1
|
from ophyd_async.epics.core import epics_signal_r
|
|
3
|
-
from ophyd_async.epics.motor import Motor
|
|
4
2
|
|
|
3
|
+
from dodal.devices.common_dcm import (
|
|
4
|
+
BaseDCM,
|
|
5
|
+
PitchAndRollCrystal,
|
|
6
|
+
RollCrystal,
|
|
7
|
+
)
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
|
|
10
|
+
class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
|
|
7
11
|
"""
|
|
8
12
|
A double crystal monocromator device, used to select the beam energy.
|
|
9
13
|
"""
|
|
10
14
|
|
|
11
15
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
12
16
|
with self.add_children_as_readables():
|
|
13
|
-
# Motors
|
|
14
|
-
self.bragg_in_degrees = Motor(prefix + "-MO-DCM-01:BRAGG")
|
|
15
|
-
self.x_translation_in_mm = Motor(prefix + "-MO-DCM-01:X")
|
|
16
|
-
self.offset_in_mm = Motor(prefix + "-MO-DCM-01:OFFSET")
|
|
17
|
-
self.gap_in_mm = Motor(prefix + "-MO-DCM-01:GAP")
|
|
18
|
-
self.energy_in_kev = Motor(prefix + "-MO-DCM-01:ENERGY")
|
|
19
|
-
self.xtal1_roll = Motor(prefix + "-MO-DCM-01:XTAL1:ROLL")
|
|
20
|
-
self.xtal2_roll = Motor(prefix + "-MO-DCM-01:XTAL2:ROLL")
|
|
21
|
-
self.xtal2_pitch = Motor(prefix + "-MO-DCM-01:XTAL2:PITCH")
|
|
22
|
-
|
|
23
|
-
# Wavelength is calculated in epics from the energy
|
|
24
|
-
self.wavelength_in_a = epics_signal_r(float, prefix + "-MO-DCM-01:LAMBDA")
|
|
25
|
-
|
|
26
17
|
# Temperatures
|
|
27
18
|
self.xtal1_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-1")
|
|
28
19
|
self.xtal1_heater_temp = epics_signal_r(
|
|
@@ -39,4 +30,4 @@ class DCM(StandardReadable):
|
|
|
39
30
|
self.b1_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-7")
|
|
40
31
|
self.gap_temp = epics_signal_r(float, prefix + "-DI-DCM-01:TC-1")
|
|
41
32
|
|
|
42
|
-
super().__init__(name)
|
|
33
|
+
super().__init__(prefix + "-MO-DCM-01:", RollCrystal, PitchAndRollCrystal, name)
|
dodal/devices/motors.py
CHANGED
|
@@ -38,3 +38,24 @@ class XYZPositioner(StandardReadable):
|
|
|
38
38
|
self.y = Motor(prefix + infix[1])
|
|
39
39
|
self.z = Motor(prefix + infix[2])
|
|
40
40
|
super().__init__(name=name)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SixAxisGonio(XYZPositioner):
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
prefix: str,
|
|
47
|
+
name: str = "",
|
|
48
|
+
infix: tuple[str, str, str, str, str, str] = (
|
|
49
|
+
"X",
|
|
50
|
+
"Y",
|
|
51
|
+
"Z",
|
|
52
|
+
"KAPPA",
|
|
53
|
+
"PHI",
|
|
54
|
+
"OMEGA",
|
|
55
|
+
),
|
|
56
|
+
):
|
|
57
|
+
with self.add_children_as_readables():
|
|
58
|
+
self.kappa = Motor(prefix + infix[3])
|
|
59
|
+
self.phi = Motor(prefix + infix[4])
|
|
60
|
+
self.omega = Motor(prefix + infix[5])
|
|
61
|
+
super().__init__(name=name, prefix=prefix, infix=infix[0:3])
|
dodal/devices/tetramm.py
CHANGED
|
@@ -244,10 +244,9 @@ class TetrammDetector(StandardDetector):
|
|
|
244
244
|
super().__init__(
|
|
245
245
|
controller,
|
|
246
246
|
ADHDFWriter(
|
|
247
|
-
self.hdf,
|
|
248
|
-
path_provider,
|
|
249
|
-
|
|
250
|
-
TetrammDatasetDescriber(controller),
|
|
247
|
+
fileio=self.hdf,
|
|
248
|
+
path_provider=path_provider,
|
|
249
|
+
dataset_describer=TetrammDatasetDescriber(controller),
|
|
251
250
|
plugins=plugins,
|
|
252
251
|
),
|
|
253
252
|
config_signals,
|
dodal/devices/turbo_slit.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from
|
|
1
|
+
from bluesky.protocols import Movable
|
|
2
|
+
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
2
3
|
from ophyd_async.epics.motor import Motor
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
class TurboSlit(StandardReadable):
|
|
6
|
+
class TurboSlit(StandardReadable, Movable[float]):
|
|
6
7
|
"""
|
|
7
8
|
This collection of motors coordinates time resolved XAS experiments.
|
|
8
9
|
It selects a beam out of the polychromatic fan.
|
|
@@ -12,9 +13,9 @@ class TurboSlit(StandardReadable):
|
|
|
12
13
|
The xfine motor can move the slit in x direction at high frequencies for different scans.
|
|
13
14
|
These slits can be scanned continously or in step mode.
|
|
14
15
|
The relationship between the three motors is as follows:
|
|
15
|
-
- arc - position of the middle of the gap (coarse/ macro) extension
|
|
16
|
+
- arc - position of the middle of the gap in degrees (coarse/ macro) extension
|
|
16
17
|
- gap - width in mm, provides energy resolution
|
|
17
|
-
- xfine selects the energy as part of the high frequency scan
|
|
18
|
+
- xfine - main scanning axis in mm, selects the energy as part of the high frequency scan
|
|
18
19
|
"""
|
|
19
20
|
|
|
20
21
|
def __init__(self, prefix: str, name: str = ""):
|
|
@@ -23,3 +24,8 @@ class TurboSlit(StandardReadable):
|
|
|
23
24
|
self.arc = Motor(prefix=prefix + "ARC")
|
|
24
25
|
self.xfine = Motor(prefix=prefix + "XFINE")
|
|
25
26
|
super().__init__(name=name)
|
|
27
|
+
|
|
28
|
+
@AsyncStatus.wrap
|
|
29
|
+
async def set(self, value: float):
|
|
30
|
+
"""This will move the default XFINE"""
|
|
31
|
+
await self.xfine.set(value)
|
dodal/devices/undulator.py
CHANGED
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from bluesky.protocols import Movable
|
|
5
|
-
from numpy import
|
|
5
|
+
from numpy import ndarray
|
|
6
6
|
from ophyd_async.core import (
|
|
7
7
|
AsyncStatus,
|
|
8
8
|
StandardReadable,
|
|
@@ -38,12 +38,12 @@ class UndulatorGapAccess(StrictEnum):
|
|
|
38
38
|
DISABLED = "DISABLED"
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def
|
|
41
|
+
def _get_gap_for_energy(
|
|
42
42
|
dcm_energy_ev: float, energy_to_distance_table: ndarray
|
|
43
43
|
) -> float:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
return np.interp(
|
|
45
|
+
dcm_energy_ev, energy_to_distance_table[:, 0], energy_to_distance_table[:, 1]
|
|
46
|
+
)
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class Undulator(StandardReadable, Movable[float]):
|
|
@@ -115,8 +115,10 @@ class Undulator(StandardReadable, Movable[float]):
|
|
|
115
115
|
|
|
116
116
|
async def _set_undulator_gap(self, energy_kev: float) -> None:
|
|
117
117
|
await self.raise_if_not_enabled()
|
|
118
|
-
LOGGER.info(f"Setting undulator gap to {energy_kev:.2f} kev")
|
|
119
118
|
target_gap = await self._get_gap_to_match_energy(energy_kev)
|
|
119
|
+
LOGGER.info(
|
|
120
|
+
f"Setting undulator gap to {target_gap:.3f}mm based on {energy_kev:.2f}kev"
|
|
121
|
+
)
|
|
120
122
|
|
|
121
123
|
# Check if undulator gap is close enough to the value from the DCM
|
|
122
124
|
current_gap = await self.current_gap.get_value()
|
|
@@ -152,7 +154,7 @@ class Undulator(StandardReadable, Movable[float]):
|
|
|
152
154
|
)
|
|
153
155
|
|
|
154
156
|
# Use the lookup table to get the undulator gap associated with this dcm energy
|
|
155
|
-
return
|
|
157
|
+
return _get_gap_for_energy(
|
|
156
158
|
energy_kev * 1000,
|
|
157
159
|
energy_to_distance_table,
|
|
158
160
|
)
|
|
@@ -7,14 +7,13 @@ from collections.abc import Callable, Generator
|
|
|
7
7
|
|
|
8
8
|
from bluesky import plan_stubs as bps
|
|
9
9
|
from bluesky.utils import Msg
|
|
10
|
-
from ophyd.epics_motor import EpicsMotor
|
|
11
10
|
from ophyd_async.epics.motor import Motor
|
|
12
11
|
|
|
13
12
|
from dodal.log import LOGGER
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def lookup_table_adjuster(
|
|
17
|
-
lookup_table: Callable[[float], float], output_device:
|
|
16
|
+
lookup_table: Callable[[float], float], output_device: Motor, input
|
|
18
17
|
):
|
|
19
18
|
"""Returns a callable that adjusts a value according to a lookup table"""
|
|
20
19
|
|
|
@@ -64,3 +64,41 @@ def linear_interpolation_lut(
|
|
|
64
64
|
return float(interp(s, s_values, t_values))
|
|
65
65
|
|
|
66
66
|
return s_to_t2
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def linear_extrapolation_lut(
|
|
70
|
+
s_values: Sequence[float], t_values: Sequence[float]
|
|
71
|
+
) -> Callable[[float], float]:
|
|
72
|
+
"""
|
|
73
|
+
Return a callable that implements f(s) = t according to the conversion table data
|
|
74
|
+
supplied, with linear extrapolation outside that range. Inside the range of the table,
|
|
75
|
+
the function is equivalent to that returned by linear_interpolation_lut
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
s_values: Values of the independent axis
|
|
79
|
+
t_values: Values of the dependent axis
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A callable that returns t for the given s
|
|
83
|
+
"""
|
|
84
|
+
assert len(s_values) == len(t_values), (
|
|
85
|
+
"Lookup table does not have the same number of values for each axis"
|
|
86
|
+
)
|
|
87
|
+
assert len(s_values) > 1, "Need at least 2 points in the lookup table"
|
|
88
|
+
interp = linear_interpolation_lut(s_values, t_values)
|
|
89
|
+
s_min = s_values[0]
|
|
90
|
+
s_max = s_values[-1]
|
|
91
|
+
|
|
92
|
+
def s_to_t(s: float) -> float:
|
|
93
|
+
if s < s_min:
|
|
94
|
+
return t_values[0] + (s - s_min) * (t_values[1] - t_values[0]) / (
|
|
95
|
+
s_values[1] - s_values[0]
|
|
96
|
+
)
|
|
97
|
+
elif s > s_max:
|
|
98
|
+
return t_values[-1] + (s - s_max) * (t_values[-1] - t_values[-2]) / (
|
|
99
|
+
s_values[-1] - s_values[-2]
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
return interp(s)
|
|
103
|
+
|
|
104
|
+
return s_to_t
|
dodal/devices/util/test_utils.py
CHANGED
|
@@ -11,6 +11,7 @@ def patch_motor(motor: Motor, initial_position=0):
|
|
|
11
11
|
set_mock_value(motor.deadband, 0.001)
|
|
12
12
|
set_mock_value(motor.motor_done_move, 1)
|
|
13
13
|
set_mock_value(motor.velocity, 3)
|
|
14
|
+
set_mock_value(motor.max_velocity, 5)
|
|
14
15
|
return callback_on_mock_put(
|
|
15
16
|
motor.user_setpoint,
|
|
16
17
|
lambda pos, *args, **kwargs: set_mock_value(motor.user_readback, pos),
|
dodal/devices/zebra/zebra.py
CHANGED
dodal/plan_stubs/data_session.py
CHANGED
|
@@ -33,7 +33,11 @@ def attach_data_session_metadata_wrapper(
|
|
|
33
33
|
Yields:
|
|
34
34
|
Iterator[Msg]: Plan messages
|
|
35
35
|
"""
|
|
36
|
-
|
|
36
|
+
try:
|
|
37
|
+
provider = provider or get_path_provider()
|
|
38
|
+
except NameError:
|
|
39
|
+
provider = None
|
|
40
|
+
|
|
37
41
|
if isinstance(provider, UpdatingPathProvider):
|
|
38
42
|
yield from bps.wait_for([provider.update])
|
|
39
43
|
ress = yield from bps.wait_for([provider.data_session])
|
|
@@ -41,6 +45,11 @@ def attach_data_session_metadata_wrapper(
|
|
|
41
45
|
# https://github.com/DiamondLightSource/dodal/issues/452
|
|
42
46
|
# As part of 452, write each dataCollection into their own folder, then can use resource_dir directly
|
|
43
47
|
yield from bpp.inject_md_wrapper(plan, md={DATA_SESSION: data_session})
|
|
48
|
+
elif provider is None:
|
|
49
|
+
logging.warning(
|
|
50
|
+
f"There is no PathProvider set, {attach_data_session_metadata_wrapper.__name__} will have no effect"
|
|
51
|
+
)
|
|
52
|
+
yield from plan
|
|
44
53
|
else:
|
|
45
54
|
logging.warning(
|
|
46
55
|
f"{provider} is not an UpdatingPathProvider, {attach_data_session_metadata_wrapper.__name__} will have no effect"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from bluesky import plan_stubs as bps
|
|
2
|
+
|
|
3
|
+
from dodal.devices.electron_analyser.abstract_analyser_io import (
|
|
4
|
+
AbstractAnalyserDriverIO,
|
|
5
|
+
)
|
|
6
|
+
from dodal.devices.electron_analyser.abstract_region import (
|
|
7
|
+
AbstractBaseRegion,
|
|
8
|
+
)
|
|
9
|
+
from dodal.devices.electron_analyser.specs_analyser_io import (
|
|
10
|
+
SpecsAnalyserDriverIO,
|
|
11
|
+
)
|
|
12
|
+
from dodal.devices.electron_analyser.specs_region import SpecsRegion
|
|
13
|
+
from dodal.devices.electron_analyser.vgscienta_analyser_io import (
|
|
14
|
+
VGScientaAnalyserDriverIO,
|
|
15
|
+
)
|
|
16
|
+
from dodal.devices.electron_analyser.vgscienta_region import (
|
|
17
|
+
VGScientaRegion,
|
|
18
|
+
)
|
|
19
|
+
from dodal.log import LOGGER
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def configure_analyser(
|
|
23
|
+
analyser: AbstractAnalyserDriverIO,
|
|
24
|
+
region: AbstractBaseRegion,
|
|
25
|
+
excitation_energy: float,
|
|
26
|
+
):
|
|
27
|
+
LOGGER.info(f'Configuring analyser with region "{region.name}"')
|
|
28
|
+
low_energy = region.to_kinetic_energy(region.low_energy, excitation_energy)
|
|
29
|
+
high_energy = region.to_kinetic_energy(region.high_energy, excitation_energy)
|
|
30
|
+
pass_energy_type = analyser.pass_energy_type
|
|
31
|
+
pass_energy = pass_energy_type(region.pass_energy)
|
|
32
|
+
# Set detector settings, wait for them all to have completed
|
|
33
|
+
# fmt: off
|
|
34
|
+
yield from bps.mv(
|
|
35
|
+
analyser.low_energy, low_energy,
|
|
36
|
+
analyser.high_energy, high_energy,
|
|
37
|
+
analyser.slices, region.slices,
|
|
38
|
+
analyser.lens_mode, region.lens_mode,
|
|
39
|
+
analyser.pass_energy, pass_energy,
|
|
40
|
+
analyser.iterations, region.iterations,
|
|
41
|
+
analyser.acquisition_mode, region.acquisition_mode,
|
|
42
|
+
)
|
|
43
|
+
# fmt: on
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def configure_specs(
|
|
47
|
+
analyser: SpecsAnalyserDriverIO, region: SpecsRegion, excitation_energy: float
|
|
48
|
+
):
|
|
49
|
+
yield from configure_analyser(analyser, region, excitation_energy)
|
|
50
|
+
# fmt: off
|
|
51
|
+
yield from bps.mv(
|
|
52
|
+
analyser.values, region.values,
|
|
53
|
+
analyser.psu_mode, region.psu_mode,
|
|
54
|
+
)
|
|
55
|
+
# fmt: on
|
|
56
|
+
if region.acquisition_mode == "Fixed Transmission":
|
|
57
|
+
yield from bps.mv(analyser.centre_energy, region.centre_energy)
|
|
58
|
+
|
|
59
|
+
if region.acquisition_mode == "Fixed Energy":
|
|
60
|
+
yield from bps.mv(analyser.energy_step, region.energy_step)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def configure_vgscienta(
|
|
64
|
+
analyser: VGScientaAnalyserDriverIO, region: VGScientaRegion, excitation_energy
|
|
65
|
+
):
|
|
66
|
+
yield from configure_analyser(analyser, region, excitation_energy)
|
|
67
|
+
centre_energy = region.to_kinetic_energy(region.fix_energy, excitation_energy)
|
|
68
|
+
|
|
69
|
+
# fmt: off
|
|
70
|
+
yield from bps.mv(
|
|
71
|
+
analyser.centre_energy, centre_energy,
|
|
72
|
+
analyser.energy_step, region.energy_step,
|
|
73
|
+
analyser.first_x_channel, region.first_x_channel,
|
|
74
|
+
analyser.first_y_channel, region.first_y_channel,
|
|
75
|
+
analyser.x_channel_size, region.x_channel_size(),
|
|
76
|
+
analyser.y_channel_size, region.y_channel_size(),
|
|
77
|
+
analyser.detector_mode, region.detector_mode,
|
|
78
|
+
analyser.image_mode, "Single",
|
|
79
|
+
)
|
|
80
|
+
# fmt: on
|
|
@@ -2,14 +2,14 @@ from typing import Protocol, runtime_checkable
|
|
|
2
2
|
|
|
3
3
|
from bluesky import plan_stubs as bps
|
|
4
4
|
|
|
5
|
-
from dodal.devices.
|
|
5
|
+
from dodal.devices.common_dcm import BaseDCM
|
|
6
6
|
from dodal.devices.undulator import Undulator
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@runtime_checkable
|
|
10
10
|
class CheckUndulatorDevices(Protocol):
|
|
11
11
|
undulator: Undulator
|
|
12
|
-
dcm:
|
|
12
|
+
dcm: BaseDCM
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def verify_undulator_gap(devices: CheckUndulatorDevices):
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from typing import Any, Generic, TypeVar
|
|
3
|
-
|
|
4
|
-
from pydantic import BaseModel, Field, model_validator
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class EnergyMode(str, Enum):
|
|
8
|
-
KINETIC = "Kinetic"
|
|
9
|
-
BINDING = "Binding"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class BaseRegion(BaseModel):
|
|
13
|
-
"""
|
|
14
|
-
Generic region model that holds the data. Specialised region models should inherit
|
|
15
|
-
this to extend functionality.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
name: str = "New_region"
|
|
19
|
-
enabled: bool = False
|
|
20
|
-
slices: int = 1
|
|
21
|
-
iterations: int = 1
|
|
22
|
-
# These ones we need subclasses to provide default values
|
|
23
|
-
lensMode: str
|
|
24
|
-
passEnergy: int | float
|
|
25
|
-
acquisitionMode: str
|
|
26
|
-
lowEnergy: float
|
|
27
|
-
highEnergy: float
|
|
28
|
-
stepTime: float
|
|
29
|
-
energyStep: float
|
|
30
|
-
energyMode: EnergyMode = EnergyMode.KINETIC
|
|
31
|
-
|
|
32
|
-
@model_validator(mode="before")
|
|
33
|
-
@classmethod
|
|
34
|
-
def check_energy_mode(cls, data: Any) -> Any:
|
|
35
|
-
if isinstance(data, dict):
|
|
36
|
-
# convert bindingEnergy to energyMode to make base region more generic
|
|
37
|
-
if "bindingEnergy" in data:
|
|
38
|
-
is_binding_energy = data["bindingEnergy"]
|
|
39
|
-
del data["bindingEnergy"]
|
|
40
|
-
data["energyMode"] = (
|
|
41
|
-
EnergyMode.BINDING if is_binding_energy else EnergyMode.KINETIC
|
|
42
|
-
)
|
|
43
|
-
return data
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
TBaseRegion = TypeVar("TBaseRegion", bound=BaseRegion)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class BaseSequence(BaseModel, Generic[TBaseRegion]):
|
|
50
|
-
"""
|
|
51
|
-
Generic sequence model that holds the list of region data. Specialised sequence models
|
|
52
|
-
should inherit this to extend functionality.
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
regions: list[TBaseRegion] = Field(default_factory=lambda: [])
|
|
56
|
-
|
|
57
|
-
def get_enabled_regions(self) -> list[BaseRegion]:
|
|
58
|
-
return [r for r in self.regions if r.enabled]
|
|
59
|
-
|
|
60
|
-
def get_region_names(self) -> list[str]:
|
|
61
|
-
return [r.name for r in self.regions]
|
|
62
|
-
|
|
63
|
-
def get_enabled_region_names(self) -> list[str]:
|
|
64
|
-
return [r.name for r in self.get_enabled_regions()]
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
from pydantic import Field
|
|
2
|
-
|
|
3
|
-
from dodal.devices.electron_analyser.base_region import BaseRegion, BaseSequence
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class SpecsRegion(BaseRegion):
|
|
7
|
-
# Override base class with defaults
|
|
8
|
-
lensMode: str = "SmallArea"
|
|
9
|
-
passEnergy: int | float = 5.0
|
|
10
|
-
acquisitionMode: str = "Fixed Transmission"
|
|
11
|
-
lowEnergy: float = Field(default=800, alias="startEnergy")
|
|
12
|
-
highEnergy: float = Field(default=850, alias="endEnergy")
|
|
13
|
-
stepTime: float = Field(default=1.0, alias="exposureTime")
|
|
14
|
-
energyStep: float = Field(default=0.1, alias="stepEnergy")
|
|
15
|
-
# Specific to this class
|
|
16
|
-
values: float = 1
|
|
17
|
-
centreEnergy: float = 0
|
|
18
|
-
psuMode: str = "1.5keV"
|
|
19
|
-
acquisitionMode: str = ""
|
|
20
|
-
estimatedTimeInMs: float = 0
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class SpecsSequence(BaseSequence):
|
|
24
|
-
regions: list[SpecsRegion] = Field(default_factory=lambda: [])
|
|
File without changes
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import uuid
|
|
2
|
-
from enum import Enum
|
|
3
|
-
|
|
4
|
-
from ophyd_async.core import StrictEnum
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
|
|
7
|
-
from dodal.devices.electron_analyser.base_region import BaseRegion, BaseSequence
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class Status(str, Enum):
|
|
11
|
-
READY = "Ready"
|
|
12
|
-
RUNNING = "Running"
|
|
13
|
-
COMPLETED = "Completed"
|
|
14
|
-
INVALID = "Invalid"
|
|
15
|
-
ABORTED = "Aborted"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class DetectorMode(StrictEnum):
|
|
19
|
-
ADC = "ADC"
|
|
20
|
-
PULSE_COUNTING = "Pulse Counting"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class AcquisitionMode(str, Enum):
|
|
24
|
-
SWEPT = "Swept"
|
|
25
|
-
FIXED = "Fixed"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class VGScientaRegion(BaseRegion):
|
|
29
|
-
# Override defaults of base region class
|
|
30
|
-
lensMode: str = "Angular45"
|
|
31
|
-
passEnergy: int | float = 5
|
|
32
|
-
acquisitionMode: str = AcquisitionMode.SWEPT
|
|
33
|
-
lowEnergy: float = 8.0
|
|
34
|
-
highEnergy: float = 10.0
|
|
35
|
-
stepTime: float = 1.0
|
|
36
|
-
energyStep: float = Field(default=200.0)
|
|
37
|
-
# Specific to this class
|
|
38
|
-
regionId: str = Field(default=str(uuid.uuid4()))
|
|
39
|
-
excitationEnergySource: str = "source1"
|
|
40
|
-
fixEnergy: float = 9.0
|
|
41
|
-
totalSteps: float = 13.0
|
|
42
|
-
totalTime: float = 13.0
|
|
43
|
-
exposureTime: float = 1.0
|
|
44
|
-
firstXChannel: int = 1
|
|
45
|
-
lastXChannel: int = 1000
|
|
46
|
-
firstYChannel: int = 101
|
|
47
|
-
lastYChannel: int = 800
|
|
48
|
-
detectorMode: DetectorMode = DetectorMode.ADC
|
|
49
|
-
status: Status = Status.READY
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class VGScientaExcitationEnergySource(BaseModel):
|
|
53
|
-
name: str = "source1"
|
|
54
|
-
scannableName: str = ""
|
|
55
|
-
value: float = 0
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class VGScientaSequence(BaseSequence):
|
|
59
|
-
elementSet: str = Field(default="Unknown")
|
|
60
|
-
excitationEnergySources: list[VGScientaExcitationEnergySource] = Field(
|
|
61
|
-
default_factory=lambda: []
|
|
62
|
-
)
|
|
63
|
-
regions: list[VGScientaRegion] = Field(default_factory=lambda: [])
|
|
64
|
-
|
|
65
|
-
def get_excitation_energy_source_by_region(
|
|
66
|
-
self, region: VGScientaRegion
|
|
67
|
-
) -> VGScientaExcitationEnergySource | None:
|
|
68
|
-
filtered_excitation_energy_sources = [
|
|
69
|
-
e
|
|
70
|
-
for e in self.excitationEnergySources
|
|
71
|
-
if e.name == region.excitationEnergySource
|
|
72
|
-
]
|
|
73
|
-
return (
|
|
74
|
-
filtered_excitation_energy_sources[0]
|
|
75
|
-
if len(filtered_excitation_energy_sources) == 1
|
|
76
|
-
else None
|
|
77
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|