ophyd-async 0.9.0a1__py3-none-any.whl → 0.9.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/_version.py +1 -1
- ophyd_async/core/__init__.py +13 -20
- ophyd_async/core/_detector.py +61 -37
- ophyd_async/core/_device.py +102 -80
- ophyd_async/core/_device_filler.py +17 -8
- ophyd_async/core/_flyer.py +2 -2
- ophyd_async/core/_readable.py +30 -23
- ophyd_async/core/_settings.py +104 -0
- ophyd_async/core/_signal.py +55 -17
- ophyd_async/core/_signal_backend.py +4 -1
- ophyd_async/core/_soft_signal_backend.py +2 -1
- ophyd_async/core/_table.py +18 -10
- ophyd_async/core/_utils.py +5 -3
- ophyd_async/core/_yaml_settings.py +64 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +49 -0
- ophyd_async/epics/adandor/_andor_io.py +36 -0
- ophyd_async/epics/adaravis/__init__.py +3 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -37
- ophyd_async/epics/adaravis/_aravis_controller.py +13 -22
- ophyd_async/epics/adcore/__init__.py +15 -8
- ophyd_async/epics/adcore/_core_detector.py +41 -0
- ophyd_async/epics/adcore/_core_io.py +35 -10
- ophyd_async/epics/adcore/_core_logic.py +98 -86
- ophyd_async/epics/adcore/_core_writer.py +219 -0
- ophyd_async/epics/adcore/_hdf_writer.py +38 -62
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +4 -3
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +29 -24
- ophyd_async/epics/adkinetix/_kinetix_controller.py +9 -21
- ophyd_async/epics/adpilatus/__init__.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus.py +27 -39
- ophyd_async/epics/adpilatus/_pilatus_controller.py +44 -22
- ophyd_async/epics/adsimdetector/__init__.py +3 -3
- ophyd_async/epics/adsimdetector/_sim.py +33 -17
- ophyd_async/epics/advimba/_vimba.py +23 -23
- ophyd_async/epics/advimba/_vimba_controller.py +10 -24
- ophyd_async/epics/core/_aioca.py +31 -14
- ophyd_async/epics/core/_p4p.py +40 -16
- ophyd_async/epics/core/_util.py +1 -1
- ophyd_async/epics/motor.py +18 -10
- ophyd_async/epics/sim/_ioc.py +29 -0
- ophyd_async/epics/{demo → sim}/_mover.py +10 -4
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +48 -65
- ophyd_async/epics/testing/_utils.py +17 -45
- ophyd_async/epics/testing/test_records.db +8 -0
- ophyd_async/fastcs/panda/__init__.py +0 -2
- ophyd_async/fastcs/panda/_control.py +7 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +3 -1
- ophyd_async/fastcs/panda/_table.py +4 -1
- ophyd_async/plan_stubs/__init__.py +14 -0
- ophyd_async/plan_stubs/_ensure_connected.py +11 -17
- ophyd_async/plan_stubs/_fly.py +1 -1
- ophyd_async/plan_stubs/_nd_attributes.py +7 -5
- ophyd_async/plan_stubs/_panda.py +13 -0
- ophyd_async/plan_stubs/_settings.py +125 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +19 -0
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
- ophyd_async/tango/core/_signal.py +3 -1
- ophyd_async/tango/core/_tango_transport.py +12 -14
- ophyd_async/tango/{demo → sim}/_mover.py +5 -2
- ophyd_async/testing/__init__.py +19 -0
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +88 -40
- ophyd_async/testing/_mock_signal_utils.py +3 -3
- ophyd_async/testing/_one_of_everything.py +126 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/METADATA +2 -2
- ophyd_async-0.9.0a2.dist-info/RECORD +129 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
- ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
- /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
- /ophyd_async/epics/{demo → sim}/_sensor.py +0 -0
- /ophyd_async/epics/{demo → sim}/mover.db +0 -0
- /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
- /ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +0 -0
- /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_counter.py +0 -0
- /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import bluesky.plan_stubs as bps
|
|
8
|
+
import numpy as np
|
|
9
|
+
from bluesky.utils import MsgGenerator, plan
|
|
10
|
+
|
|
11
|
+
from ophyd_async.core import (
|
|
12
|
+
Device,
|
|
13
|
+
Settings,
|
|
14
|
+
SettingsProvider,
|
|
15
|
+
SignalRW,
|
|
16
|
+
T,
|
|
17
|
+
walk_rw_signals,
|
|
18
|
+
)
|
|
19
|
+
from ophyd_async.core._table import Table
|
|
20
|
+
|
|
21
|
+
from ._wait_for_awaitable import wait_for_awaitable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@plan
|
|
25
|
+
def _get_values_of_signals(
|
|
26
|
+
signals: Mapping[T, SignalRW],
|
|
27
|
+
) -> MsgGenerator[dict[T, Any]]:
|
|
28
|
+
coros = [sig.get_value() for sig in signals.values()]
|
|
29
|
+
values = yield from wait_for_awaitable(asyncio.gather(*coros))
|
|
30
|
+
named_values = dict(zip(signals, values, strict=True))
|
|
31
|
+
return named_values
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@plan
|
|
35
|
+
def get_current_settings(device: Device) -> MsgGenerator[Settings]:
|
|
36
|
+
signals = walk_rw_signals(device)
|
|
37
|
+
named_values = yield from _get_values_of_signals(signals)
|
|
38
|
+
signal_values = {signals[name]: value for name, value in named_values.items()}
|
|
39
|
+
return Settings(device, signal_values)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@plan
|
|
43
|
+
def store_settings(
|
|
44
|
+
provider: SettingsProvider, name: str, device: Device
|
|
45
|
+
) -> MsgGenerator[None]:
|
|
46
|
+
"""Walk a Device for SignalRWs and store their values with a provider associated
|
|
47
|
+
with the given name.
|
|
48
|
+
"""
|
|
49
|
+
signals = walk_rw_signals(device)
|
|
50
|
+
named_values = yield from _get_values_of_signals(signals)
|
|
51
|
+
yield from wait_for_awaitable(provider.store(name, named_values))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@plan
|
|
55
|
+
def retrieve_settings(
|
|
56
|
+
provider: SettingsProvider, name: str, device: Device
|
|
57
|
+
) -> MsgGenerator[Settings]:
|
|
58
|
+
"""Retrieve named Settings for a Device from a provider."""
|
|
59
|
+
named_values = yield from wait_for_awaitable(provider.retrieve(name))
|
|
60
|
+
signals = walk_rw_signals(device)
|
|
61
|
+
unknown_names = set(named_values) - set(signals)
|
|
62
|
+
if unknown_names:
|
|
63
|
+
raise NameError(f"Unknown signal names {sorted(unknown_names)}")
|
|
64
|
+
signal_values = {signals[name]: value for name, value in named_values.items()}
|
|
65
|
+
return Settings(device, signal_values)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@plan
|
|
69
|
+
def apply_settings(settings: Settings) -> MsgGenerator[None]:
|
|
70
|
+
"""Set every SignalRW to the given value in Settings. If value is None ignore it."""
|
|
71
|
+
signal_values = {
|
|
72
|
+
signal: value for signal, value in settings.items() if value is not None
|
|
73
|
+
}
|
|
74
|
+
if signal_values:
|
|
75
|
+
for signal, value in signal_values.items():
|
|
76
|
+
yield from bps.abs_set(signal, value, group="apply_settings")
|
|
77
|
+
yield from bps.wait("apply_settings")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@plan
|
|
81
|
+
def apply_settings_if_different(
|
|
82
|
+
settings: Settings,
|
|
83
|
+
apply_plan: Callable[[Settings], MsgGenerator[None]],
|
|
84
|
+
current_settings: Settings | None = None,
|
|
85
|
+
) -> MsgGenerator[None]:
|
|
86
|
+
"""Set every SignalRW in settings to its given value if it is different to the
|
|
87
|
+
current value.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
apply_plan:
|
|
92
|
+
A device specific plan which takes the Settings to apply and applies them to
|
|
93
|
+
the Device. Used to add device specific ordering to setting the signals.
|
|
94
|
+
current_settings:
|
|
95
|
+
If given, should be a superset of settings containing the current value of
|
|
96
|
+
the Settings in the Device. If not given it will be created by reading just
|
|
97
|
+
the signals given in settings.
|
|
98
|
+
"""
|
|
99
|
+
if current_settings is None:
|
|
100
|
+
# If we aren't give the current settings, then get the
|
|
101
|
+
# values of just the signals we were asked to change.
|
|
102
|
+
# This allows us to use this plan with Settings for a subset
|
|
103
|
+
# of signals in the Device without retrieving them all
|
|
104
|
+
signal_values = yield from _get_values_of_signals(
|
|
105
|
+
{sig: sig for sig in settings}
|
|
106
|
+
)
|
|
107
|
+
current_settings = Settings(settings.device, signal_values)
|
|
108
|
+
|
|
109
|
+
def _is_different(current, required) -> bool:
|
|
110
|
+
if isinstance(current, Table):
|
|
111
|
+
current = current.model_dump()
|
|
112
|
+
if isinstance(required, Table):
|
|
113
|
+
required = required.model_dump()
|
|
114
|
+
return current.keys() != required.keys() or any(
|
|
115
|
+
_is_different(current[k], required[k]) for k in current
|
|
116
|
+
)
|
|
117
|
+
elif isinstance(current, np.ndarray):
|
|
118
|
+
return not np.array_equal(current, required)
|
|
119
|
+
else:
|
|
120
|
+
return current != required
|
|
121
|
+
|
|
122
|
+
settings_to_change, _ = settings.partition(
|
|
123
|
+
lambda sig: _is_different(current_settings[sig], settings[sig])
|
|
124
|
+
)
|
|
125
|
+
yield from apply_plan(settings_to_change)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from collections.abc import Awaitable
|
|
2
|
+
|
|
3
|
+
import bluesky.plan_stubs as bps
|
|
4
|
+
from bluesky.utils import MsgGenerator, plan
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core import T
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@plan
|
|
10
|
+
def wait_for_awaitable(coro: Awaitable[T]) -> MsgGenerator[T]:
|
|
11
|
+
"""Wait for a single awaitable to complete, and return the result."""
|
|
12
|
+
(task,) = yield from bps.wait_for([lambda: coro])
|
|
13
|
+
return task.result()
|
ophyd_async/sim/__init__.py
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ._pattern_detector import (
|
|
2
|
+
DATA_PATH,
|
|
3
|
+
SUM_PATH,
|
|
4
|
+
PatternDetector,
|
|
5
|
+
PatternDetectorController,
|
|
6
|
+
PatternDetectorWriter,
|
|
7
|
+
PatternGenerator,
|
|
8
|
+
)
|
|
9
|
+
from ._sim_motor import SimMotor
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DATA_PATH",
|
|
13
|
+
"SUM_PATH",
|
|
14
|
+
"PatternGenerator",
|
|
15
|
+
"PatternDetector",
|
|
16
|
+
"PatternDetectorController",
|
|
17
|
+
"PatternDetectorWriter",
|
|
18
|
+
"SimMotor",
|
|
19
|
+
]
|
ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py
RENAMED
|
@@ -27,8 +27,15 @@ class PatternDetectorController(DetectorController):
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
async def arm(self):
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
if not hasattr(self, "_trigger_info"):
|
|
31
|
+
msg = "TriggerInfo information is missing, has 'prepare' been called?"
|
|
32
|
+
raise RuntimeError(msg)
|
|
33
|
+
if not self._trigger_info.livetime:
|
|
34
|
+
msg = "Livetime information is missing in trigger info"
|
|
35
|
+
raise ValueError(msg)
|
|
36
|
+
if not self.period:
|
|
37
|
+
msg = "Period is not set"
|
|
38
|
+
raise ValueError(msg)
|
|
32
39
|
self.task = asyncio.create_task(
|
|
33
40
|
self._coroutine_for_image_writing(
|
|
34
41
|
self._trigger_info.livetime,
|
|
@@ -67,11 +67,14 @@ class PatternGenerator:
|
|
|
67
67
|
|
|
68
68
|
def write_data_to_dataset(self, path: str, data_shape: tuple[int, ...], data):
|
|
69
69
|
"""Write data to named dataset, resizing to fit and flushing after."""
|
|
70
|
-
|
|
70
|
+
if not self._handle_for_h5_file:
|
|
71
|
+
msg = "No file has been opened!"
|
|
72
|
+
raise OSError(msg)
|
|
73
|
+
|
|
71
74
|
dset = self._handle_for_h5_file[path]
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
if not isinstance(dset, h5py.Dataset):
|
|
76
|
+
msg = f"Expected {path} to be a dataset, got {type(dset).__name__}"
|
|
77
|
+
raise TypeError(msg)
|
|
75
78
|
dset.resize((self.image_counter + 1,) + data_shape)
|
|
76
79
|
dset[self.image_counter] = data
|
|
77
80
|
dset.flush()
|
|
@@ -114,7 +117,9 @@ class PatternGenerator:
|
|
|
114
117
|
|
|
115
118
|
self._handle_for_h5_file = h5py.File(self.target_path, "w", libver="latest")
|
|
116
119
|
|
|
117
|
-
|
|
120
|
+
if not self._handle_for_h5_file:
|
|
121
|
+
msg = f"Problem opening file {self.target_path}"
|
|
122
|
+
raise OSError(msg)
|
|
118
123
|
|
|
119
124
|
self._handle_for_h5_file.create_dataset(
|
|
120
125
|
name=DATA_PATH,
|
|
@@ -184,7 +189,9 @@ class PatternGenerator:
|
|
|
184
189
|
# cannot get the full filename the HDF writer will write
|
|
185
190
|
# until the first frame comes in
|
|
186
191
|
if not self._hdf_stream_provider:
|
|
187
|
-
|
|
192
|
+
if self.target_path is None:
|
|
193
|
+
msg = "open file has not been called"
|
|
194
|
+
raise RuntimeError(msg)
|
|
188
195
|
self._hdf_stream_provider = HDFFile(
|
|
189
196
|
self.target_path,
|
|
190
197
|
self._datasets,
|
|
@@ -28,6 +28,8 @@ from tango.asyncio import DeviceProxy as AsyncDeviceProxy
|
|
|
28
28
|
|
|
29
29
|
from ._tango_transport import TangoSignalBackend, get_python_type
|
|
30
30
|
|
|
31
|
+
logger = logging.getLogger("ophyd_async")
|
|
32
|
+
|
|
31
33
|
|
|
32
34
|
def make_backend(
|
|
33
35
|
datatype: type[SignalDatatypeT] | None,
|
|
@@ -205,7 +207,7 @@ async def infer_signal_type(
|
|
|
205
207
|
if config.in_type == CmdArgType.DevVoid:
|
|
206
208
|
return SignalX
|
|
207
209
|
elif config.in_type != config.out_type:
|
|
208
|
-
|
|
210
|
+
logger.debug("Commands with different in and out dtypes are not supported")
|
|
209
211
|
return None
|
|
210
212
|
else:
|
|
211
213
|
return SignalRW
|
|
@@ -210,11 +210,11 @@ class AttributeProxy(TangoProxy):
|
|
|
210
210
|
await asyncio.sleep(A_BIT)
|
|
211
211
|
if to and (time.time() - start_time > to):
|
|
212
212
|
raise TimeoutError(
|
|
213
|
-
f"{self._name} attr put failed:
|
|
213
|
+
f"{self._name} attr put failed: Timeout"
|
|
214
214
|
) from exc
|
|
215
215
|
else:
|
|
216
216
|
raise RuntimeError(
|
|
217
|
-
f"{self._name} device failure:
|
|
217
|
+
f"{self._name} device failure: {exc.args[0].desc}"
|
|
218
218
|
) from exc
|
|
219
219
|
|
|
220
220
|
return AsyncStatus(wait_for_reply(rid, timeout))
|
|
@@ -422,7 +422,7 @@ class CommandProxy(TangoProxy):
|
|
|
422
422
|
raise TimeoutError(f"{self._name} command failed: Timeout") from te
|
|
423
423
|
except DevFailed as de:
|
|
424
424
|
raise RuntimeError(
|
|
425
|
-
f"{self._name} device
|
|
425
|
+
f"{self._name} device failure: {de.args[0].desc}"
|
|
426
426
|
) from de
|
|
427
427
|
|
|
428
428
|
else:
|
|
@@ -446,8 +446,7 @@ class CommandProxy(TangoProxy):
|
|
|
446
446
|
) from de_exc
|
|
447
447
|
else:
|
|
448
448
|
raise RuntimeError(
|
|
449
|
-
f"{self._name} device failure:"
|
|
450
|
-
f" {de_exc.args[0].desc}"
|
|
449
|
+
f"{self._name} device failure: {de_exc.args[0].desc}"
|
|
451
450
|
) from de_exc
|
|
452
451
|
|
|
453
452
|
return AsyncStatus(wait_for_reply(rid, timeout))
|
|
@@ -733,22 +732,21 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
733
732
|
" for which polling is disabled."
|
|
734
733
|
)
|
|
735
734
|
|
|
735
|
+
if callback and self.proxies[self.read_trl].has_subscription(): # type: ignore
|
|
736
|
+
msg = "Cannot set a callback when one is already set"
|
|
737
|
+
raise RuntimeError(msg)
|
|
738
|
+
|
|
739
|
+
if self.proxies[self.read_trl].has_subscription(): # type: ignore
|
|
740
|
+
self.proxies[self.read_trl].unsubscribe_callback() # type: ignore
|
|
741
|
+
|
|
736
742
|
if callback:
|
|
737
743
|
try:
|
|
738
|
-
assert not self.proxies[self.read_trl].has_subscription() # type: ignore
|
|
739
744
|
self.proxies[self.read_trl].subscribe_callback(callback) # type: ignore
|
|
740
|
-
except AssertionError as ae:
|
|
741
|
-
raise RuntimeError(
|
|
742
|
-
"Cannot set a callback when one" " is already set"
|
|
743
|
-
) from ae
|
|
744
745
|
except RuntimeError as exc:
|
|
745
746
|
raise RuntimeError(
|
|
746
|
-
f"Cannot set callback
|
|
747
|
+
f"Cannot set callback for {self.read_trl}. {exc}"
|
|
747
748
|
) from exc
|
|
748
749
|
|
|
749
|
-
else:
|
|
750
|
-
self.proxies[self.read_trl].unsubscribe_callback() # type: ignore
|
|
751
|
-
|
|
752
750
|
def set_polling(
|
|
753
751
|
self,
|
|
754
752
|
allow_polling: bool = True,
|
|
@@ -43,8 +43,11 @@ class TangoMover(TangoReadable, Movable, Stoppable):
|
|
|
43
43
|
(old_position, velocity) = await asyncio.gather(
|
|
44
44
|
self.position.get_value(), self.velocity.get_value()
|
|
45
45
|
)
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
# TODO: check whether Tango does work with negative velocity
|
|
47
|
+
if timeout is CALCULATE_TIMEOUT and velocity == 0:
|
|
48
|
+
msg = "Motor has zero velocity"
|
|
49
|
+
raise ValueError(msg)
|
|
50
|
+
else:
|
|
48
51
|
timeout = abs(value - old_position) / velocity + DEFAULT_TIMEOUT
|
|
49
52
|
|
|
50
53
|
if not (isinstance(timeout, float) or timeout is None):
|
ophyd_async/testing/__init__.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
from . import __pytest_assert_rewrite # noqa: F401
|
|
1
2
|
from ._assert import (
|
|
3
|
+
ApproxTable,
|
|
4
|
+
MonitorQueue,
|
|
5
|
+
approx_value,
|
|
2
6
|
assert_configuration,
|
|
7
|
+
assert_describe_signal,
|
|
3
8
|
assert_emitted,
|
|
4
9
|
assert_reading,
|
|
5
10
|
assert_value,
|
|
@@ -14,10 +19,18 @@ from ._mock_signal_utils import (
|
|
|
14
19
|
set_mock_value,
|
|
15
20
|
set_mock_values,
|
|
16
21
|
)
|
|
22
|
+
from ._one_of_everything import (
|
|
23
|
+
ExampleEnum,
|
|
24
|
+
ExampleTable,
|
|
25
|
+
OneOfEverythingDevice,
|
|
26
|
+
ParentOfEverythingDevice,
|
|
27
|
+
)
|
|
17
28
|
from ._wait_for_pending import wait_for_pending_wakeups
|
|
18
29
|
|
|
19
30
|
__all__ = [
|
|
31
|
+
"approx_value",
|
|
20
32
|
"assert_configuration",
|
|
33
|
+
"assert_describe_signal",
|
|
21
34
|
"assert_emitted",
|
|
22
35
|
"assert_reading",
|
|
23
36
|
"assert_value",
|
|
@@ -30,4 +43,10 @@ __all__ = [
|
|
|
30
43
|
"set_mock_value",
|
|
31
44
|
"set_mock_values",
|
|
32
45
|
"wait_for_pending_wakeups",
|
|
46
|
+
"ExampleEnum",
|
|
47
|
+
"ExampleTable",
|
|
48
|
+
"OneOfEverythingDevice",
|
|
49
|
+
"ParentOfEverythingDevice",
|
|
50
|
+
"MonitorQueue",
|
|
51
|
+
"ApproxTable",
|
|
33
52
|
]
|
ophyd_async/testing/_assert.py
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from contextlib import AbstractContextManager
|
|
2
4
|
from typing import Any
|
|
3
5
|
|
|
6
|
+
import pytest
|
|
4
7
|
from bluesky.protocols import Reading
|
|
8
|
+
from event_model import DataKey
|
|
5
9
|
|
|
6
|
-
from ophyd_async.core import
|
|
10
|
+
from ophyd_async.core import (
|
|
11
|
+
AsyncConfigurable,
|
|
12
|
+
AsyncReadable,
|
|
13
|
+
SignalDatatypeT,
|
|
14
|
+
SignalR,
|
|
15
|
+
Table,
|
|
16
|
+
)
|
|
7
17
|
|
|
8
18
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
FAIL = "\033[91m"
|
|
12
|
-
ENDC = "\033[0m"
|
|
13
|
-
return (
|
|
14
|
-
f"Expected {WARNING}{name}{ENDC} to produce"
|
|
15
|
-
+ f"\n{FAIL}{expected_result}{ENDC}"
|
|
16
|
-
+ f"\nbut actually got \n{FAIL}{actual_result}{ENDC}"
|
|
17
|
-
)
|
|
19
|
+
def approx_value(value: Any):
|
|
20
|
+
return ApproxTable(value) if isinstance(value, Table) else pytest.approx(value)
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
|
|
@@ -34,15 +37,11 @@ async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
|
|
|
34
37
|
|
|
35
38
|
"""
|
|
36
39
|
actual_value = await signal.get_value()
|
|
37
|
-
assert
|
|
38
|
-
name=signal.name,
|
|
39
|
-
expected_result=value,
|
|
40
|
-
actual_result=actual_value,
|
|
41
|
-
)
|
|
40
|
+
assert approx_value(value) == actual_value
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
async def assert_reading(
|
|
45
|
-
readable: AsyncReadable, expected_reading:
|
|
44
|
+
readable: AsyncReadable, expected_reading: dict[str, Reading]
|
|
46
45
|
) -> None:
|
|
47
46
|
"""Assert readings from readable.
|
|
48
47
|
|
|
@@ -61,16 +60,16 @@ async def assert_reading(
|
|
|
61
60
|
|
|
62
61
|
"""
|
|
63
62
|
actual_reading = await readable.read()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
approx_expected_reading = {
|
|
64
|
+
k: dict(v, value=approx_value(expected_reading[k]["value"]))
|
|
65
|
+
for k, v in expected_reading.items()
|
|
66
|
+
}
|
|
67
|
+
assert actual_reading == approx_expected_reading
|
|
69
68
|
|
|
70
69
|
|
|
71
70
|
async def assert_configuration(
|
|
72
71
|
configurable: AsyncConfigurable,
|
|
73
|
-
configuration:
|
|
72
|
+
configuration: dict[str, Reading],
|
|
74
73
|
) -> None:
|
|
75
74
|
"""Assert readings from Configurable.
|
|
76
75
|
|
|
@@ -88,15 +87,23 @@ async def assert_configuration(
|
|
|
88
87
|
await assert_configuration(configurable configuration)
|
|
89
88
|
|
|
90
89
|
"""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
actual_configuration = await configurable.read_configuration()
|
|
91
|
+
approx_expected_configuration = {
|
|
92
|
+
k: dict(v, value=approx_value(configuration[k]["value"]))
|
|
93
|
+
for k, v in configuration.items()
|
|
94
|
+
}
|
|
95
|
+
assert actual_configuration == approx_expected_configuration
|
|
97
96
|
|
|
98
97
|
|
|
99
|
-
def
|
|
98
|
+
async def assert_describe_signal(signal: SignalR, /, **metadata):
|
|
99
|
+
actual_describe = await signal.describe()
|
|
100
|
+
assert list(actual_describe) == [signal.name]
|
|
101
|
+
(actual_datakey,) = actual_describe.values()
|
|
102
|
+
expected_datakey = DataKey(source=signal.source, **metadata)
|
|
103
|
+
assert actual_datakey == expected_datakey
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def assert_emitted(docs: dict[str, list[dict]], **numbers: int):
|
|
100
107
|
"""Assert emitted document generated by running a Bluesky plan
|
|
101
108
|
|
|
102
109
|
Parameters
|
|
@@ -115,14 +122,55 @@ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
|
|
|
115
122
|
RE(my_plan())
|
|
116
123
|
assert_emitted(docs, start=1, descriptor=1, event=1, stop=1)
|
|
117
124
|
"""
|
|
118
|
-
assert list(docs) == list(numbers)
|
|
119
|
-
name="documents",
|
|
120
|
-
expected_result=list(numbers),
|
|
121
|
-
actual_result=list(docs),
|
|
122
|
-
)
|
|
125
|
+
assert list(docs) == list(numbers)
|
|
123
126
|
actual_numbers = {name: len(d) for name, d in docs.items()}
|
|
124
|
-
assert actual_numbers == numbers
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
127
|
+
assert actual_numbers == numbers
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ApproxTable:
|
|
131
|
+
def __init__(self, expected: Table, rel=None, abs=None, nan_ok: bool = False):
|
|
132
|
+
self.expected = expected
|
|
133
|
+
self.rel = rel
|
|
134
|
+
self.abs = abs
|
|
135
|
+
self.nan_ok = nan_ok
|
|
136
|
+
|
|
137
|
+
def __eq__(self, value):
|
|
138
|
+
approx_fields = {
|
|
139
|
+
k: pytest.approx(v, self.rel, self.abs, self.nan_ok)
|
|
140
|
+
for k, v in self.expected
|
|
141
|
+
}
|
|
142
|
+
expected = type(self.expected).model_construct(**approx_fields) # type: ignore
|
|
143
|
+
return expected == value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class MonitorQueue(AbstractContextManager):
|
|
147
|
+
def __init__(self, signal: SignalR):
|
|
148
|
+
self.signal = signal
|
|
149
|
+
self.updates: asyncio.Queue[dict[str, Reading]] = asyncio.Queue()
|
|
150
|
+
self.signal.subscribe(self.updates.put_nowait)
|
|
151
|
+
|
|
152
|
+
async def assert_updates(self, expected_value):
|
|
153
|
+
# Get an update, value and reading
|
|
154
|
+
expected_type = type(expected_value)
|
|
155
|
+
expected_value = approx_value(expected_value)
|
|
156
|
+
update = await self.updates.get()
|
|
157
|
+
value = await self.signal.get_value()
|
|
158
|
+
reading = await self.signal.read()
|
|
159
|
+
# Check they match what we expected
|
|
160
|
+
assert value == expected_value
|
|
161
|
+
assert type(value) is expected_type
|
|
162
|
+
expected_reading = {
|
|
163
|
+
self.signal.name: {
|
|
164
|
+
"value": expected_value,
|
|
165
|
+
"timestamp": pytest.approx(time.time(), rel=0.1),
|
|
166
|
+
"alarm_severity": 0,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
assert reading == update == expected_reading
|
|
170
|
+
|
|
171
|
+
def __enter__(self):
|
|
172
|
+
self.signal.subscribe(self.updates.put_nowait)
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
176
|
+
self.signal.clear_sub(self.updates.put_nowait)
|
|
@@ -22,9 +22,9 @@ def get_mock(device: Device | Signal) -> Mock:
|
|
|
22
22
|
def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend:
|
|
23
23
|
connector = signal._connector # noqa: SLF001
|
|
24
24
|
assert isinstance(connector, SignalConnector), f"Expected Signal, got {signal}"
|
|
25
|
-
assert isinstance(
|
|
26
|
-
|
|
27
|
-
)
|
|
25
|
+
assert isinstance(connector.backend, MockSignalBackend), (
|
|
26
|
+
f"Signal {signal} not connected in mock mode"
|
|
27
|
+
)
|
|
28
28
|
return connector.backend
|
|
29
29
|
|
|
30
30
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core import (
|
|
7
|
+
Array1D,
|
|
8
|
+
Device,
|
|
9
|
+
DTypeScalar_co,
|
|
10
|
+
SignalRW,
|
|
11
|
+
StandardReadable,
|
|
12
|
+
StrictEnum,
|
|
13
|
+
Table,
|
|
14
|
+
soft_signal_r_and_setter,
|
|
15
|
+
soft_signal_rw,
|
|
16
|
+
)
|
|
17
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
18
|
+
from ophyd_async.core._device import DeviceVector
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExampleEnum(StrictEnum):
|
|
22
|
+
A = "Aaa"
|
|
23
|
+
B = "Bbb"
|
|
24
|
+
C = "Ccc"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ExampleTable(Table):
|
|
28
|
+
bool: Array1D[np.bool_]
|
|
29
|
+
int: Array1D[np.int32]
|
|
30
|
+
float: Array1D[np.float64]
|
|
31
|
+
str: Sequence[str]
|
|
32
|
+
enum: Sequence[ExampleEnum]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def int_array_signal(
|
|
36
|
+
dtype: type[DTypeScalar_co], name: str = ""
|
|
37
|
+
) -> SignalRW[Array1D[DTypeScalar_co]]:
|
|
38
|
+
iinfo = np.iinfo(dtype) # type: ignore
|
|
39
|
+
value = np.array([iinfo.min, iinfo.max, 0, 1, 2, 3, 4], dtype=dtype)
|
|
40
|
+
return soft_signal_rw(Array1D[dtype], value, name)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def float_array_signal(
|
|
44
|
+
dtype: type[DTypeScalar_co], name: str = ""
|
|
45
|
+
) -> SignalRW[Array1D[DTypeScalar_co]]:
|
|
46
|
+
finfo = np.finfo(dtype) # type: ignore
|
|
47
|
+
value = np.array(
|
|
48
|
+
[
|
|
49
|
+
finfo.min,
|
|
50
|
+
finfo.max,
|
|
51
|
+
finfo.smallest_normal,
|
|
52
|
+
finfo.smallest_subnormal,
|
|
53
|
+
0,
|
|
54
|
+
1.234,
|
|
55
|
+
2.34e5,
|
|
56
|
+
3.45e-6,
|
|
57
|
+
],
|
|
58
|
+
dtype=dtype,
|
|
59
|
+
)
|
|
60
|
+
return soft_signal_rw(Array1D[dtype], value, name)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OneOfEverythingDevice(StandardReadable):
|
|
64
|
+
# make a detector to test assert_configuration
|
|
65
|
+
def __init__(self, name=""):
|
|
66
|
+
# add all signals to configuration
|
|
67
|
+
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
68
|
+
self.int = soft_signal_rw(int, 1)
|
|
69
|
+
self.float = soft_signal_rw(float, 1.234)
|
|
70
|
+
self.str = soft_signal_rw(str, "test_string")
|
|
71
|
+
self.bool = soft_signal_rw(bool, True)
|
|
72
|
+
self.enum = soft_signal_rw(ExampleEnum, ExampleEnum.B)
|
|
73
|
+
self.int8a = int_array_signal(np.int8)
|
|
74
|
+
self.uint8a = int_array_signal(np.uint8)
|
|
75
|
+
self.int16a = int_array_signal(np.int16)
|
|
76
|
+
self.uint16a = int_array_signal(np.uint16)
|
|
77
|
+
self.int32a = int_array_signal(np.int32)
|
|
78
|
+
self.uint32a = int_array_signal(np.uint32)
|
|
79
|
+
self.int64a = int_array_signal(np.int64)
|
|
80
|
+
self.uint64a = int_array_signal(np.uint64)
|
|
81
|
+
self.float32a = float_array_signal(np.float32)
|
|
82
|
+
self.float64a = float_array_signal(np.float64)
|
|
83
|
+
self.stra = soft_signal_rw(
|
|
84
|
+
Sequence[str],
|
|
85
|
+
["one", "two", "three"],
|
|
86
|
+
)
|
|
87
|
+
self.enuma = soft_signal_rw(
|
|
88
|
+
Sequence[ExampleEnum],
|
|
89
|
+
[ExampleEnum.A, ExampleEnum.C],
|
|
90
|
+
)
|
|
91
|
+
self.table = soft_signal_rw(
|
|
92
|
+
ExampleTable,
|
|
93
|
+
ExampleTable(
|
|
94
|
+
bool=np.array([False, False, True, True], np.bool_),
|
|
95
|
+
int=np.array([1, 8, -9, 32], np.int32),
|
|
96
|
+
float=np.array([1.8, 8.2, -6, 32.9887], np.float64),
|
|
97
|
+
str=["Hello", "World", "Foo", "Bar"],
|
|
98
|
+
enum=[ExampleEnum.A, ExampleEnum.B, ExampleEnum.A, ExampleEnum.C],
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
self.ndarray = soft_signal_rw(np.ndarray, np.array(([1, 2, 3], [4, 5, 6])))
|
|
102
|
+
super().__init__(name)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def _get_signal_values(child: Device) -> dict[SignalRW, Any]:
|
|
106
|
+
if isinstance(child, SignalRW):
|
|
107
|
+
return {child: await child.get_value()}
|
|
108
|
+
ret = {}
|
|
109
|
+
for _, c in child.children():
|
|
110
|
+
ret.update(await _get_signal_values(c))
|
|
111
|
+
return ret
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ParentOfEverythingDevice(Device):
|
|
115
|
+
def __init__(self, name=""):
|
|
116
|
+
self.child = OneOfEverythingDevice()
|
|
117
|
+
self.vector = DeviceVector(
|
|
118
|
+
{1: OneOfEverythingDevice(), 3: OneOfEverythingDevice()}
|
|
119
|
+
)
|
|
120
|
+
self.sig_rw = soft_signal_rw(str, "Top level SignalRW")
|
|
121
|
+
self.sig_r, _ = soft_signal_r_and_setter(str, "Top level SignalR")
|
|
122
|
+
self._sig_rw = soft_signal_rw(str, "Top level private SignalRW")
|
|
123
|
+
super().__init__(name=name)
|
|
124
|
+
|
|
125
|
+
async def get_signal_values(self):
|
|
126
|
+
return await _get_signal_values(self)
|