ophyd-async 0.1.0__py3-none-any.whl → 0.3.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.
- ophyd_async/__init__.py +1 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +91 -19
- ophyd_async/core/_providers.py +68 -0
- ophyd_async/core/async_status.py +90 -42
- ophyd_async/core/detector.py +341 -0
- ophyd_async/core/device.py +226 -0
- ophyd_async/core/device_save_loader.py +286 -0
- ophyd_async/core/flyer.py +85 -0
- ophyd_async/core/mock_signal_backend.py +82 -0
- ophyd_async/core/mock_signal_utils.py +145 -0
- ophyd_async/core/{_device/_signal/signal.py → signal.py} +249 -61
- ophyd_async/core/{_device/_backend/signal_backend.py → signal_backend.py} +12 -5
- ophyd_async/core/{_device/_backend/sim_signal_backend.py → soft_signal_backend.py} +54 -48
- ophyd_async/core/standard_readable.py +261 -0
- ophyd_async/core/utils.py +127 -30
- ophyd_async/epics/_backend/_aioca.py +62 -43
- ophyd_async/epics/_backend/_p4p.py +100 -52
- ophyd_async/epics/_backend/common.py +25 -0
- ophyd_async/epics/areadetector/__init__.py +16 -15
- ophyd_async/epics/areadetector/aravis.py +63 -0
- ophyd_async/epics/areadetector/controllers/__init__.py +5 -0
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +52 -0
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +78 -0
- ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +61 -0
- ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
- ophyd_async/epics/areadetector/drivers/__init__.py +21 -0
- ophyd_async/epics/areadetector/drivers/ad_base.py +107 -0
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +38 -0
- ophyd_async/epics/areadetector/drivers/kinetix_driver.py +27 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +21 -0
- ophyd_async/epics/areadetector/drivers/vimba_driver.py +63 -0
- ophyd_async/epics/areadetector/kinetix.py +46 -0
- ophyd_async/epics/areadetector/pilatus.py +45 -0
- ophyd_async/epics/areadetector/single_trigger_det.py +18 -10
- ophyd_async/epics/areadetector/utils.py +91 -13
- ophyd_async/epics/areadetector/vimba.py +43 -0
- ophyd_async/epics/areadetector/writers/__init__.py +5 -0
- ophyd_async/epics/areadetector/writers/_hdfdataset.py +10 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +54 -0
- ophyd_async/epics/areadetector/writers/hdf_writer.py +142 -0
- ophyd_async/epics/areadetector/writers/nd_file_hdf.py +40 -0
- ophyd_async/epics/areadetector/writers/nd_plugin.py +38 -0
- ophyd_async/epics/demo/__init__.py +78 -51
- ophyd_async/epics/demo/demo_ad_sim_detector.py +35 -0
- ophyd_async/epics/motion/motor.py +67 -52
- ophyd_async/epics/pvi/__init__.py +3 -0
- ophyd_async/epics/pvi/pvi.py +318 -0
- ophyd_async/epics/signal/__init__.py +8 -3
- ophyd_async/epics/signal/signal.py +27 -10
- ophyd_async/log.py +130 -0
- ophyd_async/panda/__init__.py +24 -7
- ophyd_async/panda/_common_blocks.py +49 -0
- ophyd_async/panda/_hdf_panda.py +48 -0
- ophyd_async/panda/_panda_controller.py +37 -0
- ophyd_async/panda/_table.py +158 -0
- ophyd_async/panda/_trigger.py +39 -0
- ophyd_async/panda/_utils.py +15 -0
- ophyd_async/panda/writers/__init__.py +3 -0
- ophyd_async/panda/writers/_hdf_writer.py +220 -0
- ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
- ophyd_async/plan_stubs/__init__.py +13 -0
- ophyd_async/plan_stubs/ensure_connected.py +22 -0
- ophyd_async/plan_stubs/fly.py +149 -0
- ophyd_async/protocols.py +126 -0
- ophyd_async/sim/__init__.py +11 -0
- ophyd_async/sim/demo/__init__.py +3 -0
- ophyd_async/sim/demo/sim_motor.py +103 -0
- ophyd_async/sim/pattern_generator.py +318 -0
- ophyd_async/sim/sim_pattern_detector_control.py +55 -0
- ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
- ophyd_async/sim/sim_pattern_generator.py +37 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/METADATA +35 -67
- ophyd_async-0.3.0.dist-info/RECORD +86 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device/__init__.py +0 -0
- ophyd_async/core/_device/_backend/__init__.py +0 -0
- ophyd_async/core/_device/_signal/__init__.py +0 -0
- ophyd_async/core/_device/device.py +0 -60
- ophyd_async/core/_device/device_collector.py +0 -121
- ophyd_async/core/_device/device_vector.py +0 -14
- ophyd_async/core/_device/standard_readable.py +0 -72
- ophyd_async/epics/areadetector/ad_driver.py +0 -18
- ophyd_async/epics/areadetector/directory_provider.py +0 -18
- ophyd_async/epics/areadetector/hdf_streamer_det.py +0 -167
- ophyd_async/epics/areadetector/nd_file_hdf.py +0 -22
- ophyd_async/epics/areadetector/nd_plugin.py +0 -13
- ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd_async/panda/panda.py +0 -332
- ophyd_async-0.1.0.dist-info/RECORD +0 -45
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Iterator, List
|
|
3
|
+
|
|
4
|
+
from event_model import StreamDatum, StreamResource, compose_stream_resource
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core import DirectoryInfo
|
|
7
|
+
|
|
8
|
+
from ._hdfdataset import _HDFDataset
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _HDFFile:
|
|
12
|
+
"""
|
|
13
|
+
:param directory_info: Contains information about how to construct a StreamResource
|
|
14
|
+
:param full_file_name: Absolute path to the file to be written
|
|
15
|
+
:param datasets: Datasets to write into the file
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
directory_info: DirectoryInfo,
|
|
21
|
+
full_file_name: Path,
|
|
22
|
+
datasets: List[_HDFDataset],
|
|
23
|
+
) -> None:
|
|
24
|
+
self._last_emitted = 0
|
|
25
|
+
self._bundles = [
|
|
26
|
+
compose_stream_resource(
|
|
27
|
+
spec="AD_HDF5_SWMR_SLICE",
|
|
28
|
+
root=str(directory_info.root),
|
|
29
|
+
data_key=ds.name,
|
|
30
|
+
resource_path=str(full_file_name.relative_to(directory_info.root)),
|
|
31
|
+
resource_kwargs={
|
|
32
|
+
"path": ds.path,
|
|
33
|
+
"multiplier": ds.multiplier,
|
|
34
|
+
"timestamps": "/entry/instrument/NDAttributes/NDArrayTimeStamp",
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
for ds in datasets
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def stream_resources(self) -> Iterator[StreamResource]:
|
|
41
|
+
for bundle in self._bundles:
|
|
42
|
+
yield bundle.stream_resource_doc
|
|
43
|
+
|
|
44
|
+
def stream_data(self, indices_written: int) -> Iterator[StreamDatum]:
|
|
45
|
+
# Indices are relative to resource
|
|
46
|
+
if indices_written > self._last_emitted:
|
|
47
|
+
indices = {
|
|
48
|
+
"start": self._last_emitted,
|
|
49
|
+
"stop": indices_written,
|
|
50
|
+
}
|
|
51
|
+
self._last_emitted = indices_written
|
|
52
|
+
for bundle in self._bundles:
|
|
53
|
+
yield bundle.compose_stream_datum(indices)
|
|
54
|
+
return None
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import AsyncGenerator, AsyncIterator, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from bluesky.protocols import DataKey, Hints, StreamAsset
|
|
6
|
+
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
DEFAULT_TIMEOUT,
|
|
9
|
+
AsyncStatus,
|
|
10
|
+
DetectorWriter,
|
|
11
|
+
DirectoryProvider,
|
|
12
|
+
NameProvider,
|
|
13
|
+
ShapeProvider,
|
|
14
|
+
set_and_wait_for_value,
|
|
15
|
+
wait_for_value,
|
|
16
|
+
)
|
|
17
|
+
from ophyd_async.core.signal import observe_value
|
|
18
|
+
|
|
19
|
+
from ._hdfdataset import _HDFDataset
|
|
20
|
+
from ._hdffile import _HDFFile
|
|
21
|
+
from .nd_file_hdf import FileWriteMode, NDFileHDF
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HDFWriter(DetectorWriter):
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
hdf: NDFileHDF,
|
|
28
|
+
directory_provider: DirectoryProvider,
|
|
29
|
+
name_provider: NameProvider,
|
|
30
|
+
shape_provider: ShapeProvider,
|
|
31
|
+
**scalar_datasets_paths: str,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.hdf = hdf
|
|
34
|
+
self._directory_provider = directory_provider
|
|
35
|
+
self._name_provider = name_provider
|
|
36
|
+
self._shape_provider = shape_provider
|
|
37
|
+
self._scalar_datasets_paths = scalar_datasets_paths
|
|
38
|
+
self._capture_status: Optional[AsyncStatus] = None
|
|
39
|
+
self._datasets: List[_HDFDataset] = []
|
|
40
|
+
self._file: Optional[_HDFFile] = None
|
|
41
|
+
self._multiplier = 1
|
|
42
|
+
|
|
43
|
+
async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
|
|
44
|
+
self._file = None
|
|
45
|
+
info = self._directory_provider()
|
|
46
|
+
file_path = str(info.root / info.resource_dir)
|
|
47
|
+
await asyncio.gather(
|
|
48
|
+
self.hdf.num_extra_dims.set(0),
|
|
49
|
+
self.hdf.lazy_open.set(True),
|
|
50
|
+
self.hdf.swmr_mode.set(True),
|
|
51
|
+
# See https://github.com/bluesky/ophyd-async/issues/122
|
|
52
|
+
self.hdf.file_path.set(file_path),
|
|
53
|
+
self.hdf.file_name.set(f"{info.prefix}{self.hdf.name}{info.suffix}"),
|
|
54
|
+
self.hdf.file_template.set("%s/%s.h5"),
|
|
55
|
+
self.hdf.file_write_mode.set(FileWriteMode.stream),
|
|
56
|
+
# Never use custom xml layout file but use the one defined
|
|
57
|
+
# in the source code file NDFileHDF5LayoutXML.cpp
|
|
58
|
+
self.hdf.xml_file_name.set(""),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
assert (
|
|
62
|
+
await self.hdf.file_path_exists.get_value()
|
|
63
|
+
), f"File path {file_path} for hdf plugin does not exist"
|
|
64
|
+
|
|
65
|
+
# Overwrite num_capture to go forever
|
|
66
|
+
await self.hdf.num_capture.set(0)
|
|
67
|
+
# Wait for it to start, stashing the status that tells us when it finishes
|
|
68
|
+
self._capture_status = await set_and_wait_for_value(self.hdf.capture, True)
|
|
69
|
+
name = self._name_provider()
|
|
70
|
+
detector_shape = tuple(await self._shape_provider())
|
|
71
|
+
self._multiplier = multiplier
|
|
72
|
+
outer_shape = (multiplier,) if multiplier > 1 else ()
|
|
73
|
+
# Add the main data
|
|
74
|
+
self._datasets = [
|
|
75
|
+
_HDFDataset(name, "/entry/data/data", detector_shape, multiplier)
|
|
76
|
+
]
|
|
77
|
+
# And all the scalar datasets
|
|
78
|
+
for ds_name, ds_path in self._scalar_datasets_paths.items():
|
|
79
|
+
self._datasets.append(
|
|
80
|
+
_HDFDataset(
|
|
81
|
+
f"{name}-{ds_name}",
|
|
82
|
+
f"/entry/instrument/NDAttributes/{ds_path}",
|
|
83
|
+
(),
|
|
84
|
+
multiplier,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
describe = {
|
|
88
|
+
ds.name: DataKey(
|
|
89
|
+
source=self.hdf.full_file_name.source,
|
|
90
|
+
shape=outer_shape + tuple(ds.shape),
|
|
91
|
+
dtype="array" if ds.shape else "number",
|
|
92
|
+
external="STREAM:",
|
|
93
|
+
)
|
|
94
|
+
for ds in self._datasets
|
|
95
|
+
}
|
|
96
|
+
return describe
|
|
97
|
+
|
|
98
|
+
async def observe_indices_written(
|
|
99
|
+
self, timeout=DEFAULT_TIMEOUT
|
|
100
|
+
) -> AsyncGenerator[int, None]:
|
|
101
|
+
"""Wait until a specific index is ready to be collected"""
|
|
102
|
+
async for num_captured in observe_value(self.hdf.num_captured, timeout):
|
|
103
|
+
yield num_captured // self._multiplier
|
|
104
|
+
|
|
105
|
+
async def get_indices_written(self) -> int:
|
|
106
|
+
num_captured = await self.hdf.num_captured.get_value()
|
|
107
|
+
return num_captured // self._multiplier
|
|
108
|
+
|
|
109
|
+
async def collect_stream_docs(
|
|
110
|
+
self, indices_written: int
|
|
111
|
+
) -> AsyncIterator[StreamAsset]:
|
|
112
|
+
# TODO: fail if we get dropped frames
|
|
113
|
+
await self.hdf.flush_now.set(True)
|
|
114
|
+
if indices_written:
|
|
115
|
+
if not self._file:
|
|
116
|
+
path = Path(await self.hdf.full_file_name.get_value())
|
|
117
|
+
self._file = _HDFFile(
|
|
118
|
+
self._directory_provider(),
|
|
119
|
+
# See https://github.com/bluesky/ophyd-async/issues/122
|
|
120
|
+
path,
|
|
121
|
+
self._datasets,
|
|
122
|
+
)
|
|
123
|
+
# stream resource says "here is a dataset",
|
|
124
|
+
# stream datum says "here are N frames in that stream resource",
|
|
125
|
+
# you get one stream resource and many stream datums per scan
|
|
126
|
+
|
|
127
|
+
for doc in self._file.stream_resources():
|
|
128
|
+
yield "stream_resource", doc
|
|
129
|
+
for doc in self._file.stream_data(indices_written):
|
|
130
|
+
yield "stream_datum", doc
|
|
131
|
+
|
|
132
|
+
async def close(self):
|
|
133
|
+
# Already done a caput callback in _capture_status, so can't do one here
|
|
134
|
+
await self.hdf.capture.set(0, wait=False)
|
|
135
|
+
await wait_for_value(self.hdf.capture, 0, DEFAULT_TIMEOUT)
|
|
136
|
+
if self._capture_status:
|
|
137
|
+
# We kicked off an open, so wait for it to return
|
|
138
|
+
await self._capture_status
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def hints(self) -> Hints:
|
|
142
|
+
return {"fields": [self._name_provider()]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ...signal.signal import epics_signal_r, epics_signal_rw, epics_signal_rw_rbv
|
|
4
|
+
from ..utils import FileWriteMode
|
|
5
|
+
from .nd_plugin import NDPluginBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Compression(str, Enum):
|
|
9
|
+
none = "None"
|
|
10
|
+
nbit = "N-bit"
|
|
11
|
+
szip = "szip"
|
|
12
|
+
zlib = "zlib"
|
|
13
|
+
blosc = "Blosc"
|
|
14
|
+
bslz4 = "BSLZ4"
|
|
15
|
+
lz4 = "LZ4"
|
|
16
|
+
jpeg = "JPEG"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NDFileHDF(NDPluginBase):
|
|
20
|
+
def __init__(self, prefix: str, name="") -> None:
|
|
21
|
+
# Define some signals
|
|
22
|
+
self.position_mode = epics_signal_rw_rbv(bool, prefix + "PositionMode")
|
|
23
|
+
self.compression = epics_signal_rw_rbv(Compression, prefix + "Compression")
|
|
24
|
+
self.num_extra_dims = epics_signal_rw_rbv(int, prefix + "NumExtraDims")
|
|
25
|
+
self.file_path = epics_signal_rw_rbv(str, prefix + "FilePath")
|
|
26
|
+
self.file_name = epics_signal_rw_rbv(str, prefix + "FileName")
|
|
27
|
+
self.file_path_exists = epics_signal_r(bool, prefix + "FilePathExists_RBV")
|
|
28
|
+
self.file_template = epics_signal_rw_rbv(str, prefix + "FileTemplate")
|
|
29
|
+
self.full_file_name = epics_signal_r(str, prefix + "FullFileName_RBV")
|
|
30
|
+
self.file_write_mode = epics_signal_rw_rbv(
|
|
31
|
+
FileWriteMode, prefix + "FileWriteMode"
|
|
32
|
+
)
|
|
33
|
+
self.num_capture = epics_signal_rw_rbv(int, prefix + "NumCapture")
|
|
34
|
+
self.num_captured = epics_signal_r(int, prefix + "NumCaptured_RBV")
|
|
35
|
+
self.swmr_mode = epics_signal_rw_rbv(bool, prefix + "SWMRMode")
|
|
36
|
+
self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen")
|
|
37
|
+
self.capture = epics_signal_rw_rbv(bool, prefix + "Capture")
|
|
38
|
+
self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
|
|
39
|
+
self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName")
|
|
40
|
+
super().__init__(prefix, name)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import Device
|
|
4
|
+
from ophyd_async.epics.signal import epics_signal_rw
|
|
5
|
+
from ophyd_async.epics.signal.signal import epics_signal_r, epics_signal_rw_rbv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Callback(str, Enum):
|
|
9
|
+
Enable = "Enable"
|
|
10
|
+
Disable = "Disable"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NDArrayBase(Device):
|
|
14
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
15
|
+
self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV")
|
|
16
|
+
self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile")
|
|
17
|
+
self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire")
|
|
18
|
+
self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
|
|
19
|
+
self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
|
|
20
|
+
self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter")
|
|
21
|
+
# There is no _RBV for this one
|
|
22
|
+
self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins")
|
|
23
|
+
|
|
24
|
+
super().__init__(name=name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NDPluginBase(NDArrayBase):
|
|
28
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
29
|
+
self.nd_array_port = epics_signal_rw_rbv(str, prefix + "NDArrayPort")
|
|
30
|
+
self.enable_callback = epics_signal_rw_rbv(Callback, prefix + "EnableCallbacks")
|
|
31
|
+
self.nd_array_address = epics_signal_rw_rbv(int, prefix + "NDArrayAddress")
|
|
32
|
+
self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV")
|
|
33
|
+
self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV")
|
|
34
|
+
super().__init__(prefix, name)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NDPluginStats(NDPluginBase):
|
|
38
|
+
pass
|
|
@@ -6,20 +6,33 @@ import random
|
|
|
6
6
|
import string
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
|
-
import time
|
|
10
9
|
from enum import Enum
|
|
11
10
|
from pathlib import Path
|
|
12
|
-
from typing import Callable, List, Optional
|
|
13
11
|
|
|
14
12
|
import numpy as np
|
|
15
13
|
from bluesky.protocols import Movable, Stoppable
|
|
16
14
|
|
|
17
|
-
from ophyd_async.core import
|
|
15
|
+
from ophyd_async.core import (
|
|
16
|
+
ConfigSignal,
|
|
17
|
+
Device,
|
|
18
|
+
DeviceVector,
|
|
19
|
+
HintedSignal,
|
|
20
|
+
StandardReadable,
|
|
21
|
+
WatchableAsyncStatus,
|
|
22
|
+
observe_value,
|
|
23
|
+
)
|
|
24
|
+
from ophyd_async.core.async_status import AsyncStatus
|
|
25
|
+
from ophyd_async.core.utils import (
|
|
26
|
+
DEFAULT_TIMEOUT,
|
|
27
|
+
CalculatableTimeout,
|
|
28
|
+
CalculateTimeout,
|
|
29
|
+
WatcherUpdate,
|
|
30
|
+
)
|
|
18
31
|
|
|
19
32
|
from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
20
33
|
|
|
21
34
|
|
|
22
|
-
class EnergyMode(Enum):
|
|
35
|
+
class EnergyMode(str, Enum):
|
|
23
36
|
"""Energy mode for `Sensor`"""
|
|
24
37
|
|
|
25
38
|
#: Low energy mode
|
|
@@ -33,35 +46,41 @@ class Sensor(StandardReadable):
|
|
|
33
46
|
|
|
34
47
|
def __init__(self, prefix: str, name="") -> None:
|
|
35
48
|
# Define some signals
|
|
36
|
-
self.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
config=[self.mode],
|
|
42
|
-
)
|
|
49
|
+
with self.add_children_as_readables(HintedSignal):
|
|
50
|
+
self.value = epics_signal_r(float, prefix + "Value")
|
|
51
|
+
with self.add_children_as_readables(ConfigSignal):
|
|
52
|
+
self.mode = epics_signal_rw(EnergyMode, prefix + "Mode")
|
|
53
|
+
|
|
43
54
|
super().__init__(name=name)
|
|
44
55
|
|
|
45
56
|
|
|
57
|
+
class SensorGroup(StandardReadable):
|
|
58
|
+
def __init__(self, prefix: str, name: str = "", sensor_count: int = 3) -> None:
|
|
59
|
+
with self.add_children_as_readables():
|
|
60
|
+
self.sensors = DeviceVector(
|
|
61
|
+
{i: Sensor(f"{prefix}{i}:") for i in range(1, sensor_count + 1)}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
super().__init__(name)
|
|
65
|
+
|
|
66
|
+
|
|
46
67
|
class Mover(StandardReadable, Movable, Stoppable):
|
|
47
68
|
"""A demo movable that moves based on velocity"""
|
|
48
69
|
|
|
49
70
|
def __init__(self, prefix: str, name="") -> None:
|
|
50
71
|
# Define some signals
|
|
72
|
+
with self.add_children_as_readables(HintedSignal):
|
|
73
|
+
self.readback = epics_signal_r(float, prefix + "Readback")
|
|
74
|
+
with self.add_children_as_readables(ConfigSignal):
|
|
75
|
+
self.velocity = epics_signal_rw(float, prefix + "Velocity")
|
|
76
|
+
self.units = epics_signal_r(str, prefix + "Readback.EGU")
|
|
51
77
|
self.setpoint = epics_signal_rw(float, prefix + "Setpoint")
|
|
52
|
-
self.readback = epics_signal_r(float, prefix + "Readback")
|
|
53
|
-
self.velocity = epics_signal_rw(float, prefix + "Velocity")
|
|
54
|
-
self.units = epics_signal_r(str, prefix + "Readback.EGU")
|
|
55
78
|
self.precision = epics_signal_r(int, prefix + "Readback.PREC")
|
|
56
79
|
# Signals that collide with standard methods should have a trailing underscore
|
|
57
80
|
self.stop_ = epics_signal_x(prefix + "Stop.PROC")
|
|
58
81
|
# Whether set() should complete successfully or not
|
|
59
82
|
self._set_success = True
|
|
60
|
-
|
|
61
|
-
self.set_readable_signals(
|
|
62
|
-
read=[self.readback],
|
|
63
|
-
config=[self.velocity, self.units],
|
|
64
|
-
)
|
|
83
|
+
|
|
65
84
|
super().__init__(name=name)
|
|
66
85
|
|
|
67
86
|
def set_name(self, name: str):
|
|
@@ -69,50 +88,47 @@ class Mover(StandardReadable, Movable, Stoppable):
|
|
|
69
88
|
# Readback should be named the same as its parent in read()
|
|
70
89
|
self.readback.set_name(name)
|
|
71
90
|
|
|
72
|
-
|
|
91
|
+
@WatchableAsyncStatus.wrap
|
|
92
|
+
async def set(
|
|
93
|
+
self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
|
|
94
|
+
):
|
|
73
95
|
self._set_success = True
|
|
74
|
-
|
|
75
|
-
start = time.monotonic()
|
|
76
|
-
old_position, units, precision = await asyncio.gather(
|
|
96
|
+
old_position, units, precision, velocity = await asyncio.gather(
|
|
77
97
|
self.setpoint.get_value(),
|
|
78
98
|
self.units.get_value(),
|
|
79
99
|
self.precision.get_value(),
|
|
100
|
+
self.velocity.get_value(),
|
|
80
101
|
)
|
|
102
|
+
if timeout is CalculateTimeout:
|
|
103
|
+
assert velocity > 0, "Mover has zero velocity"
|
|
104
|
+
timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
|
|
105
|
+
# Make an Event that will be set on completion, and a Status that will
|
|
106
|
+
# error if not done in time
|
|
107
|
+
done = asyncio.Event()
|
|
108
|
+
done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout))
|
|
81
109
|
# Wait for the value to set, but don't wait for put completion callback
|
|
82
110
|
await self.setpoint.set(new_position, wait=False)
|
|
83
|
-
async for current_position in observe_value(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
async for current_position in observe_value(
|
|
112
|
+
self.readback, done_status=done_status
|
|
113
|
+
):
|
|
114
|
+
yield WatcherUpdate(
|
|
115
|
+
current=current_position,
|
|
116
|
+
initial=old_position,
|
|
117
|
+
target=new_position,
|
|
118
|
+
name=self.name,
|
|
119
|
+
unit=units,
|
|
120
|
+
precision=precision,
|
|
121
|
+
)
|
|
94
122
|
if np.isclose(current_position, new_position):
|
|
123
|
+
done.set()
|
|
95
124
|
break
|
|
96
125
|
if not self._set_success:
|
|
97
126
|
raise RuntimeError("Motor was stopped")
|
|
98
127
|
|
|
99
|
-
def move(self, new_position: float, timeout: Optional[float] = None):
|
|
100
|
-
"""Commandline only synchronous move of a Motor"""
|
|
101
|
-
from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
|
|
102
|
-
|
|
103
|
-
if in_bluesky_event_loop():
|
|
104
|
-
raise RuntimeError("Will deadlock run engine if run in a plan")
|
|
105
|
-
call_in_bluesky_event_loop(self._move(new_position), timeout) # type: ignore
|
|
106
|
-
|
|
107
|
-
# TODO: this fails if we call from the cli, but works if we "ipython await" it
|
|
108
|
-
def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus:
|
|
109
|
-
watchers: List[Callable] = []
|
|
110
|
-
coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout)
|
|
111
|
-
return AsyncStatus(coro, watchers)
|
|
112
|
-
|
|
113
128
|
async def stop(self, success=True):
|
|
114
129
|
self._set_success = success
|
|
115
|
-
|
|
130
|
+
status = self.stop_.trigger()
|
|
131
|
+
await status
|
|
116
132
|
|
|
117
133
|
|
|
118
134
|
class SampleStage(Device):
|
|
@@ -134,11 +150,22 @@ def start_ioc_subprocess() -> str:
|
|
|
134
150
|
pv_prefix = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + ":"
|
|
135
151
|
here = Path(__file__).absolute().parent
|
|
136
152
|
args = [sys.executable, "-m", "epicscorelibs.ioc"]
|
|
153
|
+
|
|
154
|
+
# Create standalone sensor
|
|
137
155
|
args += ["-m", f"P={pv_prefix}"]
|
|
138
156
|
args += ["-d", str(here / "sensor.db")]
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
|
|
158
|
+
# Create sensor group
|
|
159
|
+
for suffix in ["1", "2", "3"]:
|
|
160
|
+
args += ["-m", f"P={pv_prefix}{suffix}:"]
|
|
161
|
+
args += ["-d", str(here / "sensor.db")]
|
|
162
|
+
|
|
163
|
+
# Create X and Y motors
|
|
164
|
+
for suffix in ["X", "Y"]:
|
|
165
|
+
args += ["-m", f"P={pv_prefix}{suffix}:"]
|
|
141
166
|
args += ["-d", str(here / "mover.db")]
|
|
167
|
+
|
|
168
|
+
# Start IOC
|
|
142
169
|
process = subprocess.Popen(
|
|
143
170
|
args,
|
|
144
171
|
stdin=subprocess.PIPE,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import DirectoryProvider, SignalR, StandardDetector
|
|
4
|
+
|
|
5
|
+
from ..areadetector.controllers import ADSimController
|
|
6
|
+
from ..areadetector.drivers import ADBase, ADBaseShapeProvider
|
|
7
|
+
from ..areadetector.writers import HDFWriter, NDFileHDF
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DemoADSimDetector(StandardDetector):
|
|
11
|
+
_controller: ADSimController
|
|
12
|
+
_writer: HDFWriter
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
drv: ADBase,
|
|
17
|
+
hdf: NDFileHDF,
|
|
18
|
+
directory_provider: DirectoryProvider,
|
|
19
|
+
name: str = "",
|
|
20
|
+
config_sigs: Sequence[SignalR] = (),
|
|
21
|
+
):
|
|
22
|
+
self.drv = drv
|
|
23
|
+
self.hdf = hdf
|
|
24
|
+
|
|
25
|
+
super().__init__(
|
|
26
|
+
ADSimController(self.drv),
|
|
27
|
+
HDFWriter(
|
|
28
|
+
self.hdf,
|
|
29
|
+
directory_provider,
|
|
30
|
+
lambda: self.name,
|
|
31
|
+
ADBaseShapeProvider(self.drv),
|
|
32
|
+
),
|
|
33
|
+
config_sigs=config_sigs,
|
|
34
|
+
name=name,
|
|
35
|
+
)
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import time
|
|
3
|
-
from typing import Callable, List, Optional
|
|
4
2
|
|
|
5
3
|
from bluesky.protocols import Movable, Stoppable
|
|
6
4
|
|
|
7
|
-
from ophyd_async.core import
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
ConfigSignal,
|
|
7
|
+
HintedSignal,
|
|
8
|
+
StandardReadable,
|
|
9
|
+
WatchableAsyncStatus,
|
|
10
|
+
)
|
|
11
|
+
from ophyd_async.core.signal import observe_value
|
|
12
|
+
from ophyd_async.core.utils import (
|
|
13
|
+
DEFAULT_TIMEOUT,
|
|
14
|
+
CalculatableTimeout,
|
|
15
|
+
CalculateTimeout,
|
|
16
|
+
WatcherUpdate,
|
|
17
|
+
)
|
|
8
18
|
|
|
9
19
|
from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
10
20
|
|
|
@@ -14,71 +24,76 @@ class Motor(StandardReadable, Movable, Stoppable):
|
|
|
14
24
|
|
|
15
25
|
def __init__(self, prefix: str, name="") -> None:
|
|
16
26
|
# Define some signals
|
|
17
|
-
self.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
with self.add_children_as_readables(ConfigSignal):
|
|
28
|
+
self.motor_egu = epics_signal_r(str, prefix + ".EGU")
|
|
29
|
+
self.velocity = epics_signal_rw(float, prefix + ".VELO")
|
|
30
|
+
|
|
31
|
+
with self.add_children_as_readables(HintedSignal):
|
|
32
|
+
self.user_readback = epics_signal_r(float, prefix + ".RBV")
|
|
33
|
+
|
|
34
|
+
self.user_setpoint = epics_signal_rw(float, prefix + ".VAL")
|
|
35
|
+
self.max_velocity = epics_signal_r(float, prefix + ".VMAX")
|
|
36
|
+
self.acceleration_time = epics_signal_rw(float, prefix + ".ACCL")
|
|
21
37
|
self.precision = epics_signal_r(int, prefix + ".PREC")
|
|
22
|
-
|
|
23
|
-
self.
|
|
38
|
+
self.deadband = epics_signal_r(float, prefix + ".RDBD")
|
|
39
|
+
self.motor_done_move = epics_signal_r(int, prefix + ".DMOV")
|
|
40
|
+
self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
|
|
41
|
+
self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
|
|
42
|
+
|
|
43
|
+
self.motor_stop = epics_signal_x(prefix + ".STOP")
|
|
24
44
|
# Whether set() should complete successfully or not
|
|
25
45
|
self._set_success = True
|
|
26
|
-
# Set name and signals for read() and read_configuration()
|
|
27
|
-
self.set_readable_signals(
|
|
28
|
-
read=[self.readback],
|
|
29
|
-
config=[self.velocity, self.units],
|
|
30
|
-
)
|
|
31
46
|
super().__init__(name=name)
|
|
32
47
|
|
|
33
48
|
def set_name(self, name: str):
|
|
34
49
|
super().set_name(name)
|
|
35
50
|
# Readback should be named the same as its parent in read()
|
|
36
|
-
self.
|
|
51
|
+
self.user_readback.set_name(name)
|
|
37
52
|
|
|
38
|
-
|
|
53
|
+
@WatchableAsyncStatus.wrap
|
|
54
|
+
async def set(
|
|
55
|
+
self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
|
|
56
|
+
):
|
|
39
57
|
self._set_success = True
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
(
|
|
59
|
+
old_position,
|
|
60
|
+
units,
|
|
61
|
+
precision,
|
|
62
|
+
velocity,
|
|
63
|
+
acceleration_time,
|
|
64
|
+
) = await asyncio.gather(
|
|
65
|
+
self.user_setpoint.get_value(),
|
|
66
|
+
self.motor_egu.get_value(),
|
|
44
67
|
self.precision.get_value(),
|
|
68
|
+
self.velocity.get_value(),
|
|
69
|
+
self.acceleration_time.get_value(),
|
|
45
70
|
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
if timeout is CalculateTimeout:
|
|
72
|
+
assert velocity > 0, "Motor has zero velocity"
|
|
73
|
+
timeout = (
|
|
74
|
+
abs(new_position - old_position) / velocity
|
|
75
|
+
+ 2 * acceleration_time
|
|
76
|
+
+ DEFAULT_TIMEOUT
|
|
77
|
+
)
|
|
78
|
+
move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
|
|
79
|
+
async for current_position in observe_value(
|
|
80
|
+
self.user_readback, done_status=move_status
|
|
81
|
+
):
|
|
82
|
+
yield WatcherUpdate(
|
|
83
|
+
current=current_position,
|
|
84
|
+
initial=old_position,
|
|
85
|
+
target=new_position,
|
|
86
|
+
name=self.name,
|
|
87
|
+
unit=units,
|
|
88
|
+
precision=precision,
|
|
89
|
+
)
|
|
64
90
|
if not self._set_success:
|
|
65
91
|
raise RuntimeError("Motor was stopped")
|
|
66
92
|
|
|
67
|
-
def move(self, new_position: float, timeout: Optional[float] = None):
|
|
68
|
-
"""Commandline only synchronous move of a Motor"""
|
|
69
|
-
from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
|
|
70
|
-
|
|
71
|
-
if in_bluesky_event_loop():
|
|
72
|
-
raise RuntimeError("Will deadlock run engine if run in a plan")
|
|
73
|
-
call_in_bluesky_event_loop(self._move(new_position), timeout) # type: ignore
|
|
74
|
-
|
|
75
|
-
def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus:
|
|
76
|
-
watchers: List[Callable] = []
|
|
77
|
-
coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout)
|
|
78
|
-
return AsyncStatus(coro, watchers)
|
|
79
|
-
|
|
80
93
|
async def stop(self, success=False):
|
|
81
94
|
self._set_success = success
|
|
82
95
|
# Put with completion will never complete as we are waiting for completion on
|
|
83
96
|
# the move above, so need to pass wait=False
|
|
84
|
-
await self.
|
|
97
|
+
await self.motor_stop.trigger(wait=False)
|
|
98
|
+
# Trigger any callbacks
|
|
99
|
+
await self.user_readback._backend.put(await self.user_readback.get_value())
|