ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.0a2__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 +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +97 -62
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +106 -125
- ophyd_async/core/_device.py +69 -63
- ophyd_async/core/_device_filler.py +65 -1
- ophyd_async/core/_flyer.py +14 -5
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +44 -35
- ophyd_async/core/_settings.py +36 -27
- ophyd_async/core/_signal.py +262 -170
- ophyd_async/core/_signal_backend.py +56 -13
- ophyd_async/core/_soft_signal_backend.py +16 -11
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +41 -11
- ophyd_async/core/_utils.py +96 -49
- ophyd_async/core/_yaml_settings.py +2 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/_andor.py +2 -2
- ophyd_async/epics/adandor/_andor_controller.py +4 -2
- ophyd_async/epics/adandor/_andor_io.py +2 -4
- ophyd_async/epics/adaravis/__init__.py +5 -0
- ophyd_async/epics/adaravis/_aravis.py +4 -8
- ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +23 -8
- ophyd_async/epics/adcore/_core_detector.py +42 -2
- ophyd_async/epics/adcore/_core_io.py +124 -99
- ophyd_async/epics/adcore/_core_logic.py +106 -27
- ophyd_async/epics/adcore/_core_writer.py +12 -8
- ophyd_async/epics/adcore/_hdf_writer.py +21 -38
- ophyd_async/epics/adcore/_single_trigger.py +2 -2
- ophyd_async/epics/adcore/_utils.py +2 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +3 -3
- ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +5 -0
- ophyd_async/epics/adpilatus/_pilatus.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +4 -14
- ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +3 -2
- ophyd_async/epics/advimba/_vimba_controller.py +4 -2
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +35 -16
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +10 -2
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +4 -4
- ophyd_async/epics/demo/__init__.py +16 -0
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +65 -28
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/_example_ioc.py +21 -9
- ophyd_async/epics/testing/_utils.py +3 -0
- ophyd_async/epics/testing/test_records.db +8 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -6
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +5 -0
- ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
- ophyd_async/fastcs/panda/_table.py +9 -6
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +2 -0
- ophyd_async/plan_stubs/_ensure_connected.py +1 -0
- ophyd_async/plan_stubs/_fly.py +2 -4
- ophyd_async/plan_stubs/_nd_attributes.py +2 -0
- ophyd_async/plan_stubs/_panda.py +1 -0
- ophyd_async/plan_stubs/_settings.py +43 -16
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
- ophyd_async/sim/__init__.py +24 -14
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +18 -32
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +136 -60
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/{sim → demo}/_counter.py +2 -0
- ophyd_async/tango/{sim → demo}/_detector.py +2 -0
- ophyd_async/tango/{sim → demo}/_mover.py +5 -4
- ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +29 -7
- ophyd_async/testing/_assert.py +145 -83
- ophyd_async/testing/_mock_signal_utils.py +56 -70
- ophyd_async/testing/_one_of_everything.py +41 -21
- ophyd_async/testing/_single_derived.py +89 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/sim/__init__.py +0 -54
- ophyd_async/epics/sim/_ioc.py +0 -29
- ophyd_async/epics/sim/_mover.py +0 -101
- ophyd_async/epics/sim/_sensor.py +0 -37
- ophyd_async/epics/sim/sensor.db +0 -19
- ophyd_async/sim/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
- ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
- ophyd_async/sim/_sim_motor.py +0 -107
- ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
- /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
- /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/top_level.txt +0 -0
|
@@ -7,17 +7,24 @@ from bluesky.protocols import Reading
|
|
|
7
7
|
from event_model import DataKey, Dtype, Limits
|
|
8
8
|
|
|
9
9
|
from ._table import Table
|
|
10
|
-
from ._utils import Callback, StrictEnum,
|
|
10
|
+
from ._utils import Callback, StrictEnum, get_enum_cls
|
|
11
11
|
|
|
12
12
|
DTypeScalar_co = TypeVar("DTypeScalar_co", covariant=True, bound=np.generic)
|
|
13
|
+
"""A numpy dtype like [](#numpy.float64)."""
|
|
14
|
+
|
|
15
|
+
|
|
13
16
|
# To be a 1D array shape should really be tuple[int], but np.array()
|
|
14
17
|
# currently produces tuple[int, ...] even when it has 1D input args
|
|
15
18
|
# https://github.com/numpy/numpy/issues/28077#issuecomment-2566485178
|
|
16
19
|
Array1D = np.ndarray[tuple[int, ...], np.dtype[DTypeScalar_co]]
|
|
20
|
+
"""A type alias for a 1D numpy array with a specific scalar data type.
|
|
21
|
+
|
|
22
|
+
E.g. `Array1D[np.float64]` is a 1D numpy array of 64-bit floats."""
|
|
23
|
+
|
|
17
24
|
Primitive = bool | int | float | str
|
|
18
|
-
# NOTE: if you change this union then update the docs to match
|
|
19
25
|
SignalDatatype = (
|
|
20
26
|
Primitive
|
|
27
|
+
| StrictEnum
|
|
21
28
|
| Array1D[np.bool_]
|
|
22
29
|
| Array1D[np.int8]
|
|
23
30
|
| Array1D[np.uint8]
|
|
@@ -30,23 +37,33 @@ SignalDatatype = (
|
|
|
30
37
|
| Array1D[np.float32]
|
|
31
38
|
| Array1D[np.float64]
|
|
32
39
|
| np.ndarray
|
|
33
|
-
| StrictEnum
|
|
34
40
|
| Sequence[str]
|
|
35
41
|
| Sequence[StrictEnum]
|
|
36
42
|
| Table
|
|
37
43
|
)
|
|
44
|
+
"""The supported [](#Signal) datatypes:
|
|
45
|
+
|
|
46
|
+
- A python primitive [](#bool), [](#int), [](#float), [](#str)
|
|
47
|
+
- A [](#StrictEnum) or [](#SubsetEnum) subclass
|
|
48
|
+
- A fixed datatype [](#Array1D) of numpy bool, signed and unsigned integers or float
|
|
49
|
+
- A [](#numpy.ndarray) which can change dimensions and datatype at runtime
|
|
50
|
+
- A sequence of [](#str)
|
|
51
|
+
- A sequence of [](#StrictEnum) or [](#SubsetEnum) subclass
|
|
52
|
+
- A [](#Table) subclass
|
|
53
|
+
"""
|
|
38
54
|
# TODO: These typevars will not be needed when we drop python 3.11
|
|
39
55
|
# as you can do MyConverter[SignalType: SignalTypeUnion]:
|
|
40
56
|
# rather than MyConverter(Generic[SignalType])
|
|
41
57
|
PrimitiveT = TypeVar("PrimitiveT", bound=Primitive)
|
|
42
58
|
SignalDatatypeT = TypeVar("SignalDatatypeT", bound=SignalDatatype)
|
|
59
|
+
"""A typevar for a [](#SignalDatatype)."""
|
|
43
60
|
SignalDatatypeV = TypeVar("SignalDatatypeV", bound=SignalDatatype)
|
|
44
61
|
EnumT = TypeVar("EnumT", bound=StrictEnum)
|
|
45
62
|
TableT = TypeVar("TableT", bound=Table)
|
|
46
63
|
|
|
47
64
|
|
|
48
65
|
class SignalBackend(Generic[SignalDatatypeT]):
|
|
49
|
-
"""A read/write/monitor backend for a Signals"""
|
|
66
|
+
"""A read/write/monitor backend for a Signals."""
|
|
50
67
|
|
|
51
68
|
def __init__(self, datatype: type[SignalDatatypeT] | None):
|
|
52
69
|
self.datatype = datatype
|
|
@@ -55,36 +72,37 @@ class SignalBackend(Generic[SignalDatatypeT]):
|
|
|
55
72
|
def source(self, name: str, read: bool) -> str:
|
|
56
73
|
"""Return source of signal.
|
|
57
74
|
|
|
58
|
-
|
|
75
|
+
:param name: The name of the signal, which can be used or discarded.
|
|
76
|
+
:param read: True if we want the source for reading, False if writing.
|
|
59
77
|
"""
|
|
60
78
|
|
|
61
79
|
@abstractmethod
|
|
62
80
|
async def connect(self, timeout: float):
|
|
63
|
-
"""Connect to underlying hardware"""
|
|
81
|
+
"""Connect to underlying hardware."""
|
|
64
82
|
|
|
65
83
|
@abstractmethod
|
|
66
84
|
async def put(self, value: SignalDatatypeT | None, wait: bool):
|
|
67
|
-
"""Put a value to the PV, if wait then wait for completion"""
|
|
85
|
+
"""Put a value to the PV, if wait then wait for completion."""
|
|
68
86
|
|
|
69
87
|
@abstractmethod
|
|
70
88
|
async def get_datakey(self, source: str) -> DataKey:
|
|
71
|
-
"""Metadata like source, dtype, shape, precision, units"""
|
|
89
|
+
"""Metadata like source, dtype, shape, precision, units."""
|
|
72
90
|
|
|
73
91
|
@abstractmethod
|
|
74
92
|
async def get_reading(self) -> Reading[SignalDatatypeT]:
|
|
75
|
-
"""
|
|
93
|
+
"""Return the current value, timestamp and severity."""
|
|
76
94
|
|
|
77
95
|
@abstractmethod
|
|
78
96
|
async def get_value(self) -> SignalDatatypeT:
|
|
79
|
-
"""
|
|
97
|
+
"""Return the current value."""
|
|
80
98
|
|
|
81
99
|
@abstractmethod
|
|
82
100
|
async def get_setpoint(self) -> SignalDatatypeT:
|
|
83
|
-
"""
|
|
101
|
+
"""Return the point that a signal was requested to move to."""
|
|
84
102
|
|
|
85
103
|
@abstractmethod
|
|
86
|
-
def set_callback(self, callback: Callback[
|
|
87
|
-
"""Observe changes to the current value, timestamp and severity"""
|
|
104
|
+
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
105
|
+
"""Observe changes to the current value, timestamp and severity."""
|
|
88
106
|
|
|
89
107
|
|
|
90
108
|
_primitive_dtype: dict[type[Primitive], Dtype] = {
|
|
@@ -96,10 +114,19 @@ _primitive_dtype: dict[type[Primitive], Dtype] = {
|
|
|
96
114
|
|
|
97
115
|
|
|
98
116
|
class SignalMetadata(TypedDict, total=False):
|
|
117
|
+
"""Metadata for a signal. No field is required."""
|
|
118
|
+
|
|
99
119
|
limits: Limits
|
|
120
|
+
"""The control, display, warning and alarm limits for a numeric datatype."""
|
|
121
|
+
|
|
100
122
|
choices: list[str]
|
|
123
|
+
"""The choice of possible values for an enum datatype."""
|
|
124
|
+
|
|
101
125
|
precision: int
|
|
126
|
+
"""The number of digits after the decimal place to display for a float datatype."""
|
|
127
|
+
|
|
102
128
|
units: str
|
|
129
|
+
"""The engineering units of the value for a numeric datatype."""
|
|
103
130
|
|
|
104
131
|
|
|
105
132
|
def _datakey_dtype(datatype: type[SignalDatatype]) -> Dtype:
|
|
@@ -156,6 +183,7 @@ def make_datakey(
|
|
|
156
183
|
source: str,
|
|
157
184
|
metadata: SignalMetadata,
|
|
158
185
|
) -> DataKey:
|
|
186
|
+
"""Make a DataKey for a given datatype."""
|
|
159
187
|
dtn = _datakey_dtype_numpy(datatype, value)
|
|
160
188
|
return DataKey(
|
|
161
189
|
dtype=_datakey_dtype(datatype),
|
|
@@ -165,3 +193,18 @@ def make_datakey(
|
|
|
165
193
|
source=source,
|
|
166
194
|
**metadata,
|
|
167
195
|
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def make_metadata(
|
|
199
|
+
datatype: type[SignalDatatypeT] | None,
|
|
200
|
+
units: str | None = None,
|
|
201
|
+
precision: int | None = None,
|
|
202
|
+
) -> SignalMetadata:
|
|
203
|
+
metadata: SignalMetadata = {}
|
|
204
|
+
if units is not None:
|
|
205
|
+
metadata["units"] = units
|
|
206
|
+
if precision is not None:
|
|
207
|
+
metadata["precision"] = precision
|
|
208
|
+
if enum_cls := get_enum_cls(datatype):
|
|
209
|
+
metadata["choices"] = [v.value for v in enum_cls]
|
|
210
|
+
return metadata
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
import typing
|
|
4
5
|
from abc import abstractmethod
|
|
5
6
|
from collections.abc import Sequence
|
|
6
7
|
from dataclasses import dataclass
|
|
@@ -19,9 +20,9 @@ from ._signal_backend import (
|
|
|
19
20
|
SignalBackend,
|
|
20
21
|
SignalDatatype,
|
|
21
22
|
SignalDatatypeT,
|
|
22
|
-
SignalMetadata,
|
|
23
23
|
TableT,
|
|
24
24
|
make_datakey,
|
|
25
|
+
make_metadata,
|
|
25
26
|
)
|
|
26
27
|
from ._table import Table
|
|
27
28
|
from ._utils import Callback, get_dtype, get_enum_cls
|
|
@@ -94,9 +95,9 @@ class TableSoftConverter(SoftConverter[TableT]):
|
|
|
94
95
|
@lru_cache
|
|
95
96
|
def make_converter(datatype: type[SignalDatatype]) -> SoftConverter:
|
|
96
97
|
enum_cls = get_enum_cls(datatype)
|
|
97
|
-
if datatype
|
|
98
|
+
if datatype in (Sequence[str], typing.Sequence[str]):
|
|
98
99
|
return SequenceStrSoftConverter()
|
|
99
|
-
elif get_origin(datatype)
|
|
100
|
+
elif get_origin(datatype) in (Sequence, typing.Sequence) and enum_cls:
|
|
100
101
|
return SequenceEnumSoftConverter(enum_cls)
|
|
101
102
|
elif datatype is np.ndarray:
|
|
102
103
|
return NDArraySoftConverter()
|
|
@@ -114,7 +115,16 @@ def make_converter(datatype: type[SignalDatatype]) -> SoftConverter:
|
|
|
114
115
|
|
|
115
116
|
|
|
116
117
|
class SoftSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
117
|
-
"""An backend to a soft Signal, for test signals see
|
|
118
|
+
"""An backend to a soft Signal, for test signals see [](#MockSignalBackend).
|
|
119
|
+
|
|
120
|
+
:param datatype: The datatype of the signal, defaults to float if not given.
|
|
121
|
+
:param initial_value:
|
|
122
|
+
The initial value of the signal, defaults to the "empty", "zero" or
|
|
123
|
+
"default" value of the datatype if not given.
|
|
124
|
+
:param units: The units for numeric datatypes.
|
|
125
|
+
:param precision:
|
|
126
|
+
The number of digits after the decimal place to display for a float datatype.
|
|
127
|
+
"""
|
|
118
128
|
|
|
119
129
|
def __init__(
|
|
120
130
|
self,
|
|
@@ -126,13 +136,7 @@ class SoftSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
126
136
|
# Create the right converter for the datatype
|
|
127
137
|
self.converter = make_converter(datatype or float)
|
|
128
138
|
# Add the extra static metadata to the dictionary
|
|
129
|
-
self.metadata
|
|
130
|
-
if units is not None:
|
|
131
|
-
self.metadata["units"] = units
|
|
132
|
-
if precision is not None:
|
|
133
|
-
self.metadata["precision"] = precision
|
|
134
|
-
if enum_cls := get_enum_cls(datatype):
|
|
135
|
-
self.metadata["choices"] = [v.value for v in enum_cls]
|
|
139
|
+
self.metadata = make_metadata(datatype, units, precision)
|
|
136
140
|
# Create and set the initial value
|
|
137
141
|
self.initial_value = self.converter.write_value(initial_value)
|
|
138
142
|
self.reading: Reading[SignalDatatypeT]
|
|
@@ -141,6 +145,7 @@ class SoftSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
141
145
|
super().__init__(datatype)
|
|
142
146
|
|
|
143
147
|
def set_value(self, value: SignalDatatypeT):
|
|
148
|
+
"""Set the current value, alarm and timestamp."""
|
|
144
149
|
self.reading = Reading(
|
|
145
150
|
value=self.converter.write_value(value),
|
|
146
151
|
timestamp=time.monotonic(),
|
ophyd_async/core/_status.py
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
"""Equivalent of bluesky.protocols.Status for asynchronous tasks."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import asyncio
|
|
4
6
|
import functools
|
|
5
7
|
import time
|
|
6
|
-
from collections.abc import AsyncIterator, Callable, Coroutine
|
|
8
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine
|
|
7
9
|
from dataclasses import asdict, replace
|
|
8
|
-
from typing import
|
|
9
|
-
Generic,
|
|
10
|
-
TypeVar,
|
|
11
|
-
cast,
|
|
12
|
-
)
|
|
10
|
+
from typing import Generic
|
|
13
11
|
|
|
14
12
|
from bluesky.protocols import Status
|
|
15
13
|
|
|
@@ -17,12 +15,9 @@ from ._device import Device
|
|
|
17
15
|
from ._protocol import Watcher
|
|
18
16
|
from ._utils import Callback, P, T, WatcherUpdate
|
|
19
17
|
|
|
20
|
-
AS = TypeVar("AS", bound="AsyncStatus")
|
|
21
|
-
WAS = TypeVar("WAS", bound="WatchableAsyncStatus")
|
|
22
|
-
|
|
23
18
|
|
|
24
|
-
class AsyncStatusBase(Status):
|
|
25
|
-
"""Convert asyncio awaitable to bluesky Status interface"""
|
|
19
|
+
class AsyncStatusBase(Status, Awaitable[None]):
|
|
20
|
+
"""Convert asyncio awaitable to bluesky Status interface."""
|
|
26
21
|
|
|
27
22
|
def __init__(self, awaitable: Coroutine | asyncio.Task, name: str | None = None):
|
|
28
23
|
if isinstance(awaitable, asyncio.Task):
|
|
@@ -47,6 +42,12 @@ class AsyncStatusBase(Status):
|
|
|
47
42
|
callback(self)
|
|
48
43
|
|
|
49
44
|
def exception(self, timeout: float | None = 0.0) -> BaseException | None:
|
|
45
|
+
"""Return any exception raised by the task.
|
|
46
|
+
|
|
47
|
+
:param timeout:
|
|
48
|
+
Taken for compatibility with the Status interface, but must be 0.0 as we
|
|
49
|
+
cannot wait for an async function in a sync call.
|
|
50
|
+
"""
|
|
50
51
|
if timeout != 0.0:
|
|
51
52
|
raise ValueError(
|
|
52
53
|
"cannot honour any timeout other than 0 in an asynchronous function"
|
|
@@ -88,27 +89,52 @@ class AsyncStatusBase(Status):
|
|
|
88
89
|
|
|
89
90
|
|
|
90
91
|
class AsyncStatus(AsyncStatusBase):
|
|
91
|
-
"""Convert asyncio awaitable to bluesky Status interface
|
|
92
|
+
"""Convert an asyncio awaitable to bluesky Status interface.
|
|
93
|
+
|
|
94
|
+
:param awaitable: The coroutine or task to await.
|
|
95
|
+
:param name: The name of the device, if available.
|
|
96
|
+
|
|
97
|
+
For example:
|
|
98
|
+
```python
|
|
99
|
+
status = AsyncStatus(asyncio.sleep(1))
|
|
100
|
+
assert not status.done
|
|
101
|
+
await status # waits for 1 second
|
|
102
|
+
assert status.done
|
|
103
|
+
```
|
|
104
|
+
"""
|
|
92
105
|
|
|
93
106
|
@classmethod
|
|
94
|
-
def wrap(cls
|
|
95
|
-
"""Wrap an async function in an AsyncStatus.
|
|
107
|
+
def wrap(cls, f: Callable[P, Coroutine]) -> Callable[P, AsyncStatus]:
|
|
108
|
+
"""Wrap an async function in an AsyncStatus and return it.
|
|
109
|
+
|
|
110
|
+
Used to make an async function conform to a bluesky protocol.
|
|
111
|
+
|
|
112
|
+
For example:
|
|
113
|
+
```python
|
|
114
|
+
class MyDevice(Device):
|
|
115
|
+
@AsyncStatus.wrap
|
|
116
|
+
async def trigger(self):
|
|
117
|
+
await asyncio.sleep(1)
|
|
118
|
+
```
|
|
119
|
+
"""
|
|
96
120
|
|
|
97
121
|
@functools.wraps(f)
|
|
98
|
-
def wrap_f(*args: P.args, **kwargs: P.kwargs) ->
|
|
122
|
+
def wrap_f(*args: P.args, **kwargs: P.kwargs) -> AsyncStatus:
|
|
99
123
|
if args and isinstance(args[0], Device):
|
|
100
124
|
name = args[0].name
|
|
101
125
|
else:
|
|
102
126
|
name = None
|
|
103
127
|
return cls(f(*args, **kwargs), name=name)
|
|
104
128
|
|
|
105
|
-
|
|
106
|
-
# but functools._Wrapped is not necessarily available
|
|
107
|
-
return cast(Callable[P, AS], wrap_f)
|
|
129
|
+
return wrap_f
|
|
108
130
|
|
|
109
131
|
|
|
110
132
|
class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
|
|
111
|
-
"""Convert
|
|
133
|
+
"""Convert an asyncio async iterable to bluesky Status and Watcher interface.
|
|
134
|
+
|
|
135
|
+
:param iterator: The async iterable to await.
|
|
136
|
+
:param name: The name of the device, if available.
|
|
137
|
+
"""
|
|
112
138
|
|
|
113
139
|
def __init__(
|
|
114
140
|
self, iterator: AsyncIterator[WatcherUpdate[T]], name: str | None = None
|
|
@@ -135,30 +161,52 @@ class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
|
|
|
135
161
|
watcher(**vals)
|
|
136
162
|
|
|
137
163
|
def watch(self, watcher: Watcher):
|
|
164
|
+
"""Add a watcher to the status.
|
|
165
|
+
|
|
166
|
+
It is called:
|
|
167
|
+
- immediately if there has already been an update
|
|
168
|
+
- on every subsequent update
|
|
169
|
+
"""
|
|
138
170
|
self._watchers.append(watcher)
|
|
139
171
|
if self._last_update:
|
|
140
172
|
self._update_watcher(watcher, self._last_update)
|
|
141
173
|
|
|
142
174
|
@classmethod
|
|
143
175
|
def wrap(
|
|
144
|
-
cls
|
|
176
|
+
cls,
|
|
145
177
|
f: Callable[P, AsyncIterator[WatcherUpdate[T]]],
|
|
146
|
-
) -> Callable[P,
|
|
147
|
-
"""Wrap an AsyncIterator in a WatchableAsyncStatus.
|
|
178
|
+
) -> Callable[P, WatchableAsyncStatus[T]]:
|
|
179
|
+
"""Wrap an AsyncIterator in a WatchableAsyncStatus.
|
|
180
|
+
|
|
181
|
+
For example:
|
|
182
|
+
```python
|
|
183
|
+
class MyDevice(Device):
|
|
184
|
+
@WatchableAsyncStatus.wrap
|
|
185
|
+
async def trigger(self):
|
|
186
|
+
# sleep for a second, updating on progress every 0.1 seconds
|
|
187
|
+
for i in range(10):
|
|
188
|
+
yield WatcherUpdate(initial=0, current=i*0.1, target=1)
|
|
189
|
+
await asyncio.sleep(0.1)
|
|
190
|
+
```
|
|
191
|
+
"""
|
|
148
192
|
|
|
149
193
|
@functools.wraps(f)
|
|
150
|
-
def wrap_f(*args: P.args, **kwargs: P.kwargs) ->
|
|
194
|
+
def wrap_f(*args: P.args, **kwargs: P.kwargs) -> WatchableAsyncStatus[T]:
|
|
151
195
|
if args and isinstance(args[0], Device):
|
|
152
196
|
name = args[0].name
|
|
153
197
|
else:
|
|
154
198
|
name = None
|
|
155
199
|
return cls(f(*args, **kwargs), name=name)
|
|
156
200
|
|
|
157
|
-
return
|
|
201
|
+
return wrap_f
|
|
158
202
|
|
|
159
203
|
|
|
160
204
|
@AsyncStatus.wrap
|
|
161
205
|
async def completed_status(exception: Exception | None = None):
|
|
206
|
+
"""Return a completed AsyncStatus.
|
|
207
|
+
|
|
208
|
+
:param exception: If given, then raise this exception when awaited.
|
|
209
|
+
"""
|
|
162
210
|
if exception:
|
|
163
211
|
raise exception
|
|
164
212
|
return None
|
ophyd_async/core/_table.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable, Sequence
|
|
4
|
-
from typing import Annotated, Any, TypeVar, get_origin
|
|
4
|
+
from typing import Annotated, Any, TypeVar, get_origin, get_type_hints
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
@@ -27,7 +27,30 @@ def _make_default_factory(dtype: np.dtype) -> Callable[[], np.ndarray]:
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class Table(BaseModel):
|
|
30
|
-
"""An abstraction of a Table
|
|
30
|
+
"""An abstraction of a Table where each field is a column.
|
|
31
|
+
|
|
32
|
+
For example:
|
|
33
|
+
```python
|
|
34
|
+
>>> from ophyd_async.core import Table, Array1D
|
|
35
|
+
>>> import numpy as np
|
|
36
|
+
>>> from collections.abc import Sequence
|
|
37
|
+
>>> class MyTable(Table):
|
|
38
|
+
... a: Array1D[np.int8]
|
|
39
|
+
... b: Sequence[str]
|
|
40
|
+
...
|
|
41
|
+
>>> t = MyTable(a=[1, 2], b=["x", "y"])
|
|
42
|
+
>>> len(t) # the length is the number of rows
|
|
43
|
+
2
|
|
44
|
+
>>> t2 = t + t # adding tables together concatenates them
|
|
45
|
+
>>> t2.a
|
|
46
|
+
array([1, 2, 1, 2], dtype=int8)
|
|
47
|
+
>>> t2.b
|
|
48
|
+
['x', 'y', 'x', 'y']
|
|
49
|
+
>>> t2[1] # slice a row
|
|
50
|
+
array([(2, b'y')], dtype=[('a', 'i1'), ('b', 'S40')])
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
31
54
|
|
|
32
55
|
# You can use Table in 2 ways:
|
|
33
56
|
# 1. Table(**whatever_pva_gives_us) when pvi adds a Signal to a Device that is not
|
|
@@ -40,16 +63,19 @@ class Table(BaseModel):
|
|
|
40
63
|
model_config = ConfigDict(extra="allow")
|
|
41
64
|
|
|
42
65
|
# Add an init method to match the above model config, otherwise the type
|
|
43
|
-
# checker will not think we can pass arbitrary kwargs into the base class init
|
|
66
|
+
# checker will not think we can pass arbitrary kwargs into the base class init...
|
|
44
67
|
def __init__(self, **kwargs):
|
|
45
68
|
super().__init__(**kwargs)
|
|
46
69
|
|
|
47
70
|
@classmethod
|
|
48
71
|
def __init_subclass__(cls):
|
|
49
|
-
#
|
|
72
|
+
# ...but forbid extra in subclasses so it gets validated
|
|
50
73
|
cls.model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
51
74
|
# Change fields to have the correct annotations
|
|
52
|
-
|
|
75
|
+
# TODO: refactor so we don't need this to break circular imports
|
|
76
|
+
from ._signal_backend import Array1D
|
|
77
|
+
|
|
78
|
+
for k, anno in get_type_hints(cls, localns={"Array1D": Array1D}).items():
|
|
53
79
|
if get_origin(anno) is np.ndarray:
|
|
54
80
|
dtype = get_dtype(anno)
|
|
55
81
|
new_anno = Annotated[
|
|
@@ -67,23 +93,24 @@ class Table(BaseModel):
|
|
|
67
93
|
|
|
68
94
|
def __add__(self, right: TableSubclass) -> TableSubclass:
|
|
69
95
|
"""Concatenate the arrays in field values."""
|
|
70
|
-
|
|
71
|
-
if type(
|
|
96
|
+
cls = type(right)
|
|
97
|
+
if type(self) is not cls:
|
|
72
98
|
raise RuntimeError(
|
|
73
99
|
f"{right} is not a `Table`, or is not the same "
|
|
74
100
|
f"type of `Table` as {self}."
|
|
75
101
|
)
|
|
76
102
|
|
|
77
|
-
return
|
|
103
|
+
return cls(
|
|
78
104
|
**{
|
|
79
105
|
field_name: _concat(
|
|
80
106
|
getattr(self, field_name), getattr(right, field_name)
|
|
81
107
|
)
|
|
82
|
-
for field_name in
|
|
108
|
+
for field_name in cls.model_fields
|
|
83
109
|
}
|
|
84
110
|
)
|
|
85
111
|
|
|
86
112
|
def numpy_dtype(self) -> np.dtype:
|
|
113
|
+
"""Return a numpy dtype for a single row."""
|
|
87
114
|
dtype = []
|
|
88
115
|
for k, v in self:
|
|
89
116
|
if isinstance(v, np.ndarray):
|
|
@@ -95,6 +122,7 @@ class Table(BaseModel):
|
|
|
95
122
|
return np.dtype(dtype)
|
|
96
123
|
|
|
97
124
|
def numpy_table(self, selection: slice | None = None) -> np.ndarray:
|
|
125
|
+
"""Return a numpy array of the whole table."""
|
|
98
126
|
array = None
|
|
99
127
|
for k, v in self:
|
|
100
128
|
if selection:
|
|
@@ -109,7 +137,9 @@ class Table(BaseModel):
|
|
|
109
137
|
|
|
110
138
|
@model_validator(mode="before")
|
|
111
139
|
@classmethod
|
|
112
|
-
def
|
|
140
|
+
def _validate_array_dtypes(cls, data: Any) -> Any:
|
|
141
|
+
# Validates that array datatypes given in the table are of the
|
|
142
|
+
# correct format.
|
|
113
143
|
if isinstance(data, dict):
|
|
114
144
|
data_dict = data
|
|
115
145
|
elif isinstance(data, Table):
|
|
@@ -137,7 +167,7 @@ class Table(BaseModel):
|
|
|
137
167
|
return data_dict
|
|
138
168
|
|
|
139
169
|
@model_validator(mode="after")
|
|
140
|
-
def
|
|
170
|
+
def _validate_lengths(self) -> Table:
|
|
141
171
|
lengths: dict[int, set[str]] = {}
|
|
142
172
|
for field_name, field_value in self:
|
|
143
173
|
lengths.setdefault(len(field_value), set()).add(field_name)
|