ophyd-async 0.5.0__py3-none-any.whl → 0.5.2__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 +6 -3
- ophyd_async/core/_detector.py +38 -28
- ophyd_async/core/_hdf_dataset.py +1 -5
- ophyd_async/core/_mock_signal_utils.py +4 -3
- ophyd_async/core/_providers.py +30 -39
- ophyd_async/core/_signal.py +73 -28
- ophyd_async/core/_status.py +17 -1
- ophyd_async/epics/adaravis/_aravis.py +1 -1
- ophyd_async/epics/adcore/__init__.py +16 -5
- ophyd_async/epics/adcore/_core_io.py +29 -5
- ophyd_async/epics/adcore/_core_logic.py +7 -4
- ophyd_async/epics/adcore/_hdf_writer.py +51 -33
- ophyd_async/epics/adcore/_utils.py +69 -70
- ophyd_async/epics/adkinetix/_kinetix.py +1 -1
- ophyd_async/epics/adkinetix/_kinetix_io.py +4 -1
- ophyd_async/epics/adpilatus/_pilatus.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_controller.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_io.py +1 -1
- ophyd_async/epics/adsimdetector/_sim.py +1 -1
- ophyd_async/epics/advimba/_vimba.py +1 -1
- ophyd_async/epics/advimba/_vimba_controller.py +3 -3
- ophyd_async/epics/advimba/_vimba_io.py +6 -4
- ophyd_async/epics/eiger/__init__.py +5 -0
- ophyd_async/epics/eiger/_eiger.py +43 -0
- ophyd_async/epics/eiger/_eiger_controller.py +66 -0
- ophyd_async/epics/eiger/_eiger_io.py +42 -0
- ophyd_async/epics/eiger/_odin_io.py +125 -0
- ophyd_async/epics/motor.py +16 -3
- ophyd_async/epics/signal/_aioca.py +12 -5
- ophyd_async/epics/signal/_common.py +1 -1
- ophyd_async/epics/signal/_p4p.py +14 -11
- ophyd_async/fastcs/panda/__init__.py +3 -3
- ophyd_async/fastcs/panda/{_common_blocks.py → _block.py} +2 -0
- ophyd_async/fastcs/panda/{_panda_controller.py → _control.py} +1 -1
- ophyd_async/fastcs/panda/_hdf_panda.py +4 -4
- ophyd_async/fastcs/panda/_trigger.py +1 -1
- ophyd_async/fastcs/panda/{_hdf_writer.py → _writer.py} +29 -22
- ophyd_async/plan_stubs/__init__.py +3 -0
- ophyd_async/plan_stubs/_nd_attributes.py +63 -0
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +5 -2
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +1 -3
- {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/METADATA +46 -44
- {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/RECORD +48 -42
- {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/WHEEL +1 -1
- {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import Device
|
|
4
|
+
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw_rbv, epics_signal_w
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EigerTriggerMode(str, Enum):
|
|
8
|
+
internal = "ints"
|
|
9
|
+
edge = "exts"
|
|
10
|
+
gate = "exte"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EigerDriverIO(Device):
|
|
14
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
15
|
+
self.bit_depth = epics_signal_r(int, f"{prefix}BitDepthReadout")
|
|
16
|
+
self.stale_parameters = epics_signal_r(bool, f"{prefix}StaleParameters")
|
|
17
|
+
self.state = epics_signal_r(str, f"{prefix}DetectorState")
|
|
18
|
+
self.roi_mode = epics_signal_rw_rbv(str, f"{prefix}RoiMode")
|
|
19
|
+
|
|
20
|
+
self.acquire_time = epics_signal_rw_rbv(float, f"{prefix}CountTime")
|
|
21
|
+
self.acquire_period = epics_signal_rw_rbv(float, f"{prefix}FrameTime")
|
|
22
|
+
|
|
23
|
+
self.num_images = epics_signal_rw_rbv(int, f"{prefix}Nimages")
|
|
24
|
+
self.num_triggers = epics_signal_rw_rbv(int, f"{prefix}Ntrigger")
|
|
25
|
+
|
|
26
|
+
# TODO: Should be EigerTriggerMode enum, see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
|
|
27
|
+
self.trigger_mode = epics_signal_rw_rbv(str, f"{prefix}TriggerMode")
|
|
28
|
+
|
|
29
|
+
self.arm = epics_signal_w(int, f"{prefix}Arm")
|
|
30
|
+
self.disarm = epics_signal_w(int, f"{prefix}Disarm")
|
|
31
|
+
self.abort = epics_signal_w(int, f"{prefix}Abort")
|
|
32
|
+
|
|
33
|
+
self.beam_centre_x = epics_signal_rw_rbv(float, f"{prefix}BeamCenterX")
|
|
34
|
+
self.beam_centre_y = epics_signal_rw_rbv(float, f"{prefix}BeamCenterY")
|
|
35
|
+
|
|
36
|
+
self.det_distance = epics_signal_rw_rbv(float, f"{prefix}DetectorDistance")
|
|
37
|
+
self.omega_start = epics_signal_rw_rbv(float, f"{prefix}OmegaStart")
|
|
38
|
+
self.omega_increment = epics_signal_rw_rbv(float, f"{prefix}OmegaIncrement")
|
|
39
|
+
|
|
40
|
+
self.photon_energy = epics_signal_rw_rbv(float, f"{prefix}PhotonEnergy")
|
|
41
|
+
|
|
42
|
+
super().__init__(name)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import AsyncGenerator, AsyncIterator, Dict
|
|
4
|
+
|
|
5
|
+
from bluesky.protocols import StreamAsset
|
|
6
|
+
from event_model.documents.event_descriptor import DataKey
|
|
7
|
+
|
|
8
|
+
from ophyd_async.core import (
|
|
9
|
+
DEFAULT_TIMEOUT,
|
|
10
|
+
DetectorWriter,
|
|
11
|
+
Device,
|
|
12
|
+
DeviceVector,
|
|
13
|
+
NameProvider,
|
|
14
|
+
PathProvider,
|
|
15
|
+
observe_value,
|
|
16
|
+
set_and_wait_for_value,
|
|
17
|
+
)
|
|
18
|
+
from ophyd_async.epics.signal import (
|
|
19
|
+
epics_signal_r,
|
|
20
|
+
epics_signal_rw,
|
|
21
|
+
epics_signal_rw_rbv,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Writing(str, Enum):
|
|
26
|
+
ON = "ON"
|
|
27
|
+
OFF = "OFF"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OdinNode(Device):
|
|
31
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
32
|
+
self.writing = epics_signal_r(Writing, f"{prefix}HDF:Writing")
|
|
33
|
+
self.connected = epics_signal_r(bool, f"{prefix}Connected")
|
|
34
|
+
|
|
35
|
+
super().__init__(name)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Odin(Device):
|
|
39
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
40
|
+
self.nodes = DeviceVector({i: OdinNode(f"{prefix}FP{i}:") for i in range(4)})
|
|
41
|
+
|
|
42
|
+
self.capture = epics_signal_rw(
|
|
43
|
+
Writing, f"{prefix}Writing", f"{prefix}ConfigHdfWrite"
|
|
44
|
+
)
|
|
45
|
+
self.num_captured = epics_signal_r(int, f"{prefix}FramesWritten")
|
|
46
|
+
self.num_to_capture = epics_signal_rw_rbv(int, f"{prefix}ConfigHdfFrames")
|
|
47
|
+
|
|
48
|
+
self.start_timeout = epics_signal_rw_rbv(int, f"{prefix}TimeoutTimerPeriod")
|
|
49
|
+
|
|
50
|
+
self.image_height = epics_signal_rw_rbv(int, f"{prefix}DatasetDataDims0")
|
|
51
|
+
self.image_width = epics_signal_rw_rbv(int, f"{prefix}DatasetDataDims1")
|
|
52
|
+
|
|
53
|
+
self.num_row_chunks = epics_signal_rw_rbv(int, f"{prefix}DatasetDataChunks1")
|
|
54
|
+
self.num_col_chunks = epics_signal_rw_rbv(int, f"{prefix}DatasetDataChunks2")
|
|
55
|
+
|
|
56
|
+
self.file_path = epics_signal_rw_rbv(str, f"{prefix}ConfigHdfFilePath")
|
|
57
|
+
self.file_name = epics_signal_rw_rbv(str, f"{prefix}ConfigHdfFilePrefix")
|
|
58
|
+
|
|
59
|
+
self.acquisition_id = epics_signal_rw_rbv(
|
|
60
|
+
str, f"{prefix}ConfigHdfAcquisitionId"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.data_type = epics_signal_rw_rbv(str, f"{prefix}DatasetDataDatatype")
|
|
64
|
+
|
|
65
|
+
super().__init__(name)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class OdinWriter(DetectorWriter):
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
path_provider: PathProvider,
|
|
72
|
+
name_provider: NameProvider,
|
|
73
|
+
odin_driver: Odin,
|
|
74
|
+
) -> None:
|
|
75
|
+
self._drv = odin_driver
|
|
76
|
+
self._path_provider = path_provider
|
|
77
|
+
self._name_provider = name_provider
|
|
78
|
+
super().__init__()
|
|
79
|
+
|
|
80
|
+
async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
|
|
81
|
+
info = self._path_provider(device_name=self._name_provider())
|
|
82
|
+
|
|
83
|
+
await asyncio.gather(
|
|
84
|
+
self._drv.file_path.set(str(info.directory_path)),
|
|
85
|
+
self._drv.file_name.set(info.filename),
|
|
86
|
+
self._drv.data_type.set(
|
|
87
|
+
"uint16"
|
|
88
|
+
), # TODO: Get from eiger https://github.com/bluesky/ophyd-async/issues/529
|
|
89
|
+
self._drv.num_to_capture.set(0),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
await self._drv.capture.set(Writing.ON)
|
|
93
|
+
|
|
94
|
+
return await self._describe()
|
|
95
|
+
|
|
96
|
+
async def _describe(self) -> Dict[str, DataKey]:
|
|
97
|
+
data_shape = await asyncio.gather(
|
|
98
|
+
self._drv.image_height.get_value(), self._drv.image_width.get_value()
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"data": DataKey(
|
|
103
|
+
source=self._drv.file_name.source,
|
|
104
|
+
shape=data_shape,
|
|
105
|
+
dtype="array",
|
|
106
|
+
dtype_numpy="<u2", # TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529
|
|
107
|
+
external="STREAM:",
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async def observe_indices_written(
|
|
112
|
+
self, timeout=DEFAULT_TIMEOUT
|
|
113
|
+
) -> AsyncGenerator[int, None]:
|
|
114
|
+
async for num_captured in observe_value(self._drv.num_captured, timeout):
|
|
115
|
+
yield num_captured
|
|
116
|
+
|
|
117
|
+
async def get_indices_written(self) -> int:
|
|
118
|
+
return await self._drv.num_captured.get_value()
|
|
119
|
+
|
|
120
|
+
def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
|
|
121
|
+
# TODO: Correctly return stream https://github.com/bluesky/ophyd-async/issues/530
|
|
122
|
+
raise NotImplementedError()
|
|
123
|
+
|
|
124
|
+
async def close(self) -> None:
|
|
125
|
+
await set_and_wait_for_value(self._drv.capture, Writing.OFF)
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
|
-
from bluesky.protocols import
|
|
4
|
+
from bluesky.protocols import (
|
|
5
|
+
Flyable,
|
|
6
|
+
Locatable,
|
|
7
|
+
Location,
|
|
8
|
+
Preparable,
|
|
9
|
+
Stoppable,
|
|
10
|
+
)
|
|
5
11
|
from pydantic import BaseModel, Field
|
|
6
12
|
|
|
7
13
|
from ophyd_async.core import (
|
|
@@ -51,7 +57,7 @@ class FlyMotorInfo(BaseModel):
|
|
|
51
57
|
timeout: CalculatableTimeout = Field(frozen=True, default=CalculateTimeout)
|
|
52
58
|
|
|
53
59
|
|
|
54
|
-
class Motor(StandardReadable,
|
|
60
|
+
class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
55
61
|
"""Device that moves a motor record"""
|
|
56
62
|
|
|
57
63
|
def __init__(self, prefix: str, name="") -> None:
|
|
@@ -113,7 +119,7 @@ class Motor(StandardReadable, Movable, Stoppable, Flyable, Preparable):
|
|
|
113
119
|
)
|
|
114
120
|
|
|
115
121
|
await self.set(fly_prepared_position)
|
|
116
|
-
await self.velocity.set(fly_velocity)
|
|
122
|
+
await self.velocity.set(abs(fly_velocity))
|
|
117
123
|
|
|
118
124
|
@AsyncStatus.wrap
|
|
119
125
|
async def kickoff(self):
|
|
@@ -193,6 +199,13 @@ class Motor(StandardReadable, Movable, Stoppable, Flyable, Preparable):
|
|
|
193
199
|
await self.velocity.set(abs(max_speed))
|
|
194
200
|
return fly_velocity
|
|
195
201
|
|
|
202
|
+
async def locate(self) -> Location[float]:
|
|
203
|
+
location: Location = {
|
|
204
|
+
"setpoint": await self.user_setpoint.get_value(),
|
|
205
|
+
"readback": await self.user_readback.get_value(),
|
|
206
|
+
}
|
|
207
|
+
return location
|
|
208
|
+
|
|
196
209
|
async def _prepare_motor_path(
|
|
197
210
|
self, fly_velocity: float, start_position: float, end_position: float
|
|
198
211
|
) -> float:
|
|
@@ -47,7 +47,7 @@ def _data_key_from_augmented_value(
|
|
|
47
47
|
value: AugmentedValue,
|
|
48
48
|
*,
|
|
49
49
|
choices: Optional[List[str]] = None,
|
|
50
|
-
dtype: Optional[
|
|
50
|
+
dtype: Optional[Dtype] = None,
|
|
51
51
|
) -> DataKey:
|
|
52
52
|
"""Use the return value of get with FORMAT_CTRL to construct a DataKey
|
|
53
53
|
describing the signal. See docstring of AugmentedValue for expected
|
|
@@ -175,7 +175,7 @@ class CaBoolConverter(CaConverter):
|
|
|
175
175
|
return bool(value)
|
|
176
176
|
|
|
177
177
|
def get_datakey(self, value: AugmentedValue) -> DataKey:
|
|
178
|
-
return _data_key_from_augmented_value(value, dtype="
|
|
178
|
+
return _data_key_from_augmented_value(value, dtype="boolean")
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
class DisconnectedCaConverter(CaConverter):
|
|
@@ -229,10 +229,17 @@ def make_converter(
|
|
|
229
229
|
value = list(values.values())[0]
|
|
230
230
|
# Done the dbr check, so enough to check one of the values
|
|
231
231
|
if datatype and not isinstance(value, datatype):
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
# Allow int signals to represent float records when prec is 0
|
|
233
|
+
is_prec_zero_float = (
|
|
234
|
+
isinstance(value, float)
|
|
235
|
+
and get_unique({k: v.precision for k, v in values.items()}, "precision")
|
|
236
|
+
== 0
|
|
235
237
|
)
|
|
238
|
+
if not (datatype is int and is_prec_zero_float):
|
|
239
|
+
raise TypeError(
|
|
240
|
+
f"{pv} has type {type(value).__name__.replace('ca_', '')} "
|
|
241
|
+
+ f"not {datatype.__name__}"
|
|
242
|
+
)
|
|
236
243
|
return CaConverter(pv_dbr, None)
|
|
237
244
|
|
|
238
245
|
|
|
@@ -55,7 +55,7 @@ def get_supported_values(
|
|
|
55
55
|
f"which do not match {datatype}, which has {choices}."
|
|
56
56
|
)
|
|
57
57
|
return {x: datatype(x) if x else "_" for x in pv_choices}
|
|
58
|
-
elif datatype is None:
|
|
58
|
+
elif datatype is None or datatype is str:
|
|
59
59
|
return {x: x or "_" for x in pv_choices}
|
|
60
60
|
|
|
61
61
|
raise TypeError(
|
ophyd_async/epics/signal/_p4p.py
CHANGED
|
@@ -64,7 +64,7 @@ def _data_key_from_value(
|
|
|
64
64
|
*,
|
|
65
65
|
shape: Optional[list[int]] = None,
|
|
66
66
|
choices: Optional[list[str]] = None,
|
|
67
|
-
dtype: Optional[
|
|
67
|
+
dtype: Optional[Dtype] = None,
|
|
68
68
|
) -> DataKey:
|
|
69
69
|
"""
|
|
70
70
|
Args:
|
|
@@ -85,7 +85,7 @@ def _data_key_from_value(
|
|
|
85
85
|
if isinstance(type_code, tuple):
|
|
86
86
|
dtype_numpy = ""
|
|
87
87
|
if type_code[1] == "enum_t":
|
|
88
|
-
if dtype == "
|
|
88
|
+
if dtype == "boolean":
|
|
89
89
|
dtype_numpy = "<i2"
|
|
90
90
|
else:
|
|
91
91
|
for item in type_code[2]:
|
|
@@ -241,7 +241,7 @@ class PvaEmumBoolConverter(PvaConverter):
|
|
|
241
241
|
return bool(value["value"]["index"])
|
|
242
242
|
|
|
243
243
|
def get_datakey(self, source: str, value) -> DataKey:
|
|
244
|
-
return _data_key_from_value(source, value, dtype="
|
|
244
|
+
return _data_key_from_value(source, value, dtype="boolean")
|
|
245
245
|
|
|
246
246
|
|
|
247
247
|
class PvaTableConverter(PvaConverter):
|
|
@@ -335,14 +335,17 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
|
|
|
335
335
|
return PvaEnumConverter(
|
|
336
336
|
get_supported_values(pv, datatype, datatype.choices)
|
|
337
337
|
)
|
|
338
|
-
elif (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
338
|
+
elif datatype and not issubclass(typ, datatype):
|
|
339
|
+
# Allow int signals to represent float records when prec is 0
|
|
340
|
+
is_prec_zero_float = typ is float and (
|
|
341
|
+
get_unique(
|
|
342
|
+
{k: v["display"]["precision"] for k, v in values.items()},
|
|
343
|
+
"precision",
|
|
344
|
+
)
|
|
345
|
+
== 0
|
|
346
|
+
)
|
|
347
|
+
if not (datatype is int and is_prec_zero_float):
|
|
348
|
+
raise TypeError(f"{pv} has type {typ.__name__} not {datatype.__name__}")
|
|
346
349
|
return PvaConverter()
|
|
347
350
|
elif "NTTable" in typeid:
|
|
348
351
|
return PvaTableConverter()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from ._block import (
|
|
2
2
|
CommonPandaBlocks,
|
|
3
3
|
DataBlock,
|
|
4
4
|
EnableDisableOptions,
|
|
@@ -9,9 +9,8 @@ from ._common_blocks import (
|
|
|
9
9
|
SeqBlock,
|
|
10
10
|
TimeUnits,
|
|
11
11
|
)
|
|
12
|
+
from ._control import PandaPcapController
|
|
12
13
|
from ._hdf_panda import HDFPanda
|
|
13
|
-
from ._hdf_writer import PandaHDFWriter
|
|
14
|
-
from ._panda_controller import PandaPcapController
|
|
15
14
|
from ._table import (
|
|
16
15
|
DatasetTable,
|
|
17
16
|
PandaHdf5DatasetType,
|
|
@@ -28,6 +27,7 @@ from ._trigger import (
|
|
|
28
27
|
StaticSeqTableTriggerLogic,
|
|
29
28
|
)
|
|
30
29
|
from ._utils import phase_sorter
|
|
30
|
+
from ._writer import PandaHDFWriter
|
|
31
31
|
|
|
32
32
|
__all__ = [
|
|
33
33
|
"CommonPandaBlocks",
|
|
@@ -13,6 +13,8 @@ class DataBlock(Device):
|
|
|
13
13
|
hdf_file_name: SignalRW[str]
|
|
14
14
|
num_capture: SignalRW[int]
|
|
15
15
|
num_captured: SignalR[int]
|
|
16
|
+
create_directory: SignalRW[int]
|
|
17
|
+
directory_exists: SignalR[bool]
|
|
16
18
|
capture: SignalRW[bool]
|
|
17
19
|
flush_period: SignalRW[float]
|
|
18
20
|
datasets: SignalR[DatasetTable]
|
|
@@ -5,9 +5,9 @@ from typing import Sequence
|
|
|
5
5
|
from ophyd_async.core import DEFAULT_TIMEOUT, PathProvider, SignalR, StandardDetector
|
|
6
6
|
from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries
|
|
7
7
|
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
8
|
+
from ._block import CommonPandaBlocks
|
|
9
|
+
from ._control import PandaPcapController
|
|
10
|
+
from ._writer import PandaHDFWriter
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class HDFPanda(CommonPandaBlocks, StandardDetector):
|
|
@@ -26,7 +26,7 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
|
|
|
26
26
|
prefix=prefix,
|
|
27
27
|
path_provider=path_provider,
|
|
28
28
|
name_provider=lambda: name,
|
|
29
|
-
|
|
29
|
+
panda_data_block=self.data,
|
|
30
30
|
)
|
|
31
31
|
super().__init__(
|
|
32
32
|
controller=controller,
|
|
@@ -5,7 +5,7 @@ from pydantic import BaseModel, Field
|
|
|
5
5
|
|
|
6
6
|
from ophyd_async.core import TriggerLogic, wait_for_value
|
|
7
7
|
|
|
8
|
-
from .
|
|
8
|
+
from ._block import PcompBlock, PcompDirectionOptions, SeqBlock, TimeUnits
|
|
9
9
|
from ._table import SeqTable
|
|
10
10
|
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ from ophyd_async.core import (
|
|
|
16
16
|
wait_for_value,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
-
from .
|
|
19
|
+
from ._block import DataBlock
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class PandaHDFWriter(DetectorWriter):
|
|
@@ -27,9 +27,9 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
27
27
|
prefix: str,
|
|
28
28
|
path_provider: PathProvider,
|
|
29
29
|
name_provider: NameProvider,
|
|
30
|
-
|
|
30
|
+
panda_data_block: DataBlock,
|
|
31
31
|
) -> None:
|
|
32
|
-
self.
|
|
32
|
+
self.panda_data_block = panda_data_block
|
|
33
33
|
self._prefix = prefix
|
|
34
34
|
self._path_provider = path_provider
|
|
35
35
|
self._name_provider = name_provider
|
|
@@ -42,25 +42,33 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
42
42
|
"""Retrieve and get descriptor of all PandA signals marked for capture"""
|
|
43
43
|
|
|
44
44
|
# Ensure flushes are immediate
|
|
45
|
-
await self.
|
|
45
|
+
await self.panda_data_block.flush_period.set(0)
|
|
46
46
|
|
|
47
47
|
self._file = None
|
|
48
|
-
info = self._path_provider(device_name=self.
|
|
48
|
+
info = self._path_provider(device_name=self._name_provider())
|
|
49
|
+
|
|
50
|
+
# Set create dir depth first to guarantee that callback when setting
|
|
51
|
+
# directory path has correct value
|
|
52
|
+
await self.panda_data_block.create_directory.set(info.create_dir_depth)
|
|
53
|
+
|
|
49
54
|
# Set the initial values
|
|
50
55
|
await asyncio.gather(
|
|
51
|
-
self.
|
|
52
|
-
|
|
53
|
-
),
|
|
54
|
-
self.panda_device.data.hdf_file_name.set(
|
|
56
|
+
self.panda_data_block.hdf_directory.set(str(info.directory_path)),
|
|
57
|
+
self.panda_data_block.hdf_file_name.set(
|
|
55
58
|
f"{info.filename}.h5",
|
|
56
59
|
),
|
|
57
|
-
self.
|
|
58
|
-
# TODO: Set create_dir_depth once available
|
|
59
|
-
# https://github.com/bluesky/ophyd-async/issues/317
|
|
60
|
+
self.panda_data_block.num_capture.set(0),
|
|
60
61
|
)
|
|
61
62
|
|
|
63
|
+
# Make sure that directory exists or has been created.
|
|
64
|
+
if not await self.panda_data_block.directory_exists.get_value() == 1:
|
|
65
|
+
raise OSError(
|
|
66
|
+
f"Directory {info.directory_path} does not exist or "
|
|
67
|
+
"is not writable by the PandABlocks-ioc!"
|
|
68
|
+
)
|
|
69
|
+
|
|
62
70
|
# Wait for it to start, stashing the status that tells us when it finishes
|
|
63
|
-
await self.
|
|
71
|
+
await self.panda_data_block.capture.set(True)
|
|
64
72
|
if multiplier > 1:
|
|
65
73
|
raise ValueError(
|
|
66
74
|
"All PandA datasets should be scalar, multiplier should be 1"
|
|
@@ -76,7 +84,7 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
76
84
|
await self._update_datasets()
|
|
77
85
|
describe = {
|
|
78
86
|
ds.data_key: DataKey(
|
|
79
|
-
source=self.
|
|
87
|
+
source=self.panda_data_block.hdf_directory.source,
|
|
80
88
|
shape=ds.shape,
|
|
81
89
|
dtype="array" if ds.shape != [1] else "number",
|
|
82
90
|
dtype_numpy="<f8", # PandA data should always be written as Float64
|
|
@@ -92,7 +100,7 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
92
100
|
representation of datasets that the panda will write.
|
|
93
101
|
"""
|
|
94
102
|
|
|
95
|
-
capture_table = await self.
|
|
103
|
+
capture_table = await self.panda_data_block.datasets.get_value()
|
|
96
104
|
self._datasets = [
|
|
97
105
|
HDFDataset(dataset_name, "/" + dataset_name, [1], multiplier=1)
|
|
98
106
|
for dataset_name in capture_table["name"]
|
|
@@ -108,18 +116,18 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
108
116
|
|
|
109
117
|
matcher.__name__ = f"index_at_least_{index}"
|
|
110
118
|
await wait_for_value(
|
|
111
|
-
self.
|
|
119
|
+
self.panda_data_block.num_captured, matcher, timeout=timeout
|
|
112
120
|
)
|
|
113
121
|
|
|
114
122
|
async def get_indices_written(self) -> int:
|
|
115
|
-
return await self.
|
|
123
|
+
return await self.panda_data_block.num_captured.get_value()
|
|
116
124
|
|
|
117
125
|
async def observe_indices_written(
|
|
118
126
|
self, timeout=DEFAULT_TIMEOUT
|
|
119
127
|
) -> AsyncGenerator[int, None]:
|
|
120
128
|
"""Wait until a specific index is ready to be collected"""
|
|
121
129
|
async for num_captured in observe_value(
|
|
122
|
-
self.
|
|
130
|
+
self.panda_data_block.num_captured, timeout
|
|
123
131
|
):
|
|
124
132
|
yield num_captured // self._multiplier
|
|
125
133
|
|
|
@@ -130,9 +138,8 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
130
138
|
if indices_written:
|
|
131
139
|
if not self._file:
|
|
132
140
|
self._file = HDFFile(
|
|
133
|
-
self.
|
|
134
|
-
Path(await self.
|
|
135
|
-
/ Path(await self.panda_device.data.hdf_file_name.get_value()),
|
|
141
|
+
Path(await self.panda_data_block.hdf_directory.get_value())
|
|
142
|
+
/ Path(await self.panda_data_block.hdf_file_name.get_value()),
|
|
136
143
|
self._datasets,
|
|
137
144
|
)
|
|
138
145
|
for doc in self._file.stream_resources():
|
|
@@ -142,6 +149,6 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
142
149
|
|
|
143
150
|
# Could put this function as default for StandardDetector
|
|
144
151
|
async def close(self):
|
|
145
|
-
await self.
|
|
152
|
+
await self.panda_data_block.capture.set(
|
|
146
153
|
False, wait=True, timeout=DEFAULT_TIMEOUT
|
|
147
154
|
)
|
|
@@ -4,10 +4,13 @@ from ._fly import (
|
|
|
4
4
|
prepare_static_seq_table_flyer_and_detectors_with_same_trigger,
|
|
5
5
|
time_resolved_fly_and_collect_with_static_seq_table,
|
|
6
6
|
)
|
|
7
|
+
from ._nd_attributes import setup_ndattributes, setup_ndstats_sum
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"fly_and_collect",
|
|
10
11
|
"prepare_static_seq_table_flyer_and_detectors_with_same_trigger",
|
|
11
12
|
"time_resolved_fly_and_collect_with_static_seq_table",
|
|
12
13
|
"ensure_connected",
|
|
14
|
+
"setup_ndattributes",
|
|
15
|
+
"setup_ndstats_sum",
|
|
13
16
|
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
from xml.etree import cElementTree as ET
|
|
3
|
+
|
|
4
|
+
import bluesky.plan_stubs as bps
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core._device import Device
|
|
7
|
+
from ophyd_async.epics.adcore._core_io import NDArrayBaseIO
|
|
8
|
+
from ophyd_async.epics.adcore._utils import (
|
|
9
|
+
NDAttributeDataType,
|
|
10
|
+
NDAttributeParam,
|
|
11
|
+
NDAttributePv,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def setup_ndattributes(
|
|
16
|
+
device: NDArrayBaseIO, ndattributes: Sequence[NDAttributePv | NDAttributeParam]
|
|
17
|
+
):
|
|
18
|
+
xml_text = ET.Element("Attributes")
|
|
19
|
+
|
|
20
|
+
for ndattribute in ndattributes:
|
|
21
|
+
if isinstance(ndattribute, NDAttributeParam):
|
|
22
|
+
ET.SubElement(
|
|
23
|
+
xml_text,
|
|
24
|
+
"Attribute",
|
|
25
|
+
name=ndattribute.name,
|
|
26
|
+
type="PARAM",
|
|
27
|
+
source=ndattribute.param,
|
|
28
|
+
addr=str(ndattribute.addr),
|
|
29
|
+
datatype=ndattribute.datatype.value,
|
|
30
|
+
description=ndattribute.description,
|
|
31
|
+
)
|
|
32
|
+
elif isinstance(ndattribute, NDAttributePv):
|
|
33
|
+
ET.SubElement(
|
|
34
|
+
xml_text,
|
|
35
|
+
"Attribute",
|
|
36
|
+
name=ndattribute.name,
|
|
37
|
+
type="EPICS_PV",
|
|
38
|
+
source=ndattribute.signal.source.split("ca://")[-1],
|
|
39
|
+
dbrtype=ndattribute.dbrtype.value,
|
|
40
|
+
description=ndattribute.description,
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"Invalid type for ndattributes: {type(ndattribute)}. "
|
|
45
|
+
"Expected NDAttributePv or NDAttributeParam."
|
|
46
|
+
)
|
|
47
|
+
yield from bps.mv(device.nd_attributes_file, xml_text)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def setup_ndstats_sum(detector: Device):
|
|
51
|
+
yield from (
|
|
52
|
+
setup_ndattributes(
|
|
53
|
+
detector.hdf,
|
|
54
|
+
[
|
|
55
|
+
NDAttributeParam(
|
|
56
|
+
name=f"{detector.name}-sum",
|
|
57
|
+
param="NDPluginStatsTotal",
|
|
58
|
+
datatype=NDAttributeDataType.DOUBLE,
|
|
59
|
+
description="Sum of the array",
|
|
60
|
+
)
|
|
61
|
+
],
|
|
62
|
+
)
|
|
63
|
+
)
|
|
@@ -14,6 +14,8 @@ class PatternDetectorController(DetectorControl):
|
|
|
14
14
|
exposure: float = 0.1,
|
|
15
15
|
) -> None:
|
|
16
16
|
self.pattern_generator: PatternGenerator = pattern_generator
|
|
17
|
+
if exposure is None:
|
|
18
|
+
exposure = 0.1
|
|
17
19
|
self.pattern_generator.set_exposure(exposure)
|
|
18
20
|
self.path_provider: PathProvider = path_provider
|
|
19
21
|
self.task: Optional[asyncio.Task] = None
|
|
@@ -25,7 +27,8 @@ class PatternDetectorController(DetectorControl):
|
|
|
25
27
|
trigger: DetectorTrigger = DetectorTrigger.internal,
|
|
26
28
|
exposure: Optional[float] = 0.01,
|
|
27
29
|
) -> AsyncStatus:
|
|
28
|
-
|
|
30
|
+
if exposure is None:
|
|
31
|
+
exposure = 0.1
|
|
29
32
|
period: float = exposure + self.get_deadtime(exposure)
|
|
30
33
|
task = asyncio.create_task(
|
|
31
34
|
self._coroutine_for_image_writing(exposure, period, num)
|
|
@@ -42,7 +45,7 @@ class PatternDetectorController(DetectorControl):
|
|
|
42
45
|
pass
|
|
43
46
|
self.task = None
|
|
44
47
|
|
|
45
|
-
def get_deadtime(self, exposure: float) -> float:
|
|
48
|
+
def get_deadtime(self, exposure: float | None) -> float:
|
|
46
49
|
return 0.001
|
|
47
50
|
|
|
48
51
|
async def _coroutine_for_image_writing(
|
|
@@ -166,8 +166,7 @@ class PatternGenerator:
|
|
|
166
166
|
|
|
167
167
|
def _get_new_path(self, path_provider: PathProvider) -> Path:
|
|
168
168
|
info = path_provider(device_name="pattern")
|
|
169
|
-
|
|
170
|
-
new_path: Path = info.root / info.resource_dir / filename
|
|
169
|
+
new_path: Path = info.directory_path / info.filename
|
|
171
170
|
return new_path
|
|
172
171
|
|
|
173
172
|
async def collect_stream_docs(
|
|
@@ -188,7 +187,6 @@ class PatternGenerator:
|
|
|
188
187
|
if not self._hdf_stream_provider:
|
|
189
188
|
assert self.target_path, "open file has not been called"
|
|
190
189
|
self._hdf_stream_provider = HDFFile(
|
|
191
|
-
self._path_provider(),
|
|
192
190
|
self.target_path,
|
|
193
191
|
self._datasets,
|
|
194
192
|
)
|