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/core/_utils.py
CHANGED
|
@@ -5,7 +5,15 @@ import logging
|
|
|
5
5
|
from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from enum import Enum, EnumMeta
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Generic,
|
|
11
|
+
Literal,
|
|
12
|
+
ParamSpec,
|
|
13
|
+
TypeVar,
|
|
14
|
+
get_args,
|
|
15
|
+
get_origin,
|
|
16
|
+
)
|
|
9
17
|
from unittest.mock import Mock
|
|
10
18
|
|
|
11
19
|
import numpy as np
|
|
@@ -19,20 +27,16 @@ DEFAULT_TIMEOUT = 10.0
|
|
|
19
27
|
logger = logging.getLogger("ophyd_async")
|
|
20
28
|
|
|
21
29
|
|
|
22
|
-
class
|
|
23
|
-
def __new__(
|
|
24
|
-
ret = super().__new__(
|
|
30
|
+
class UppercaseNameEnumMeta(EnumMeta):
|
|
31
|
+
def __new__(cls, *args, **kwargs):
|
|
32
|
+
ret = super().__new__(cls, *args, **kwargs)
|
|
25
33
|
lowercase_names = [x.name for x in ret if not x.name.isupper()] # type: ignore
|
|
26
34
|
if lowercase_names:
|
|
27
35
|
raise TypeError(f"Names {lowercase_names} should be uppercase")
|
|
28
36
|
return ret
|
|
29
37
|
|
|
30
38
|
|
|
31
|
-
class
|
|
32
|
-
"""All members should exist in the Backend, and there will be no extras."""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class SubsetEnumMeta(StrictEnumMeta):
|
|
39
|
+
class AnyStringUppercaseNameEnumMeta(UppercaseNameEnumMeta):
|
|
36
40
|
def __call__(self, value, *args, **kwargs): # type: ignore
|
|
37
41
|
"""Return given value if it is a string and not a member of the enum.
|
|
38
42
|
|
|
@@ -54,10 +58,21 @@ class SubsetEnumMeta(StrictEnumMeta):
|
|
|
54
58
|
return super().__call__(value, *args, **kwargs)
|
|
55
59
|
|
|
56
60
|
|
|
57
|
-
class
|
|
61
|
+
class StrictEnum(str, Enum, metaclass=UppercaseNameEnumMeta):
|
|
62
|
+
"""All members should exist in the Backend, and there will be no extras."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SubsetEnum(str, Enum, metaclass=AnyStringUppercaseNameEnumMeta):
|
|
58
66
|
"""All members should exist in the Backend, but there may be extras."""
|
|
59
67
|
|
|
60
68
|
|
|
69
|
+
class SupersetEnum(str, Enum, metaclass=UppercaseNameEnumMeta):
|
|
70
|
+
"""Some members should exist in the Backend, and there should be no extras."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
EnumTypes = StrictEnum | SubsetEnum | SupersetEnum
|
|
74
|
+
|
|
75
|
+
|
|
61
76
|
CALCULATE_TIMEOUT = "CALCULATE_TIMEOUT"
|
|
62
77
|
"""Sentinel used to implement ``myfunc(timeout=CalculateTimeout)``
|
|
63
78
|
|
|
@@ -207,10 +222,11 @@ def get_dtype(datatype: type) -> np.dtype:
|
|
|
207
222
|
return np.dtype(get_args(get_args(datatype)[1])[0])
|
|
208
223
|
|
|
209
224
|
|
|
210
|
-
def get_enum_cls(datatype: type | None) -> type[
|
|
225
|
+
def get_enum_cls(datatype: type | None) -> type[EnumTypes] | None:
|
|
211
226
|
"""Get the enum class from a datatype.
|
|
212
227
|
|
|
213
|
-
:raises TypeError: if type is not a [](#StrictEnum) or [](#SubsetEnum)
|
|
228
|
+
:raises TypeError: if type is not a [](#StrictEnum) or [](#SubsetEnum)
|
|
229
|
+
or [](#SupersetEnum) subclass
|
|
214
230
|
```python
|
|
215
231
|
>>> from ophyd_async.core import StrictEnum
|
|
216
232
|
>>> from collections.abc import Sequence
|
|
@@ -227,10 +243,11 @@ def get_enum_cls(datatype: type | None) -> type[StrictEnum] | None:
|
|
|
227
243
|
if get_origin(datatype) is Sequence:
|
|
228
244
|
datatype = get_args(datatype)[0]
|
|
229
245
|
if datatype and issubclass(datatype, Enum):
|
|
230
|
-
if not issubclass(datatype,
|
|
246
|
+
if not issubclass(datatype, EnumTypes):
|
|
231
247
|
raise TypeError(
|
|
232
248
|
f"{datatype} should inherit from ophyd_async.core.SubsetEnum "
|
|
233
|
-
"or ophyd_async.core.StrictEnum"
|
|
249
|
+
"or ophyd_async.core.StrictEnum "
|
|
250
|
+
"or ophyd_async.core.SupersetEnum."
|
|
234
251
|
)
|
|
235
252
|
return datatype
|
|
236
253
|
return None
|
|
@@ -32,7 +32,7 @@ class Andor2Controller(adcore.ADBaseController[Andor2DriverIO]):
|
|
|
32
32
|
await asyncio.gather(
|
|
33
33
|
self.driver.trigger_mode.set(self._get_trigger_mode(trigger_info.trigger)),
|
|
34
34
|
self.driver.num_images.set(
|
|
35
|
-
trigger_info.
|
|
35
|
+
trigger_info.total_number_of_exposures or _MAX_NUM_IMAGE
|
|
36
36
|
),
|
|
37
37
|
self.driver.image_mode.set(adcore.ADImageMode.MULTIPLE),
|
|
38
38
|
)
|
|
@@ -35,11 +35,11 @@ class AravisController(adcore.ADBaseController[AravisDriverIO]):
|
|
|
35
35
|
else:
|
|
36
36
|
raise ValueError(f"ADAravis does not support {trigger_info.trigger}")
|
|
37
37
|
|
|
38
|
-
if trigger_info.
|
|
38
|
+
if trigger_info.total_number_of_exposures == 0:
|
|
39
39
|
image_mode = adcore.ADImageMode.CONTINUOUS
|
|
40
40
|
else:
|
|
41
41
|
image_mode = adcore.ADImageMode.MULTIPLE
|
|
42
42
|
await asyncio.gather(
|
|
43
|
-
self.driver.num_images.set(trigger_info.
|
|
43
|
+
self.driver.num_images.set(trigger_info.total_number_of_exposures),
|
|
44
44
|
self.driver.image_mode.set(image_mode),
|
|
45
45
|
)
|
|
@@ -22,8 +22,8 @@ class AreaDetector(StandardDetector[ADBaseControllerT, ADWriter]):
|
|
|
22
22
|
self.fileio = writer.fileio
|
|
23
23
|
|
|
24
24
|
if plugins is not None:
|
|
25
|
-
for
|
|
26
|
-
setattr(self,
|
|
25
|
+
for plugin_name, plugin in plugins.items():
|
|
26
|
+
setattr(self, plugin_name, plugin)
|
|
27
27
|
|
|
28
28
|
super().__init__(
|
|
29
29
|
controller,
|
|
@@ -70,14 +70,14 @@ class NDPluginStatsIO(NDPluginBaseIO):
|
|
|
70
70
|
# Basic statistics
|
|
71
71
|
compute_statistics: A[SignalRW[bool], PvSuffix.rbv("ComputeStatistics")]
|
|
72
72
|
bgd_width: A[SignalRW[int], PvSuffix.rbv("BgdWidth")]
|
|
73
|
-
|
|
73
|
+
total: A[SignalR[float], PvSuffix.rbv("Total")]
|
|
74
74
|
# Centroid statistics
|
|
75
75
|
compute_centroid: A[SignalRW[bool], PvSuffix.rbv("ComputeCentroid")]
|
|
76
76
|
centroid_threshold: A[SignalRW[float], PvSuffix.rbv("CentroidThreshold")]
|
|
77
77
|
# X and Y Profiles
|
|
78
78
|
compute_profiles: A[SignalRW[bool], PvSuffix.rbv("ComputeProfiles")]
|
|
79
|
-
profile_size_x: A[
|
|
80
|
-
profile_size_y: A[
|
|
79
|
+
profile_size_x: A[SignalR[int], PvSuffix.rbv("ProfileSizeX")]
|
|
80
|
+
profile_size_y: A[SignalR[int], PvSuffix.rbv("ProfileSizeY")]
|
|
81
81
|
cursor_x: A[SignalRW[int], PvSuffix.rbv("CursorX")]
|
|
82
82
|
cursor_y: A[SignalRW[int], PvSuffix.rbv("CursorY")]
|
|
83
83
|
# Array Histogram
|
|
@@ -33,7 +33,7 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
|
|
|
33
33
|
driver: ADBaseIOT,
|
|
34
34
|
good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
|
|
35
35
|
) -> None:
|
|
36
|
-
self.driver = driver
|
|
36
|
+
self.driver: ADBaseIOT = driver
|
|
37
37
|
self.good_states = good_states
|
|
38
38
|
self.frame_timeout = DEFAULT_TIMEOUT
|
|
39
39
|
self._arm_status: AsyncStatus | None = None
|
|
@@ -49,7 +49,7 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
|
|
|
49
49
|
DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
|
|
50
50
|
)
|
|
51
51
|
await asyncio.gather(
|
|
52
|
-
self.driver.num_images.set(trigger_info.
|
|
52
|
+
self.driver.num_images.set(trigger_info.total_number_of_exposures),
|
|
53
53
|
self.driver.image_mode.set(ADImageMode.MULTIPLE),
|
|
54
54
|
)
|
|
55
55
|
|
|
@@ -176,7 +176,7 @@ class ADBaseContAcqController(ADBaseController[ADBaseIO]):
|
|
|
176
176
|
await asyncio.gather(
|
|
177
177
|
self.cb_plugin.enable_callbacks.set(ADCallbacks.ENABLE),
|
|
178
178
|
self.cb_plugin.pre_count.set(0),
|
|
179
|
-
self.cb_plugin.post_count.set(trigger_info.
|
|
179
|
+
self.cb_plugin.post_count.set(trigger_info.total_number_of_exposures),
|
|
180
180
|
self.cb_plugin.preset_trigger_count.set(1),
|
|
181
181
|
self.cb_plugin.flush_on_soft_trg.set(NDCBFlushOnSoftTrgMode.ON_NEW_IMAGE),
|
|
182
182
|
)
|
|
@@ -5,14 +5,15 @@ from typing import Generic, TypeVar, get_args
|
|
|
5
5
|
from urllib.parse import urlunparse
|
|
6
6
|
|
|
7
7
|
from bluesky.protocols import Hints, StreamAsset
|
|
8
|
-
from event_model import (
|
|
8
|
+
from event_model import ( # type: ignore
|
|
9
9
|
ComposeStreamResource,
|
|
10
10
|
DataKey,
|
|
11
11
|
StreamRange,
|
|
12
12
|
)
|
|
13
|
+
from pydantic import PositiveInt
|
|
13
14
|
|
|
14
15
|
from ophyd_async.core._detector import DetectorWriter
|
|
15
|
-
from ophyd_async.core._providers import DatasetDescriber,
|
|
16
|
+
from ophyd_async.core._providers import DatasetDescriber, PathProvider
|
|
16
17
|
from ophyd_async.core._signal import (
|
|
17
18
|
observe_value,
|
|
18
19
|
set_and_wait_for_value,
|
|
@@ -44,7 +45,6 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
|
|
|
44
45
|
self,
|
|
45
46
|
fileio: NDFileIOT,
|
|
46
47
|
path_provider: PathProvider,
|
|
47
|
-
name_provider: NameProvider,
|
|
48
48
|
dataset_describer: DatasetDescriber,
|
|
49
49
|
file_extension: str = "",
|
|
50
50
|
mimetype: str = "",
|
|
@@ -53,7 +53,6 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
|
|
|
53
53
|
self._plugins = plugins or {}
|
|
54
54
|
self.fileio = fileio
|
|
55
55
|
self._path_provider = path_provider
|
|
56
|
-
self._name_provider = name_provider
|
|
57
56
|
self._dataset_describer = dataset_describer
|
|
58
57
|
self._file_extension = file_extension
|
|
59
58
|
self._mimetype = mimetype
|
|
@@ -61,7 +60,6 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
|
|
|
61
60
|
self._emitted_resource = None
|
|
62
61
|
|
|
63
62
|
self._capture_status: AsyncStatus | None = None
|
|
64
|
-
self._multiplier = 1
|
|
65
63
|
self._filename_template = "%s%s_%6.6d"
|
|
66
64
|
|
|
67
65
|
@classmethod
|
|
@@ -81,18 +79,11 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
|
|
|
81
79
|
fileio = fileio_cls(prefix + (fileio_suffix or cls.default_suffix))
|
|
82
80
|
dataset_describer = ADBaseDatasetDescriber(dataset_source or fileio)
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
if fileio.parent == "Not attached to a detector":
|
|
86
|
-
raise RuntimeError("Initializing writer without parent detector!")
|
|
87
|
-
return fileio.parent.name
|
|
88
|
-
|
|
89
|
-
writer = cls(
|
|
90
|
-
fileio, path_provider, name_provider, dataset_describer, plugins=plugins
|
|
91
|
-
)
|
|
82
|
+
writer = cls(fileio, path_provider, dataset_describer, plugins=plugins)
|
|
92
83
|
return writer
|
|
93
84
|
|
|
94
|
-
async def begin_capture(self) -> None:
|
|
95
|
-
info = self._path_provider(device_name=
|
|
85
|
+
async def begin_capture(self, name: str) -> None:
|
|
86
|
+
info = self._path_provider(device_name=name)
|
|
96
87
|
|
|
97
88
|
await self.fileio.enable_callbacks.set(ADCallbacks.ENABLE)
|
|
98
89
|
|
|
@@ -125,19 +116,21 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
|
|
|
125
116
|
self.fileio.capture, True, wait_for_set_completion=False
|
|
126
117
|
)
|
|
127
118
|
|
|
128
|
-
async def open(
|
|
119
|
+
async def open(
|
|
120
|
+
self, name: str, exposures_per_event: PositiveInt = 1
|
|
121
|
+
) -> dict[str, DataKey]:
|
|
129
122
|
self._emitted_resource = None
|
|
130
123
|
self._last_emitted = 0
|
|
131
|
-
self.
|
|
124
|
+
self._exposures_per_event = exposures_per_event
|
|
132
125
|
frame_shape = await self._dataset_describer.shape()
|
|
133
126
|
dtype_numpy = await self._dataset_describer.np_datatype()
|
|
134
127
|
|
|
135
|
-
await self.begin_capture()
|
|
128
|
+
await self.begin_capture(name)
|
|
136
129
|
|
|
137
130
|
describe = {
|
|
138
|
-
|
|
131
|
+
name: DataKey(
|
|
139
132
|
source=self.fileio.full_file_name.source,
|
|
140
|
-
shape=
|
|
133
|
+
shape=[exposures_per_event, *frame_shape],
|
|
141
134
|
dtype="array",
|
|
142
135
|
dtype_numpy=dtype_numpy,
|
|
143
136
|
external="STREAM:",
|
|
@@ -150,14 +143,14 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
|
|
|
150
143
|
) -> AsyncGenerator[int, None]:
|
|
151
144
|
"""Wait until a specific index is ready to be collected."""
|
|
152
145
|
async for num_captured in observe_value(self.fileio.num_captured, timeout):
|
|
153
|
-
yield num_captured // self.
|
|
146
|
+
yield num_captured // self._exposures_per_event
|
|
154
147
|
|
|
155
148
|
async def get_indices_written(self) -> int:
|
|
156
149
|
num_captured = await self.fileio.num_captured.get_value()
|
|
157
|
-
return num_captured // self.
|
|
150
|
+
return num_captured // self._exposures_per_event
|
|
158
151
|
|
|
159
152
|
async def collect_stream_docs(
|
|
160
|
-
self, indices_written: int
|
|
153
|
+
self, name: str, indices_written: int
|
|
161
154
|
) -> AsyncIterator[StreamAsset]:
|
|
162
155
|
if indices_written:
|
|
163
156
|
if not self._emitted_resource:
|
|
@@ -183,13 +176,14 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
|
|
|
183
176
|
self._emitted_resource = bundler_composer(
|
|
184
177
|
mimetype=self._mimetype,
|
|
185
178
|
uri=uri,
|
|
186
|
-
|
|
179
|
+
# TODO no reference to detector's name
|
|
180
|
+
data_key=name,
|
|
187
181
|
parameters={
|
|
188
|
-
# Assume that we always write 1 frame per file/chunk
|
|
182
|
+
# Assume that we always write 1 frame per file/chunk, this
|
|
183
|
+
# may change to self._exposures_per_event in the future
|
|
189
184
|
"chunk_shape": (1, *frame_shape),
|
|
190
185
|
# Include file template for reconstruction in consolidator
|
|
191
186
|
"template": file_template,
|
|
192
|
-
"multiplier": self._multiplier,
|
|
193
187
|
},
|
|
194
188
|
uid=None,
|
|
195
189
|
validate=True,
|
|
@@ -218,6 +212,5 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
|
|
|
218
212
|
await self._capture_status
|
|
219
213
|
self._capture_status = None
|
|
220
214
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return {"fields": [self._name_provider()]}
|
|
215
|
+
def get_hints(self, name: str) -> Hints:
|
|
216
|
+
return {"fields": [name]}
|
|
@@ -5,12 +5,12 @@ from xml.etree import ElementTree as ET
|
|
|
5
5
|
|
|
6
6
|
from bluesky.protocols import StreamAsset
|
|
7
7
|
from event_model import DataKey
|
|
8
|
+
from pydantic import PositiveInt
|
|
8
9
|
|
|
9
10
|
from ophyd_async.core import (
|
|
10
11
|
DatasetDescriber,
|
|
11
12
|
HDFDatasetDescription,
|
|
12
13
|
HDFDocumentComposer,
|
|
13
|
-
NameProvider,
|
|
14
14
|
PathProvider,
|
|
15
15
|
)
|
|
16
16
|
|
|
@@ -31,14 +31,12 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
|
|
|
31
31
|
self,
|
|
32
32
|
fileio: NDFileHDFIO,
|
|
33
33
|
path_provider: PathProvider,
|
|
34
|
-
name_provider: NameProvider,
|
|
35
34
|
dataset_describer: DatasetDescriber,
|
|
36
35
|
plugins: dict[str, NDPluginBaseIO] | None = None,
|
|
37
36
|
) -> None:
|
|
38
37
|
super().__init__(
|
|
39
38
|
fileio,
|
|
40
39
|
path_provider,
|
|
41
|
-
name_provider,
|
|
42
40
|
dataset_describer,
|
|
43
41
|
plugins=plugins,
|
|
44
42
|
file_extension=".h5",
|
|
@@ -48,7 +46,9 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
|
|
|
48
46
|
self._composer: HDFDocumentComposer | None = None
|
|
49
47
|
self._filename_template = "%s%s"
|
|
50
48
|
|
|
51
|
-
async def open(
|
|
49
|
+
async def open(
|
|
50
|
+
self, name: str, exposures_per_event: PositiveInt = 1
|
|
51
|
+
) -> dict[str, DataKey]:
|
|
52
52
|
self._composer = None
|
|
53
53
|
|
|
54
54
|
# Setting HDF writer specific signals
|
|
@@ -64,13 +64,13 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
|
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
# Set common AD file plugin params, begin capturing
|
|
67
|
-
await self.begin_capture()
|
|
67
|
+
await self.begin_capture(name)
|
|
68
68
|
|
|
69
|
-
name = self._name_provider()
|
|
70
69
|
detector_shape = await self._dataset_describer.shape()
|
|
71
70
|
np_dtype = await self._dataset_describer.np_datatype()
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
|
|
72
|
+
# Used by the base class
|
|
73
|
+
self._exposures_per_event = exposures_per_event
|
|
74
74
|
|
|
75
75
|
# Determine number of frames that will be saved per HDF chunk
|
|
76
76
|
frames_per_chunk = await self.fileio.num_frames_chunks.get_value()
|
|
@@ -80,9 +80,8 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
|
|
|
80
80
|
HDFDatasetDescription(
|
|
81
81
|
data_key=name,
|
|
82
82
|
dataset="/entry/data/data",
|
|
83
|
-
shape=detector_shape,
|
|
83
|
+
shape=(exposures_per_event, *detector_shape),
|
|
84
84
|
dtype_numpy=np_dtype,
|
|
85
|
-
multiplier=multiplier,
|
|
86
85
|
chunk_shape=(frames_per_chunk, *detector_shape),
|
|
87
86
|
)
|
|
88
87
|
]
|
|
@@ -107,20 +106,23 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
|
|
|
107
106
|
HDFDatasetDescription(
|
|
108
107
|
data_key=data_key,
|
|
109
108
|
dataset=f"/entry/instrument/NDAttributes/{data_key}",
|
|
110
|
-
shape=()
|
|
109
|
+
shape=(exposures_per_event,)
|
|
110
|
+
if exposures_per_event > 1
|
|
111
|
+
else (),
|
|
111
112
|
dtype_numpy=np_datatype,
|
|
112
113
|
# NDAttributes appear to always be configured with
|
|
113
114
|
# this chunk size
|
|
114
115
|
chunk_shape=(16384,),
|
|
115
|
-
multiplier=multiplier,
|
|
116
116
|
)
|
|
117
117
|
)
|
|
118
118
|
|
|
119
119
|
describe = {
|
|
120
120
|
ds.data_key: DataKey(
|
|
121
121
|
source=self.fileio.full_file_name.source,
|
|
122
|
-
shape=list(
|
|
123
|
-
dtype="array"
|
|
122
|
+
shape=list(ds.shape),
|
|
123
|
+
dtype="array"
|
|
124
|
+
if exposures_per_event > 1 or len(ds.shape) > 1
|
|
125
|
+
else "number",
|
|
124
126
|
dtype_numpy=ds.dtype_numpy,
|
|
125
127
|
external="STREAM:",
|
|
126
128
|
)
|
|
@@ -129,7 +131,7 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
|
|
|
129
131
|
return describe
|
|
130
132
|
|
|
131
133
|
async def collect_stream_docs(
|
|
132
|
-
self, indices_written: int
|
|
134
|
+
self, name: str, indices_written: int
|
|
133
135
|
) -> AsyncIterator[StreamAsset]:
|
|
134
136
|
# TODO: fail if we get dropped frames
|
|
135
137
|
await self.fileio.flush_now.set(True)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ophyd_async.core import DatasetDescriber,
|
|
1
|
+
from ophyd_async.core import DatasetDescriber, PathProvider
|
|
2
2
|
|
|
3
3
|
from ._core_io import NDFileIO, NDPluginBaseIO
|
|
4
4
|
from ._core_writer import ADWriter
|
|
@@ -11,14 +11,12 @@ class ADJPEGWriter(ADWriter[NDFileIO]):
|
|
|
11
11
|
self,
|
|
12
12
|
fileio: NDFileIO,
|
|
13
13
|
path_provider: PathProvider,
|
|
14
|
-
name_provider: NameProvider,
|
|
15
14
|
dataset_describer: DatasetDescriber,
|
|
16
15
|
plugins: dict[str, NDPluginBaseIO] | None = None,
|
|
17
16
|
) -> None:
|
|
18
17
|
super().__init__(
|
|
19
18
|
fileio,
|
|
20
19
|
path_provider,
|
|
21
|
-
name_provider,
|
|
22
20
|
dataset_describer,
|
|
23
21
|
plugins=plugins,
|
|
24
22
|
file_extension=".jpg",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ophyd_async.core import DatasetDescriber,
|
|
1
|
+
from ophyd_async.core import DatasetDescriber, PathProvider
|
|
2
2
|
|
|
3
3
|
from ._core_io import NDFileIO, NDPluginBaseIO
|
|
4
4
|
from ._core_writer import ADWriter
|
|
@@ -11,14 +11,12 @@ class ADTIFFWriter(ADWriter[NDFileIO]):
|
|
|
11
11
|
self,
|
|
12
12
|
fileio: NDFileIO,
|
|
13
13
|
path_provider: PathProvider,
|
|
14
|
-
name_provider: NameProvider,
|
|
15
14
|
dataset_describer: DatasetDescriber,
|
|
16
15
|
plugins: dict[str, NDPluginBaseIO] | None = None,
|
|
17
16
|
) -> None:
|
|
18
17
|
super().__init__(
|
|
19
18
|
fileio,
|
|
20
19
|
path_provider,
|
|
21
|
-
name_provider,
|
|
22
20
|
dataset_describer,
|
|
23
21
|
plugins=plugins,
|
|
24
22
|
file_extension=".tiff",
|
|
@@ -7,11 +7,12 @@ from ophyd_async.core import (
|
|
|
7
7
|
SignalRW,
|
|
8
8
|
StrictEnum,
|
|
9
9
|
SubsetEnum,
|
|
10
|
+
SupersetEnum,
|
|
10
11
|
wait_for_value,
|
|
11
12
|
)
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
class ADBaseDataType(
|
|
15
|
+
class ADBaseDataType(SupersetEnum):
|
|
15
16
|
INT8 = "Int8"
|
|
16
17
|
UINT8 = "UInt8"
|
|
17
18
|
INT16 = "Int16"
|
|
@@ -22,6 +23,9 @@ class ADBaseDataType(StrictEnum):
|
|
|
22
23
|
UINT64 = "UInt64"
|
|
23
24
|
FLOAT32 = "Float32"
|
|
24
25
|
FLOAT64 = "Float64"
|
|
26
|
+
# Driver database override will blank the enum string if it doesn't
|
|
27
|
+
# support a datatype
|
|
28
|
+
UNDEFINED = ""
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
def convert_ad_dtype_to_np(ad_dtype: ADBaseDataType) -> str:
|
|
@@ -37,7 +41,12 @@ def convert_ad_dtype_to_np(ad_dtype: ADBaseDataType) -> str:
|
|
|
37
41
|
ADBaseDataType.FLOAT32: "<f4",
|
|
38
42
|
ADBaseDataType.FLOAT64: "<f8",
|
|
39
43
|
}
|
|
40
|
-
|
|
44
|
+
np_type = ad_dtype_to_np_dtype.get(ad_dtype)
|
|
45
|
+
if np_type is None:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
"Areadetector driver has a blank DataType, this is not supported"
|
|
48
|
+
)
|
|
49
|
+
return np_type
|
|
41
50
|
|
|
42
51
|
|
|
43
52
|
def convert_pv_dtype_to_np(datatype: str) -> str:
|
|
@@ -34,7 +34,7 @@ class KinetixController(adcore.ADBaseController[KinetixDriverIO]):
|
|
|
34
34
|
self.driver.trigger_mode.set(
|
|
35
35
|
KINETIX_TRIGGER_MODE_MAP[trigger_info.trigger]
|
|
36
36
|
),
|
|
37
|
-
self.driver.num_images.set(trigger_info.
|
|
37
|
+
self.driver.num_images.set(trigger_info.total_number_of_exposures),
|
|
38
38
|
self.driver.image_mode.set(adcore.ADImageMode.MULTIPLE),
|
|
39
39
|
)
|
|
40
40
|
if trigger_info.livetime is not None and trigger_info.trigger not in [
|
|
@@ -27,7 +27,7 @@ class PilatusDetector(AreaDetector[PilatusController]):
|
|
|
27
27
|
config_sigs: Sequence[SignalR] = (),
|
|
28
28
|
):
|
|
29
29
|
driver = PilatusDriverIO(prefix + drv_suffix)
|
|
30
|
-
controller = PilatusController(driver)
|
|
30
|
+
controller = PilatusController(driver, readout_time=readout_time)
|
|
31
31
|
|
|
32
32
|
writer = writer_cls.with_io(
|
|
33
33
|
prefix,
|
|
@@ -29,6 +29,7 @@ class PilatusController(adcore.ADBaseController[PilatusDriverIO]):
|
|
|
29
29
|
DetectorTrigger.INTERNAL: PilatusTriggerMode.INTERNAL,
|
|
30
30
|
DetectorTrigger.CONSTANT_GATE: PilatusTriggerMode.EXT_ENABLE,
|
|
31
31
|
DetectorTrigger.VARIABLE_GATE: PilatusTriggerMode.EXT_ENABLE,
|
|
32
|
+
DetectorTrigger.EDGE_TRIGGER: PilatusTriggerMode.EXT_TRIGGER,
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
def __init__(
|
|
@@ -49,11 +50,13 @@ class PilatusController(adcore.ADBaseController[PilatusDriverIO]):
|
|
|
49
50
|
trigger_info.livetime
|
|
50
51
|
)
|
|
51
52
|
await asyncio.gather(
|
|
52
|
-
self.driver.trigger_mode.set(
|
|
53
|
+
self.driver.trigger_mode.set(
|
|
54
|
+
self._supported_trigger_types[trigger_info.trigger]
|
|
55
|
+
),
|
|
53
56
|
self.driver.num_images.set(
|
|
54
57
|
999_999
|
|
55
|
-
if trigger_info.
|
|
56
|
-
else trigger_info.
|
|
58
|
+
if trigger_info.total_number_of_exposures == 0
|
|
59
|
+
else trigger_info.total_number_of_exposures
|
|
57
60
|
),
|
|
58
61
|
self.driver.image_mode.set(adcore.ADImageMode.MULTIPLE),
|
|
59
62
|
)
|
|
@@ -70,13 +73,3 @@ class PilatusController(adcore.ADBaseController[PilatusDriverIO]):
|
|
|
70
73
|
True,
|
|
71
74
|
timeout=DEFAULT_TIMEOUT,
|
|
72
75
|
)
|
|
73
|
-
|
|
74
|
-
@classmethod
|
|
75
|
-
def _get_trigger_mode(cls, trigger: DetectorTrigger) -> PilatusTriggerMode:
|
|
76
|
-
if trigger not in cls._supported_trigger_types.keys():
|
|
77
|
-
raise ValueError(
|
|
78
|
-
f"{cls.__name__} only supports the following trigger "
|
|
79
|
-
f"types: {cls._supported_trigger_types.keys()} but was asked to "
|
|
80
|
-
f"use {trigger}"
|
|
81
|
-
)
|
|
82
|
-
return cls._supported_trigger_types[trigger]
|
|
@@ -21,4 +21,4 @@ class PilatusDriverIO(adcore.ADBaseIO):
|
|
|
21
21
|
"""This mirrors the interface provided by ADPilatus/db/pilatus.template."""
|
|
22
22
|
"""See HTML docs at https://areadetector.github.io/areaDetector/ADPilatus/pilatusDoc.html"""
|
|
23
23
|
trigger_mode: A[SignalRW[PilatusTriggerMode], PvSuffix.rbv("TriggerMode")]
|
|
24
|
-
armed: A[SignalR[bool], PvSuffix
|
|
24
|
+
armed: A[SignalR[bool], PvSuffix("Armed")]
|
|
@@ -40,7 +40,7 @@ class VimbaController(adcore.ADBaseController[VimbaDriverIO]):
|
|
|
40
40
|
await asyncio.gather(
|
|
41
41
|
self.driver.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
|
|
42
42
|
self.driver.exposure_mode.set(EXPOSE_OUT_MODE[trigger_info.trigger]),
|
|
43
|
-
self.driver.num_images.set(trigger_info.
|
|
43
|
+
self.driver.num_images.set(trigger_info.total_number_of_exposures),
|
|
44
44
|
self.driver.image_mode.set(adcore.ADImageMode.MULTIPLE),
|
|
45
45
|
)
|
|
46
46
|
if trigger_info.livetime is not None and trigger_info.trigger not in [
|
ophyd_async/epics/core/_aioca.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import sys
|
|
3
3
|
import typing
|
|
4
|
-
from collections.abc import Sequence
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
5
|
from functools import cache
|
|
6
6
|
from math import isnan, nan
|
|
7
7
|
from typing import Any, Generic, cast
|
|
@@ -146,7 +146,7 @@ class CaBoolConverter(CaConverter[bool]):
|
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
class CaEnumConverter(CaConverter[str]):
|
|
149
|
-
def __init__(self, supported_values:
|
|
149
|
+
def __init__(self, supported_values: Mapping[str, str]):
|
|
150
150
|
self.supported_values = supported_values
|
|
151
151
|
super().__init__(
|
|
152
152
|
str, dbr.DBR_STRING, metadata=SignalMetadata(choices=list(supported_values))
|
ophyd_async/epics/core/_p4p.py
CHANGED
|
@@ -383,7 +383,7 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
383
383
|
|
|
384
384
|
async def put(self, value: SignalDatatypeT | None, wait: bool):
|
|
385
385
|
if value is None:
|
|
386
|
-
write_value = self.initial_values[self.write_pv]
|
|
386
|
+
write_value = self.initial_values[self.write_pv]["value"]
|
|
387
387
|
else:
|
|
388
388
|
write_value = self.converter.write_value(value)
|
|
389
389
|
await context().put(self.write_pv, {"value": write_value}, wait=wait)
|
|
@@ -43,10 +43,10 @@ FastCSPVIVector = dict[Literal["d"], Entry]
|
|
|
43
43
|
|
|
44
44
|
def _get_signal_details(entry: Entry) -> tuple[type[Signal], str, str]:
|
|
45
45
|
match entry:
|
|
46
|
-
case {"r": read_pv}:
|
|
47
|
-
return SignalR, read_pv, read_pv
|
|
48
46
|
case {"r": read_pv, "w": write_pv}:
|
|
49
47
|
return SignalRW, read_pv, write_pv
|
|
48
|
+
case {"r": read_pv}:
|
|
49
|
+
return SignalR, read_pv, read_pv
|
|
50
50
|
case {"w": write_pv}:
|
|
51
51
|
return SignalW, write_pv, write_pv
|
|
52
52
|
case {"rw": read_write_pv}:
|
|
@@ -77,6 +77,8 @@ class PviDeviceConnector(DeviceConnector):
|
|
|
77
77
|
hinted Signals are not present.
|
|
78
78
|
"""
|
|
79
79
|
|
|
80
|
+
mock_device_vector_len: int = 2
|
|
81
|
+
|
|
80
82
|
def __init__(self, prefix: str = "", error_hint: str = "") -> None:
|
|
81
83
|
# TODO: what happens if we get a leading "pva://" here?
|
|
82
84
|
self.prefix = prefix
|
|
@@ -110,7 +112,7 @@ class PviDeviceConnector(DeviceConnector):
|
|
|
110
112
|
backend.write_pv = write_pv
|
|
111
113
|
|
|
112
114
|
async def connect_mock(self, device: Device, mock: LazyMock):
|
|
113
|
-
self.filler.create_device_vector_entries_to_mock(
|
|
115
|
+
self.filler.create_device_vector_entries_to_mock(self.mock_device_vector_len)
|
|
114
116
|
# Set the name of the device to name all children
|
|
115
117
|
device.set_name(device.name)
|
|
116
118
|
return await super().connect_mock(device, mock)
|