dls-dodal 1.52.0__py3-none-any.whl → 1.54.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.52.0.dist-info → dls_dodal-1.54.0.dist-info}/METADATA +4 -6
- {dls_dodal-1.52.0.dist-info → dls_dodal-1.54.0.dist-info}/RECORD +53 -47
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +1 -0
- dodal/beamlines/adsim.py +12 -11
- dodal/beamlines/b01_1.py +2 -2
- dodal/beamlines/b07.py +6 -3
- dodal/beamlines/b07_1.py +17 -6
- dodal/beamlines/b21.py +4 -4
- dodal/beamlines/i04.py +12 -7
- dodal/beamlines/i05.py +22 -0
- dodal/beamlines/i05_1.py +17 -0
- dodal/beamlines/i09.py +7 -3
- dodal/beamlines/i09_1.py +6 -3
- dodal/beamlines/i19_1.py +1 -1
- dodal/beamlines/i20_1.py +38 -9
- dodal/beamlines/p60.py +13 -3
- dodal/common/beamlines/beamline_parameters.py +1 -1
- dodal/devices/b07/__init__.py +2 -2
- dodal/devices/b07/enums.py +15 -0
- dodal/devices/b07_1/__init__.py +10 -1
- dodal/devices/b07_1/ccmc.py +79 -0
- dodal/devices/b07_1/enums.py +3 -0
- dodal/devices/electron_analyser/abstract/base_driver_io.py +25 -48
- dodal/devices/electron_analyser/abstract/base_region.py +9 -11
- dodal/devices/electron_analyser/abstract/types.py +12 -0
- dodal/devices/electron_analyser/specs/detector.py +9 -9
- dodal/devices/electron_analyser/specs/driver_io.py +54 -21
- dodal/devices/electron_analyser/specs/region.py +13 -8
- dodal/devices/electron_analyser/types.py +15 -6
- dodal/devices/electron_analyser/vgscienta/detector.py +18 -8
- dodal/devices/electron_analyser/vgscienta/driver_io.py +62 -24
- dodal/devices/electron_analyser/vgscienta/region.py +33 -16
- dodal/devices/i05/__init__.py +3 -0
- dodal/devices/i05/enums.py +8 -0
- dodal/devices/i09/__init__.py +2 -2
- dodal/devices/i09/enums.py +16 -0
- dodal/devices/i09_1/__init__.py +2 -2
- dodal/devices/i09_1/enums.py +13 -0
- dodal/devices/i13_1/merlin_controller.py +1 -1
- dodal/devices/i19/beamstop.py +2 -2
- dodal/devices/i24/aperture.py +1 -1
- dodal/devices/oav/oav_to_redis_forwarder.py +1 -1
- dodal/devices/oav/pin_image_recognition/__init__.py +1 -2
- dodal/devices/p60/__init__.py +8 -2
- dodal/devices/p60/enums.py +16 -0
- dodal/devices/robot.py +6 -3
- dodal/devices/tetramm.py +1 -2
- dodal/utils.py +3 -10
- {dls_dodal-1.52.0.dist-info → dls_dodal-1.54.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.52.0.dist-info → dls_dodal-1.54.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.52.0.dist-info → dls_dodal-1.54.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.52.0.dist-info → dls_dodal-1.54.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/i20_1.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
+
from ophyd_async.epics.motor import Motor
|
|
4
|
+
from ophyd_async.fastcs.panda import HDFPanda
|
|
5
|
+
|
|
3
6
|
from dodal.common.beamlines.beamline_utils import (
|
|
4
7
|
device_factory,
|
|
8
|
+
get_path_provider,
|
|
5
9
|
set_path_provider,
|
|
6
10
|
)
|
|
7
11
|
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
|
|
8
12
|
from dodal.common.visit import RemoteDirectoryServiceClient, StaticVisitPathProvider
|
|
9
|
-
from dodal.devices.synchrotron import Synchrotron
|
|
10
13
|
from dodal.devices.turbo_slit import TurboSlit
|
|
11
14
|
from dodal.devices.xspress3.xspress3 import Xspress3
|
|
12
15
|
from dodal.log import set_beamline as set_log_beamline
|
|
@@ -31,9 +34,14 @@ set_path_provider(
|
|
|
31
34
|
)
|
|
32
35
|
)
|
|
33
36
|
|
|
37
|
+
"""
|
|
38
|
+
NOTE: Due to the CA gateway machine being switched off, PVs are not available remotely
|
|
39
|
+
and you need to be on the beamline network to access them.
|
|
40
|
+
The simplest way to do this is to `ssh i20-1-ws001` and run dodal connect i20_1 from there.
|
|
41
|
+
"""
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
@device_factory(
|
|
43
|
+
|
|
44
|
+
@device_factory()
|
|
37
45
|
def turbo_slit() -> TurboSlit:
|
|
38
46
|
"""
|
|
39
47
|
turboslit for selecting energy from the polychromator
|
|
@@ -42,18 +50,39 @@ def turbo_slit() -> TurboSlit:
|
|
|
42
50
|
return TurboSlit(f"{PREFIX.beamline_prefix}-OP-PCHRO-01:TS:")
|
|
43
51
|
|
|
44
52
|
|
|
53
|
+
@device_factory()
|
|
54
|
+
def turbo_slit_x() -> Motor:
|
|
55
|
+
"""
|
|
56
|
+
turbo slit x motor
|
|
57
|
+
"""
|
|
58
|
+
return Motor(f"{PREFIX.beamline_prefix}-OP-PCHRO-01:TS:XFINE")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@device_factory()
|
|
62
|
+
def panda() -> HDFPanda:
|
|
63
|
+
return HDFPanda(
|
|
64
|
+
f"{PREFIX.beamline_prefix}-EA-PANDA-02:", path_provider=get_path_provider()
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Use mock device until motors are reconnected on the beamline
|
|
69
|
+
@device_factory(mock=True)
|
|
70
|
+
def alignment_x() -> Motor:
|
|
71
|
+
return Motor(f"{PREFIX.beamline_prefix}-MO-STAGE-01:X")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Use mock device until motors are reconnected on the beamline
|
|
75
|
+
@device_factory(mock=True)
|
|
76
|
+
def alignment_y() -> Motor:
|
|
77
|
+
return Motor(f"{PREFIX.beamline_prefix}-MO-STAGE-01:Y")
|
|
78
|
+
|
|
79
|
+
|
|
45
80
|
@device_factory(skip=True)
|
|
46
81
|
def xspress3() -> Xspress3:
|
|
47
82
|
"""
|
|
48
83
|
16 channels Xspress3 detector
|
|
49
84
|
"""
|
|
50
|
-
|
|
51
85
|
return Xspress3(
|
|
52
86
|
f"{PREFIX.beamline_prefix}-EA-DET-03:",
|
|
53
87
|
num_channels=16,
|
|
54
88
|
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@device_factory()
|
|
58
|
-
def synchrotron() -> Synchrotron:
|
|
59
|
-
return Synchrotron()
|
dodal/beamlines/p60.py
CHANGED
|
@@ -3,7 +3,13 @@ from dodal.common.beamlines.beamline_utils import (
|
|
|
3
3
|
)
|
|
4
4
|
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
|
|
5
5
|
from dodal.devices.electron_analyser.vgscienta import VGScientaAnalyserDriverIO
|
|
6
|
-
from dodal.devices.p60 import
|
|
6
|
+
from dodal.devices.p60 import (
|
|
7
|
+
LabXraySource,
|
|
8
|
+
LabXraySourceReadable,
|
|
9
|
+
LensMode,
|
|
10
|
+
PassEnergy,
|
|
11
|
+
PsuMode,
|
|
12
|
+
)
|
|
7
13
|
from dodal.log import set_beamline as set_log_beamline
|
|
8
14
|
from dodal.utils import BeamlinePrefix, get_beamline_name
|
|
9
15
|
|
|
@@ -23,14 +29,18 @@ def mg_kalpha_source() -> LabXraySourceReadable:
|
|
|
23
29
|
return LabXraySourceReadable(LabXraySource.MG_KALPHA)
|
|
24
30
|
|
|
25
31
|
|
|
32
|
+
# Connect will work again after this work completed
|
|
33
|
+
# https://jira.diamond.ac.uk/browse/P60-13
|
|
26
34
|
@device_factory()
|
|
27
|
-
def analyser_driver() -> VGScientaAnalyserDriverIO[LensMode]:
|
|
35
|
+
def analyser_driver() -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]:
|
|
28
36
|
energy_sources = {
|
|
29
37
|
"source1": al_kalpha_source().energy_ev,
|
|
30
38
|
"source2": mg_kalpha_source().energy_ev,
|
|
31
39
|
}
|
|
32
|
-
return VGScientaAnalyserDriverIO[LensMode](
|
|
40
|
+
return VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy](
|
|
33
41
|
prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:",
|
|
34
42
|
lens_mode_type=LensMode,
|
|
43
|
+
psu_mode_type=PsuMode,
|
|
44
|
+
pass_energy_type=PassEnergy,
|
|
35
45
|
energy_sources=energy_sources,
|
|
36
46
|
)
|
|
@@ -8,7 +8,7 @@ BEAMLINE_PARAMETER_KEYWORDS = ["FB", "FULL", "deadtime"]
|
|
|
8
8
|
|
|
9
9
|
BEAMLINE_PARAMETER_PATHS = {
|
|
10
10
|
"i03": "/dls_sw/i03/software/daq_configuration/domain/beamlineParameters",
|
|
11
|
-
"i04": "/dls_sw/i04/software/gda_versions/
|
|
11
|
+
"i04": "/dls_sw/i04/software/gda_versions/gda_9_37/workspace_git/gda-mx.git/configurations/i04-config/scripts/beamlineParameters",
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
|
dodal/devices/b07/__init__.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from dodal.devices.b07.enums import Grating, LensMode
|
|
1
|
+
from dodal.devices.b07.enums import Grating, LensMode, PsuMode
|
|
2
2
|
|
|
3
|
-
__all__ = ["Grating", "LensMode"]
|
|
3
|
+
__all__ = ["Grating", "LensMode", "PsuMode"]
|
dodal/devices/b07/enums.py
CHANGED
|
@@ -21,4 +21,19 @@ class LensMode(SupersetEnum):
|
|
|
21
21
|
MEDIUM_AREA = "MediumArea"
|
|
22
22
|
SMALL_AREA = "SmallArea"
|
|
23
23
|
HIGH_MAGNIFICATION2 = "HighMagnification2"
|
|
24
|
+
# This is connected to the device separately and will only have "Not connected" as
|
|
25
|
+
# option if disconnected. Once it is connected, "Not connected" is replaced with the
|
|
26
|
+
# options above. This is also why this must be a SupersetEnum.
|
|
27
|
+
NOT_CONNECTED = "Not connected"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PsuMode(SupersetEnum):
|
|
31
|
+
V3500 = "3.5kV"
|
|
32
|
+
V1500 = "1.5kV"
|
|
33
|
+
V400 = "400V"
|
|
34
|
+
V100 = "100V"
|
|
35
|
+
V10 = "10V"
|
|
36
|
+
# This is connected to the device separately and will only have "Not connected" as
|
|
37
|
+
# option if disconnected. Once it is connected, "Not connected" is replaced with the
|
|
38
|
+
# options above. This is also why this must be a SupersetEnum.
|
|
24
39
|
NOT_CONNECTED = "Not connected"
|
dodal/devices/b07_1/__init__.py
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
from dodal.devices.b07_1.ccmc import (
|
|
2
|
+
ChannelCutMonochromator,
|
|
3
|
+
ChannelCutMonochromatorPositions,
|
|
4
|
+
)
|
|
1
5
|
from dodal.devices.b07_1.enums import Grating, LensMode
|
|
2
6
|
|
|
3
|
-
__all__ = [
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Grating",
|
|
9
|
+
"LensMode",
|
|
10
|
+
"ChannelCutMonochromator",
|
|
11
|
+
"ChannelCutMonochromatorPositions",
|
|
12
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from bluesky.protocols import Movable
|
|
2
|
+
from ophyd_async.core import (
|
|
3
|
+
AsyncStatus,
|
|
4
|
+
StandardReadable,
|
|
5
|
+
StrictEnum,
|
|
6
|
+
derived_signal_r,
|
|
7
|
+
)
|
|
8
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
9
|
+
|
|
10
|
+
from dodal.devices.motors import XYZStage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChannelCutMonochromatorPositions(StrictEnum):
|
|
14
|
+
OUT = "Out of Beam"
|
|
15
|
+
XTAL_2000 = "Xtal_2000"
|
|
16
|
+
XTAL_2250 = "Xtal_2250"
|
|
17
|
+
XTAL_2500 = "Xtal_2500"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ccmc_lower_limit = 1500.0
|
|
21
|
+
ccmc_upper_limit = 3000.0
|
|
22
|
+
error_message = "Can not get energy value in eV from ccmc position: "
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChannelCutMonochromator(
|
|
26
|
+
StandardReadable, Movable[ChannelCutMonochromatorPositions]
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Device to move the channel cut monochromator (ccmc). CCMC has three
|
|
30
|
+
choices of crystal (Xtal for short). Setting energy is by means of a
|
|
31
|
+
multi-positioner. The positions are named after the nominal energies of the
|
|
32
|
+
crystals. To change energy select one of the crystals from the list.
|
|
33
|
+
This causes the Y motor to move that crystal into the beam and other
|
|
34
|
+
motors have to align the angles correctly.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
prefix: str,
|
|
40
|
+
name: str = "",
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
prefix:
|
|
46
|
+
Beamline specific part of the PV
|
|
47
|
+
name:
|
|
48
|
+
Name of the device
|
|
49
|
+
"""
|
|
50
|
+
with self.add_children_as_readables():
|
|
51
|
+
# crystal motors
|
|
52
|
+
self._xyz = XYZStage(prefix)
|
|
53
|
+
# piezo motor in epics
|
|
54
|
+
self._y_rotation = epics_signal_rw(
|
|
55
|
+
float,
|
|
56
|
+
read_pv=prefix + "ROTY:POS:RD",
|
|
57
|
+
write_pv=prefix + "ROTY:MOV:WR",
|
|
58
|
+
)
|
|
59
|
+
# Must be a CHILD as read() must return this signal
|
|
60
|
+
self.crystal = epics_signal_rw(
|
|
61
|
+
ChannelCutMonochromatorPositions, prefix + "CRYSTAL:MP:SELECT"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# energy derived signal as property
|
|
65
|
+
self.energy_in_ev = derived_signal_r(
|
|
66
|
+
self._convert_pos_to_ev, pos_signal=self.crystal
|
|
67
|
+
)
|
|
68
|
+
super().__init__(name=name)
|
|
69
|
+
|
|
70
|
+
def _convert_pos_to_ev(self, pos_signal: ChannelCutMonochromatorPositions) -> float:
|
|
71
|
+
if pos_signal != ChannelCutMonochromatorPositions.OUT:
|
|
72
|
+
energy = float(str(pos_signal.value).split("Xtal_")[1])
|
|
73
|
+
if ccmc_lower_limit < energy < ccmc_upper_limit:
|
|
74
|
+
return energy
|
|
75
|
+
raise ValueError(error_message)
|
|
76
|
+
|
|
77
|
+
@AsyncStatus.wrap
|
|
78
|
+
async def set(self, value: ChannelCutMonochromatorPositions) -> None:
|
|
79
|
+
await self.crystal.set(value, wait=True)
|
dodal/devices/b07_1/enums.py
CHANGED
|
@@ -15,4 +15,7 @@ class LensMode(SupersetEnum):
|
|
|
15
15
|
ANGLE_RESOLVED_MODE_22 = "AngleResolvedMode22"
|
|
16
16
|
ANGLE_RESOLVED_MODE_30 = "AngleResolvedMode30"
|
|
17
17
|
LARGE_AREA = "LargeArea"
|
|
18
|
+
# This is connected to the device separately and will only have "Not connected" as
|
|
19
|
+
# option if disconnected. Once it is connected, "Not connected" is replaced with the
|
|
20
|
+
# options above. This is also why this must be a SupersetEnum.
|
|
18
21
|
NOT_CONNECTED = "Not connected"
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
from abc import ABC, abstractmethod
|
|
3
2
|
from collections.abc import Mapping
|
|
4
3
|
from typing import Generic, TypeVar
|
|
@@ -19,11 +18,15 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
|
19
18
|
|
|
20
19
|
from dodal.devices.electron_analyser.abstract.base_region import (
|
|
21
20
|
TAbstractBaseRegion,
|
|
21
|
+
)
|
|
22
|
+
from dodal.devices.electron_analyser.abstract.types import (
|
|
22
23
|
TAcquisitionMode,
|
|
23
24
|
TLensMode,
|
|
25
|
+
TPassEnergy,
|
|
26
|
+
TPsuMode,
|
|
24
27
|
)
|
|
25
28
|
from dodal.devices.electron_analyser.enums import EnergyMode
|
|
26
|
-
from dodal.devices.electron_analyser.util import to_binding_energy
|
|
29
|
+
from dodal.devices.electron_analyser.util import to_binding_energy
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
class AbstractAnalyserDriverIO(
|
|
@@ -31,11 +34,12 @@ class AbstractAnalyserDriverIO(
|
|
|
31
34
|
StandardReadable,
|
|
32
35
|
ADBaseIO,
|
|
33
36
|
Movable[TAbstractBaseRegion],
|
|
34
|
-
Generic[TAbstractBaseRegion, TAcquisitionMode, TLensMode],
|
|
37
|
+
Generic[TAbstractBaseRegion, TAcquisitionMode, TLensMode, TPsuMode, TPassEnergy],
|
|
35
38
|
):
|
|
36
39
|
"""
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
Driver device that defines signals and readables that should be common to all
|
|
41
|
+
electron analysers. Implementations of electron analyser devices should inherit
|
|
42
|
+
from this class and define additional specialised signals and methods.
|
|
39
43
|
"""
|
|
40
44
|
|
|
41
45
|
def __init__(
|
|
@@ -43,6 +47,8 @@ class AbstractAnalyserDriverIO(
|
|
|
43
47
|
prefix: str,
|
|
44
48
|
acquisition_mode_type: type[TAcquisitionMode],
|
|
45
49
|
lens_mode_type: type[TLensMode],
|
|
50
|
+
psu_mode_type: type[TPsuMode],
|
|
51
|
+
pass_energy_type: type[TPassEnergy],
|
|
46
52
|
energy_sources: Mapping[str, SignalR[float]],
|
|
47
53
|
name: str = "",
|
|
48
54
|
) -> None:
|
|
@@ -55,6 +61,10 @@ class AbstractAnalyserDriverIO(
|
|
|
55
61
|
for this device.
|
|
56
62
|
lens_mode_type: Enum that determines the available lens mode for this
|
|
57
63
|
device.
|
|
64
|
+
psu_mode_type: Enum that determines the available psu modes for this device.
|
|
65
|
+
pass_energy_type: Can be enum or float, depends on electron analyser model.
|
|
66
|
+
If enum, it determines the available pass energies for
|
|
67
|
+
this device.
|
|
58
68
|
energy_sources: Map that pairs a source name to an energy value signal
|
|
59
69
|
(in eV).
|
|
60
70
|
name: Name of the device.
|
|
@@ -62,6 +72,8 @@ class AbstractAnalyserDriverIO(
|
|
|
62
72
|
self.energy_sources = energy_sources
|
|
63
73
|
self.acquisition_mode_type = acquisition_mode_type
|
|
64
74
|
self.lens_mode_type = lens_mode_type
|
|
75
|
+
self.psu_mode_type = psu_mode_type
|
|
76
|
+
self.pass_energy_type = pass_energy_type
|
|
65
77
|
|
|
66
78
|
with self.add_children_as_readables():
|
|
67
79
|
self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE")
|
|
@@ -78,18 +90,20 @@ class AbstractAnalyserDriverIO(
|
|
|
78
90
|
EnergyMode, initial_value=EnergyMode.KINETIC
|
|
79
91
|
)
|
|
80
92
|
self.low_energy = epics_signal_rw(float, prefix + "LOW_ENERGY")
|
|
93
|
+
self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
|
|
81
94
|
self.high_energy = epics_signal_rw(float, prefix + "HIGH_ENERGY")
|
|
82
95
|
self.slices = epics_signal_rw(int, prefix + "SLICES")
|
|
83
96
|
self.lens_mode = epics_signal_rw(lens_mode_type, prefix + "LENS_MODE")
|
|
84
|
-
self.pass_energy = epics_signal_rw(
|
|
85
|
-
self.pass_energy_type, prefix + "PASS_ENERGY"
|
|
86
|
-
)
|
|
97
|
+
self.pass_energy = epics_signal_rw(pass_energy_type, prefix + "PASS_ENERGY")
|
|
87
98
|
self.energy_step = epics_signal_rw(float, prefix + "STEP_SIZE")
|
|
88
99
|
self.iterations = epics_signal_rw(int, prefix + "NumExposures")
|
|
89
100
|
self.acquisition_mode = epics_signal_rw(
|
|
90
101
|
acquisition_mode_type, prefix + "ACQ_MODE"
|
|
91
102
|
)
|
|
92
103
|
self.excitation_energy_source = soft_signal_rw(str, initial_value="")
|
|
104
|
+
# This is used by each electron analyser, however it depends on the electron
|
|
105
|
+
# analyser type to know if is moved with region settings.
|
|
106
|
+
self.psu_mode = epics_signal_rw(psu_mode_type, prefix + "PSU_MODE")
|
|
93
107
|
|
|
94
108
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
95
109
|
# Read once per scan after data acquired
|
|
@@ -114,42 +128,17 @@ class AbstractAnalyserDriverIO(
|
|
|
114
128
|
|
|
115
129
|
super().__init__(prefix=prefix, name=name)
|
|
116
130
|
|
|
131
|
+
@abstractmethod
|
|
117
132
|
@AsyncStatus.wrap
|
|
118
133
|
async def set(self, region: TAbstractBaseRegion):
|
|
119
134
|
"""
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
Move a group of signals defined in a region. Each implementation of this class
|
|
136
|
+
is responsible for implementing this method correctly.
|
|
122
137
|
|
|
123
138
|
Args:
|
|
124
139
|
region: Contains the parameters to setup the driver for a scan.
|
|
125
140
|
"""
|
|
126
141
|
|
|
127
|
-
source = self._get_energy_source(region.excitation_energy_source)
|
|
128
|
-
excitation_energy = await source.get_value() # eV
|
|
129
|
-
|
|
130
|
-
pass_energy_type = self.pass_energy_type
|
|
131
|
-
pass_energy = pass_energy_type(region.pass_energy)
|
|
132
|
-
|
|
133
|
-
low_energy = to_kinetic_energy(
|
|
134
|
-
region.low_energy, region.energy_mode, excitation_energy
|
|
135
|
-
)
|
|
136
|
-
high_energy = to_kinetic_energy(
|
|
137
|
-
region.high_energy, region.energy_mode, excitation_energy
|
|
138
|
-
)
|
|
139
|
-
await asyncio.gather(
|
|
140
|
-
self.region_name.set(region.name),
|
|
141
|
-
self.energy_mode.set(region.energy_mode),
|
|
142
|
-
self.low_energy.set(low_energy),
|
|
143
|
-
self.high_energy.set(high_energy),
|
|
144
|
-
self.slices.set(region.slices),
|
|
145
|
-
self.lens_mode.set(region.lens_mode),
|
|
146
|
-
self.pass_energy.set(pass_energy),
|
|
147
|
-
self.iterations.set(region.iterations),
|
|
148
|
-
self.acquisition_mode.set(region.acquisition_mode),
|
|
149
|
-
self.excitation_energy.set(excitation_energy),
|
|
150
|
-
self.excitation_energy_source.set(source.name),
|
|
151
|
-
)
|
|
152
|
-
|
|
153
142
|
def _get_energy_source(self, alias_name: str) -> SignalR[float]:
|
|
154
143
|
energy_source = self.energy_sources.get(alias_name)
|
|
155
144
|
if energy_source is None:
|
|
@@ -233,18 +222,6 @@ class AbstractAnalyserDriverIO(
|
|
|
233
222
|
def _calculate_total_intensity(self, spectrum: Array1D[np.float64]) -> float:
|
|
234
223
|
return float(np.sum(spectrum, dtype=np.float64))
|
|
235
224
|
|
|
236
|
-
@property
|
|
237
|
-
@abstractmethod
|
|
238
|
-
def pass_energy_type(self) -> type:
|
|
239
|
-
"""
|
|
240
|
-
Return the type the pass_energy should be. Depends on underlying analyser
|
|
241
|
-
software.
|
|
242
|
-
|
|
243
|
-
Returns:
|
|
244
|
-
Type the pass energy parameter from a region needs to be cast to so it can
|
|
245
|
-
be set correctly on the signal.
|
|
246
|
-
"""
|
|
247
|
-
|
|
248
225
|
|
|
249
226
|
TAbstractAnalyserDriverIO = TypeVar(
|
|
250
227
|
"TAbstractAnalyserDriverIO", bound=AbstractAnalyserDriverIO
|
|
@@ -3,9 +3,13 @@ from abc import ABC
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import Generic, TypeVar
|
|
5
5
|
|
|
6
|
-
from ophyd_async.core import StrictEnum, SupersetEnum
|
|
7
6
|
from pydantic import BaseModel, Field, model_validator
|
|
8
7
|
|
|
8
|
+
from dodal.devices.electron_analyser.abstract.types import (
|
|
9
|
+
TAcquisitionMode,
|
|
10
|
+
TLensMode,
|
|
11
|
+
TPassEnergy,
|
|
12
|
+
)
|
|
9
13
|
from dodal.devices.electron_analyser.enums import EnergyMode
|
|
10
14
|
|
|
11
15
|
|
|
@@ -44,17 +48,10 @@ def energy_mode_validation(data: dict) -> dict:
|
|
|
44
48
|
return data
|
|
45
49
|
|
|
46
50
|
|
|
47
|
-
TAcquisitionMode = TypeVar("TAcquisitionMode", bound=StrictEnum)
|
|
48
|
-
# Allow SupersetEnum. Specs analysers can connect to Lens mode separately to the
|
|
49
|
-
# analyser which leaves the enum to either be "Not connected" OR the available enums
|
|
50
|
-
# when connected.
|
|
51
|
-
TLensMode = TypeVar("TLensMode", bound=SupersetEnum | StrictEnum)
|
|
52
|
-
|
|
53
|
-
|
|
54
51
|
class AbstractBaseRegion(
|
|
55
52
|
ABC,
|
|
56
53
|
JavaToPythonModel,
|
|
57
|
-
Generic[TAcquisitionMode, TLensMode],
|
|
54
|
+
Generic[TAcquisitionMode, TLensMode, TPassEnergy],
|
|
58
55
|
):
|
|
59
56
|
"""
|
|
60
57
|
Generic region model that holds the data. Specialised region models should inherit
|
|
@@ -68,9 +65,10 @@ class AbstractBaseRegion(
|
|
|
68
65
|
excitation_energy_source: str = "source1"
|
|
69
66
|
# These ones we need subclasses to provide default values
|
|
70
67
|
lens_mode: TLensMode
|
|
71
|
-
pass_energy:
|
|
68
|
+
pass_energy: TPassEnergy
|
|
72
69
|
acquisition_mode: TAcquisitionMode
|
|
73
70
|
low_energy: float
|
|
71
|
+
centre_energy: float
|
|
74
72
|
high_energy: float
|
|
75
73
|
step_time: float
|
|
76
74
|
energy_step: float # in eV
|
|
@@ -95,7 +93,7 @@ TAbstractBaseRegion = TypeVar("TAbstractBaseRegion", bound=AbstractBaseRegion)
|
|
|
95
93
|
class AbstractBaseSequence(
|
|
96
94
|
ABC,
|
|
97
95
|
JavaToPythonModel,
|
|
98
|
-
Generic[TAbstractBaseRegion
|
|
96
|
+
Generic[TAbstractBaseRegion],
|
|
99
97
|
):
|
|
100
98
|
"""
|
|
101
99
|
Generic sequence model that holds the list of region data. Specialised sequence
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import StrictEnum, SupersetEnum
|
|
4
|
+
|
|
5
|
+
TAcquisitionMode = TypeVar("TAcquisitionMode", bound=StrictEnum)
|
|
6
|
+
# Allow SupersetEnum. Specs analysers can connect to Lens and Psu mode separately to the
|
|
7
|
+
# analyser which leaves the enum to either be "Not connected" OR the available enums
|
|
8
|
+
# when connected.
|
|
9
|
+
TLensMode = TypeVar("TLensMode", bound=SupersetEnum | StrictEnum)
|
|
10
|
+
TPsuMode = TypeVar("TPsuMode", bound=SupersetEnum | StrictEnum)
|
|
11
|
+
TPassEnergy = TypeVar("TPassEnergy", bound=StrictEnum | float)
|
|
12
|
+
TPassEnergyEnum = TypeVar("TPassEnergyEnum", bound=StrictEnum)
|
|
@@ -3,7 +3,7 @@ from typing import Generic
|
|
|
3
3
|
|
|
4
4
|
from ophyd_async.core import SignalR
|
|
5
5
|
|
|
6
|
-
from dodal.devices.electron_analyser.abstract.
|
|
6
|
+
from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
|
|
7
7
|
from dodal.devices.electron_analyser.detector import (
|
|
8
8
|
ElectronAnalyserDetector,
|
|
9
9
|
)
|
|
@@ -13,21 +13,21 @@ from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSeque
|
|
|
13
13
|
|
|
14
14
|
class SpecsDetector(
|
|
15
15
|
ElectronAnalyserDetector[
|
|
16
|
-
SpecsAnalyserDriverIO[TLensMode],
|
|
17
|
-
SpecsSequence[TLensMode],
|
|
18
|
-
SpecsRegion[TLensMode],
|
|
16
|
+
SpecsAnalyserDriverIO[TLensMode, TPsuMode],
|
|
17
|
+
SpecsSequence[TLensMode, TPsuMode],
|
|
18
|
+
SpecsRegion[TLensMode, TPsuMode],
|
|
19
19
|
],
|
|
20
|
-
Generic[TLensMode],
|
|
20
|
+
Generic[TLensMode, TPsuMode],
|
|
21
21
|
):
|
|
22
22
|
def __init__(
|
|
23
23
|
self,
|
|
24
24
|
prefix: str,
|
|
25
25
|
lens_mode_type: type[TLensMode],
|
|
26
|
+
psu_mode_type: type[TPsuMode],
|
|
26
27
|
energy_sources: Mapping[str, SignalR[float]],
|
|
27
28
|
name: str = "",
|
|
28
29
|
):
|
|
29
|
-
driver = SpecsAnalyserDriverIO[TLensMode](
|
|
30
|
-
prefix, lens_mode_type, energy_sources
|
|
30
|
+
driver = SpecsAnalyserDriverIO[TLensMode, TPsuMode](
|
|
31
|
+
prefix, lens_mode_type, psu_mode_type, energy_sources
|
|
31
32
|
)
|
|
32
|
-
|
|
33
|
-
super().__init__(seq, driver, name)
|
|
33
|
+
super().__init__(SpecsSequence[lens_mode_type, psu_mode_type], driver, name)
|
|
@@ -15,51 +15,88 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
|
15
15
|
from dodal.devices.electron_analyser.abstract.base_driver_io import (
|
|
16
16
|
AbstractAnalyserDriverIO,
|
|
17
17
|
)
|
|
18
|
-
from dodal.devices.electron_analyser.abstract.
|
|
18
|
+
from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
|
|
19
19
|
from dodal.devices.electron_analyser.specs.enums import AcquisitionMode
|
|
20
20
|
from dodal.devices.electron_analyser.specs.region import SpecsRegion
|
|
21
|
+
from dodal.devices.electron_analyser.util import to_kinetic_energy
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class SpecsAnalyserDriverIO(
|
|
24
|
-
AbstractAnalyserDriverIO[
|
|
25
|
-
|
|
25
|
+
AbstractAnalyserDriverIO[
|
|
26
|
+
SpecsRegion[TLensMode, TPsuMode],
|
|
27
|
+
AcquisitionMode,
|
|
28
|
+
TLensMode,
|
|
29
|
+
TPsuMode,
|
|
30
|
+
float,
|
|
31
|
+
],
|
|
32
|
+
Generic[TLensMode, TPsuMode],
|
|
26
33
|
):
|
|
27
34
|
def __init__(
|
|
28
35
|
self,
|
|
29
36
|
prefix: str,
|
|
30
37
|
lens_mode_type: type[TLensMode],
|
|
38
|
+
psu_mode_type: type[TPsuMode],
|
|
31
39
|
energy_sources: Mapping[str, SignalR[float]],
|
|
32
40
|
name: str = "",
|
|
33
41
|
) -> None:
|
|
34
42
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
35
43
|
# Used for setting up region data acquisition.
|
|
36
|
-
self.psu_mode = epics_signal_rw(str, prefix + "SCAN_RANGE")
|
|
37
44
|
self.snapshot_values = epics_signal_rw(int, prefix + "VALUES")
|
|
38
|
-
self.centre_energy = epics_signal_rw(float, prefix + "KINETIC_ENERGY")
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
self.energy_channels = epics_signal_r(
|
|
44
|
-
int, prefix + "TOTAL_POINTS_ITERATION_RBV"
|
|
45
|
-
)
|
|
46
|
+
# Used to calculate the angle axis.
|
|
47
|
+
self.min_angle_axis = epics_signal_r(float, prefix + "Y_MIN_RBV")
|
|
48
|
+
self.max_angle_axis = epics_signal_r(float, prefix + "Y_MAX_RBV")
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
# Used to calculate the energy axis.
|
|
51
|
+
self.energy_channels = epics_signal_r(
|
|
52
|
+
int, prefix + "TOTAL_POINTS_ITERATION_RBV"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
super().__init__(
|
|
56
|
+
prefix=prefix,
|
|
57
|
+
acquisition_mode_type=AcquisitionMode,
|
|
58
|
+
lens_mode_type=lens_mode_type,
|
|
59
|
+
psu_mode_type=psu_mode_type,
|
|
60
|
+
pass_energy_type=float,
|
|
61
|
+
energy_sources=energy_sources,
|
|
62
|
+
name=name,
|
|
63
|
+
)
|
|
48
64
|
|
|
49
65
|
@AsyncStatus.wrap
|
|
50
|
-
async def set(self, region: SpecsRegion[TLensMode]):
|
|
51
|
-
|
|
66
|
+
async def set(self, region: SpecsRegion[TLensMode, TPsuMode]):
|
|
67
|
+
source = self._get_energy_source(region.excitation_energy_source)
|
|
68
|
+
excitation_energy = await source.get_value() # eV
|
|
52
69
|
|
|
70
|
+
low_energy = to_kinetic_energy(
|
|
71
|
+
region.low_energy, region.energy_mode, excitation_energy
|
|
72
|
+
)
|
|
73
|
+
centre_energy = to_kinetic_energy(
|
|
74
|
+
region.centre_energy, region.energy_mode, excitation_energy
|
|
75
|
+
)
|
|
76
|
+
high_energy = to_kinetic_energy(
|
|
77
|
+
region.high_energy, region.energy_mode, excitation_energy
|
|
78
|
+
)
|
|
53
79
|
await asyncio.gather(
|
|
80
|
+
self.region_name.set(region.name),
|
|
81
|
+
self.energy_mode.set(region.energy_mode),
|
|
82
|
+
self.low_energy.set(low_energy),
|
|
83
|
+
self.high_energy.set(high_energy),
|
|
84
|
+
self.slices.set(region.slices),
|
|
85
|
+
self.lens_mode.set(region.lens_mode),
|
|
86
|
+
self.pass_energy.set(region.pass_energy),
|
|
87
|
+
self.iterations.set(region.iterations),
|
|
88
|
+
self.acquisition_mode.set(region.acquisition_mode),
|
|
89
|
+
self.excitation_energy.set(excitation_energy),
|
|
90
|
+
self.excitation_energy_source.set(source.name),
|
|
54
91
|
self.snapshot_values.set(region.values),
|
|
55
92
|
self.psu_mode.set(region.psu_mode),
|
|
56
93
|
)
|
|
57
94
|
if region.acquisition_mode == AcquisitionMode.FIXED_TRANSMISSION:
|
|
58
|
-
await self.centre_energy.set(region.centre_energy)
|
|
59
|
-
|
|
60
|
-
if self.acquisition_mode == AcquisitionMode.FIXED_ENERGY:
|
|
61
95
|
await self.energy_step.set(region.energy_step)
|
|
62
96
|
|
|
97
|
+
if region.acquisition_mode == AcquisitionMode.FIXED_ENERGY:
|
|
98
|
+
await self.centre_energy.set(centre_energy)
|
|
99
|
+
|
|
63
100
|
def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
|
|
64
101
|
angle_axis = derived_signal_r(
|
|
65
102
|
self._calculate_angle_axis,
|
|
@@ -97,7 +134,3 @@ class SpecsAnalyserDriverIO(
|
|
|
97
134
|
step = (max_energy - min_energy) / (total_points_iterations - 1)
|
|
98
135
|
axis = np.array([min_energy + i * step for i in range(total_points_iterations)])
|
|
99
136
|
return axis
|
|
100
|
-
|
|
101
|
-
@property
|
|
102
|
-
def pass_energy_type(self) -> type:
|
|
103
|
-
return float
|
|
@@ -5,27 +5,32 @@ from pydantic import Field
|
|
|
5
5
|
from dodal.devices.electron_analyser.abstract.base_region import (
|
|
6
6
|
AbstractBaseRegion,
|
|
7
7
|
AbstractBaseSequence,
|
|
8
|
-
TLensMode,
|
|
9
8
|
)
|
|
9
|
+
from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
|
|
10
10
|
from dodal.devices.electron_analyser.specs.enums import AcquisitionMode
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class SpecsRegion(
|
|
13
|
+
class SpecsRegion(
|
|
14
|
+
AbstractBaseRegion[AcquisitionMode, TLensMode, float],
|
|
15
|
+
Generic[TLensMode, TPsuMode],
|
|
16
|
+
):
|
|
14
17
|
# Override base class with defaults
|
|
15
18
|
lens_mode: TLensMode
|
|
16
|
-
pass_energy:
|
|
19
|
+
pass_energy: float = 5
|
|
17
20
|
acquisition_mode: AcquisitionMode = AcquisitionMode.FIXED_TRANSMISSION
|
|
18
21
|
low_energy: float = Field(default=800, alias="start_energy")
|
|
22
|
+
centre_energy: float = 0
|
|
19
23
|
high_energy: float = Field(default=850, alias="end_energy")
|
|
20
24
|
step_time: float = Field(default=1.0, alias="exposure_time")
|
|
21
25
|
energy_step: float = Field(default=0.1, alias="step_energy")
|
|
26
|
+
|
|
22
27
|
# Specific to this class
|
|
23
28
|
values: int = 1
|
|
24
|
-
|
|
25
|
-
# ToDo - Update to an enum https://github.com/DiamondLightSource/dodal/issues/1328
|
|
26
|
-
psu_mode: str = "1.5kV"
|
|
29
|
+
psu_mode: TPsuMode
|
|
27
30
|
estimated_time_in_ms: float = 0
|
|
28
31
|
|
|
29
32
|
|
|
30
|
-
class SpecsSequence(
|
|
31
|
-
|
|
33
|
+
class SpecsSequence(
|
|
34
|
+
AbstractBaseSequence[SpecsRegion[TLensMode, TPsuMode]], Generic[TLensMode, TPsuMode]
|
|
35
|
+
):
|
|
36
|
+
regions: list[SpecsRegion[TLensMode, TPsuMode]] = Field(default_factory=lambda: [])
|