ophyd-async 0.5.2__py3-none-any.whl → 0.6.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 +10 -1
- ophyd_async/__main__.py +12 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +11 -3
- ophyd_async/core/_detector.py +72 -63
- ophyd_async/core/_device.py +13 -15
- ophyd_async/core/_device_save_loader.py +30 -19
- ophyd_async/core/_flyer.py +6 -4
- ophyd_async/core/_hdf_dataset.py +8 -9
- ophyd_async/core/_log.py +3 -1
- ophyd_async/core/_mock_signal_backend.py +11 -9
- ophyd_async/core/_mock_signal_utils.py +8 -5
- ophyd_async/core/_protocol.py +7 -7
- ophyd_async/core/_providers.py +11 -11
- ophyd_async/core/_readable.py +30 -22
- ophyd_async/core/_signal.py +52 -51
- ophyd_async/core/_signal_backend.py +20 -7
- ophyd_async/core/_soft_signal_backend.py +62 -32
- ophyd_async/core/_status.py +7 -9
- ophyd_async/core/_table.py +63 -0
- ophyd_async/core/_utils.py +24 -28
- ophyd_async/epics/adaravis/_aravis_controller.py +17 -16
- ophyd_async/epics/adaravis/_aravis_io.py +2 -1
- ophyd_async/epics/adcore/_core_io.py +2 -0
- ophyd_async/epics/adcore/_core_logic.py +2 -3
- ophyd_async/epics/adcore/_hdf_writer.py +19 -8
- ophyd_async/epics/adcore/_single_trigger.py +1 -1
- ophyd_async/epics/adcore/_utils.py +5 -6
- ophyd_async/epics/adkinetix/_kinetix_controller.py +19 -14
- ophyd_async/epics/adpilatus/_pilatus_controller.py +18 -16
- ophyd_async/epics/adsimdetector/_sim.py +6 -5
- ophyd_async/epics/adsimdetector/_sim_controller.py +20 -15
- ophyd_async/epics/advimba/_vimba_controller.py +21 -16
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/sensor.db +0 -1
- ophyd_async/epics/eiger/_eiger.py +1 -1
- ophyd_async/epics/eiger/_eiger_controller.py +16 -16
- ophyd_async/epics/eiger/_odin_io.py +6 -5
- ophyd_async/epics/motor.py +8 -10
- ophyd_async/epics/pvi/_pvi.py +30 -33
- ophyd_async/epics/signal/_aioca.py +55 -25
- ophyd_async/epics/signal/_common.py +3 -10
- ophyd_async/epics/signal/_epics_transport.py +11 -8
- ophyd_async/epics/signal/_p4p.py +79 -30
- ophyd_async/epics/signal/_signal.py +6 -8
- ophyd_async/fastcs/panda/__init__.py +0 -6
- ophyd_async/fastcs/panda/_control.py +14 -15
- ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
- ophyd_async/fastcs/panda/_table.py +111 -138
- ophyd_async/fastcs/panda/_trigger.py +1 -2
- ophyd_async/fastcs/panda/_utils.py +3 -2
- ophyd_async/fastcs/panda/_writer.py +28 -13
- ophyd_async/plan_stubs/_fly.py +16 -16
- ophyd_async/plan_stubs/_nd_attributes.py +12 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +24 -20
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
- ophyd_async/sim/demo/_sim_motor.py +2 -1
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/METADATA +46 -45
- ophyd_async-0.6.0.dist-info/RECORD +96 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/WHEEL +1 -1
- ophyd_async-0.5.2.dist-info/RECORD +0 -95
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/top_level.txt +0 -0
ophyd_async/core/_signal.py
CHANGED
|
@@ -2,22 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
AsyncGenerator,
|
|
8
|
-
Callable,
|
|
9
|
-
Dict,
|
|
10
|
-
Generic,
|
|
11
|
-
Mapping,
|
|
12
|
-
Optional,
|
|
13
|
-
Tuple,
|
|
14
|
-
Type,
|
|
15
|
-
TypeVar,
|
|
16
|
-
Union,
|
|
17
|
-
)
|
|
5
|
+
from collections.abc import AsyncGenerator, Callable, Mapping
|
|
6
|
+
from typing import Any, Generic, TypeVar, cast
|
|
18
7
|
|
|
19
8
|
from bluesky.protocols import (
|
|
20
|
-
DataKey,
|
|
21
9
|
Locatable,
|
|
22
10
|
Location,
|
|
23
11
|
Movable,
|
|
@@ -25,6 +13,7 @@ from bluesky.protocols import (
|
|
|
25
13
|
Status,
|
|
26
14
|
Subscribable,
|
|
27
15
|
)
|
|
16
|
+
from event_model import DataKey
|
|
28
17
|
|
|
29
18
|
from ._device import Device
|
|
30
19
|
from ._mock_signal_backend import MockSignalBackend
|
|
@@ -32,7 +21,7 @@ from ._protocol import AsyncConfigurable, AsyncReadable, AsyncStageable
|
|
|
32
21
|
from ._signal_backend import SignalBackend
|
|
33
22
|
from ._soft_signal_backend import SignalMetadata, SoftSignalBackend
|
|
34
23
|
from ._status import AsyncStatus
|
|
35
|
-
from ._utils import DEFAULT_TIMEOUT, CalculatableTimeout,
|
|
24
|
+
from ._utils import CALCULATE_TIMEOUT, DEFAULT_TIMEOUT, CalculatableTimeout, Callback, T
|
|
36
25
|
|
|
37
26
|
S = TypeVar("S")
|
|
38
27
|
|
|
@@ -45,13 +34,26 @@ def _add_timeout(func):
|
|
|
45
34
|
return wrapper
|
|
46
35
|
|
|
47
36
|
|
|
37
|
+
def _fail(*args, **kwargs):
|
|
38
|
+
raise RuntimeError("Signal has not been supplied a backend yet")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DisconnectedBackend(SignalBackend):
|
|
42
|
+
source = connect = put = get_datakey = get_reading = get_value = get_setpoint = (
|
|
43
|
+
set_callback
|
|
44
|
+
) = _fail
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
DISCONNECTED_BACKEND = DisconnectedBackend()
|
|
48
|
+
|
|
49
|
+
|
|
48
50
|
class Signal(Device, Generic[T]):
|
|
49
51
|
"""A Device with the concept of a value, with R, RW, W and X flavours"""
|
|
50
52
|
|
|
51
53
|
def __init__(
|
|
52
54
|
self,
|
|
53
|
-
backend:
|
|
54
|
-
timeout:
|
|
55
|
+
backend: SignalBackend[T] = DISCONNECTED_BACKEND,
|
|
56
|
+
timeout: float | None = DEFAULT_TIMEOUT,
|
|
55
57
|
name: str = "",
|
|
56
58
|
) -> None:
|
|
57
59
|
self._timeout = timeout
|
|
@@ -63,10 +65,13 @@ class Signal(Device, Generic[T]):
|
|
|
63
65
|
mock=False,
|
|
64
66
|
timeout=DEFAULT_TIMEOUT,
|
|
65
67
|
force_reconnect: bool = False,
|
|
66
|
-
backend:
|
|
68
|
+
backend: SignalBackend[T] | None = None,
|
|
67
69
|
):
|
|
68
70
|
if backend:
|
|
69
|
-
if
|
|
71
|
+
if (
|
|
72
|
+
self._backend is not DISCONNECTED_BACKEND
|
|
73
|
+
and backend is not self._backend
|
|
74
|
+
):
|
|
70
75
|
raise ValueError("Backend at connection different from previous one.")
|
|
71
76
|
|
|
72
77
|
self._backend = backend
|
|
@@ -114,10 +119,10 @@ class _SignalCache(Generic[T]):
|
|
|
114
119
|
def __init__(self, backend: SignalBackend[T], signal: Signal):
|
|
115
120
|
self._signal = signal
|
|
116
121
|
self._staged = False
|
|
117
|
-
self._listeners:
|
|
122
|
+
self._listeners: dict[Callback, bool] = {}
|
|
118
123
|
self._valid = asyncio.Event()
|
|
119
|
-
self._reading:
|
|
120
|
-
self._value:
|
|
124
|
+
self._reading: Reading | None = None
|
|
125
|
+
self._value: T | None = None
|
|
121
126
|
|
|
122
127
|
self.backend = backend
|
|
123
128
|
signal.log.debug(f"Making subscription on source {signal.source}")
|
|
@@ -171,11 +176,9 @@ class _SignalCache(Generic[T]):
|
|
|
171
176
|
class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
|
|
172
177
|
"""Signal that can be read from and monitored"""
|
|
173
178
|
|
|
174
|
-
_cache:
|
|
179
|
+
_cache: _SignalCache | None = None
|
|
175
180
|
|
|
176
|
-
def _backend_or_cache(
|
|
177
|
-
self, cached: Optional[bool]
|
|
178
|
-
) -> Union[_SignalCache, SignalBackend]:
|
|
181
|
+
def _backend_or_cache(self, cached: bool | None) -> _SignalCache | SignalBackend:
|
|
179
182
|
# If cached is None then calculate it based on whether we already have a cache
|
|
180
183
|
if cached is None:
|
|
181
184
|
cached = self._cache is not None
|
|
@@ -196,17 +199,17 @@ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
|
|
|
196
199
|
self._cache = None
|
|
197
200
|
|
|
198
201
|
@_add_timeout
|
|
199
|
-
async def read(self, cached:
|
|
202
|
+
async def read(self, cached: bool | None = None) -> dict[str, Reading]:
|
|
200
203
|
"""Return a single item dict with the reading in it"""
|
|
201
204
|
return {self.name: await self._backend_or_cache(cached).get_reading()}
|
|
202
205
|
|
|
203
206
|
@_add_timeout
|
|
204
|
-
async def describe(self) ->
|
|
207
|
+
async def describe(self) -> dict[str, DataKey]:
|
|
205
208
|
"""Return a single item dict with the descriptor in it"""
|
|
206
209
|
return {self.name: await self._backend.get_datakey(self.source)}
|
|
207
210
|
|
|
208
211
|
@_add_timeout
|
|
209
|
-
async def get_value(self, cached:
|
|
212
|
+
async def get_value(self, cached: bool | None = None) -> T:
|
|
210
213
|
"""The current value"""
|
|
211
214
|
value = await self._backend_or_cache(cached).get_value()
|
|
212
215
|
self.log.debug(f"get_value() on source {self.source} returned {value}")
|
|
@@ -216,7 +219,7 @@ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
|
|
|
216
219
|
"""Subscribe to updates in value of a device"""
|
|
217
220
|
self._get_cache().subscribe(function, want_value=True)
|
|
218
221
|
|
|
219
|
-
def subscribe(self, function: Callback[
|
|
222
|
+
def subscribe(self, function: Callback[dict[str, Reading]]) -> None:
|
|
220
223
|
"""Subscribe to updates in the reading"""
|
|
221
224
|
self._get_cache().subscribe(function, want_value=False)
|
|
222
225
|
|
|
@@ -239,10 +242,10 @@ class SignalW(Signal[T], Movable):
|
|
|
239
242
|
"""Signal that can be set"""
|
|
240
243
|
|
|
241
244
|
def set(
|
|
242
|
-
self, value: T, wait=True, timeout: CalculatableTimeout =
|
|
245
|
+
self, value: T, wait=True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
|
|
243
246
|
) -> AsyncStatus:
|
|
244
247
|
"""Set the value and return a status saying when it's done"""
|
|
245
|
-
if timeout is
|
|
248
|
+
if timeout is CALCULATE_TIMEOUT:
|
|
246
249
|
timeout = self._timeout
|
|
247
250
|
|
|
248
251
|
async def do_set():
|
|
@@ -270,18 +273,18 @@ class SignalX(Signal):
|
|
|
270
273
|
"""Signal that puts the default value"""
|
|
271
274
|
|
|
272
275
|
def trigger(
|
|
273
|
-
self, wait=True, timeout: CalculatableTimeout =
|
|
276
|
+
self, wait=True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
|
|
274
277
|
) -> AsyncStatus:
|
|
275
278
|
"""Trigger the action and return a status saying when it's done"""
|
|
276
|
-
if timeout is
|
|
279
|
+
if timeout is CALCULATE_TIMEOUT:
|
|
277
280
|
timeout = self._timeout
|
|
278
281
|
coro = self._backend.put(None, wait=wait, timeout=timeout)
|
|
279
282
|
return AsyncStatus(coro)
|
|
280
283
|
|
|
281
284
|
|
|
282
285
|
def soft_signal_rw(
|
|
283
|
-
datatype:
|
|
284
|
-
initial_value:
|
|
286
|
+
datatype: type[T] | None = None,
|
|
287
|
+
initial_value: T | None = None,
|
|
285
288
|
name: str = "",
|
|
286
289
|
units: str | None = None,
|
|
287
290
|
precision: int | None = None,
|
|
@@ -298,12 +301,12 @@ def soft_signal_rw(
|
|
|
298
301
|
|
|
299
302
|
|
|
300
303
|
def soft_signal_r_and_setter(
|
|
301
|
-
datatype:
|
|
302
|
-
initial_value:
|
|
304
|
+
datatype: type[T] | None = None,
|
|
305
|
+
initial_value: T | None = None,
|
|
303
306
|
name: str = "",
|
|
304
307
|
units: str | None = None,
|
|
305
308
|
precision: int | None = None,
|
|
306
|
-
) ->
|
|
309
|
+
) -> tuple[SignalR[T], Callable[[T], None]]:
|
|
307
310
|
"""Returns a tuple of a read-only Signal and a callable through
|
|
308
311
|
which the signal can be internally modified within the device.
|
|
309
312
|
May pass metadata, which are propagated into describe.
|
|
@@ -316,9 +319,7 @@ def soft_signal_r_and_setter(
|
|
|
316
319
|
return (signal, backend.set_value)
|
|
317
320
|
|
|
318
321
|
|
|
319
|
-
def _generate_assert_error_msg(
|
|
320
|
-
name: str, expected_result: str, actual_result: str
|
|
321
|
-
) -> str:
|
|
322
|
+
def _generate_assert_error_msg(name: str, expected_result, actual_result) -> str:
|
|
322
323
|
WARNING = "\033[93m"
|
|
323
324
|
FAIL = "\033[91m"
|
|
324
325
|
ENDC = "\033[0m"
|
|
@@ -484,14 +485,14 @@ async def observe_value(
|
|
|
484
485
|
else:
|
|
485
486
|
break
|
|
486
487
|
else:
|
|
487
|
-
yield item
|
|
488
|
+
yield cast(T, item)
|
|
488
489
|
finally:
|
|
489
490
|
signal.clear_sub(q.put_nowait)
|
|
490
491
|
|
|
491
492
|
|
|
492
493
|
class _ValueChecker(Generic[T]):
|
|
493
494
|
def __init__(self, matcher: Callable[[T], bool], matcher_name: str):
|
|
494
|
-
self._last_value:
|
|
495
|
+
self._last_value: T | None = None
|
|
495
496
|
self._matcher = matcher
|
|
496
497
|
self._matcher_name = matcher_name
|
|
497
498
|
|
|
@@ -501,11 +502,11 @@ class _ValueChecker(Generic[T]):
|
|
|
501
502
|
if self._matcher(value):
|
|
502
503
|
return
|
|
503
504
|
|
|
504
|
-
async def wait_for_value(self, signal: SignalR[T], timeout:
|
|
505
|
+
async def wait_for_value(self, signal: SignalR[T], timeout: float | None):
|
|
505
506
|
try:
|
|
506
507
|
await asyncio.wait_for(self._wait_for_value(signal), timeout)
|
|
507
508
|
except asyncio.TimeoutError as e:
|
|
508
|
-
raise TimeoutError(
|
|
509
|
+
raise asyncio.TimeoutError(
|
|
509
510
|
f"{signal.name} didn't match {self._matcher_name} in {timeout}s, "
|
|
510
511
|
f"last value {self._last_value!r}"
|
|
511
512
|
) from e
|
|
@@ -513,8 +514,8 @@ class _ValueChecker(Generic[T]):
|
|
|
513
514
|
|
|
514
515
|
async def wait_for_value(
|
|
515
516
|
signal: SignalR[T],
|
|
516
|
-
match:
|
|
517
|
-
timeout:
|
|
517
|
+
match: T | Callable[[T], bool],
|
|
518
|
+
timeout: float | None,
|
|
518
519
|
):
|
|
519
520
|
"""Wait for a signal to have a matching value.
|
|
520
521
|
|
|
@@ -540,7 +541,7 @@ async def wait_for_value(
|
|
|
540
541
|
wait_for_value(device.num_captured, lambda v: v > 45, timeout=1)
|
|
541
542
|
"""
|
|
542
543
|
if callable(match):
|
|
543
|
-
checker = _ValueChecker(match, match.__name__)
|
|
544
|
+
checker = _ValueChecker(match, match.__name__) # type: ignore
|
|
544
545
|
else:
|
|
545
546
|
checker = _ValueChecker(lambda v: v == match, repr(match))
|
|
546
547
|
await checker.wait_for_value(signal, timeout)
|
|
@@ -552,7 +553,7 @@ async def set_and_wait_for_other_value(
|
|
|
552
553
|
read_signal: SignalR[S],
|
|
553
554
|
read_value: S,
|
|
554
555
|
timeout: float = DEFAULT_TIMEOUT,
|
|
555
|
-
set_timeout:
|
|
556
|
+
set_timeout: float | None = None,
|
|
556
557
|
) -> AsyncStatus:
|
|
557
558
|
"""Set a signal and monitor another signal until it has the specified value.
|
|
558
559
|
|
|
@@ -610,7 +611,7 @@ async def set_and_wait_for_value(
|
|
|
610
611
|
signal: SignalRW[T],
|
|
611
612
|
value: T,
|
|
612
613
|
timeout: float = DEFAULT_TIMEOUT,
|
|
613
|
-
status_timeout:
|
|
614
|
+
status_timeout: float | None = None,
|
|
614
615
|
) -> AsyncStatus:
|
|
615
616
|
"""Set a signal and monitor it until it has that value.
|
|
616
617
|
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import (
|
|
3
|
+
TYPE_CHECKING,
|
|
4
|
+
Any,
|
|
5
|
+
ClassVar,
|
|
6
|
+
Generic,
|
|
7
|
+
Literal,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from bluesky.protocols import Reading
|
|
11
|
+
from event_model import DataKey
|
|
3
12
|
|
|
4
|
-
from ._protocol import DataKey, Reading
|
|
5
13
|
from ._utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
|
|
6
14
|
|
|
7
15
|
|
|
@@ -9,7 +17,12 @@ class SignalBackend(Generic[T]):
|
|
|
9
17
|
"""A read/write/monitor backend for a Signals"""
|
|
10
18
|
|
|
11
19
|
#: Datatype of the signal value
|
|
12
|
-
datatype:
|
|
20
|
+
datatype: type[T] | None = None
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def datatype_allowed(cls, dtype: Any) -> bool:
|
|
25
|
+
"""Check if a given datatype is acceptable for this signal backend."""
|
|
13
26
|
|
|
14
27
|
#: Like ca://PV_PREFIX:SIGNAL
|
|
15
28
|
@abstractmethod
|
|
@@ -22,7 +35,7 @@ class SignalBackend(Generic[T]):
|
|
|
22
35
|
"""Connect to underlying hardware"""
|
|
23
36
|
|
|
24
37
|
@abstractmethod
|
|
25
|
-
async def put(self, value:
|
|
38
|
+
async def put(self, value: T | None, wait=True, timeout=None):
|
|
26
39
|
"""Put a value to the PV, if wait then wait for completion for up to timeout"""
|
|
27
40
|
|
|
28
41
|
@abstractmethod
|
|
@@ -42,14 +55,14 @@ class SignalBackend(Generic[T]):
|
|
|
42
55
|
"""The point that a signal was requested to move to."""
|
|
43
56
|
|
|
44
57
|
@abstractmethod
|
|
45
|
-
def set_callback(self, callback:
|
|
58
|
+
def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
|
|
46
59
|
"""Observe changes to the current value, timestamp and severity"""
|
|
47
60
|
|
|
48
61
|
|
|
49
62
|
class _RuntimeSubsetEnumMeta(type):
|
|
50
63
|
def __str__(cls):
|
|
51
64
|
if hasattr(cls, "choices"):
|
|
52
|
-
return f"SubsetEnum{list(cls.choices)}"
|
|
65
|
+
return f"SubsetEnum{list(cls.choices)}" # type: ignore
|
|
53
66
|
return "SubsetEnum"
|
|
54
67
|
|
|
55
68
|
def __getitem__(cls, _choices):
|
|
@@ -72,7 +85,7 @@ class _RuntimeSubsetEnumMeta(type):
|
|
|
72
85
|
|
|
73
86
|
|
|
74
87
|
class RuntimeSubsetEnum(metaclass=_RuntimeSubsetEnumMeta):
|
|
75
|
-
choices: ClassVar[
|
|
88
|
+
choices: ClassVar[tuple[str, ...]]
|
|
76
89
|
|
|
77
90
|
def __init__(self):
|
|
78
91
|
raise RuntimeError("SubsetEnum cannot be instantiated")
|
|
@@ -4,16 +4,28 @@ import inspect
|
|
|
4
4
|
import time
|
|
5
5
|
from collections import abc
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Generic, cast, get_origin
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
|
-
from bluesky.protocols import
|
|
10
|
+
from bluesky.protocols import Reading
|
|
11
|
+
from event_model import DataKey
|
|
12
|
+
from event_model.documents.event_descriptor import Dtype
|
|
13
|
+
from pydantic import BaseModel
|
|
11
14
|
from typing_extensions import TypedDict
|
|
12
15
|
|
|
13
|
-
from ._signal_backend import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
from ._signal_backend import (
|
|
17
|
+
RuntimeSubsetEnum,
|
|
18
|
+
SignalBackend,
|
|
19
|
+
)
|
|
20
|
+
from ._utils import (
|
|
21
|
+
DEFAULT_TIMEOUT,
|
|
22
|
+
ReadingValueCallback,
|
|
23
|
+
T,
|
|
24
|
+
get_dtype,
|
|
25
|
+
is_pydantic_model,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
primitive_dtypes: dict[type, Dtype] = {
|
|
17
29
|
str: "string",
|
|
18
30
|
int: "integer",
|
|
19
31
|
float: "number",
|
|
@@ -22,8 +34,8 @@ primitive_dtypes: Dict[type, Dtype] = {
|
|
|
22
34
|
|
|
23
35
|
|
|
24
36
|
class SignalMetadata(TypedDict):
|
|
25
|
-
units: str | None
|
|
26
|
-
precision: int | None
|
|
37
|
+
units: str | None
|
|
38
|
+
precision: int | None
|
|
27
39
|
|
|
28
40
|
|
|
29
41
|
class SoftConverter(Generic[T]):
|
|
@@ -41,7 +53,7 @@ class SoftConverter(Generic[T]):
|
|
|
41
53
|
)
|
|
42
54
|
|
|
43
55
|
def get_datakey(self, source: str, value, **metadata) -> DataKey:
|
|
44
|
-
dk = {"source": source, "shape": [], **metadata}
|
|
56
|
+
dk: DataKey = {"source": source, "shape": [], **metadata} # type: ignore
|
|
45
57
|
dtype = type(value)
|
|
46
58
|
if np.issubdtype(dtype, np.integer):
|
|
47
59
|
dtype = int
|
|
@@ -51,13 +63,14 @@ class SoftConverter(Generic[T]):
|
|
|
51
63
|
dtype in primitive_dtypes
|
|
52
64
|
), f"invalid converter for value of type {type(value)}"
|
|
53
65
|
dk["dtype"] = primitive_dtypes[dtype]
|
|
66
|
+
# type ignore until https://github.com/bluesky/event-model/issues/308
|
|
54
67
|
try:
|
|
55
|
-
dk["dtype_numpy"] = np.dtype(dtype).descr[0][1]
|
|
68
|
+
dk["dtype_numpy"] = np.dtype(dtype).descr[0][1] # type: ignore
|
|
56
69
|
except TypeError:
|
|
57
|
-
dk["dtype_numpy"] = ""
|
|
70
|
+
dk["dtype_numpy"] = "" # type: ignore
|
|
58
71
|
return dk
|
|
59
72
|
|
|
60
|
-
def make_initial_value(self, datatype:
|
|
73
|
+
def make_initial_value(self, datatype: type[T] | None) -> T:
|
|
61
74
|
if datatype is None:
|
|
62
75
|
return cast(T, None)
|
|
63
76
|
|
|
@@ -76,12 +89,12 @@ class SoftArrayConverter(SoftConverter):
|
|
|
76
89
|
return {
|
|
77
90
|
"source": source,
|
|
78
91
|
"dtype": "array",
|
|
79
|
-
"dtype_numpy": dtype_numpy,
|
|
92
|
+
"dtype_numpy": dtype_numpy, # type: ignore
|
|
80
93
|
"shape": [len(value)],
|
|
81
94
|
**metadata,
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
def make_initial_value(self, datatype:
|
|
97
|
+
def make_initial_value(self, datatype: type[T] | None) -> T:
|
|
85
98
|
if datatype is None:
|
|
86
99
|
return cast(T, None)
|
|
87
100
|
|
|
@@ -92,28 +105,29 @@ class SoftArrayConverter(SoftConverter):
|
|
|
92
105
|
|
|
93
106
|
|
|
94
107
|
class SoftEnumConverter(SoftConverter):
|
|
95
|
-
choices:
|
|
108
|
+
choices: tuple[str, ...]
|
|
96
109
|
|
|
97
|
-
def __init__(self, datatype:
|
|
98
|
-
if issubclass(datatype, Enum):
|
|
110
|
+
def __init__(self, datatype: RuntimeSubsetEnum | type[Enum]):
|
|
111
|
+
if issubclass(datatype, Enum): # type: ignore
|
|
99
112
|
self.choices = tuple(v.value for v in datatype)
|
|
100
113
|
else:
|
|
101
114
|
self.choices = datatype.choices
|
|
102
115
|
|
|
103
|
-
def write_value(self, value:
|
|
104
|
-
return value
|
|
116
|
+
def write_value(self, value: Enum | str) -> str:
|
|
117
|
+
return value # type: ignore
|
|
105
118
|
|
|
106
119
|
def get_datakey(self, source: str, value, **metadata) -> DataKey:
|
|
107
120
|
return {
|
|
108
121
|
"source": source,
|
|
109
122
|
"dtype": "string",
|
|
110
|
-
|
|
123
|
+
# type ignore until https://github.com/bluesky/event-model/issues/308
|
|
124
|
+
"dtype_numpy": "|S40", # type: ignore
|
|
111
125
|
"shape": [],
|
|
112
126
|
"choices": self.choices,
|
|
113
127
|
**metadata,
|
|
114
128
|
}
|
|
115
129
|
|
|
116
|
-
def make_initial_value(self, datatype:
|
|
130
|
+
def make_initial_value(self, datatype: type[T] | None) -> T:
|
|
117
131
|
if datatype is None:
|
|
118
132
|
return cast(T, None)
|
|
119
133
|
|
|
@@ -122,6 +136,16 @@ class SoftEnumConverter(SoftConverter):
|
|
|
122
136
|
return cast(T, self.choices[0])
|
|
123
137
|
|
|
124
138
|
|
|
139
|
+
class SoftPydanticModelConverter(SoftConverter):
|
|
140
|
+
def __init__(self, datatype: type[BaseModel]):
|
|
141
|
+
self.datatype = datatype
|
|
142
|
+
|
|
143
|
+
def write_value(self, value):
|
|
144
|
+
if isinstance(value, dict):
|
|
145
|
+
return self.datatype(**value)
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
|
|
125
149
|
def make_converter(datatype):
|
|
126
150
|
is_array = get_dtype(datatype) is not None
|
|
127
151
|
is_sequence = get_origin(datatype) == abc.Sequence
|
|
@@ -132,7 +156,9 @@ def make_converter(datatype):
|
|
|
132
156
|
if is_array or is_sequence:
|
|
133
157
|
return SoftArrayConverter()
|
|
134
158
|
if is_enum:
|
|
135
|
-
return SoftEnumConverter(datatype)
|
|
159
|
+
return SoftEnumConverter(datatype) # type: ignore
|
|
160
|
+
if is_pydantic_model(datatype):
|
|
161
|
+
return SoftPydanticModelConverter(datatype) # type: ignore
|
|
136
162
|
|
|
137
163
|
return SoftConverter()
|
|
138
164
|
|
|
@@ -141,15 +167,19 @@ class SoftSignalBackend(SignalBackend[T]):
|
|
|
141
167
|
"""An backend to a soft Signal, for test signals see ``MockSignalBackend``."""
|
|
142
168
|
|
|
143
169
|
_value: T
|
|
144
|
-
_initial_value:
|
|
170
|
+
_initial_value: T | None
|
|
145
171
|
_timestamp: float
|
|
146
172
|
_severity: int
|
|
147
173
|
|
|
174
|
+
@classmethod
|
|
175
|
+
def datatype_allowed(cls, dtype: type) -> bool:
|
|
176
|
+
return True # Any value allowed in a soft signal
|
|
177
|
+
|
|
148
178
|
def __init__(
|
|
149
179
|
self,
|
|
150
|
-
datatype:
|
|
151
|
-
initial_value:
|
|
152
|
-
metadata: SignalMetadata = None,
|
|
180
|
+
datatype: type[T] | None,
|
|
181
|
+
initial_value: T | None = None,
|
|
182
|
+
metadata: SignalMetadata = None, # type: ignore
|
|
153
183
|
) -> None:
|
|
154
184
|
self.datatype = datatype
|
|
155
185
|
self._initial_value = initial_value
|
|
@@ -158,11 +188,11 @@ class SoftSignalBackend(SignalBackend[T]):
|
|
|
158
188
|
if self._initial_value is None:
|
|
159
189
|
self._initial_value = self.converter.make_initial_value(self.datatype)
|
|
160
190
|
else:
|
|
161
|
-
self._initial_value = self.converter.write_value(self._initial_value)
|
|
191
|
+
self._initial_value = self.converter.write_value(self._initial_value) # type: ignore
|
|
162
192
|
|
|
163
|
-
self.callback:
|
|
193
|
+
self.callback: ReadingValueCallback[T] | None = None
|
|
164
194
|
self._severity = 0
|
|
165
|
-
self.set_value(self._initial_value)
|
|
195
|
+
self.set_value(self._initial_value) # type: ignore
|
|
166
196
|
|
|
167
197
|
def source(self, name: str) -> str:
|
|
168
198
|
return f"soft://{name}"
|
|
@@ -171,14 +201,14 @@ class SoftSignalBackend(SignalBackend[T]):
|
|
|
171
201
|
"""Connection isn't required for soft signals."""
|
|
172
202
|
pass
|
|
173
203
|
|
|
174
|
-
async def put(self, value:
|
|
204
|
+
async def put(self, value: T | None, wait=True, timeout=None):
|
|
175
205
|
write_value = (
|
|
176
206
|
self.converter.write_value(value)
|
|
177
207
|
if value is not None
|
|
178
208
|
else self._initial_value
|
|
179
209
|
)
|
|
180
210
|
|
|
181
|
-
self.set_value(write_value)
|
|
211
|
+
self.set_value(write_value) # type: ignore
|
|
182
212
|
|
|
183
213
|
def set_value(self, value: T):
|
|
184
214
|
"""Method to bypass asynchronous logic."""
|
|
@@ -204,7 +234,7 @@ class SoftSignalBackend(SignalBackend[T]):
|
|
|
204
234
|
"""For a soft signal, the setpoint and readback values are the same."""
|
|
205
235
|
return await self.get_value()
|
|
206
236
|
|
|
207
|
-
def set_callback(self, callback:
|
|
237
|
+
def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
|
|
208
238
|
if callback:
|
|
209
239
|
assert not self.callback, "Cannot set a callback when one is already set"
|
|
210
240
|
reading: Reading = self.converter.reading(
|
ophyd_async/core/_status.py
CHANGED
|
@@ -3,14 +3,10 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
5
|
import time
|
|
6
|
+
from collections.abc import AsyncIterator, Callable, Coroutine
|
|
6
7
|
from dataclasses import asdict, replace
|
|
7
8
|
from typing import (
|
|
8
|
-
AsyncIterator,
|
|
9
|
-
Awaitable,
|
|
10
|
-
Callable,
|
|
11
9
|
Generic,
|
|
12
|
-
Optional,
|
|
13
|
-
Type,
|
|
14
10
|
TypeVar,
|
|
15
11
|
cast,
|
|
16
12
|
)
|
|
@@ -27,7 +23,7 @@ WAS = TypeVar("WAS", bound="WatchableAsyncStatus")
|
|
|
27
23
|
class AsyncStatusBase(Status):
|
|
28
24
|
"""Convert asyncio awaitable to bluesky Status interface"""
|
|
29
25
|
|
|
30
|
-
def __init__(self, awaitable:
|
|
26
|
+
def __init__(self, awaitable: Coroutine | asyncio.Task):
|
|
31
27
|
if isinstance(awaitable, asyncio.Task):
|
|
32
28
|
self.task = awaitable
|
|
33
29
|
else:
|
|
@@ -86,8 +82,10 @@ class AsyncStatusBase(Status):
|
|
|
86
82
|
|
|
87
83
|
|
|
88
84
|
class AsyncStatus(AsyncStatusBase):
|
|
85
|
+
"""Convert asyncio awaitable to bluesky Status interface"""
|
|
86
|
+
|
|
89
87
|
@classmethod
|
|
90
|
-
def wrap(cls:
|
|
88
|
+
def wrap(cls: type[AS], f: Callable[P, Coroutine]) -> Callable[P, AS]:
|
|
91
89
|
"""Wrap an async function in an AsyncStatus."""
|
|
92
90
|
|
|
93
91
|
@functools.wraps(f)
|
|
@@ -131,7 +129,7 @@ class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
|
|
|
131
129
|
|
|
132
130
|
@classmethod
|
|
133
131
|
def wrap(
|
|
134
|
-
cls:
|
|
132
|
+
cls: type[WAS],
|
|
135
133
|
f: Callable[P, AsyncIterator[WatcherUpdate[T]]],
|
|
136
134
|
) -> Callable[P, WAS]:
|
|
137
135
|
"""Wrap an AsyncIterator in a WatchableAsyncStatus."""
|
|
@@ -144,7 +142,7 @@ class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
|
|
|
144
142
|
|
|
145
143
|
|
|
146
144
|
@AsyncStatus.wrap
|
|
147
|
-
async def completed_status(exception:
|
|
145
|
+
async def completed_status(exception: Exception | None = None):
|
|
148
146
|
if exception:
|
|
149
147
|
raise exception
|
|
150
148
|
return None
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
5
|
+
|
|
6
|
+
TableSubclass = TypeVar("TableSubclass", bound="Table")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Table(BaseModel):
|
|
10
|
+
"""An abstraction of a Table of str to numpy array."""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(validate_assignment=True, strict=False)
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def row(cls: type[TableSubclass], **kwargs) -> TableSubclass: # type: ignore
|
|
16
|
+
arrayified_kwargs = {
|
|
17
|
+
field_name: np.concatenate(
|
|
18
|
+
(
|
|
19
|
+
(default_arr := field_value.default_factory()), # type: ignore
|
|
20
|
+
np.array([kwargs[field_name]], dtype=default_arr.dtype),
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
for field_name, field_value in cls.model_fields.items()
|
|
24
|
+
}
|
|
25
|
+
return cls(**arrayified_kwargs)
|
|
26
|
+
|
|
27
|
+
def __add__(self, right: TableSubclass) -> TableSubclass:
|
|
28
|
+
"""Concatenate the arrays in field values."""
|
|
29
|
+
|
|
30
|
+
assert type(right) is type(self), (
|
|
31
|
+
f"{right} is not a `Table`, or is not the same "
|
|
32
|
+
f"type of `Table` as {self}."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return type(right)(
|
|
36
|
+
**{
|
|
37
|
+
field_name: np.concatenate(
|
|
38
|
+
(getattr(self, field_name), getattr(right, field_name))
|
|
39
|
+
)
|
|
40
|
+
for field_name in self.model_fields
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@model_validator(mode="after")
|
|
45
|
+
def validate_arrays(self) -> "Table":
|
|
46
|
+
first_length = len(next(iter(self))[1])
|
|
47
|
+
assert all(
|
|
48
|
+
len(field_value) == first_length for _, field_value in self
|
|
49
|
+
), "Rows should all be of equal size."
|
|
50
|
+
|
|
51
|
+
if not all(
|
|
52
|
+
np.issubdtype(
|
|
53
|
+
self.model_fields[field_name].default_factory().dtype, # type: ignore
|
|
54
|
+
field_value.dtype,
|
|
55
|
+
)
|
|
56
|
+
for field_name, field_value in self
|
|
57
|
+
):
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Cannot construct a `{type(self).__name__}`, "
|
|
60
|
+
"some rows have incorrect types."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return self
|