dls-dodal 1.42.0__py3-none-any.whl → 1.44.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.42.0.dist-info → dls_dodal-1.44.0.dist-info}/METADATA +5 -4
- {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/RECORD +62 -54
- {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/b01_1.py +8 -0
- dodal/beamlines/i03.py +11 -0
- dodal/beamlines/i13_1.py +22 -48
- dodal/beamlines/i19_1.py +16 -5
- dodal/beamlines/i19_2.py +12 -3
- dodal/beamlines/i19_optics.py +4 -2
- dodal/beamlines/training_rig.py +5 -1
- dodal/common/beamlines/beamline_utils.py +6 -9
- dodal/common/data_util.py +20 -0
- dodal/common/signal_utils.py +43 -4
- dodal/common/visit.py +41 -1
- dodal/devices/aperturescatterguard.py +4 -4
- dodal/devices/apple2_undulator.py +10 -8
- dodal/devices/attenuator/attenuator.py +1 -1
- dodal/devices/backlight.py +1 -1
- dodal/devices/baton.py +17 -0
- dodal/devices/bimorph_mirror.py +2 -2
- dodal/devices/current_amplifiers/current_amplifier.py +1 -6
- dodal/devices/current_amplifiers/current_amplifier_detector.py +2 -2
- dodal/devices/current_amplifiers/femto.py +0 -5
- dodal/devices/current_amplifiers/sr570.py +0 -5
- dodal/devices/electron_analyser/__init__.py +0 -0
- dodal/devices/electron_analyser/base_region.py +64 -0
- dodal/devices/electron_analyser/specs/__init__.py +0 -0
- dodal/devices/electron_analyser/specs/specs_region.py +24 -0
- dodal/devices/electron_analyser/vgscienta/__init__.py +0 -0
- dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +77 -0
- dodal/devices/fast_grid_scan.py +2 -2
- dodal/devices/hutch_shutter.py +1 -1
- dodal/devices/i03/beamstop.py +2 -2
- dodal/devices/i10/i10_apple2.py +3 -3
- dodal/devices/i13_1/merlin.py +1 -2
- dodal/devices/i13_1/merlin_controller.py +12 -8
- dodal/devices/i19/beamstop.py +30 -0
- dodal/devices/i19/hutch_access.py +2 -0
- dodal/devices/i19/shutter.py +52 -30
- dodal/devices/i22/nxsas.py +1 -3
- dodal/devices/i24/focus_mirrors.py +3 -3
- dodal/devices/i24/pilatus_metadata.py +2 -2
- dodal/devices/i24/pmac.py +2 -2
- dodal/devices/oav/oav_detector.py +8 -10
- dodal/devices/oav/snapshots/snapshot.py +21 -0
- dodal/devices/oav/snapshots/snapshot_image_processing.py +74 -0
- dodal/devices/pressure_jump_cell.py +7 -3
- dodal/devices/robot.py +1 -1
- dodal/devices/thawer.py +4 -4
- dodal/devices/undulator.py +1 -1
- dodal/devices/undulator_dcm.py +1 -1
- dodal/devices/util/epics_util.py +1 -1
- dodal/devices/zebra/zebra.py +4 -3
- dodal/devices/zebra/zebra_controlled_shutter.py +1 -1
- dodal/plan_stubs/data_session.py +17 -9
- dodal/plan_stubs/motor_utils.py +10 -12
- dodal/plan_stubs/wrapped.py +10 -12
- dodal/utils.py +0 -7
- dodal/devices/i13_1/merlin_io.py +0 -17
- dodal/devices/oav/microns_for_zoom_levels.json +0 -55
- dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +0 -64
- {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info/licenses}/LICENSE +0 -0
- {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/top_level.txt +0 -0
dodal/common/signal_utils.py
CHANGED
|
@@ -2,22 +2,26 @@ from collections.abc import Callable, Coroutine
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import Reading
|
|
5
|
-
from ophyd_async.core import SignalDatatypeT, SignalR, SoftSignalBackend
|
|
5
|
+
from ophyd_async.core import SignalDatatypeT, SignalR, SignalRW, SoftSignalBackend
|
|
6
|
+
|
|
7
|
+
SetHardwareType = Callable[[SignalDatatypeT], Coroutine[Any, Any, None]]
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class HardwareBackedSoftSignalBackend(SoftSignalBackend[SignalDatatypeT]):
|
|
9
11
|
def __init__(
|
|
10
12
|
self,
|
|
11
13
|
get_from_hardware_func: Callable[[], Coroutine[Any, Any, SignalDatatypeT]],
|
|
14
|
+
set_to_hardware_func: SetHardwareType | None = None,
|
|
12
15
|
*args,
|
|
13
16
|
**kwargs,
|
|
14
17
|
) -> None:
|
|
15
18
|
self.get_from_hardware_func = get_from_hardware_func
|
|
19
|
+
self.set_to_hardware_func = set_to_hardware_func
|
|
16
20
|
super().__init__(*args, **kwargs)
|
|
17
21
|
|
|
18
22
|
async def _update_value(self):
|
|
19
23
|
new_value = await self.get_from_hardware_func()
|
|
20
|
-
|
|
24
|
+
self.set_value(new_value)
|
|
21
25
|
|
|
22
26
|
async def get_reading(self) -> Reading:
|
|
23
27
|
await self._update_value()
|
|
@@ -27,8 +31,39 @@ class HardwareBackedSoftSignalBackend(SoftSignalBackend[SignalDatatypeT]):
|
|
|
27
31
|
await self._update_value()
|
|
28
32
|
return await super().get_value()
|
|
29
33
|
|
|
34
|
+
async def put(self, value: SignalDatatypeT | None, wait: bool) -> None:
|
|
35
|
+
if self.set_to_hardware_func:
|
|
36
|
+
write_value = self.initial_value if value is None else value
|
|
37
|
+
await self.set_to_hardware_func(write_value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_rw_hardware_backed_soft_signal(
|
|
41
|
+
datatype: type[SignalDatatypeT],
|
|
42
|
+
get_from_hardware_func: Callable[[], Coroutine[Any, Any, SignalDatatypeT]],
|
|
43
|
+
set_to_hardware_func: SetHardwareType,
|
|
44
|
+
units: str | None = None,
|
|
45
|
+
precision: int | None = None,
|
|
46
|
+
):
|
|
47
|
+
"""Creates a soft signal that, when read will call the function passed into
|
|
48
|
+
`get_from_hardware_func` and return this. When set it will call `set_to_hardware_func`
|
|
49
|
+
and send something to the hardware.
|
|
50
|
+
|
|
51
|
+
This will allow you to make soft signals derived from arbitrary hardware signals.
|
|
52
|
+
However, calling subscribe on this signal does not give you a sensible value. See https://github.com/bluesky/ophyd-async/issues/525
|
|
53
|
+
for a more full solution.
|
|
54
|
+
"""
|
|
55
|
+
return SignalRW(
|
|
56
|
+
backend=HardwareBackedSoftSignalBackend(
|
|
57
|
+
get_from_hardware_func,
|
|
58
|
+
set_to_hardware_func,
|
|
59
|
+
datatype,
|
|
60
|
+
units=units,
|
|
61
|
+
precision=precision,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
30
65
|
|
|
31
|
-
def
|
|
66
|
+
def create_r_hardware_backed_soft_signal(
|
|
32
67
|
datatype: type[SignalDatatypeT],
|
|
33
68
|
get_from_hardware_func: Callable[[], Coroutine[Any, Any, SignalDatatypeT]],
|
|
34
69
|
units: str | None = None,
|
|
@@ -44,6 +79,10 @@ def create_hardware_backed_soft_signal(
|
|
|
44
79
|
"""
|
|
45
80
|
return SignalR(
|
|
46
81
|
backend=HardwareBackedSoftSignalBackend(
|
|
47
|
-
get_from_hardware_func,
|
|
82
|
+
get_from_hardware_func,
|
|
83
|
+
None,
|
|
84
|
+
datatype,
|
|
85
|
+
units=units,
|
|
86
|
+
precision=precision,
|
|
48
87
|
)
|
|
49
88
|
)
|
dodal/common/visit.py
CHANGED
|
@@ -3,7 +3,8 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
5
5
|
from aiohttp import ClientSession
|
|
6
|
-
from
|
|
6
|
+
from event_model import RunStart
|
|
7
|
+
from ophyd_async.core import FilenameProvider, PathInfo, PathProvider
|
|
7
8
|
from pydantic import BaseModel
|
|
8
9
|
|
|
9
10
|
from dodal.common.types import UpdatingPathProvider
|
|
@@ -150,3 +151,42 @@ class StaticVisitPathProvider(UpdatingPathProvider):
|
|
|
150
151
|
return PathInfo(
|
|
151
152
|
directory_path=self._root, filename=self._filename_provider(device_name)
|
|
152
153
|
)
|
|
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)
|
|
@@ -12,7 +12,7 @@ from ophyd_async.core import (
|
|
|
12
12
|
from pydantic import BaseModel, Field
|
|
13
13
|
|
|
14
14
|
from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
|
|
15
|
-
from dodal.common.signal_utils import
|
|
15
|
+
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
16
16
|
from dodal.devices.aperture import Aperture
|
|
17
17
|
from dodal.devices.scatterguard import Scatterguard
|
|
18
18
|
|
|
@@ -123,7 +123,7 @@ def load_positions_from_beamline_parameters(
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
class ApertureScatterguard(StandardReadable, Movable, Preparable):
|
|
126
|
+
class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable):
|
|
127
127
|
"""Move the aperture and scatterguard assembly in a safe way. There are two ways to
|
|
128
128
|
interact with the device depending on if you want simplicity or move flexibility.
|
|
129
129
|
|
|
@@ -164,7 +164,7 @@ class ApertureScatterguard(StandardReadable, Movable, Preparable):
|
|
|
164
164
|
) -> None:
|
|
165
165
|
self.aperture = Aperture(prefix + "-MO-MAPT-01:")
|
|
166
166
|
self.scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
|
|
167
|
-
self.radius =
|
|
167
|
+
self.radius = create_r_hardware_backed_soft_signal(
|
|
168
168
|
float, self._get_current_radius, units="µm"
|
|
169
169
|
)
|
|
170
170
|
self._loaded_positions = loaded_positions
|
|
@@ -181,7 +181,7 @@ class ApertureScatterguard(StandardReadable, Movable, Preparable):
|
|
|
181
181
|
)
|
|
182
182
|
|
|
183
183
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
184
|
-
self.selected_aperture =
|
|
184
|
+
self.selected_aperture = create_r_hardware_backed_soft_signal(
|
|
185
185
|
ApertureValue, self._get_current_aperture_position
|
|
186
186
|
)
|
|
187
187
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import asyncio
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, Generic, TypeVar
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from bluesky.protocols import Movable
|
|
@@ -21,6 +21,8 @@ from pydantic import BaseModel, ConfigDict, RootModel
|
|
|
21
21
|
|
|
22
22
|
from dodal.log import LOGGER
|
|
23
23
|
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
|
|
24
26
|
|
|
25
27
|
class UndulatorGateStatus(StrictEnum):
|
|
26
28
|
OPEN = "Open"
|
|
@@ -99,7 +101,7 @@ async def estimate_motor_timeout(
|
|
|
99
101
|
return abs((target_pos - cur_pos) * 2.0 / vel) + 1
|
|
100
102
|
|
|
101
103
|
|
|
102
|
-
class SafeUndulatorMover(StandardReadable, Movable):
|
|
104
|
+
class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
|
|
103
105
|
"""A device that will check it's safe to move the undulator before moving it and
|
|
104
106
|
wait for the undulator to be safe again before calling the move complete.
|
|
105
107
|
"""
|
|
@@ -115,7 +117,7 @@ class SafeUndulatorMover(StandardReadable, Movable):
|
|
|
115
117
|
super().__init__(name)
|
|
116
118
|
|
|
117
119
|
@AsyncStatus.wrap
|
|
118
|
-
async def set(self, value) -> None:
|
|
120
|
+
async def set(self, value: T) -> None:
|
|
119
121
|
LOGGER.info(f"Setting {self.name} to {value}")
|
|
120
122
|
await self.raise_if_cannot_move()
|
|
121
123
|
await self._set_demand_positions(value)
|
|
@@ -125,7 +127,7 @@ class SafeUndulatorMover(StandardReadable, Movable):
|
|
|
125
127
|
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
126
128
|
|
|
127
129
|
@abc.abstractmethod
|
|
128
|
-
async def _set_demand_positions(self, value) -> None:
|
|
130
|
+
async def _set_demand_positions(self, value: T) -> None:
|
|
129
131
|
"""Set the demand positions on the device without actually hitting move."""
|
|
130
132
|
|
|
131
133
|
@abc.abstractmethod
|
|
@@ -139,7 +141,7 @@ class SafeUndulatorMover(StandardReadable, Movable):
|
|
|
139
141
|
raise RuntimeError(f"{self.name} is already in motion.")
|
|
140
142
|
|
|
141
143
|
|
|
142
|
-
class UndulatorGap(SafeUndulatorMover):
|
|
144
|
+
class UndulatorGap(SafeUndulatorMover[float]):
|
|
143
145
|
"""A device with a collection of epics signals to set Apple 2 undulator gap motion.
|
|
144
146
|
Only PV used by beamline are added the full list is here:
|
|
145
147
|
/dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDGapVelocityControl.template
|
|
@@ -185,7 +187,7 @@ class UndulatorGap(SafeUndulatorMover):
|
|
|
185
187
|
self.user_readback = epics_signal_r(float, prefix + "CURRGAPD")
|
|
186
188
|
super().__init__(self.set_move, prefix, name)
|
|
187
189
|
|
|
188
|
-
async def _set_demand_positions(self, value) -> None:
|
|
190
|
+
async def _set_demand_positions(self, value: float) -> None:
|
|
189
191
|
await self.user_setpoint.set(str(value))
|
|
190
192
|
|
|
191
193
|
async def get_timeout(self) -> float:
|
|
@@ -234,7 +236,7 @@ class UndulatorPhaseMotor(StandardReadable):
|
|
|
234
236
|
super().__init__(name=name)
|
|
235
237
|
|
|
236
238
|
|
|
237
|
-
class UndulatorPhaseAxes(SafeUndulatorMover):
|
|
239
|
+
class UndulatorPhaseAxes(SafeUndulatorMover[Apple2PhasesVal]):
|
|
238
240
|
"""
|
|
239
241
|
A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
|
|
240
242
|
e.g. top_outer == Q1
|
|
@@ -290,7 +292,7 @@ class UndulatorPhaseAxes(SafeUndulatorMover):
|
|
|
290
292
|
return np.max(timeouts)
|
|
291
293
|
|
|
292
294
|
|
|
293
|
-
class UndulatorJawPhase(SafeUndulatorMover):
|
|
295
|
+
class UndulatorJawPhase(SafeUndulatorMover[float]):
|
|
294
296
|
"""
|
|
295
297
|
A JawPhase movable, this is use for moving the jaw phase which is use to control the
|
|
296
298
|
linear arbitrary polarisation but only one some of the beamline.
|
|
@@ -29,7 +29,7 @@ class ReadOnlyAttenuator(StandardReadable):
|
|
|
29
29
|
super().__init__(name)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable):
|
|
32
|
+
class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable[float]):
|
|
33
33
|
"""The attenuator will insert filters into the beam to reduce its transmission.
|
|
34
34
|
In this attenuator, each filter can be in one of two states: IN or OUT
|
|
35
35
|
|
dodal/devices/backlight.py
CHANGED
|
@@ -15,7 +15,7 @@ class BacklightPosition(StrictEnum):
|
|
|
15
15
|
OUT = "Out"
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class Backlight(StandardReadable, Movable):
|
|
18
|
+
class Backlight(StandardReadable, Movable[BacklightPosition]):
|
|
19
19
|
"""Simple device to trigger the pneumatic in/out."""
|
|
20
20
|
|
|
21
21
|
TIME_TO_MOVE_S = 1.0 # Tested using a stopwatch on the beamline 09/2024
|
dodal/devices/baton.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Annotated as A
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
SignalRW,
|
|
5
|
+
StandardReadable,
|
|
6
|
+
)
|
|
7
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
8
|
+
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Baton(StandardReadable, EpicsDevice):
|
|
12
|
+
requested_user: A[
|
|
13
|
+
SignalRW[str], PvSuffix("REQUESTED_USER"), Format.HINTED_UNCACHED_SIGNAL
|
|
14
|
+
]
|
|
15
|
+
current_user: A[
|
|
16
|
+
SignalRW[str], PvSuffix("CURRENT_USER"), Format.HINTED_UNCACHED_SIGNAL
|
|
17
|
+
]
|
dodal/devices/bimorph_mirror.py
CHANGED
|
@@ -41,7 +41,7 @@ class BimorphMirrorStatus(StrictEnum):
|
|
|
41
41
|
ERROR = "Error"
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
class BimorphMirrorChannel(StandardReadable, Movable, EpicsDevice):
|
|
44
|
+
class BimorphMirrorChannel(StandardReadable, Movable[float], EpicsDevice):
|
|
45
45
|
"""Collection of PVs comprising a single bimorph channel.
|
|
46
46
|
|
|
47
47
|
Attributes:
|
|
@@ -66,7 +66,7 @@ class BimorphMirrorChannel(StandardReadable, Movable, EpicsDevice):
|
|
|
66
66
|
await self.output_voltage.set(value)
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
class BimorphMirror(StandardReadable, Movable):
|
|
69
|
+
class BimorphMirror(StandardReadable, Movable[Mapping[int, float]]):
|
|
70
70
|
"""Class to represent CAENels Bimorph Mirrors.
|
|
71
71
|
|
|
72
72
|
Attributes:
|
|
@@ -23,7 +23,6 @@ class CurrentAmp(ABC, StandardReadable, Movable):
|
|
|
23
23
|
super().__init__(name)
|
|
24
24
|
|
|
25
25
|
@abstractmethod
|
|
26
|
-
@AsyncStatus.wrap
|
|
27
26
|
async def increase_gain(self, value: int = 1) -> None:
|
|
28
27
|
"""Increase gain, increment by 1 by default.
|
|
29
28
|
|
|
@@ -31,7 +30,6 @@ class CurrentAmp(ABC, StandardReadable, Movable):
|
|
|
31
30
|
bool: True if success.
|
|
32
31
|
"""
|
|
33
32
|
|
|
34
|
-
@AsyncStatus.wrap
|
|
35
33
|
@abstractmethod
|
|
36
34
|
async def decrease_gain(self, value: int = 1) -> None:
|
|
37
35
|
"""Decrease gain, decrement by 1 by default.
|
|
@@ -40,21 +38,18 @@ class CurrentAmp(ABC, StandardReadable, Movable):
|
|
|
40
38
|
bool: True if success.
|
|
41
39
|
"""
|
|
42
40
|
|
|
43
|
-
@AsyncStatus.wrap
|
|
44
41
|
@abstractmethod
|
|
45
|
-
async def get_gain(self) ->
|
|
42
|
+
async def get_gain(self) -> Enum:
|
|
46
43
|
"""Get the current gain setting
|
|
47
44
|
|
|
48
45
|
Returns:
|
|
49
46
|
Enum: The member name of the current gain setting in gain_conversion_table.
|
|
50
47
|
"""
|
|
51
48
|
|
|
52
|
-
@AsyncStatus.wrap
|
|
53
49
|
@abstractmethod
|
|
54
50
|
async def get_upperlimit(self) -> float:
|
|
55
51
|
"""Get the upper limit of the current amplifier"""
|
|
56
52
|
|
|
57
|
-
@AsyncStatus.wrap
|
|
58
53
|
@abstractmethod
|
|
59
54
|
async def get_lowerlimit(self) -> float:
|
|
60
55
|
"""Get the lower limit of the current amplifier"""
|
|
@@ -92,8 +92,8 @@ class CurrentAmpDet(StandardReadable, Preparable):
|
|
|
92
92
|
self.current_amp().get_gain(),
|
|
93
93
|
self.counter().get_voltage_per_sec(),
|
|
94
94
|
)
|
|
95
|
-
|
|
96
|
-
corrected_current = voltage_per_sec /
|
|
95
|
+
assert isinstance(current_gain.value, float)
|
|
96
|
+
corrected_current = voltage_per_sec / current_gain.value
|
|
97
97
|
return corrected_current
|
|
98
98
|
|
|
99
99
|
@AsyncStatus.wrap
|
|
@@ -114,7 +114,6 @@ class FemtoDDPCA(CurrentAmp):
|
|
|
114
114
|
# wait for current amplifier's bandpass filter to settle.
|
|
115
115
|
await asyncio.sleep(self.raise_timetable[SEN_setting].value)
|
|
116
116
|
|
|
117
|
-
@AsyncStatus.wrap
|
|
118
117
|
async def increase_gain(self, value: int = 1) -> None:
|
|
119
118
|
current_gain = int((await self.get_gain()).name.split("_")[-1])
|
|
120
119
|
current_gain += value
|
|
@@ -122,7 +121,6 @@ class FemtoDDPCA(CurrentAmp):
|
|
|
122
121
|
raise ValueError("Gain at max value")
|
|
123
122
|
await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
|
|
124
123
|
|
|
125
|
-
@AsyncStatus.wrap
|
|
126
124
|
async def decrease_gain(self, value: int = 1) -> None:
|
|
127
125
|
current_gain = int((await self.get_gain()).name.split("_")[-1])
|
|
128
126
|
current_gain -= value
|
|
@@ -130,14 +128,11 @@ class FemtoDDPCA(CurrentAmp):
|
|
|
130
128
|
raise ValueError("Gain at min value")
|
|
131
129
|
await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
|
|
132
130
|
|
|
133
|
-
@AsyncStatus.wrap
|
|
134
131
|
async def get_gain(self) -> Enum:
|
|
135
132
|
return self.gain_conversion_table[(await self.gain.get_value()).name]
|
|
136
133
|
|
|
137
|
-
@AsyncStatus.wrap
|
|
138
134
|
async def get_upperlimit(self) -> float:
|
|
139
135
|
return self.upperlimit
|
|
140
136
|
|
|
141
|
-
@AsyncStatus.wrap
|
|
142
137
|
async def get_lowerlimit(self) -> float:
|
|
143
138
|
return self.lowerlimit
|
|
@@ -178,7 +178,6 @@ class SR570(CurrentAmp):
|
|
|
178
178
|
)
|
|
179
179
|
await asyncio.sleep(self.raise_timetable[coarse_gain.name].value)
|
|
180
180
|
|
|
181
|
-
@AsyncStatus.wrap
|
|
182
181
|
async def increase_gain(self, value=3) -> None:
|
|
183
182
|
current_gain = int((await self.get_gain()).name.split("_")[-1])
|
|
184
183
|
current_gain += value
|
|
@@ -189,7 +188,6 @@ class SR570(CurrentAmp):
|
|
|
189
188
|
raise ValueError("Gain at max value")
|
|
190
189
|
await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
|
|
191
190
|
|
|
192
|
-
@AsyncStatus.wrap
|
|
193
191
|
async def decrease_gain(self, value=3) -> None:
|
|
194
192
|
current_gain = int((await self.get_gain()).name.split("_")[-1])
|
|
195
193
|
current_gain -= value
|
|
@@ -198,17 +196,14 @@ class SR570(CurrentAmp):
|
|
|
198
196
|
raise ValueError("Gain at min value")
|
|
199
197
|
await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
|
|
200
198
|
|
|
201
|
-
@AsyncStatus.wrap
|
|
202
199
|
async def get_gain(self) -> Enum:
|
|
203
200
|
result = await asyncio.gather(
|
|
204
201
|
self.coarse_gain.get_value(), self.fine_gain.get_value()
|
|
205
202
|
)
|
|
206
203
|
return self.gain_conversion_table[self.combined_table(result).name]
|
|
207
204
|
|
|
208
|
-
@AsyncStatus.wrap
|
|
209
205
|
async def get_upperlimit(self) -> float:
|
|
210
206
|
return self.upperlimit
|
|
211
207
|
|
|
212
|
-
@AsyncStatus.wrap
|
|
213
208
|
async def get_lowerlimit(self) -> float:
|
|
214
209
|
return self.lowerlimit
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
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()]
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
)
|
dodal/devices/fast_grid_scan.py
CHANGED
|
@@ -23,7 +23,7 @@ from ophyd_async.epics.core import (
|
|
|
23
23
|
from pydantic import field_validator
|
|
24
24
|
from pydantic.dataclasses import dataclass
|
|
25
25
|
|
|
26
|
-
from dodal.common.signal_utils import
|
|
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,7 +203,7 @@ 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 =
|
|
206
|
+
self.expected_images = create_r_hardware_backed_soft_signal(
|
|
207
207
|
float, self._calculate_expected_images
|
|
208
208
|
)
|
|
209
209
|
|
dodal/devices/hutch_shutter.py
CHANGED
|
@@ -55,7 +55,7 @@ class HutchInterlock(StandardReadable):
|
|
|
55
55
|
return interlock_state == HUTCH_SAFE_FOR_OPERATIONS
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
class HutchShutter(StandardReadable, Movable):
|
|
58
|
+
class HutchShutter(StandardReadable, Movable[ShutterDemand]):
|
|
59
59
|
"""Device to operate the hutch shutter.
|
|
60
60
|
|
|
61
61
|
When a demand is sent, the device should first check the hutch status \
|
dodal/devices/i03/beamstop.py
CHANGED
|
@@ -5,7 +5,7 @@ from ophyd_async.core import StandardReadable, StrictEnum
|
|
|
5
5
|
from ophyd_async.epics.motor import Motor
|
|
6
6
|
|
|
7
7
|
from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
|
|
8
|
-
from dodal.common.signal_utils import
|
|
8
|
+
from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BeamstopPositions(StrictEnum):
|
|
@@ -53,7 +53,7 @@ class Beamstop(StandardReadable):
|
|
|
53
53
|
self.x_mm = Motor(prefix + "X")
|
|
54
54
|
self.y_mm = Motor(prefix + "Y")
|
|
55
55
|
self.z_mm = Motor(prefix + "Z")
|
|
56
|
-
self.selected_pos =
|
|
56
|
+
self.selected_pos = create_r_hardware_backed_soft_signal(
|
|
57
57
|
BeamstopPositions, self._get_selected_position
|
|
58
58
|
)
|
|
59
59
|
|
dodal/devices/i10/i10_apple2.py
CHANGED
|
@@ -175,7 +175,7 @@ class I10Apple2(Apple2):
|
|
|
175
175
|
self._available_pol = list(self.lookup_tables["Gap"].keys())
|
|
176
176
|
|
|
177
177
|
|
|
178
|
-
class I10Apple2PGM(StandardReadable, Movable):
|
|
178
|
+
class I10Apple2PGM(StandardReadable, Movable[float]):
|
|
179
179
|
"""
|
|
180
180
|
Compound device to set both ID and PGM energy at the sample time,poly_deg
|
|
181
181
|
|
|
@@ -211,7 +211,7 @@ class I10Apple2PGM(StandardReadable, Movable):
|
|
|
211
211
|
)
|
|
212
212
|
|
|
213
213
|
|
|
214
|
-
class I10Apple2Pol(StandardReadable, Movable):
|
|
214
|
+
class I10Apple2Pol(StandardReadable, Movable[str]):
|
|
215
215
|
"""
|
|
216
216
|
Compound device to set polorisation of ID.
|
|
217
217
|
"""
|
|
@@ -240,7 +240,7 @@ class I10Apple2Pol(StandardReadable, Movable):
|
|
|
240
240
|
) # Move id to new polarisation
|
|
241
241
|
|
|
242
242
|
|
|
243
|
-
class LinearArbitraryAngle(StandardReadable, Movable):
|
|
243
|
+
class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
|
|
244
244
|
"""
|
|
245
245
|
Device to set polorisation angle of the ID. Linear Arbitrary Angle (laa)
|
|
246
246
|
is the direction of the magnetic field which can be change by varying the jaw_phase
|