ophyd-async 0.10.0a2__py3-none-any.whl → 0.10.0a4__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.
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +4 -2
- ophyd_async/core/_derived_signal.py +42 -14
- ophyd_async/core/_derived_signal_backend.py +4 -4
- ophyd_async/core/_detector.py +71 -57
- ophyd_async/core/_device.py +3 -3
- ophyd_async/core/_hdf_dataset.py +1 -5
- ophyd_async/core/_providers.py +0 -8
- ophyd_async/core/_readable.py +13 -1
- ophyd_async/core/_signal.py +21 -5
- ophyd_async/core/_signal_backend.py +18 -8
- ophyd_async/core/_utils.py +31 -14
- ophyd_async/epics/adandor/_andor_controller.py +1 -1
- ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
- ophyd_async/epics/adcore/_core_detector.py +2 -2
- ophyd_async/epics/adcore/_core_io.py +3 -3
- ophyd_async/epics/adcore/_core_logic.py +3 -3
- ophyd_async/epics/adcore/_core_writer.py +22 -29
- ophyd_async/epics/adcore/_hdf_writer.py +17 -15
- ophyd_async/epics/adcore/_jpeg_writer.py +1 -3
- ophyd_async/epics/adcore/_tiff_writer.py +1 -3
- ophyd_async/epics/adcore/_utils.py +11 -2
- ophyd_async/epics/adkinetix/_kinetix_controller.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_controller.py +6 -13
- ophyd_async/epics/adpilatus/_pilatus_io.py +1 -1
- ophyd_async/epics/advimba/_vimba_controller.py +1 -1
- ophyd_async/epics/core/_aioca.py +2 -2
- ophyd_async/epics/core/_p4p.py +1 -1
- ophyd_async/epics/core/_pvi_connector.py +5 -3
- ophyd_async/epics/core/_util.py +21 -13
- ophyd_async/epics/eiger/__init__.py +2 -4
- ophyd_async/epics/eiger/_odin_io.py +58 -36
- ophyd_async/epics/motor.py +3 -2
- ophyd_async/epics/testing/_example_ioc.py +1 -0
- ophyd_async/epics/testing/test_records.db +5 -0
- ophyd_async/fastcs/eiger/__init__.py +13 -0
- ophyd_async/{epics → fastcs}/eiger/_eiger.py +15 -6
- ophyd_async/{epics → fastcs}/eiger/_eiger_controller.py +17 -27
- ophyd_async/fastcs/eiger/_eiger_io.py +54 -0
- ophyd_async/fastcs/panda/_block.py +2 -0
- ophyd_async/fastcs/panda/_hdf_panda.py +0 -1
- ophyd_async/fastcs/panda/_writer.py +23 -22
- ophyd_async/plan_stubs/_fly.py +2 -2
- ophyd_async/sim/_blob_detector.py +0 -1
- ophyd_async/sim/_blob_detector_controller.py +1 -1
- ophyd_async/sim/_blob_detector_writer.py +15 -19
- ophyd_async/sim/_motor.py +2 -2
- ophyd_async/sim/_pattern_generator.py +2 -0
- ophyd_async/tango/core/_base_device.py +2 -1
- ophyd_async/tango/core/_converters.py +2 -6
- ophyd_async/tango/core/_signal.py +8 -8
- ophyd_async/tango/core/_tango_transport.py +12 -12
- ophyd_async/tango/demo/_tango/_servers.py +0 -1
- ophyd_async/tango/testing/_one_of_everything.py +2 -2
- {ophyd_async-0.10.0a2.dist-info → ophyd_async-0.10.0a4.dist-info}/METADATA +1 -1
- {ophyd_async-0.10.0a2.dist-info → ophyd_async-0.10.0a4.dist-info}/RECORD +60 -59
- {ophyd_async-0.10.0a2.dist-info → ophyd_async-0.10.0a4.dist-info}/WHEEL +1 -1
- ophyd_async/epics/eiger/_eiger_io.py +0 -42
- {ophyd_async-0.10.0a2.dist-info → ophyd_async-0.10.0a4.dist-info}/licenses/LICENSE +0 -0
- {ophyd_async-0.10.0a2.dist-info → ophyd_async-0.10.0a4.dist-info}/top_level.txt +0 -0
ophyd_async/epics/core/_util.py
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
from collections.abc import Sequence
|
|
2
|
-
from typing import Any, get_args, get_origin
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from typing import Any, TypeVar, get_args, get_origin
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
6
|
from ophyd_async.core import (
|
|
7
7
|
SignalBackend,
|
|
8
8
|
SignalDatatypeT,
|
|
9
|
+
StrictEnum,
|
|
9
10
|
SubsetEnum,
|
|
11
|
+
SupersetEnum,
|
|
10
12
|
get_dtype,
|
|
11
13
|
get_enum_cls,
|
|
12
14
|
)
|
|
13
15
|
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
def get_pv_basename_and_field(pv: str) -> tuple[str, str | None]:
|
|
16
20
|
"""Split PV into record name and field."""
|
|
@@ -23,26 +27,30 @@ def get_pv_basename_and_field(pv: str) -> tuple[str, str | None]:
|
|
|
23
27
|
|
|
24
28
|
def get_supported_values(
|
|
25
29
|
pv: str,
|
|
26
|
-
datatype: type,
|
|
30
|
+
datatype: type[T],
|
|
27
31
|
pv_choices: Sequence[str],
|
|
28
|
-
) ->
|
|
32
|
+
) -> Mapping[str, T | str]:
|
|
29
33
|
enum_cls = get_enum_cls(datatype)
|
|
30
34
|
if not enum_cls:
|
|
31
35
|
raise TypeError(f"{datatype} is not an Enum")
|
|
32
36
|
choices = [v.value for v in enum_cls]
|
|
33
37
|
error_msg = f"{pv} has choices {pv_choices}, but {datatype} requested {choices} "
|
|
34
|
-
if issubclass(enum_cls,
|
|
38
|
+
if issubclass(enum_cls, StrictEnum):
|
|
39
|
+
if set(choices) != set(pv_choices):
|
|
40
|
+
raise TypeError(error_msg + "to be strictly equal to them.")
|
|
41
|
+
elif issubclass(enum_cls, SubsetEnum):
|
|
35
42
|
if not set(choices).issubset(pv_choices):
|
|
36
43
|
raise TypeError(error_msg + "to be a subset of them.")
|
|
44
|
+
elif issubclass(enum_cls, SupersetEnum):
|
|
45
|
+
if not set(pv_choices).issubset(choices):
|
|
46
|
+
raise TypeError(error_msg + "to be a superset of them.")
|
|
37
47
|
else:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for v in enum_cls:
|
|
45
|
-
supported_values[v.value] = v
|
|
48
|
+
raise TypeError(f"{datatype} is not a StrictEnum, SubsetEnum, or SupersetEnum")
|
|
49
|
+
# Create a map from the string value to the enum instance
|
|
50
|
+
# For StrictEnum and SupersetEnum, all values here will be enum values
|
|
51
|
+
# For SubsetEnum, only the values in choices will be enum values, the rest will be
|
|
52
|
+
# strings
|
|
53
|
+
supported_values = {x: enum_cls(x) for x in pv_choices}
|
|
46
54
|
return supported_values
|
|
47
55
|
|
|
48
56
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from ._eiger_controller import EigerController
|
|
3
|
-
from ._eiger_io import EigerDriverIO
|
|
1
|
+
from ._odin_io import Odin, OdinWriter, Writing
|
|
4
2
|
|
|
5
|
-
__all__ = ["
|
|
3
|
+
__all__ = ["Odin", "OdinWriter", "Writing"]
|
|
@@ -2,17 +2,20 @@ import asyncio
|
|
|
2
2
|
from collections.abc import AsyncGenerator, AsyncIterator
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import StreamAsset
|
|
5
|
-
from event_model import DataKey
|
|
5
|
+
from event_model import DataKey # type: ignore
|
|
6
6
|
|
|
7
7
|
from ophyd_async.core import (
|
|
8
|
+
DEFAULT_TIMEOUT,
|
|
8
9
|
DetectorWriter,
|
|
9
10
|
Device,
|
|
10
11
|
DeviceVector,
|
|
11
|
-
NameProvider,
|
|
12
12
|
PathProvider,
|
|
13
|
+
Reference,
|
|
14
|
+
SignalR,
|
|
13
15
|
StrictEnum,
|
|
14
16
|
observe_value,
|
|
15
17
|
set_and_wait_for_value,
|
|
18
|
+
wait_for_value,
|
|
16
19
|
)
|
|
17
20
|
from ophyd_async.epics.core import (
|
|
18
21
|
epics_signal_r,
|
|
@@ -22,44 +25,53 @@ from ophyd_async.epics.core import (
|
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
class Writing(StrictEnum):
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
CAPTURE = "Capture"
|
|
29
|
+
DONE = "Done"
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
class OdinNode(Device):
|
|
30
33
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
31
|
-
self.writing = epics_signal_r(
|
|
32
|
-
self.
|
|
34
|
+
self.writing = epics_signal_r(str, f"{prefix}Writing_RBV")
|
|
35
|
+
self.frames_dropped = epics_signal_r(int, f"{prefix}FramesDropped_RBV")
|
|
36
|
+
self.frames_time_out = epics_signal_r(int, f"{prefix}FramesTimedOut_RBV")
|
|
37
|
+
self.error_status = epics_signal_r(str, f"{prefix}FPErrorState_RBV")
|
|
38
|
+
self.fp_initialised = epics_signal_r(int, f"{prefix}FPProcessConnected_RBV")
|
|
39
|
+
self.fr_initialised = epics_signal_r(int, f"{prefix}FRProcessConnected_RBV")
|
|
40
|
+
self.num_captured = epics_signal_r(int, f"{prefix}NumCaptured_RBV")
|
|
41
|
+
self.clear_errors = epics_signal_rw(int, f"{prefix}FPClearErrors")
|
|
42
|
+
self.error_message = epics_signal_rw(str, f"{prefix}FPErrorMessage_RBV")
|
|
33
43
|
|
|
34
44
|
super().__init__(name)
|
|
35
45
|
|
|
36
46
|
|
|
37
47
|
class Odin(Device):
|
|
38
48
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
39
|
-
self.nodes = DeviceVector(
|
|
40
|
-
|
|
41
|
-
self.capture = epics_signal_rw(
|
|
42
|
-
Writing, f"{prefix}Writing", f"{prefix}ConfigHdfWrite"
|
|
49
|
+
self.nodes = DeviceVector(
|
|
50
|
+
{i: OdinNode(f"{prefix[:-1]}{i + 1}:") for i in range(4)}
|
|
43
51
|
)
|
|
44
|
-
self.num_captured = epics_signal_r(int, f"{prefix}FramesWritten")
|
|
45
|
-
self.num_to_capture = epics_signal_rw_rbv(int, f"{prefix}ConfigHdfFrames")
|
|
46
52
|
|
|
47
|
-
self.
|
|
53
|
+
self.capture = epics_signal_rw(Writing, f"{prefix}Capture")
|
|
54
|
+
self.capture_rbv = epics_signal_r(str, prefix + "Capture_RBV")
|
|
55
|
+
self.num_captured = epics_signal_r(int, f"{prefix}NumCaptured_RBV")
|
|
56
|
+
self.num_to_capture = epics_signal_rw_rbv(int, f"{prefix}NumCapture")
|
|
48
57
|
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
58
|
+
self.start_timeout = epics_signal_rw(str, f"{prefix}StartTimeout")
|
|
59
|
+
self.timeout_active_rbv = epics_signal_r(str, f"{prefix}TimeoutActive_RBV")
|
|
51
60
|
|
|
52
|
-
self.
|
|
53
|
-
self.
|
|
61
|
+
self.image_height = epics_signal_rw_rbv(int, f"{prefix}ImageHeight")
|
|
62
|
+
self.image_width = epics_signal_rw_rbv(int, f"{prefix}ImageWidth")
|
|
54
63
|
|
|
55
|
-
self.
|
|
56
|
-
self.
|
|
64
|
+
self.num_row_chunks = epics_signal_rw_rbv(int, f"{prefix}NumRowChunks")
|
|
65
|
+
self.num_col_chunks = epics_signal_rw_rbv(int, f"{prefix}NumColChunks")
|
|
57
66
|
|
|
58
|
-
self.
|
|
59
|
-
|
|
60
|
-
)
|
|
67
|
+
self.file_path = epics_signal_rw_rbv(str, f"{prefix}FilePath")
|
|
68
|
+
self.file_name = epics_signal_rw_rbv(str, f"{prefix}FileName")
|
|
61
69
|
|
|
62
|
-
self.
|
|
70
|
+
self.num_frames_chunks = epics_signal_rw(int, prefix + "NumFramesChunks")
|
|
71
|
+
self.meta_active = epics_signal_r(str, prefix + "META:AcquisitionActive_RBV")
|
|
72
|
+
self.meta_writing = epics_signal_r(str, prefix + "META:Writing_RBV")
|
|
73
|
+
|
|
74
|
+
self.data_type = epics_signal_rw_rbv(str, f"{prefix}DataType")
|
|
63
75
|
|
|
64
76
|
super().__init__(name)
|
|
65
77
|
|
|
@@ -68,27 +80,35 @@ class OdinWriter(DetectorWriter):
|
|
|
68
80
|
def __init__(
|
|
69
81
|
self,
|
|
70
82
|
path_provider: PathProvider,
|
|
71
|
-
name_provider: NameProvider,
|
|
72
83
|
odin_driver: Odin,
|
|
84
|
+
eiger_bit_depth: SignalR[int],
|
|
73
85
|
) -> None:
|
|
74
86
|
self._drv = odin_driver
|
|
75
87
|
self._path_provider = path_provider
|
|
76
|
-
self.
|
|
88
|
+
self._eiger_bit_depth = Reference(eiger_bit_depth)
|
|
77
89
|
super().__init__()
|
|
78
90
|
|
|
79
|
-
async def open(self,
|
|
80
|
-
info = self._path_provider(device_name=
|
|
91
|
+
async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]:
|
|
92
|
+
info = self._path_provider(device_name=name)
|
|
93
|
+
self._exposures_per_event = exposures_per_event
|
|
81
94
|
|
|
82
95
|
await asyncio.gather(
|
|
83
96
|
self._drv.file_path.set(str(info.directory_path)),
|
|
84
97
|
self._drv.file_name.set(info.filename),
|
|
85
|
-
self._drv.data_type.set(
|
|
86
|
-
"uint16"
|
|
87
|
-
), # TODO: Get from eiger https://github.com/bluesky/ophyd-async/issues/529
|
|
98
|
+
self._drv.data_type.set(f"UInt{await self._eiger_bit_depth().get_value()}"),
|
|
88
99
|
self._drv.num_to_capture.set(0),
|
|
89
100
|
)
|
|
90
101
|
|
|
91
|
-
await self._drv.
|
|
102
|
+
await wait_for_value(self._drv.meta_active, "Active", timeout=DEFAULT_TIMEOUT)
|
|
103
|
+
|
|
104
|
+
await self._drv.capture.set(
|
|
105
|
+
Writing.CAPTURE, wait=False
|
|
106
|
+
) # TODO: Investigate why we do not get a put callback when setting capture pv https://github.com/bluesky/ophyd-async/issues/866
|
|
107
|
+
|
|
108
|
+
await asyncio.gather(
|
|
109
|
+
wait_for_value(self._drv.capture_rbv, "Capturing", timeout=DEFAULT_TIMEOUT),
|
|
110
|
+
wait_for_value(self._drv.meta_writing, "Writing", timeout=DEFAULT_TIMEOUT),
|
|
111
|
+
)
|
|
92
112
|
|
|
93
113
|
return await self._describe()
|
|
94
114
|
|
|
@@ -100,7 +120,7 @@ class OdinWriter(DetectorWriter):
|
|
|
100
120
|
return {
|
|
101
121
|
"data": DataKey(
|
|
102
122
|
source=self._drv.file_name.source,
|
|
103
|
-
shape=
|
|
123
|
+
shape=[self._exposures_per_event, *data_shape],
|
|
104
124
|
dtype="array",
|
|
105
125
|
# TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529
|
|
106
126
|
dtype_numpy="<u2",
|
|
@@ -112,14 +132,16 @@ class OdinWriter(DetectorWriter):
|
|
|
112
132
|
self, timeout: float
|
|
113
133
|
) -> AsyncGenerator[int, None]:
|
|
114
134
|
async for num_captured in observe_value(self._drv.num_captured, timeout):
|
|
115
|
-
yield num_captured
|
|
135
|
+
yield num_captured // self._exposures_per_event
|
|
116
136
|
|
|
117
137
|
async def get_indices_written(self) -> int:
|
|
118
|
-
return await self._drv.num_captured.get_value()
|
|
138
|
+
return await self._drv.num_captured.get_value() // self._exposures_per_event
|
|
119
139
|
|
|
120
|
-
def collect_stream_docs(
|
|
140
|
+
def collect_stream_docs(
|
|
141
|
+
self, name: str, indices_written: int
|
|
142
|
+
) -> AsyncIterator[StreamAsset]:
|
|
121
143
|
# TODO: Correctly return stream https://github.com/bluesky/ophyd-async/issues/530
|
|
122
144
|
raise NotImplementedError()
|
|
123
145
|
|
|
124
146
|
async def close(self) -> None:
|
|
125
|
-
await set_and_wait_for_value(self._drv.capture, Writing.
|
|
147
|
+
await set_and_wait_for_value(self._drv.capture, Writing.DONE)
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -260,10 +260,11 @@ class Motor(
|
|
|
260
260
|
(await self.acceleration_time.get_value()) * fly_velocity * 0.5
|
|
261
261
|
)
|
|
262
262
|
|
|
263
|
-
|
|
263
|
+
direction = 1 if end_position > start_position else -1
|
|
264
|
+
self._fly_completed_position = end_position + (run_up_distance * direction)
|
|
264
265
|
|
|
265
266
|
# Prepared position not used after prepare, so no need to store in self
|
|
266
|
-
fly_prepared_position = start_position - run_up_distance
|
|
267
|
+
fly_prepared_position = start_position - (run_up_distance * direction)
|
|
267
268
|
|
|
268
269
|
motor_lower_limit, motor_upper_limit, egu = await asyncio.gather(
|
|
269
270
|
self.low_limit_travel.get_value(),
|
|
@@ -48,6 +48,7 @@ class EpicsTestCaDevice(EpicsDevice):
|
|
|
48
48
|
longstr: A[SignalRW[str], PvSuffix("longstr")]
|
|
49
49
|
longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
|
|
50
50
|
a_bool: A[SignalRW[bool], PvSuffix("bool")]
|
|
51
|
+
slowseq: A[SignalRW[int], PvSuffix("slowseq")]
|
|
51
52
|
enum: A[SignalRW[EpicsTestEnum], PvSuffix("enum")]
|
|
52
53
|
enum2: A[SignalRW[EpicsTestEnum], PvSuffix("enum2")]
|
|
53
54
|
subset_enum: A[SignalRW[EpicsTestSubsetEnum], PvSuffix("subset_enum")]
|
|
@@ -112,6 +112,11 @@ record(mbbo, "$(device)enum_str_fallback") {
|
|
|
112
112
|
field(PINI, "YES")
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
record(seq, "$(device)slowseq") {
|
|
116
|
+
field(DLY1, "0.5")
|
|
117
|
+
field(LNK1, "$(device)slowseq.DESC")
|
|
118
|
+
}
|
|
119
|
+
|
|
115
120
|
record(waveform, "$(device)uint8a") {
|
|
116
121
|
field(NELM, "3")
|
|
117
122
|
field(FTVL, "UCHAR")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from ._eiger import EigerDetector, EigerTriggerInfo
|
|
2
|
+
from ._eiger_controller import EigerController
|
|
3
|
+
from ._eiger_io import EigerDetectorIO, EigerDriverIO, EigerMonitorIO, EigerStreamIO
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"EigerDetector",
|
|
7
|
+
"EigerController",
|
|
8
|
+
"EigerDriverIO",
|
|
9
|
+
"EigerTriggerInfo",
|
|
10
|
+
"EigerDetectorIO",
|
|
11
|
+
"EigerMonitorIO",
|
|
12
|
+
"EigerStreamIO",
|
|
13
|
+
]
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
from pydantic import Field
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
AsyncStatus,
|
|
5
|
+
PathProvider,
|
|
6
|
+
StandardDetector,
|
|
7
|
+
TriggerInfo,
|
|
8
|
+
)
|
|
9
|
+
from ophyd_async.epics.eiger import Odin, OdinWriter
|
|
4
10
|
|
|
5
11
|
from ._eiger_controller import EigerController
|
|
6
12
|
from ._eiger_io import EigerDriverIO
|
|
7
|
-
from ._odin_io import Odin, OdinWriter
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
class EigerTriggerInfo(TriggerInfo):
|
|
@@ -15,22 +20,26 @@ class EigerDetector(StandardDetector):
|
|
|
15
20
|
"""Ophyd-async implementation of an Eiger Detector."""
|
|
16
21
|
|
|
17
22
|
_controller: EigerController
|
|
18
|
-
_writer:
|
|
23
|
+
_writer: OdinWriter
|
|
19
24
|
|
|
20
25
|
def __init__(
|
|
21
26
|
self,
|
|
22
27
|
prefix: str,
|
|
23
28
|
path_provider: PathProvider,
|
|
24
29
|
drv_suffix="-EA-EIGER-01:",
|
|
25
|
-
hdf_suffix="-EA-
|
|
30
|
+
hdf_suffix="-EA-EIGER-01:OD:",
|
|
26
31
|
name="",
|
|
27
32
|
):
|
|
28
33
|
self.drv = EigerDriverIO(prefix + drv_suffix)
|
|
29
|
-
self.odin = Odin(prefix + hdf_suffix
|
|
34
|
+
self.odin = Odin(prefix + hdf_suffix)
|
|
30
35
|
|
|
31
36
|
super().__init__(
|
|
32
37
|
EigerController(self.drv),
|
|
33
|
-
OdinWriter(
|
|
38
|
+
OdinWriter(
|
|
39
|
+
path_provider,
|
|
40
|
+
self.odin,
|
|
41
|
+
self.drv.detector.bit_depth_readout,
|
|
42
|
+
),
|
|
34
43
|
name=name,
|
|
35
44
|
)
|
|
36
45
|
|
|
@@ -2,11 +2,10 @@ import asyncio
|
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
DEFAULT_TIMEOUT,
|
|
5
|
-
AsyncStatus,
|
|
6
5
|
DetectorController,
|
|
7
6
|
DetectorTrigger,
|
|
8
7
|
TriggerInfo,
|
|
9
|
-
|
|
8
|
+
wait_for_value,
|
|
10
9
|
)
|
|
11
10
|
|
|
12
11
|
from ._eiger_io import EigerDriverIO, EigerTriggerMode
|
|
@@ -20,59 +19,50 @@ EIGER_TRIGGER_MODE_MAP = {
|
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class EigerController(DetectorController):
|
|
23
|
-
"""Controller for the Eiger detector."""
|
|
24
|
-
|
|
25
22
|
def __init__(
|
|
26
23
|
self,
|
|
27
24
|
driver: EigerDriverIO,
|
|
28
25
|
) -> None:
|
|
29
26
|
self._drv = driver
|
|
30
|
-
self._arm_status: AsyncStatus | None = None
|
|
31
27
|
|
|
32
28
|
def get_deadtime(self, exposure: float | None) -> float:
|
|
33
29
|
# See https://media.dectris.com/filer_public/30/14/3014704e-5f3b-43ba-8ccf-8ef720e60d2a/240202_usermanual_eiger2.pdf
|
|
34
30
|
return 0.0001
|
|
35
31
|
|
|
36
32
|
async def set_energy(self, energy: float, tolerance: float = 0.1):
|
|
37
|
-
"""
|
|
33
|
+
"""Set photon energy."""
|
|
34
|
+
"""Changing photon energy takes some time so only do so if the current energy is
|
|
35
|
+
outside the tolerance."""
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
"""
|
|
41
|
-
current_energy = await self._drv.photon_energy.get_value()
|
|
37
|
+
current_energy = await self._drv.detector.photon_energy.get_value()
|
|
42
38
|
if abs(current_energy - energy) > tolerance:
|
|
43
|
-
await self._drv.photon_energy.set(energy)
|
|
39
|
+
await self._drv.detector.photon_energy.set(energy)
|
|
44
40
|
|
|
45
41
|
async def prepare(self, trigger_info: TriggerInfo):
|
|
46
42
|
coros = [
|
|
47
|
-
self._drv.trigger_mode.set(
|
|
43
|
+
self._drv.detector.trigger_mode.set(
|
|
48
44
|
EIGER_TRIGGER_MODE_MAP[trigger_info.trigger].value
|
|
49
45
|
),
|
|
50
|
-
self._drv.
|
|
46
|
+
self._drv.detector.nimages.set(trigger_info.total_number_of_exposures),
|
|
51
47
|
]
|
|
52
48
|
if trigger_info.livetime is not None:
|
|
53
49
|
coros.extend(
|
|
54
50
|
[
|
|
55
|
-
self._drv.
|
|
56
|
-
self._drv.
|
|
51
|
+
self._drv.detector.count_time.set(trigger_info.livetime),
|
|
52
|
+
self._drv.detector.frame_time.set(trigger_info.livetime),
|
|
57
53
|
]
|
|
58
54
|
)
|
|
55
|
+
|
|
59
56
|
await asyncio.gather(*coros)
|
|
60
57
|
|
|
61
58
|
async def arm(self):
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
self._drv.state,
|
|
67
|
-
"ready",
|
|
68
|
-
timeout=DEFAULT_TIMEOUT,
|
|
69
|
-
wait_for_set_completion=False,
|
|
70
|
-
)
|
|
59
|
+
# NOTE: This will return immedietly on FastCS 0.8.0,
|
|
60
|
+
# but will return after the Eiger has completed arming in 0.9.0.
|
|
61
|
+
# https://github.com/DiamondLightSource/FastCS/pull/141
|
|
62
|
+
await self._drv.detector.arm.trigger(timeout=DEFAULT_TIMEOUT)
|
|
71
63
|
|
|
72
64
|
async def wait_for_idle(self):
|
|
73
|
-
|
|
74
|
-
await self._arm_status
|
|
75
|
-
self._arm_status = None
|
|
65
|
+
await wait_for_value(self._drv.detector.state, "idle", timeout=DEFAULT_TIMEOUT)
|
|
76
66
|
|
|
77
67
|
async def disarm(self):
|
|
78
|
-
await self._drv.disarm.
|
|
68
|
+
await self._drv.detector.disarm.trigger()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from ophyd_async.core import (
|
|
2
|
+
Device,
|
|
3
|
+
SignalR,
|
|
4
|
+
SignalRW,
|
|
5
|
+
SignalX,
|
|
6
|
+
StrictEnum,
|
|
7
|
+
)
|
|
8
|
+
from ophyd_async.fastcs.core import fastcs_connector
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EigerTriggerMode(StrictEnum):
|
|
12
|
+
INTERNAL = "ints"
|
|
13
|
+
EDGE = "exts"
|
|
14
|
+
GATE = "exte"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EigerMonitorIO(Device):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EigerStreamIO(Device):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EigerDetectorIO(Device):
|
|
26
|
+
bit_depth_readout: SignalR[int]
|
|
27
|
+
state: SignalR[str]
|
|
28
|
+
count_time: SignalRW[float]
|
|
29
|
+
frame_time: SignalRW[float]
|
|
30
|
+
nimages: SignalRW[int]
|
|
31
|
+
nexpi: SignalRW[int]
|
|
32
|
+
trigger_mode: SignalRW[str]
|
|
33
|
+
roi_mode: SignalRW[str]
|
|
34
|
+
photon_energy: SignalRW[float]
|
|
35
|
+
beam_center_x: SignalRW[float]
|
|
36
|
+
beam_center_y: SignalRW[float]
|
|
37
|
+
detector_distance: SignalRW[float]
|
|
38
|
+
omega_start: SignalRW[float]
|
|
39
|
+
omega_increment: SignalRW[float]
|
|
40
|
+
arm: SignalX
|
|
41
|
+
disarm: SignalX
|
|
42
|
+
trigger: SignalX
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EigerDriverIO(Device):
|
|
46
|
+
"""Contains signals for handling IO on the Eiger detector."""
|
|
47
|
+
|
|
48
|
+
stale_parameters: SignalR[bool]
|
|
49
|
+
monitor: EigerMonitorIO
|
|
50
|
+
stream: EigerStreamIO
|
|
51
|
+
detector: EigerDetectorIO
|
|
52
|
+
|
|
53
|
+
def __init__(self, uri: str, name: str = ""):
|
|
54
|
+
super().__init__(name=name, connector=fastcs_connector(self, uri))
|
|
@@ -10,7 +10,6 @@ from ophyd_async.core import (
|
|
|
10
10
|
DetectorWriter,
|
|
11
11
|
HDFDatasetDescription,
|
|
12
12
|
HDFDocumentComposer,
|
|
13
|
-
NameProvider,
|
|
14
13
|
PathProvider,
|
|
15
14
|
observe_value,
|
|
16
15
|
wait_for_value,
|
|
@@ -25,24 +24,22 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
25
24
|
def __init__(
|
|
26
25
|
self,
|
|
27
26
|
path_provider: PathProvider,
|
|
28
|
-
name_provider: NameProvider,
|
|
29
27
|
panda_data_block: DataBlock,
|
|
30
28
|
) -> None:
|
|
31
29
|
self.panda_data_block = panda_data_block
|
|
32
30
|
self._path_provider = path_provider
|
|
33
|
-
self._name_provider = name_provider
|
|
34
31
|
self._datasets: list[HDFDatasetDescription] = []
|
|
35
32
|
self._composer: HDFDocumentComposer | None = None
|
|
36
|
-
self._multiplier = 1
|
|
37
33
|
|
|
38
34
|
# Triggered on PCAP arm
|
|
39
|
-
async def open(self,
|
|
35
|
+
async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]:
|
|
40
36
|
"""Retrieve and get descriptor of all PandA signals marked for capture."""
|
|
37
|
+
self._exposures_per_event = exposures_per_event
|
|
41
38
|
# Ensure flushes are immediate
|
|
42
39
|
await self.panda_data_block.flush_period.set(0)
|
|
43
40
|
|
|
44
41
|
self._composer = None
|
|
45
|
-
info = self._path_provider(device_name=
|
|
42
|
+
info = self._path_provider(device_name=name)
|
|
46
43
|
|
|
47
44
|
# Set create dir depth first to guarantee that callback when setting
|
|
48
45
|
# directory path has correct value
|
|
@@ -66,21 +63,19 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
66
63
|
|
|
67
64
|
# Wait for it to start, stashing the status that tells us when it finishes
|
|
68
65
|
await self.panda_data_block.capture.set(True)
|
|
69
|
-
if multiplier > 1:
|
|
70
|
-
raise ValueError(
|
|
71
|
-
"All PandA datasets should be scalar, multiplier should be 1"
|
|
72
|
-
)
|
|
73
66
|
|
|
74
|
-
return await self._describe()
|
|
67
|
+
return await self._describe(name)
|
|
75
68
|
|
|
76
|
-
async def _describe(self) -> dict[str, DataKey]:
|
|
69
|
+
async def _describe(self, name: str) -> dict[str, DataKey]:
|
|
77
70
|
"""Return a describe based on the datasets PV."""
|
|
78
|
-
await self._update_datasets()
|
|
71
|
+
await self._update_datasets(name)
|
|
79
72
|
describe = {
|
|
80
73
|
ds.data_key: DataKey(
|
|
81
74
|
source=self.panda_data_block.hdf_directory.source,
|
|
82
75
|
shape=list(ds.shape),
|
|
83
|
-
dtype="
|
|
76
|
+
dtype="array"
|
|
77
|
+
if self._exposures_per_event > 1 or len(ds.shape) > 1
|
|
78
|
+
else "number",
|
|
84
79
|
# PandA data should always be written as Float64
|
|
85
80
|
dtype_numpy=ds.dtype_numpy,
|
|
86
81
|
external="STREAM:",
|
|
@@ -89,7 +84,7 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
89
84
|
}
|
|
90
85
|
return describe
|
|
91
86
|
|
|
92
|
-
async def _update_datasets(self) -> None:
|
|
87
|
+
async def _update_datasets(self, name: str) -> None:
|
|
93
88
|
# Load data from the datasets PV on the panda, update internal
|
|
94
89
|
# representation of datasets that the panda will write.
|
|
95
90
|
capture_table = await self.panda_data_block.datasets.get_value()
|
|
@@ -99,9 +94,10 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
99
94
|
HDFDatasetDescription(
|
|
100
95
|
data_key=dataset_name,
|
|
101
96
|
dataset="/" + dataset_name,
|
|
102
|
-
shape=()
|
|
97
|
+
shape=(self._exposures_per_event,)
|
|
98
|
+
if self._exposures_per_event > 1
|
|
99
|
+
else (),
|
|
103
100
|
dtype_numpy="<f8",
|
|
104
|
-
multiplier=1,
|
|
105
101
|
chunk_shape=(1024,),
|
|
106
102
|
)
|
|
107
103
|
for dataset_name in capture_table.name
|
|
@@ -111,7 +107,7 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
111
107
|
# i.e. no stream resources will be generated
|
|
112
108
|
if len(self._datasets) == 0:
|
|
113
109
|
self.panda_data_block.log.warning(
|
|
114
|
-
f"PandA {
|
|
110
|
+
f"PandA {name} DATASETS table is empty! "
|
|
115
111
|
"No stream resource docs will be generated. "
|
|
116
112
|
"Make sure captured positions have their corresponding "
|
|
117
113
|
"*:DATASET PV set to a scientifically relevant name."
|
|
@@ -121,7 +117,9 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
121
117
|
# StandardDetector behavior
|
|
122
118
|
async def wait_for_index(self, index: int, timeout: float | None = DEFAULT_TIMEOUT):
|
|
123
119
|
def matcher(value: int) -> bool:
|
|
124
|
-
|
|
120
|
+
# Index is already divided by exposures_per_event, so we need to also
|
|
121
|
+
# divide the value by exposures_per_event to get the correct index
|
|
122
|
+
return value // self._exposures_per_event >= index
|
|
125
123
|
|
|
126
124
|
matcher.__name__ = f"index_at_least_{index}"
|
|
127
125
|
await wait_for_value(
|
|
@@ -129,7 +127,10 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
129
127
|
)
|
|
130
128
|
|
|
131
129
|
async def get_indices_written(self) -> int:
|
|
132
|
-
return
|
|
130
|
+
return (
|
|
131
|
+
await self.panda_data_block.num_captured.get_value()
|
|
132
|
+
// self._exposures_per_event
|
|
133
|
+
)
|
|
133
134
|
|
|
134
135
|
async def observe_indices_written(
|
|
135
136
|
self, timeout: float
|
|
@@ -138,10 +139,10 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
138
139
|
async for num_captured in observe_value(
|
|
139
140
|
self.panda_data_block.num_captured, timeout
|
|
140
141
|
):
|
|
141
|
-
yield num_captured // self.
|
|
142
|
+
yield num_captured // self._exposures_per_event
|
|
142
143
|
|
|
143
144
|
async def collect_stream_docs(
|
|
144
|
-
self, indices_written: int
|
|
145
|
+
self, name: str, indices_written: int
|
|
145
146
|
) -> AsyncIterator[StreamAsset]:
|
|
146
147
|
# TODO: fail if we get dropped frames
|
|
147
148
|
if indices_written:
|
ophyd_async/plan_stubs/_fly.py
CHANGED
|
@@ -60,11 +60,11 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
|
60
60
|
deadtime = max(det._controller.get_deadtime(exposure) for det in detectors) # noqa: SLF001
|
|
61
61
|
|
|
62
62
|
trigger_info = TriggerInfo(
|
|
63
|
-
|
|
63
|
+
number_of_events=number_of_frames * repeats,
|
|
64
64
|
trigger=DetectorTrigger.CONSTANT_GATE,
|
|
65
65
|
deadtime=deadtime,
|
|
66
66
|
livetime=exposure,
|
|
67
|
-
|
|
67
|
+
exposure_timeout=frame_timeout,
|
|
68
68
|
)
|
|
69
69
|
trigger_time = number_of_frames * (exposure + deadtime)
|
|
70
70
|
pre_delay = max(period - 2 * shutter_time - trigger_time, 0)
|