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/beamlines/i23.py
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
from
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from ophyd_async.epics.adpilatus import PilatusDetector
|
|
4
|
+
|
|
5
|
+
from dodal.common.beamlines.beamline_utils import (
|
|
6
|
+
device_factory,
|
|
7
|
+
get_path_provider,
|
|
8
|
+
set_path_provider,
|
|
9
|
+
)
|
|
2
10
|
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
|
|
11
|
+
from dodal.common.beamlines.device_helpers import HDF5_SUFFIX
|
|
12
|
+
from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider
|
|
13
|
+
from dodal.devices.motors import SixAxisGonio
|
|
3
14
|
from dodal.devices.oav.pin_image_recognition import PinTipDetection
|
|
15
|
+
from dodal.devices.zebra.zebra import Zebra
|
|
16
|
+
from dodal.devices.zebra.zebra_constants_mapping import (
|
|
17
|
+
ZebraMapping,
|
|
18
|
+
ZebraSources,
|
|
19
|
+
ZebraTTLOutputs,
|
|
20
|
+
)
|
|
21
|
+
from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter
|
|
4
22
|
from dodal.log import set_beamline as set_log_beamline
|
|
5
23
|
from dodal.utils import BeamlinePrefix, get_beamline_name, get_hostname
|
|
6
24
|
|
|
@@ -8,8 +26,22 @@ BL = get_beamline_name("i23")
|
|
|
8
26
|
set_log_beamline(BL)
|
|
9
27
|
set_utils_beamline(BL)
|
|
10
28
|
|
|
29
|
+
set_path_provider(
|
|
30
|
+
StaticVisitPathProvider(
|
|
31
|
+
BL,
|
|
32
|
+
Path("/tmp"),
|
|
33
|
+
client=LocalDirectoryServiceClient(),
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
11
37
|
PREFIX = BeamlinePrefix(BL)
|
|
12
38
|
|
|
39
|
+
I23_ZEBRA_MAPPING = ZebraMapping(
|
|
40
|
+
outputs=ZebraTTLOutputs(TTL_DETECTOR=1, TTL_SHUTTER=4),
|
|
41
|
+
sources=ZebraSources(),
|
|
42
|
+
AND_GATE_FOR_AUTO_SHUTTER=2,
|
|
43
|
+
)
|
|
44
|
+
|
|
13
45
|
|
|
14
46
|
def _is_i23_machine():
|
|
15
47
|
"""
|
|
@@ -22,9 +54,42 @@ def _is_i23_machine():
|
|
|
22
54
|
|
|
23
55
|
@device_factory(skip=lambda: not _is_i23_machine())
|
|
24
56
|
def oav_pin_tip_detection() -> PinTipDetection:
|
|
25
|
-
"""Get the i23 OAV pin-tip detection device"""
|
|
57
|
+
"""Get the i23 OAV pin-tip detection device."""
|
|
26
58
|
|
|
27
59
|
return PinTipDetection(
|
|
28
60
|
f"{PREFIX.beamline_prefix}-DI-OAV-01:",
|
|
29
61
|
"pin_tip_detection",
|
|
30
62
|
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@device_factory()
|
|
66
|
+
def shutter() -> ZebraShutter:
|
|
67
|
+
"""Get the i23 zebra controlled shutter."""
|
|
68
|
+
return ZebraShutter(f"{PREFIX.beamline_prefix}-EA-SHTR-01:", "shutter")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@device_factory()
|
|
72
|
+
def gonio() -> SixAxisGonio:
|
|
73
|
+
"""Get the i23 goniometer"""
|
|
74
|
+
return SixAxisGonio(f"{PREFIX.beamline_prefix}-MO-GONIO-01:")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@device_factory()
|
|
78
|
+
def zebra() -> Zebra:
|
|
79
|
+
"""Get the i23 zebra"""
|
|
80
|
+
return Zebra(
|
|
81
|
+
name="zebra",
|
|
82
|
+
prefix=f"{PREFIX.beamline_prefix}-EA-ZEBRA-01:ZEBRA:",
|
|
83
|
+
mapping=I23_ZEBRA_MAPPING,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@device_factory()
|
|
88
|
+
def pilatus() -> PilatusDetector:
|
|
89
|
+
"""Get the i23 pilatus"""
|
|
90
|
+
return PilatusDetector(
|
|
91
|
+
prefix=f"{PREFIX.beamline_prefix}-EA-PILAT-01:",
|
|
92
|
+
path_provider=get_path_provider(),
|
|
93
|
+
drv_suffix="cam1:",
|
|
94
|
+
fileio_suffix=HDF5_SUFFIX,
|
|
95
|
+
)
|
dodal/beamlines/p38.py
CHANGED
|
@@ -16,7 +16,7 @@ from dodal.common.crystal_metadata import (
|
|
|
16
16
|
)
|
|
17
17
|
from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider
|
|
18
18
|
from dodal.devices.focusing_mirror import FocusingMirror
|
|
19
|
-
from dodal.devices.i22.dcm import
|
|
19
|
+
from dodal.devices.i22.dcm import DCM
|
|
20
20
|
from dodal.devices.i22.fswitch import FSwitch
|
|
21
21
|
from dodal.devices.linkam3 import Linkam3
|
|
22
22
|
from dodal.devices.pressure_jump_cell import PressureJumpCell
|
|
@@ -143,8 +143,8 @@ def hfm() -> FocusingMirror:
|
|
|
143
143
|
|
|
144
144
|
|
|
145
145
|
@device_factory(mock=True)
|
|
146
|
-
def dcm() ->
|
|
147
|
-
return
|
|
146
|
+
def dcm() -> DCM:
|
|
147
|
+
return DCM(
|
|
148
148
|
temperature_prefix=f"{PREFIX.beamline_prefix}-DI-DCM-01:",
|
|
149
149
|
crystal_1_metadata=make_crystal_metadata_from_material(
|
|
150
150
|
MaterialsEnum.Si, (1, 1, 1)
|
dodal/beamlines/p60.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dodal.common.beamlines.beamline_utils import (
|
|
2
|
+
device_factory,
|
|
3
|
+
)
|
|
4
|
+
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
|
|
5
|
+
from dodal.devices.electron_analyser.vgscienta_analyser_io import (
|
|
6
|
+
VGScientaAnalyserDriverIO,
|
|
7
|
+
)
|
|
8
|
+
from dodal.log import set_beamline as set_log_beamline
|
|
9
|
+
from dodal.utils import BeamlinePrefix, get_beamline_name
|
|
10
|
+
|
|
11
|
+
BL = get_beamline_name("p60")
|
|
12
|
+
PREFIX = BeamlinePrefix(BL)
|
|
13
|
+
set_log_beamline(BL)
|
|
14
|
+
set_utils_beamline(BL)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@device_factory()
|
|
18
|
+
def analyser_driver() -> VGScientaAnalyserDriverIO:
|
|
19
|
+
return VGScientaAnalyserDriverIO(
|
|
20
|
+
name="analyser_driver", prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:"
|
|
21
|
+
)
|
dodal/common/visit.py
CHANGED
|
@@ -3,8 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
5
5
|
from aiohttp import ClientSession
|
|
6
|
-
from
|
|
7
|
-
from ophyd_async.core import FilenameProvider, PathInfo, PathProvider
|
|
6
|
+
from ophyd_async.core import FilenameProvider, PathInfo
|
|
8
7
|
from pydantic import BaseModel
|
|
9
8
|
|
|
10
9
|
from dodal.common.types import UpdatingPathProvider
|
|
@@ -151,42 +150,3 @@ class StaticVisitPathProvider(UpdatingPathProvider):
|
|
|
151
150
|
return PathInfo(
|
|
152
151
|
directory_path=self._root, filename=self._filename_provider(device_name)
|
|
153
152
|
)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
DEFAULT_TEMPLATE = "{device_name}-{instrument}-{scan_id}"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
class StartDocumentPathProvider(PathProvider):
|
|
160
|
-
"""A PathProvider that sources from metadata in a RunStart document.
|
|
161
|
-
|
|
162
|
-
This uses metadata from a RunStart document to determine file names and data session
|
|
163
|
-
directories. The file naming defaults to "{device_name}-{instrument}-{scan_id}", so
|
|
164
|
-
the file name is incremented by scan number. A template can be included in the
|
|
165
|
-
StartDocument to allow for custom naming conventions.
|
|
166
|
-
|
|
167
|
-
"""
|
|
168
|
-
|
|
169
|
-
def __init__(self) -> None:
|
|
170
|
-
self._doc = {}
|
|
171
|
-
|
|
172
|
-
def update_run(self, name: str, start_doc: RunStart) -> None:
|
|
173
|
-
"""Cache a start document.
|
|
174
|
-
|
|
175
|
-
This can be plugged into the run engine's subscribe method.
|
|
176
|
-
"""
|
|
177
|
-
if name == "start":
|
|
178
|
-
self._doc = start_doc
|
|
179
|
-
|
|
180
|
-
def __call__(self, device_name: str | None = None) -> PathInfo:
|
|
181
|
-
"""Returns the directory path and filename for a given data_session.
|
|
182
|
-
|
|
183
|
-
The default template for file naming is: "{device_name}-{instrument}-{scan_id}"
|
|
184
|
-
however, this can be changed by providing a template in the start document. For
|
|
185
|
-
example: "template": "custom-{device_name}--{scan_id}".
|
|
186
|
-
|
|
187
|
-
If you do not provide a data_session_directory it will default to "/tmp".
|
|
188
|
-
"""
|
|
189
|
-
template = self._doc.get("template", DEFAULT_TEMPLATE)
|
|
190
|
-
sub_path = template.format_map(self._doc | {"device_name": device_name})
|
|
191
|
-
data_session_directory = Path(self._doc.get("data_session_directory", "/tmp"))
|
|
192
|
-
return PathInfo(directory_path=data_session_directory, filename=sub_path)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from typing import Generic, TypeVar
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
StandardReadable,
|
|
5
|
+
)
|
|
6
|
+
from ophyd_async.epics.core import epics_signal_r
|
|
7
|
+
from ophyd_async.epics.motor import Motor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StationaryCrystal(StandardReadable):
|
|
11
|
+
def __init__(self, prefix):
|
|
12
|
+
super().__init__(prefix)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RollCrystal(StationaryCrystal):
|
|
16
|
+
def __init__(self, prefix):
|
|
17
|
+
with self.add_children_as_readables():
|
|
18
|
+
self.roll_in_mrad = Motor(prefix + "ROLL")
|
|
19
|
+
super().__init__(prefix)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PitchAndRollCrystal(StationaryCrystal):
|
|
23
|
+
def __init__(self, prefix):
|
|
24
|
+
with self.add_children_as_readables():
|
|
25
|
+
self.pitch_in_mrad = Motor(prefix + "PITCH")
|
|
26
|
+
self.roll_in_mrad = Motor(prefix + "ROLL")
|
|
27
|
+
super().__init__(prefix)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Xtal_1 = TypeVar("Xtal_1", bound=StationaryCrystal)
|
|
31
|
+
Xtal_2 = TypeVar("Xtal_2", bound=StationaryCrystal)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BaseDCM(StandardReadable, Generic[Xtal_1, Xtal_2]):
|
|
35
|
+
"""
|
|
36
|
+
Common device for the double crystal monochromator (DCM), used to select the energy of the beam.
|
|
37
|
+
|
|
38
|
+
Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
|
|
39
|
+
each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
|
|
40
|
+
This base device accounts for all combinations of this.
|
|
41
|
+
|
|
42
|
+
This device should act as a parent for beamline-specific DCM's, in which any other missing signals can be added.
|
|
43
|
+
|
|
44
|
+
Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan
|
|
45
|
+
which only requires one crystal which can roll should be typed 'def my_plan(dcm: BaseDCM[RollCrystal, StationaryCrystal])`
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
|
|
50
|
+
) -> None:
|
|
51
|
+
with self.add_children_as_readables():
|
|
52
|
+
# Virtual motor PV's which set the physical motors so that the DCM produces requested
|
|
53
|
+
# wavelength/energy
|
|
54
|
+
self.energy_in_kev = Motor(prefix + "ENERGY")
|
|
55
|
+
self.wavelength_in_a = Motor(prefix + "WAVELENGTH")
|
|
56
|
+
|
|
57
|
+
# Real motors
|
|
58
|
+
self.bragg_in_degrees = Motor(prefix + "BRAGG")
|
|
59
|
+
# Offset ensures that the beam exits the DCM at the same point, regardless of energy.
|
|
60
|
+
self.offset_in_mm = Motor(prefix + "OFFSET")
|
|
61
|
+
|
|
62
|
+
self.crystal_metadata_d_spacing_a = epics_signal_r(
|
|
63
|
+
float, prefix + "DSPACING:RBV"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self._make_crystals(prefix, xtal_1, xtal_2)
|
|
67
|
+
|
|
68
|
+
super().__init__(name)
|
|
69
|
+
|
|
70
|
+
# Prefix convention is different depending on whether there are one or two controllable crystals
|
|
71
|
+
def _make_crystals(self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2]):
|
|
72
|
+
if StationaryCrystal not in [xtal_1, xtal_2]:
|
|
73
|
+
self.xtal_1 = xtal_1(f"{prefix}XTAL1:")
|
|
74
|
+
self.xtal_2 = xtal_2(f"{prefix}XTAL2:")
|
|
75
|
+
else:
|
|
76
|
+
self.xtal_1 = xtal_1(prefix)
|
|
77
|
+
self.xtal_2 = xtal_2(prefix)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from dodal.devices.util.lookup_tables import (
|
|
4
|
+
linear_extrapolation_lut,
|
|
5
|
+
parse_lookup_table,
|
|
6
|
+
)
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
class Axis(Enum):
|
|
@@ -11,12 +14,20 @@ class Axis(Enum):
|
|
|
11
14
|
class DetectorDistanceToBeamXYConverter:
|
|
12
15
|
def __init__(self, lookup_file: str):
|
|
13
16
|
self.lookup_file: str = lookup_file
|
|
14
|
-
|
|
17
|
+
lookup_table_columns: list = parse_lookup_table(self.lookup_file)
|
|
18
|
+
self._d_to_x = linear_extrapolation_lut(
|
|
19
|
+
lookup_table_columns[0], lookup_table_columns[1]
|
|
20
|
+
)
|
|
21
|
+
self._d_to_y = linear_extrapolation_lut(
|
|
22
|
+
lookup_table_columns[0], lookup_table_columns[2]
|
|
23
|
+
)
|
|
15
24
|
|
|
16
25
|
def get_beam_xy_from_det_dist(self, det_dist_mm: float, beam_axis: Axis) -> float:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
return (
|
|
27
|
+
self._d_to_x(det_dist_mm)
|
|
28
|
+
if beam_axis == Axis.X_AXIS
|
|
29
|
+
else self._d_to_y(det_dist_mm)
|
|
30
|
+
)
|
|
20
31
|
|
|
21
32
|
def get_beam_axis_pixels(
|
|
22
33
|
self,
|
|
@@ -41,21 +52,3 @@ class DetectorDistanceToBeamXYConverter:
|
|
|
41
52
|
return self.get_beam_axis_pixels(
|
|
42
53
|
det_distance, image_size_pixels, det_dim, Axis.X_AXIS
|
|
43
54
|
)
|
|
44
|
-
|
|
45
|
-
def reload_lookup_table(self):
|
|
46
|
-
self.lookup_table_values = self.parse_table()
|
|
47
|
-
|
|
48
|
-
def parse_table(self) -> list:
|
|
49
|
-
rows = loadtxt(self.lookup_file, delimiter=" ", comments=["#", "Units"])
|
|
50
|
-
columns = list(zip(*rows, strict=False))
|
|
51
|
-
|
|
52
|
-
return columns
|
|
53
|
-
|
|
54
|
-
def __eq__(self, other):
|
|
55
|
-
if not isinstance(other, DetectorDistanceToBeamXYConverter):
|
|
56
|
-
return NotImplemented
|
|
57
|
-
if self.lookup_file != other.lookup_file:
|
|
58
|
-
return False
|
|
59
|
-
if self.lookup_table_values != other.lookup_table_values:
|
|
60
|
-
return False
|
|
61
|
-
return True
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from enum import Enum, auto
|
|
2
|
+
from functools import cached_property
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from pydantic import BaseModel, Field, field_serializer, field_validator
|
|
@@ -49,7 +50,7 @@ class DetectorParams(BaseModel):
|
|
|
49
50
|
False # Remove in https://github.com/DiamondLightSource/hyperion/issues/1395
|
|
50
51
|
)
|
|
51
52
|
|
|
52
|
-
@
|
|
53
|
+
@cached_property
|
|
53
54
|
def beam_xy_converter(self) -> DetectorDistanceToBeamXYConverter:
|
|
54
55
|
return DetectorDistanceToBeamXYConverter(self.det_dist_to_beam_converter_path)
|
|
55
56
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import TypeVar
|
|
3
|
+
|
|
4
|
+
from ophyd_async.core import StandardReadable
|
|
5
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
6
|
+
|
|
7
|
+
from dodal.devices.electron_analyser.abstract_region import EnergyMode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AbstractAnalyserDriverIO(ABC, StandardReadable):
|
|
11
|
+
"""
|
|
12
|
+
Generic device to configure electron analyser with new region settings.
|
|
13
|
+
Electron analysers should inherit from this class for further specialisation.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
17
|
+
with self.add_children_as_readables():
|
|
18
|
+
self.low_energy = epics_signal_rw(float, prefix + "LOW_ENERGY")
|
|
19
|
+
self.high_energy = epics_signal_rw(float, prefix + "HIGH_ENERGY")
|
|
20
|
+
self.slices = epics_signal_rw(int, prefix + "SLICES")
|
|
21
|
+
self.lens_mode = epics_signal_rw(str, prefix + "LENS_MODE")
|
|
22
|
+
self.pass_energy = epics_signal_rw(
|
|
23
|
+
self.pass_energy_type, prefix + "PASS_ENERGY"
|
|
24
|
+
)
|
|
25
|
+
self.energy_step = epics_signal_rw(float, prefix + "STEP_SIZE")
|
|
26
|
+
self.iterations = epics_signal_rw(int, prefix + "NumExposures")
|
|
27
|
+
self.acquisition_mode = epics_signal_rw(str, prefix + "ACQ_MODE")
|
|
28
|
+
|
|
29
|
+
super().__init__(name)
|
|
30
|
+
|
|
31
|
+
def to_kinetic_energy(
|
|
32
|
+
self, value: float, excitation_energy: float, mode: EnergyMode
|
|
33
|
+
) -> float:
|
|
34
|
+
return excitation_energy - value if mode == EnergyMode.BINDING else value
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def pass_energy_type(self) -> type:
|
|
39
|
+
"""
|
|
40
|
+
Return the type the pass_energy should be. Each one is unfortunately different
|
|
41
|
+
for the underlying analyser software and cannot be changed on epics side.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
TAbstractAnalyserDriverIO = TypeVar(
|
|
46
|
+
"TAbstractAnalyserDriverIO", bound=AbstractAnalyserDriverIO
|
|
47
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, model_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def java_to_python_case(java_str: str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Convert a camelCase Java-style string to a snake_case Python-style string.
|
|
13
|
+
|
|
14
|
+
:param java_str: The Java-style camelCase string.
|
|
15
|
+
:return: The Python-style snake_case string.
|
|
16
|
+
"""
|
|
17
|
+
new_value = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", java_str)
|
|
18
|
+
new_value = re.sub("([a-z0-9])([A-Z])", r"\1_\2", new_value).lower()
|
|
19
|
+
return new_value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def switch_case_validation(data: dict, f: Callable[[str], str]) -> dict:
|
|
23
|
+
return {f(key): value for key, value in data.items()}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JavaToPythonModel(BaseModel):
|
|
27
|
+
@model_validator(mode="before")
|
|
28
|
+
@classmethod
|
|
29
|
+
def before_validation(cls, data: dict) -> dict:
|
|
30
|
+
data = switch_case_validation(data, java_to_python_case)
|
|
31
|
+
return data
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def energy_mode_validation(data: dict) -> dict:
|
|
35
|
+
# Convert binding_energy to energy_mode to make base region more generic
|
|
36
|
+
if "binding_energy" in data:
|
|
37
|
+
is_binding_energy = data["binding_energy"]
|
|
38
|
+
del data["binding_energy"]
|
|
39
|
+
data["energy_mode"] = (
|
|
40
|
+
EnergyMode.BINDING if is_binding_energy else EnergyMode.KINETIC
|
|
41
|
+
)
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EnergyMode(str, Enum):
|
|
46
|
+
KINETIC = "Kinetic"
|
|
47
|
+
BINDING = "Binding"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AbstractBaseRegion(ABC, JavaToPythonModel):
|
|
51
|
+
"""
|
|
52
|
+
Generic region model that holds the data. Specialised region models should inherit
|
|
53
|
+
this to extend functionality. All energy units are assumed to be in eV.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
name: str = "New_region"
|
|
57
|
+
enabled: bool = False
|
|
58
|
+
slices: int = 1
|
|
59
|
+
iterations: int = 1
|
|
60
|
+
# These ones we need subclasses to provide default values
|
|
61
|
+
lens_mode: str
|
|
62
|
+
pass_energy: int
|
|
63
|
+
acquisition_mode: str
|
|
64
|
+
low_energy: float
|
|
65
|
+
high_energy: float
|
|
66
|
+
step_time: float
|
|
67
|
+
energy_step: float # in eV
|
|
68
|
+
energy_mode: EnergyMode = EnergyMode.KINETIC
|
|
69
|
+
|
|
70
|
+
def is_binding_energy(self) -> bool:
|
|
71
|
+
return self.energy_mode == EnergyMode.BINDING
|
|
72
|
+
|
|
73
|
+
def is_kinetic_energy(self) -> bool:
|
|
74
|
+
return self.energy_mode == EnergyMode.KINETIC
|
|
75
|
+
|
|
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
|
+
@model_validator(mode="before")
|
|
80
|
+
@classmethod
|
|
81
|
+
def before_validation(cls, data: dict) -> dict:
|
|
82
|
+
data = switch_case_validation(data, java_to_python_case)
|
|
83
|
+
return energy_mode_validation(data)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
TAbstractBaseRegion = TypeVar("TAbstractBaseRegion", bound=AbstractBaseRegion)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AbstractBaseSequence(ABC, JavaToPythonModel, Generic[TAbstractBaseRegion]):
|
|
90
|
+
"""
|
|
91
|
+
Generic sequence model that holds the list of region data. Specialised sequence
|
|
92
|
+
models should inherit this to extend functionality and define type of region to
|
|
93
|
+
hold.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
version: float = 0.1 # If file format changes within prod, increment this number!
|
|
97
|
+
regions: list[TAbstractBaseRegion] = Field(default_factory=lambda: [])
|
|
98
|
+
|
|
99
|
+
def get_enabled_regions(self) -> list[TAbstractBaseRegion]:
|
|
100
|
+
return [r for r in self.regions if r.enabled]
|
|
101
|
+
|
|
102
|
+
def get_region_names(self) -> list[str]:
|
|
103
|
+
return [r.name for r in self.regions]
|
|
104
|
+
|
|
105
|
+
def get_enabled_region_names(self) -> list[str]:
|
|
106
|
+
return [r.name for r in self.get_enabled_regions()]
|
|
107
|
+
|
|
108
|
+
def get_region_by_name(self, name: str) -> TAbstractBaseRegion | None:
|
|
109
|
+
return next((region for region in self.regions if region.name == name), None)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
TAbstractBaseSequence = TypeVar("TAbstractBaseSequence", bound=AbstractBaseSequence)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
2
|
+
|
|
3
|
+
from dodal.devices.electron_analyser.abstract_analyser_io import (
|
|
4
|
+
AbstractAnalyserDriverIO,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO):
|
|
9
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
10
|
+
with self.add_children_as_readables():
|
|
11
|
+
self.psu_mode = epics_signal_rw(str, prefix + "SCAN_RANGE")
|
|
12
|
+
self.values = epics_signal_rw(int, prefix + "VALUES")
|
|
13
|
+
self.centre_energy = epics_signal_rw(float, prefix + "KINETIC_ENERGY")
|
|
14
|
+
|
|
15
|
+
super().__init__(prefix, name)
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def pass_energy_type(self) -> type:
|
|
19
|
+
return float
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pydantic import Field
|
|
2
|
+
|
|
3
|
+
from dodal.devices.electron_analyser.abstract_region import (
|
|
4
|
+
AbstractBaseRegion,
|
|
5
|
+
AbstractBaseSequence,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SpecsRegion(AbstractBaseRegion):
|
|
10
|
+
# Override base class with defaults
|
|
11
|
+
lens_mode: str = "SmallArea"
|
|
12
|
+
pass_energy: int = 5
|
|
13
|
+
acquisition_mode: str = "Fixed Transmission"
|
|
14
|
+
low_energy: float = Field(default=800, alias="start_energy")
|
|
15
|
+
high_energy: float = Field(default=850, alias="end_energy")
|
|
16
|
+
step_time: float = Field(default=1.0, alias="exposure_time")
|
|
17
|
+
energy_step: float = Field(default=0.1, alias="step_energy")
|
|
18
|
+
# Specific to this class
|
|
19
|
+
values: int = 1
|
|
20
|
+
centre_energy: float = 0
|
|
21
|
+
psu_mode: str = "1.5keV"
|
|
22
|
+
estimated_time_in_ms: float = 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SpecsSequence(AbstractBaseSequence[SpecsRegion]):
|
|
26
|
+
regions: list[SpecsRegion] = Field(default_factory=lambda: [])
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
2
|
+
|
|
3
|
+
from dodal.devices.electron_analyser.abstract_analyser_io import (
|
|
4
|
+
AbstractAnalyserDriverIO,
|
|
5
|
+
)
|
|
6
|
+
from dodal.devices.electron_analyser.vgscienta_region import (
|
|
7
|
+
DetectorMode,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO):
|
|
12
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
13
|
+
with self.add_children_as_readables():
|
|
14
|
+
self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
|
|
15
|
+
self.first_x_channel = epics_signal_rw(int, prefix + "MinX")
|
|
16
|
+
self.first_y_channel = epics_signal_rw(int, prefix + "MinY")
|
|
17
|
+
self.x_channel_size = epics_signal_rw(int, prefix + "SizeX")
|
|
18
|
+
self.y_channel_size = epics_signal_rw(int, prefix + "SizeY")
|
|
19
|
+
self.detector_mode = epics_signal_rw(DetectorMode, prefix + "DETECTOR_MODE")
|
|
20
|
+
self.image_mode = epics_signal_rw(str, prefix + "ImageMode")
|
|
21
|
+
|
|
22
|
+
super().__init__(prefix, name)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def pass_energy_type(self) -> type:
|
|
26
|
+
return str
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from ophyd_async.core import StrictEnum
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from dodal.devices.electron_analyser.abstract_region import (
|
|
8
|
+
AbstractBaseRegion,
|
|
9
|
+
AbstractBaseSequence,
|
|
10
|
+
JavaToPythonModel,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Status(str, Enum):
|
|
15
|
+
READY = "Ready"
|
|
16
|
+
RUNNING = "Running"
|
|
17
|
+
COMPLETED = "Completed"
|
|
18
|
+
INVALID = "Invalid"
|
|
19
|
+
ABORTED = "Aborted"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DetectorMode(StrictEnum):
|
|
23
|
+
ADC = "ADC"
|
|
24
|
+
PULSE_COUNTING = "Pulse Counting"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AcquisitionMode(str, Enum):
|
|
28
|
+
SWEPT = "Swept"
|
|
29
|
+
FIXED = "Fixed"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class VGScientaRegion(AbstractBaseRegion):
|
|
33
|
+
# Override defaults of base region class
|
|
34
|
+
lens_mode: str = "Angular45"
|
|
35
|
+
pass_energy: int = 5
|
|
36
|
+
acquisition_mode: str = AcquisitionMode.SWEPT
|
|
37
|
+
low_energy: float = 8.0
|
|
38
|
+
high_energy: float = 10.0
|
|
39
|
+
step_time: float = 1.0
|
|
40
|
+
energy_step: float = Field(default=200.0)
|
|
41
|
+
# Specific to this class
|
|
42
|
+
id: str = Field(default=str(uuid.uuid4()), alias="region_id")
|
|
43
|
+
excitation_energy_source: str = "source1"
|
|
44
|
+
fix_energy: float = 9.0
|
|
45
|
+
total_steps: float = 13.0
|
|
46
|
+
total_time: float = 13.0
|
|
47
|
+
exposure_time: float = 1.0
|
|
48
|
+
first_x_channel: int = 1
|
|
49
|
+
last_x_channel: int = 1000
|
|
50
|
+
first_y_channel: int = 101
|
|
51
|
+
last_y_channel: int = 800
|
|
52
|
+
detector_mode: DetectorMode = DetectorMode.ADC
|
|
53
|
+
status: Status = Status.READY
|
|
54
|
+
|
|
55
|
+
def x_channel_size(self) -> int:
|
|
56
|
+
return self.last_x_channel - self.first_x_channel + 1
|
|
57
|
+
|
|
58
|
+
def y_channel_size(self) -> int:
|
|
59
|
+
return self.last_y_channel - self.first_y_channel + 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class VGScientaExcitationEnergySource(JavaToPythonModel):
|
|
63
|
+
name: str = "source1"
|
|
64
|
+
device_name: str = Field(default="", alias="scannable_name")
|
|
65
|
+
value: float = 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class VGScientaSequence(AbstractBaseSequence[VGScientaRegion]):
|
|
69
|
+
element_set: str = Field(default="Unknown")
|
|
70
|
+
excitation_energy_sources: list[VGScientaExcitationEnergySource] = Field(
|
|
71
|
+
default_factory=lambda: []
|
|
72
|
+
)
|
|
73
|
+
regions: list[VGScientaRegion] = Field(default_factory=lambda: [])
|
|
74
|
+
|
|
75
|
+
def get_excitation_energy_source_by_region(
|
|
76
|
+
self, region: VGScientaRegion
|
|
77
|
+
) -> VGScientaExcitationEnergySource:
|
|
78
|
+
value = next(
|
|
79
|
+
(
|
|
80
|
+
e
|
|
81
|
+
for e in self.excitation_energy_sources
|
|
82
|
+
if region.excitation_energy_source == e.name
|
|
83
|
+
),
|
|
84
|
+
None,
|
|
85
|
+
)
|
|
86
|
+
if value is None:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f'Unable to find excitation energy source using region "{region.name}"'
|
|
89
|
+
)
|
|
90
|
+
return value
|