ophyd-async 0.10.1__py3-none-any.whl → 0.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +12 -1
- ophyd_async/core/_derived_signal.py +68 -22
- ophyd_async/core/_derived_signal_backend.py +46 -24
- ophyd_async/core/_detector.py +3 -3
- ophyd_async/core/_device.py +24 -16
- ophyd_async/core/_flyer.py +39 -5
- ophyd_async/core/_hdf_dataset.py +11 -10
- ophyd_async/core/_signal.py +58 -30
- ophyd_async/core/_table.py +3 -3
- ophyd_async/core/_utils.py +25 -0
- ophyd_async/core/_yaml_settings.py +3 -3
- ophyd_async/epics/adandor/__init__.py +7 -1
- ophyd_async/epics/adandor/_andor_controller.py +5 -8
- ophyd_async/epics/adandor/_andor_io.py +12 -19
- ophyd_async/epics/adcore/_hdf_writer.py +12 -19
- ophyd_async/epics/core/_signal.py +8 -3
- ophyd_async/epics/eiger/_odin_io.py +4 -2
- ophyd_async/epics/motor.py +47 -97
- ophyd_async/epics/pmac/__init__.py +3 -0
- ophyd_async/epics/pmac/_pmac_io.py +100 -0
- ophyd_async/fastcs/eiger/__init__.py +1 -2
- ophyd_async/fastcs/eiger/_eiger.py +3 -9
- ophyd_async/fastcs/panda/_trigger.py +8 -8
- ophyd_async/fastcs/panda/_writer.py +15 -13
- ophyd_async/plan_stubs/__init__.py +0 -8
- ophyd_async/plan_stubs/_fly.py +0 -204
- ophyd_async/sim/__init__.py +1 -2
- ophyd_async/sim/_blob_detector_writer.py +6 -12
- ophyd_async/sim/_mirror_horizontal.py +3 -2
- ophyd_async/sim/_mirror_vertical.py +1 -0
- ophyd_async/sim/_motor.py +13 -43
- ophyd_async/testing/__init__.py +2 -0
- ophyd_async/testing/_assert.py +34 -6
- {ophyd_async-0.10.1.dist-info → ophyd_async-0.12.dist-info}/METADATA +4 -3
- {ophyd_async-0.10.1.dist-info → ophyd_async-0.12.dist-info}/RECORD +39 -37
- {ophyd_async-0.10.1.dist-info → ophyd_async-0.12.dist-info}/WHEEL +0 -0
- {ophyd_async-0.10.1.dist-info → ophyd_async-0.12.dist-info}/licenses/LICENSE +0 -0
- {ophyd_async-0.10.1.dist-info → ophyd_async-0.12.dist-info}/top_level.txt +0 -0
ophyd_async/plan_stubs/_fly.py
CHANGED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import bluesky.plan_stubs as bps
|
|
2
|
-
from bluesky.utils import short_uid
|
|
3
|
-
|
|
4
|
-
from ophyd_async.core import (
|
|
5
|
-
DetectorTrigger,
|
|
6
|
-
StandardDetector,
|
|
7
|
-
StandardFlyer,
|
|
8
|
-
TriggerInfo,
|
|
9
|
-
in_micros,
|
|
10
|
-
)
|
|
11
|
-
from ophyd_async.fastcs.panda import (
|
|
12
|
-
PandaPcompDirection,
|
|
13
|
-
PcompInfo,
|
|
14
|
-
SeqTable,
|
|
15
|
-
SeqTableInfo,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def prepare_static_pcomp_flyer_and_detectors(
|
|
20
|
-
flyer: StandardFlyer[PcompInfo],
|
|
21
|
-
detectors: list[StandardDetector],
|
|
22
|
-
pcomp_info: PcompInfo,
|
|
23
|
-
trigger_info: TriggerInfo,
|
|
24
|
-
):
|
|
25
|
-
"""Prepare a hardware triggered flyable and one or more detectors.
|
|
26
|
-
|
|
27
|
-
Prepare a hardware triggered flyable and one or more detectors with the
|
|
28
|
-
same trigger.
|
|
29
|
-
|
|
30
|
-
"""
|
|
31
|
-
for det in detectors:
|
|
32
|
-
yield from bps.prepare(det, trigger_info, wait=False, group="prep")
|
|
33
|
-
yield from bps.prepare(flyer, pcomp_info, wait=False, group="prep")
|
|
34
|
-
yield from bps.wait(group="prep")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
38
|
-
flyer: StandardFlyer[SeqTableInfo],
|
|
39
|
-
detectors: list[StandardDetector],
|
|
40
|
-
number_of_frames: int,
|
|
41
|
-
exposure: float,
|
|
42
|
-
shutter_time: float,
|
|
43
|
-
repeats: int = 1,
|
|
44
|
-
period: float = 0.0,
|
|
45
|
-
frame_timeout: float | None = None,
|
|
46
|
-
):
|
|
47
|
-
"""Prepare a hardware triggered flyable and one or more detectors.
|
|
48
|
-
|
|
49
|
-
Prepare a hardware triggered flyable and one or more detectors with the
|
|
50
|
-
same trigger. This method constructs TriggerInfo and a static sequence
|
|
51
|
-
table from required parameters. The table is required to prepare the flyer,
|
|
52
|
-
and the TriggerInfo is required to prepare the detector(s).
|
|
53
|
-
|
|
54
|
-
This prepares all supplied detectors with the same trigger.
|
|
55
|
-
|
|
56
|
-
"""
|
|
57
|
-
if not detectors:
|
|
58
|
-
raise ValueError("No detectors provided. There must be at least one.")
|
|
59
|
-
|
|
60
|
-
deadtime = max(det._controller.get_deadtime(exposure) for det in detectors) # noqa: SLF001
|
|
61
|
-
|
|
62
|
-
trigger_info = TriggerInfo(
|
|
63
|
-
number_of_events=number_of_frames * repeats,
|
|
64
|
-
trigger=DetectorTrigger.CONSTANT_GATE,
|
|
65
|
-
deadtime=deadtime,
|
|
66
|
-
livetime=exposure,
|
|
67
|
-
exposure_timeout=frame_timeout,
|
|
68
|
-
)
|
|
69
|
-
trigger_time = number_of_frames * (exposure + deadtime)
|
|
70
|
-
pre_delay = max(period - 2 * shutter_time - trigger_time, 0)
|
|
71
|
-
|
|
72
|
-
table = (
|
|
73
|
-
# Wait for pre-delay then open shutter
|
|
74
|
-
SeqTable.row(
|
|
75
|
-
time1=in_micros(pre_delay),
|
|
76
|
-
time2=in_micros(shutter_time),
|
|
77
|
-
outa2=True,
|
|
78
|
-
)
|
|
79
|
-
+
|
|
80
|
-
# Keeping shutter open, do N triggers
|
|
81
|
-
SeqTable.row(
|
|
82
|
-
repeats=number_of_frames,
|
|
83
|
-
time1=in_micros(exposure),
|
|
84
|
-
outa1=True,
|
|
85
|
-
outb1=True,
|
|
86
|
-
time2=in_micros(deadtime),
|
|
87
|
-
outa2=True,
|
|
88
|
-
)
|
|
89
|
-
+
|
|
90
|
-
# Add the shutter close
|
|
91
|
-
SeqTable.row(time2=in_micros(shutter_time))
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
table_info = SeqTableInfo(sequence_table=table, repeats=repeats)
|
|
95
|
-
|
|
96
|
-
for det in detectors:
|
|
97
|
-
yield from bps.prepare(det, trigger_info, wait=False, group="prep")
|
|
98
|
-
yield from bps.prepare(flyer, table_info, wait=False, group="prep")
|
|
99
|
-
yield from bps.wait(group="prep")
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def fly_and_collect(
|
|
103
|
-
stream_name: str,
|
|
104
|
-
flyer: StandardFlyer[SeqTableInfo] | StandardFlyer[PcompInfo],
|
|
105
|
-
detectors: list[StandardDetector],
|
|
106
|
-
):
|
|
107
|
-
"""Kickoff, complete and collect with a flyer and multiple detectors.
|
|
108
|
-
|
|
109
|
-
This stub takes a flyer and one or more detectors that have been prepared. It
|
|
110
|
-
declares a stream for the detectors, then kicks off the detectors and the flyer.
|
|
111
|
-
The detectors are collected until the flyer and detectors have completed.
|
|
112
|
-
|
|
113
|
-
"""
|
|
114
|
-
yield from bps.declare_stream(*detectors, name=stream_name, collect=True)
|
|
115
|
-
yield from bps.kickoff(flyer, wait=True)
|
|
116
|
-
for detector in detectors:
|
|
117
|
-
yield from bps.kickoff(detector)
|
|
118
|
-
|
|
119
|
-
# collect_while_completing
|
|
120
|
-
group = short_uid(label="complete")
|
|
121
|
-
|
|
122
|
-
yield from bps.complete(flyer, wait=False, group=group)
|
|
123
|
-
for detector in detectors:
|
|
124
|
-
yield from bps.complete(detector, wait=False, group=group)
|
|
125
|
-
|
|
126
|
-
done = False
|
|
127
|
-
while not done:
|
|
128
|
-
try:
|
|
129
|
-
yield from bps.wait(group=group, timeout=0.5)
|
|
130
|
-
except TimeoutError:
|
|
131
|
-
pass
|
|
132
|
-
else:
|
|
133
|
-
done = True
|
|
134
|
-
yield from bps.collect(
|
|
135
|
-
*detectors,
|
|
136
|
-
return_payload=False,
|
|
137
|
-
name=stream_name,
|
|
138
|
-
)
|
|
139
|
-
yield from bps.wait(group=group)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def fly_and_collect_with_static_pcomp(
|
|
143
|
-
stream_name: str,
|
|
144
|
-
flyer: StandardFlyer[PcompInfo],
|
|
145
|
-
detectors: list[StandardDetector],
|
|
146
|
-
number_of_pulses: int,
|
|
147
|
-
pulse_width: int,
|
|
148
|
-
rising_edge_step: int,
|
|
149
|
-
direction: PandaPcompDirection,
|
|
150
|
-
trigger_info: TriggerInfo,
|
|
151
|
-
):
|
|
152
|
-
# Set up scan and prepare trigger
|
|
153
|
-
pcomp_info = PcompInfo(
|
|
154
|
-
start_postion=0,
|
|
155
|
-
pulse_width=pulse_width,
|
|
156
|
-
rising_edge_step=rising_edge_step,
|
|
157
|
-
number_of_pulses=number_of_pulses,
|
|
158
|
-
direction=direction,
|
|
159
|
-
)
|
|
160
|
-
yield from prepare_static_pcomp_flyer_and_detectors(
|
|
161
|
-
flyer, detectors, pcomp_info, trigger_info
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# Run the fly scan
|
|
165
|
-
yield from fly_and_collect(stream_name, flyer, detectors)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def time_resolved_fly_and_collect_with_static_seq_table(
|
|
169
|
-
stream_name: str,
|
|
170
|
-
flyer: StandardFlyer[SeqTableInfo],
|
|
171
|
-
detectors: list[StandardDetector],
|
|
172
|
-
number_of_frames: int,
|
|
173
|
-
exposure: float,
|
|
174
|
-
shutter_time: float,
|
|
175
|
-
repeats: int = 1,
|
|
176
|
-
period: float = 0.0,
|
|
177
|
-
frame_timeout: float | None = None,
|
|
178
|
-
):
|
|
179
|
-
"""Run a scan wth a flyer and multiple detectors.
|
|
180
|
-
|
|
181
|
-
The stub demonstrates the standard basic flow for a flyscan:
|
|
182
|
-
|
|
183
|
-
- Prepare the flyer and detectors with a trigger
|
|
184
|
-
- Fly and collect:
|
|
185
|
-
- Declare the stream and kickoff the scan
|
|
186
|
-
- Collect while completing
|
|
187
|
-
|
|
188
|
-
This needs to be used in a plan that instantates detectors and a flyer,
|
|
189
|
-
stages/unstages the devices, and opens and closes the run.
|
|
190
|
-
|
|
191
|
-
"""
|
|
192
|
-
# Set up scan and prepare trigger
|
|
193
|
-
yield from prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
194
|
-
flyer,
|
|
195
|
-
detectors,
|
|
196
|
-
number_of_frames=number_of_frames,
|
|
197
|
-
exposure=exposure,
|
|
198
|
-
shutter_time=shutter_time,
|
|
199
|
-
repeats=repeats,
|
|
200
|
-
period=period,
|
|
201
|
-
frame_timeout=frame_timeout,
|
|
202
|
-
)
|
|
203
|
-
# Run the fly scan
|
|
204
|
-
yield from fly_and_collect(stream_name, flyer, detectors)
|
ophyd_async/sim/__init__.py
CHANGED
|
@@ -8,14 +8,13 @@ from ._mirror_vertical import (
|
|
|
8
8
|
TwoJackTransform,
|
|
9
9
|
VerticalMirror,
|
|
10
10
|
)
|
|
11
|
-
from ._motor import
|
|
11
|
+
from ._motor import SimMotor
|
|
12
12
|
from ._pattern_generator import PatternGenerator
|
|
13
13
|
from ._point_detector import SimPointDetector
|
|
14
14
|
from ._stage import SimStage
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
17
17
|
"SimMotor",
|
|
18
|
-
"FlySimMotorInfo",
|
|
19
18
|
"SimStage",
|
|
20
19
|
"PatternGenerator",
|
|
21
20
|
"SimPointDetector",
|
|
@@ -52,7 +52,7 @@ class BlobDetectorWriter(DetectorWriter):
|
|
|
52
52
|
chunk_shape=(1024,),
|
|
53
53
|
),
|
|
54
54
|
]
|
|
55
|
-
self.composer =
|
|
55
|
+
self.composer = HDFDocumentComposer(self.path, self.datasets)
|
|
56
56
|
describe = {
|
|
57
57
|
ds.data_key: DataKey(
|
|
58
58
|
source="sim://pattern-generator-hdf-file",
|
|
@@ -85,17 +85,11 @@ class BlobDetectorWriter(DetectorWriter):
|
|
|
85
85
|
self, name: str, indices_written: int
|
|
86
86
|
) -> AsyncIterator[StreamAsset]:
|
|
87
87
|
# When we have written something to the file
|
|
88
|
-
if
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
raise RuntimeError(f"open() not called on {self}")
|
|
94
|
-
self.composer = HDFDocumentComposer(self.path, self.datasets)
|
|
95
|
-
for doc in self.composer.stream_resources():
|
|
96
|
-
yield "stream_resource", doc
|
|
97
|
-
for doc in self.composer.stream_data(indices_written):
|
|
98
|
-
yield "stream_datum", doc
|
|
88
|
+
if self.composer is None:
|
|
89
|
+
msg = f"open() not called on {self}"
|
|
90
|
+
raise RuntimeError(msg)
|
|
91
|
+
for doc in self.composer.make_stream_docs(indices_written):
|
|
92
|
+
yield doc
|
|
99
93
|
|
|
100
94
|
async def close(self) -> None:
|
|
101
95
|
self.pattern_generator.close_file()
|
|
@@ -3,7 +3,7 @@ from typing import TypedDict
|
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import Movable
|
|
5
5
|
|
|
6
|
-
from ophyd_async.core import AsyncStatus, DerivedSignalFactory, Device
|
|
6
|
+
from ophyd_async.core import AsyncStatus, DerivedSignalFactory, Device
|
|
7
7
|
|
|
8
8
|
from ._mirror_vertical import TwoJackDerived, TwoJackTransform
|
|
9
9
|
from ._motor import SimMotor
|
|
@@ -20,7 +20,8 @@ class HorizontalMirror(Device, Movable):
|
|
|
20
20
|
self.x1 = SimMotor()
|
|
21
21
|
self.x2 = SimMotor()
|
|
22
22
|
# Parameter
|
|
23
|
-
|
|
23
|
+
# This could also be set as 'soft_signal_rw(float, initial_value=1)'
|
|
24
|
+
self.x1_x2_distance = 1.0
|
|
24
25
|
# Derived signals
|
|
25
26
|
self._factory = DerivedSignalFactory(
|
|
26
27
|
TwoJackTransform,
|
|
@@ -51,6 +51,7 @@ class VerticalMirror(Device, Movable[TwoJackDerived]):
|
|
|
51
51
|
self.y1 = SimMotor()
|
|
52
52
|
self.y2 = SimMotor()
|
|
53
53
|
# Parameter
|
|
54
|
+
# This could also be set as '1.0', if constant.
|
|
54
55
|
self.y1_y2_distance = soft_signal_rw(float, initial_value=1)
|
|
55
56
|
# Derived signals
|
|
56
57
|
self._factory = DerivedSignalFactory(
|
ophyd_async/sim/_motor.py
CHANGED
|
@@ -4,14 +4,15 @@ import time
|
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from bluesky.protocols import Locatable, Location, Reading, Stoppable, Subscribable
|
|
7
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
8
7
|
|
|
9
8
|
from ophyd_async.core import (
|
|
10
9
|
AsyncStatus,
|
|
11
10
|
Callback,
|
|
11
|
+
FlyMotorInfo,
|
|
12
12
|
StandardReadable,
|
|
13
13
|
WatchableAsyncStatus,
|
|
14
14
|
WatcherUpdate,
|
|
15
|
+
error_if_none,
|
|
15
16
|
observe_value,
|
|
16
17
|
soft_signal_r_and_setter,
|
|
17
18
|
soft_signal_rw,
|
|
@@ -19,37 +20,6 @@ from ophyd_async.core import (
|
|
|
19
20
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
class FlySimMotorInfo(BaseModel):
|
|
23
|
-
"""Minimal set of information required to fly a [](#SimMotor)."""
|
|
24
|
-
|
|
25
|
-
model_config = ConfigDict(frozen=True)
|
|
26
|
-
|
|
27
|
-
cv_start: float
|
|
28
|
-
"""Absolute position of the motor once it finishes accelerating to desired
|
|
29
|
-
velocity, in motor EGUs"""
|
|
30
|
-
|
|
31
|
-
cv_end: float
|
|
32
|
-
"""Absolute position of the motor once it begins decelerating from desired
|
|
33
|
-
velocity, in EGUs"""
|
|
34
|
-
|
|
35
|
-
cv_time: float = Field(gt=0)
|
|
36
|
-
"""Time taken for the motor to get from start_position to end_position, excluding
|
|
37
|
-
run-up and run-down, in seconds."""
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def velocity(self) -> float:
|
|
41
|
-
"""Calculate the velocity of the constant velocity phase."""
|
|
42
|
-
return (self.cv_end - self.cv_start) / self.cv_time
|
|
43
|
-
|
|
44
|
-
def start_position(self, acceleration_time: float) -> float:
|
|
45
|
-
"""Calculate the start position with run-up distance added on."""
|
|
46
|
-
return self.cv_start - acceleration_time * self.velocity / 2
|
|
47
|
-
|
|
48
|
-
def end_position(self, acceleration_time: float) -> float:
|
|
49
|
-
"""Calculate the end position with run-down distance added on."""
|
|
50
|
-
return self.cv_end + acceleration_time * self.velocity / 2
|
|
51
|
-
|
|
52
|
-
|
|
53
23
|
class SimMotor(StandardReadable, Stoppable, Subscribable[float], Locatable[float]):
|
|
54
24
|
"""For usage when simulating a motor."""
|
|
55
25
|
|
|
@@ -74,7 +44,7 @@ class SimMotor(StandardReadable, Stoppable, Subscribable[float], Locatable[float
|
|
|
74
44
|
self._set_success = True
|
|
75
45
|
self._move_status: AsyncStatus | None = None
|
|
76
46
|
# Stored in prepare
|
|
77
|
-
self._fly_info:
|
|
47
|
+
self._fly_info: FlyMotorInfo | None = None
|
|
78
48
|
# Set on kickoff(), complete when motor reaches end position
|
|
79
49
|
self._fly_status: WatchableAsyncStatus | None = None
|
|
80
50
|
|
|
@@ -86,12 +56,14 @@ class SimMotor(StandardReadable, Stoppable, Subscribable[float], Locatable[float
|
|
|
86
56
|
self.user_readback.set_name(name)
|
|
87
57
|
|
|
88
58
|
@AsyncStatus.wrap
|
|
89
|
-
async def prepare(self, value:
|
|
59
|
+
async def prepare(self, value: FlyMotorInfo):
|
|
90
60
|
"""Calculate run-up and move there, setting fly velocity when there."""
|
|
91
61
|
self._fly_info = value
|
|
92
62
|
# Move to start as fast as we can
|
|
93
63
|
await self.velocity.set(0)
|
|
94
|
-
await self.set(
|
|
64
|
+
await self.set(
|
|
65
|
+
value.ramp_up_start_pos(await self.acceleration_time.get_value())
|
|
66
|
+
)
|
|
95
67
|
# Set the velocity for the actual move
|
|
96
68
|
await self.velocity.set(value.velocity)
|
|
97
69
|
|
|
@@ -111,20 +83,18 @@ class SimMotor(StandardReadable, Stoppable, Subscribable[float], Locatable[float
|
|
|
111
83
|
@AsyncStatus.wrap
|
|
112
84
|
async def kickoff(self):
|
|
113
85
|
"""Begin moving motor from prepared position to final position."""
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
86
|
+
fly_info = error_if_none(
|
|
87
|
+
self._fly_info, "Motor must be prepared before attempting to kickoff"
|
|
88
|
+
)
|
|
117
89
|
acceleration_time = await self.acceleration_time.get_value()
|
|
118
|
-
self._fly_status = self.set(
|
|
90
|
+
self._fly_status = self.set(fly_info.ramp_down_end_pos(acceleration_time))
|
|
119
91
|
# Wait for the acceleration time to ensure we are at velocity
|
|
120
92
|
await asyncio.sleep(acceleration_time)
|
|
121
93
|
|
|
122
94
|
def complete(self) -> WatchableAsyncStatus:
|
|
123
95
|
"""Mark as complete once motor reaches completed position."""
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
raise RuntimeError(msg)
|
|
127
|
-
return self._fly_status
|
|
96
|
+
fly_status = error_if_none(self._fly_status, "kickoff not called")
|
|
97
|
+
return fly_status
|
|
128
98
|
|
|
129
99
|
async def _move(self, old_position: float, new_position: float, velocity: float):
|
|
130
100
|
if old_position == new_position:
|
ophyd_async/testing/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ from ._assert import (
|
|
|
11
11
|
assert_emitted,
|
|
12
12
|
assert_reading,
|
|
13
13
|
assert_value,
|
|
14
|
+
partial_reading,
|
|
14
15
|
)
|
|
15
16
|
from ._mock_signal_utils import (
|
|
16
17
|
callback_on_mock_put,
|
|
@@ -47,6 +48,7 @@ __all__ = [
|
|
|
47
48
|
"assert_configuration",
|
|
48
49
|
"assert_describe_signal",
|
|
49
50
|
"assert_emitted",
|
|
51
|
+
"partial_reading",
|
|
50
52
|
# Mocking utilities
|
|
51
53
|
"get_mock",
|
|
52
54
|
"set_mock_value",
|
ophyd_async/testing/_assert.py
CHANGED
|
@@ -21,6 +21,15 @@ from ophyd_async.core import (
|
|
|
21
21
|
from ._utils import T
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def partial_reading(val: Any) -> dict[str, Any]:
|
|
25
|
+
"""Helper function for building expected reading or configuration dicts.
|
|
26
|
+
|
|
27
|
+
:param val: Value to be wrapped in dict with "value" as the key.
|
|
28
|
+
:return: The dict that has wrapped the val with key "value".
|
|
29
|
+
"""
|
|
30
|
+
return {"value": val}
|
|
31
|
+
|
|
32
|
+
|
|
24
33
|
def approx_value(value: Any):
|
|
25
34
|
"""Allow any value to be compared to another in tests.
|
|
26
35
|
|
|
@@ -45,14 +54,18 @@ async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
|
|
|
45
54
|
async def assert_reading(
|
|
46
55
|
readable: AsyncReadable,
|
|
47
56
|
expected_reading: Mapping[str, Mapping[str, Any]],
|
|
57
|
+
full_match: bool = True,
|
|
48
58
|
) -> None:
|
|
49
59
|
"""Assert that a readable Device has the given reading.
|
|
50
60
|
|
|
51
61
|
:param readable: Device with an async ``read()`` method to get the reading from.
|
|
52
62
|
:param expected_reading: The expected reading from the readable.
|
|
63
|
+
:param full_match: if expected_reading keys set is same as actual keys set.
|
|
64
|
+
true: exact match
|
|
65
|
+
false: expected_reading keys is subset of actual reading keys
|
|
53
66
|
"""
|
|
54
67
|
actual_reading = await readable.read()
|
|
55
|
-
_assert_readings_approx_equal(expected_reading, actual_reading)
|
|
68
|
+
_assert_readings_approx_equal(expected_reading, actual_reading, full_match)
|
|
56
69
|
|
|
57
70
|
|
|
58
71
|
def _approx_reading(expected: Mapping[str, Any], actual: Reading) -> Reading:
|
|
@@ -69,16 +82,26 @@ def _approx_reading(expected: Mapping[str, Any], actual: Reading) -> Reading:
|
|
|
69
82
|
|
|
70
83
|
|
|
71
84
|
def _assert_readings_approx_equal(
|
|
72
|
-
expected: Mapping[str, Mapping[str, Any]],
|
|
85
|
+
expected: Mapping[str, Mapping[str, Any]],
|
|
86
|
+
actual: Mapping[str, Reading],
|
|
87
|
+
full_match: bool = True,
|
|
73
88
|
):
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
# expand the expected keys to include actual if we allow partial matches
|
|
90
|
+
if not full_match:
|
|
91
|
+
expected = dict(actual, **expected)
|
|
92
|
+
# now make them approximate if they are in actual so we get a nicer diff
|
|
93
|
+
approx_expected = {
|
|
94
|
+
k: _approx_reading(v, actual[k]) if k in actual else v
|
|
95
|
+
for k, v in expected.items()
|
|
76
96
|
}
|
|
97
|
+
# now we can compare them
|
|
98
|
+
assert actual == approx_expected
|
|
77
99
|
|
|
78
100
|
|
|
79
101
|
async def assert_configuration(
|
|
80
102
|
configurable: AsyncConfigurable,
|
|
81
|
-
|
|
103
|
+
expected_configuration: dict[str, dict[str, Any]],
|
|
104
|
+
full_match: bool = True,
|
|
82
105
|
) -> None:
|
|
83
106
|
"""Assert that a configurable Device has the given configuration.
|
|
84
107
|
|
|
@@ -86,9 +109,14 @@ async def assert_configuration(
|
|
|
86
109
|
Device with an async ``read_configuration()`` method to get the
|
|
87
110
|
configuration from.
|
|
88
111
|
:param configuration: The expected configuration from the configurable.
|
|
112
|
+
:param full_match: if expected_reading keys set is same as actual keys set.
|
|
113
|
+
true: exact match
|
|
114
|
+
false: expected_reading keys is subset of actual reading keys
|
|
89
115
|
"""
|
|
90
116
|
actual_configuration = await configurable.read_configuration()
|
|
91
|
-
_assert_readings_approx_equal(
|
|
117
|
+
_assert_readings_approx_equal(
|
|
118
|
+
expected_configuration, actual_configuration, full_match
|
|
119
|
+
)
|
|
92
120
|
|
|
93
121
|
|
|
94
122
|
async def assert_describe_signal(signal: SignalR, /, **metadata):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ophyd-async
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12
|
|
4
4
|
Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
|
|
5
5
|
Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -48,6 +48,7 @@ Requires-Dist: pyyaml
|
|
|
48
48
|
Requires-Dist: colorlog
|
|
49
49
|
Requires-Dist: pydantic>=2.0
|
|
50
50
|
Requires-Dist: pydantic-numpy
|
|
51
|
+
Requires-Dist: stamina>=23.1.0
|
|
51
52
|
Provides-Extra: sim
|
|
52
53
|
Requires-Dist: h5py; extra == "sim"
|
|
53
54
|
Provides-Extra: ca
|
|
@@ -70,7 +71,7 @@ Requires-Dist: inflection; extra == "dev"
|
|
|
70
71
|
Requires-Dist: import-linter; extra == "dev"
|
|
71
72
|
Requires-Dist: myst-parser; extra == "dev"
|
|
72
73
|
Requires-Dist: numpydoc; extra == "dev"
|
|
73
|
-
Requires-Dist: ophyd; extra == "dev"
|
|
74
|
+
Requires-Dist: ophyd>=1.10.7; extra == "dev"
|
|
74
75
|
Requires-Dist: pickleshare; extra == "dev"
|
|
75
76
|
Requires-Dist: pipdeptree; extra == "dev"
|
|
76
77
|
Requires-Dist: pre-commit; extra == "dev"
|
|
@@ -120,7 +121,7 @@ The main differences from ophyd sync are:
|
|
|
120
121
|
- Support for [EPICS][] PVA and [Tango][] as well as the traditional EPICS CA
|
|
121
122
|
- Better library support for splitting the logic from the hardware interface to avoid complex class heirarchies
|
|
122
123
|
|
|
123
|
-
It was written with the aim of implementing
|
|
124
|
+
It was written with the aim of implementing fly scanning in a generic and extensible way with highly customizable devices like PandABox and the Delta Tau PMAC products. Using async code makes it possible to do the "put 3 PVs in parallel, then get from another PV" logic that is common in fly scanning without the performance and complexity overhead of multiple threads.
|
|
124
125
|
|
|
125
126
|
Devices from both ophyd sync and ophyd-async can be used in the same RunEngine and even in the same scan. This allows a per-device migration where devices are reimplemented in ophyd-async one by one.
|
|
126
127
|
|