ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__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 +102 -74
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +158 -153
- ophyd_async/core/_device.py +143 -115
- ophyd_async/core/_device_filler.py +82 -9
- ophyd_async/core/_flyer.py +16 -7
- 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 +74 -58
- ophyd_async/core/_settings.py +113 -0
- ophyd_async/core/_signal.py +304 -174
- ophyd_async/core/_signal_backend.py +60 -14
- ophyd_async/core/_soft_signal_backend.py +18 -12
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +54 -17
- ophyd_async/core/_utils.py +101 -52
- ophyd_async/core/_yaml_settings.py +66 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +51 -0
- ophyd_async/epics/adandor/_andor_io.py +34 -0
- ophyd_async/epics/adaravis/__init__.py +8 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -41
- ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +36 -14
- ophyd_async/epics/adcore/_core_detector.py +81 -0
- ophyd_async/epics/adcore/_core_io.py +145 -95
- ophyd_async/epics/adcore/_core_logic.py +179 -88
- ophyd_async/epics/adcore/_core_writer.py +223 -0
- ophyd_async/epics/adcore/_hdf_writer.py +51 -92
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +6 -5
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +3 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +32 -27
- ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +7 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +22 -16
- ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +26 -25
- ophyd_async/epics/advimba/_vimba_controller.py +12 -24
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +66 -30
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +50 -18
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +5 -5
- ophyd_async/epics/demo/__init__.py +11 -49
- 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/demo/{mover.db → 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 +83 -38
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +68 -73
- ophyd_async/epics/testing/_utils.py +19 -44
- ophyd_async/epics/testing/test_records.db +16 -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 -8
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +12 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
- ophyd_async/fastcs/panda/_table.py +13 -7
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +16 -0
- ophyd_async/plan_stubs/_ensure_connected.py +12 -17
- ophyd_async/plan_stubs/_fly.py +3 -5
- ophyd_async/plan_stubs/_nd_attributes.py +9 -5
- ophyd_async/plan_stubs/_panda.py +14 -0
- ophyd_async/plan_stubs/_settings.py +152 -0
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +29 -0
- 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 +21 -33
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +148 -74
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/demo/_counter.py +2 -0
- ophyd_async/tango/demo/_detector.py +2 -0
- ophyd_async/tango/demo/_mover.py +10 -6
- ophyd_async/tango/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 +48 -7
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +200 -96
- ophyd_async/testing/_mock_signal_utils.py +59 -73
- ophyd_async/testing/_one_of_everything.py +146 -0
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/demo/_mover.py +0 -95
- ophyd_async/epics/demo/_sensor.py +0 -37
- ophyd_async/epics/demo/sensor.db +0 -19
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
- ophyd_async/sim/demo/_sim_motor.py +0 -107
- 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-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from bluesky.protocols import Location, Reading, Stoppable, Subscribable
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from ophyd_async.core import (
|
|
10
|
+
AsyncStatus,
|
|
11
|
+
Callback,
|
|
12
|
+
StandardReadable,
|
|
13
|
+
WatchableAsyncStatus,
|
|
14
|
+
WatcherUpdate,
|
|
15
|
+
observe_value,
|
|
16
|
+
soft_signal_r_and_setter,
|
|
17
|
+
soft_signal_rw,
|
|
18
|
+
)
|
|
19
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
20
|
+
|
|
21
|
+
|
|
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
|
+
class SimMotor(StandardReadable, Stoppable, Subscribable[float]):
|
|
54
|
+
"""For usage when simulating a motor."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, name="", instant=True) -> None:
|
|
57
|
+
"""Simulate a motor, with optional velocity.
|
|
58
|
+
|
|
59
|
+
:param name: name of device
|
|
60
|
+
:param instant: whether to move instantly or calculate move time using velocity
|
|
61
|
+
"""
|
|
62
|
+
# Define some signals
|
|
63
|
+
with self.add_children_as_readables(Format.HINTED_SIGNAL):
|
|
64
|
+
self.user_readback, self._user_readback_set = soft_signal_r_and_setter(
|
|
65
|
+
float, 0
|
|
66
|
+
)
|
|
67
|
+
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
68
|
+
self.velocity = soft_signal_rw(float, 0 if instant else 1.0)
|
|
69
|
+
self.acceleration_time = soft_signal_rw(float, 0.5)
|
|
70
|
+
self.units = soft_signal_rw(str, "mm")
|
|
71
|
+
self.user_setpoint = soft_signal_rw(float, 0)
|
|
72
|
+
|
|
73
|
+
# Whether set() should complete successfully or not
|
|
74
|
+
self._set_success = True
|
|
75
|
+
self._move_status: AsyncStatus | None = None
|
|
76
|
+
# Stored in prepare
|
|
77
|
+
self._fly_info: FlySimMotorInfo | None = None
|
|
78
|
+
# Set on kickoff(), complete when motor reaches end position
|
|
79
|
+
self._fly_status: WatchableAsyncStatus | None = None
|
|
80
|
+
|
|
81
|
+
super().__init__(name=name)
|
|
82
|
+
|
|
83
|
+
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
84
|
+
super().set_name(name, child_name_separator=child_name_separator)
|
|
85
|
+
# Readback should be named the same as its parent in read()
|
|
86
|
+
self.user_readback.set_name(name)
|
|
87
|
+
|
|
88
|
+
@AsyncStatus.wrap
|
|
89
|
+
async def prepare(self, value: FlySimMotorInfo):
|
|
90
|
+
"""Calculate run-up and move there, setting fly velocity when there."""
|
|
91
|
+
self._fly_info = value
|
|
92
|
+
# Move to start as fast as we can
|
|
93
|
+
await self.velocity.set(0)
|
|
94
|
+
await self.set(value.start_position(await self.acceleration_time.get_value()))
|
|
95
|
+
# Set the velocity for the actual move
|
|
96
|
+
await self.velocity.set(value.velocity)
|
|
97
|
+
|
|
98
|
+
async def locate(self) -> Location[float]:
|
|
99
|
+
"""Return the current setpoint and readback of the motor."""
|
|
100
|
+
setpoint, readback = await asyncio.gather(
|
|
101
|
+
self.user_setpoint.get_value(), self.user_readback.get_value()
|
|
102
|
+
)
|
|
103
|
+
return Location(setpoint=setpoint, readback=readback)
|
|
104
|
+
|
|
105
|
+
def subscribe(self, function: Callback[dict[str, Reading[float]]]) -> None:
|
|
106
|
+
self.user_readback.subscribe(function)
|
|
107
|
+
|
|
108
|
+
def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None:
|
|
109
|
+
self.user_readback.clear_sub(function)
|
|
110
|
+
|
|
111
|
+
@AsyncStatus.wrap
|
|
112
|
+
async def kickoff(self):
|
|
113
|
+
"""Begin moving motor from prepared position to final position."""
|
|
114
|
+
if not self._fly_info:
|
|
115
|
+
msg = "Motor must be prepared before attempting to kickoff"
|
|
116
|
+
raise RuntimeError(msg)
|
|
117
|
+
acceleration_time = await self.acceleration_time.get_value()
|
|
118
|
+
self._fly_status = self.set(self._fly_info.end_position(acceleration_time))
|
|
119
|
+
# Wait for the acceleration time to ensure we are at velocity
|
|
120
|
+
await asyncio.sleep(acceleration_time)
|
|
121
|
+
|
|
122
|
+
def complete(self) -> WatchableAsyncStatus:
|
|
123
|
+
"""Mark as complete once motor reaches completed position."""
|
|
124
|
+
if not self._fly_status:
|
|
125
|
+
msg = "kickoff not called"
|
|
126
|
+
raise RuntimeError(msg)
|
|
127
|
+
return self._fly_status
|
|
128
|
+
|
|
129
|
+
async def _move(self, old_position: float, new_position: float, velocity: float):
|
|
130
|
+
if old_position == new_position:
|
|
131
|
+
return
|
|
132
|
+
start = time.monotonic()
|
|
133
|
+
acceleration_time = abs(await self.acceleration_time.get_value())
|
|
134
|
+
sign = np.sign(new_position - old_position)
|
|
135
|
+
velocity = abs(velocity) * sign
|
|
136
|
+
# The total distance to move
|
|
137
|
+
total_distance = new_position - old_position
|
|
138
|
+
# The ramp distance is the distance taken to ramp up (the same distance
|
|
139
|
+
# is taken to ramp down). This is the area under the triangle of the
|
|
140
|
+
# velocity ramp up (base * height / 2)
|
|
141
|
+
ramp_distance = acceleration_time * velocity / 2
|
|
142
|
+
if abs(ramp_distance * 2) >= abs(total_distance):
|
|
143
|
+
# All time is ramp up and down, so recalculate the maximum velocity
|
|
144
|
+
# we get to. We know the area under the ramp up triangle is half the
|
|
145
|
+
# total distance, and we also know the ratio of velocity over
|
|
146
|
+
# acceleration_time is the same as the ration of max_velocity over
|
|
147
|
+
# ramp_time, so solve the simultaneous equations to get
|
|
148
|
+
# max_velocity and ramp_time.
|
|
149
|
+
max_velocity = np.sqrt(total_distance * velocity / acceleration_time) * sign
|
|
150
|
+
ramp_time = total_distance / max_velocity
|
|
151
|
+
# So move time is just the ramp up and ramp down with no constant
|
|
152
|
+
# velocity section
|
|
153
|
+
move_time = 2 * ramp_time
|
|
154
|
+
else:
|
|
155
|
+
# Middle segments of constant velocity
|
|
156
|
+
max_velocity = velocity
|
|
157
|
+
# Ramp up and down time is exactly the requested acceleration time
|
|
158
|
+
ramp_time = acceleration_time
|
|
159
|
+
# So move time is twice this, plus the time taken to move the
|
|
160
|
+
# remaining distance at constant velocity
|
|
161
|
+
move_time = ramp_time * 2 + (total_distance - ramp_distance * 2) / velocity
|
|
162
|
+
# Make an array of relative update times at 10Hz intervals
|
|
163
|
+
update_times = list(np.arange(0.1, move_time, 0.1, dtype=float))
|
|
164
|
+
# With the end position appended
|
|
165
|
+
if update_times and np.isclose(update_times[-1], move_time):
|
|
166
|
+
update_times[-1] = move_time
|
|
167
|
+
else:
|
|
168
|
+
update_times.append(move_time)
|
|
169
|
+
# Iterate through the update times, calculating new position for each
|
|
170
|
+
for t in update_times:
|
|
171
|
+
if t <= ramp_time:
|
|
172
|
+
# Ramp up phase, calculate area under the ramp up triangle
|
|
173
|
+
current_velocity = t / ramp_time * max_velocity
|
|
174
|
+
position = old_position + current_velocity * t / 2
|
|
175
|
+
elif t >= move_time - ramp_time:
|
|
176
|
+
# Ramp down phase, subtract area under the ramp down triangle
|
|
177
|
+
time_left = move_time - t
|
|
178
|
+
current_velocity = time_left / ramp_time * max_velocity
|
|
179
|
+
position = new_position - current_velocity * time_left / 2
|
|
180
|
+
else:
|
|
181
|
+
# Constant velocity phase
|
|
182
|
+
position = old_position + ramp_distance + (t - ramp_time) * max_velocity
|
|
183
|
+
# Calculate how long to wait to get there
|
|
184
|
+
relative_time = time.monotonic() - start
|
|
185
|
+
await asyncio.sleep(t - relative_time)
|
|
186
|
+
# Update the readback position
|
|
187
|
+
self._user_readback_set(position)
|
|
188
|
+
|
|
189
|
+
@WatchableAsyncStatus.wrap
|
|
190
|
+
async def set(self, value: float):
|
|
191
|
+
"""Asynchronously move the motor to a new position."""
|
|
192
|
+
new_position = value
|
|
193
|
+
# Make sure any existing move tasks are stopped
|
|
194
|
+
if self._move_status:
|
|
195
|
+
self._move_status.task.cancel()
|
|
196
|
+
self._move_status = None
|
|
197
|
+
# work out where we were
|
|
198
|
+
old_position, units, velocity = await asyncio.gather(
|
|
199
|
+
self.user_setpoint.get_value(),
|
|
200
|
+
self.units.get_value(),
|
|
201
|
+
self.velocity.get_value(),
|
|
202
|
+
)
|
|
203
|
+
# update the setpoint to where we want to be
|
|
204
|
+
await self.user_setpoint.set(new_position)
|
|
205
|
+
# If zero velocity, do instant move
|
|
206
|
+
if velocity == 0:
|
|
207
|
+
self._user_readback_set(new_position)
|
|
208
|
+
else:
|
|
209
|
+
self._move_status = AsyncStatus(
|
|
210
|
+
self._move(old_position, new_position, velocity)
|
|
211
|
+
)
|
|
212
|
+
# If stop is called then this will raise a CancelledError, ignore it
|
|
213
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
214
|
+
async for current_position in observe_value(
|
|
215
|
+
self.user_readback, done_status=self._move_status
|
|
216
|
+
):
|
|
217
|
+
yield WatcherUpdate(
|
|
218
|
+
current=current_position,
|
|
219
|
+
initial=old_position,
|
|
220
|
+
target=new_position,
|
|
221
|
+
name=self.name,
|
|
222
|
+
unit=units,
|
|
223
|
+
)
|
|
224
|
+
if not self._set_success:
|
|
225
|
+
raise RuntimeError("Motor was stopped")
|
|
226
|
+
|
|
227
|
+
async def stop(self, success=True):
|
|
228
|
+
"""Stop the motor if it is moving."""
|
|
229
|
+
self._set_success = success
|
|
230
|
+
if self._move_status:
|
|
231
|
+
self._move_status.task.cancel()
|
|
232
|
+
self._move_status = None
|
|
233
|
+
await self.user_setpoint.set(await self.user_readback.get_value())
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import h5py
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
# raw data path
|
|
11
|
+
DATA_PATH = "/entry/data/data"
|
|
12
|
+
|
|
13
|
+
# pixel sum path
|
|
14
|
+
SUM_PATH = "/entry/sum"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_gaussian_blob(height: int, width: int) -> np.ndarray:
|
|
18
|
+
"""Make a Gaussian Blob with float values in range 0..1."""
|
|
19
|
+
x, y = np.meshgrid(np.linspace(-1, 1, width), np.linspace(-1, 1, height))
|
|
20
|
+
d = np.sqrt(x * x + y * y)
|
|
21
|
+
blob = np.exp(-(d**2))
|
|
22
|
+
return blob
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_interesting_pattern(
|
|
26
|
+
x: float, y: float, channel: int, offset: float
|
|
27
|
+
) -> float:
|
|
28
|
+
"""Return a float value in range 0..1.
|
|
29
|
+
|
|
30
|
+
Interesting in x and y in range -10..10
|
|
31
|
+
"""
|
|
32
|
+
return (np.sin(x) ** channel + np.cos(x * y + offset) + 2) / 4
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PatternFile:
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
path: Path,
|
|
39
|
+
width: int = 320,
|
|
40
|
+
height: int = 240,
|
|
41
|
+
):
|
|
42
|
+
self.file = h5py.File(path, "w", libver="latest")
|
|
43
|
+
self.data = self.file.create_dataset(
|
|
44
|
+
name=DATA_PATH,
|
|
45
|
+
shape=(0, height, width),
|
|
46
|
+
dtype=np.uint8,
|
|
47
|
+
maxshape=(None, height, width),
|
|
48
|
+
)
|
|
49
|
+
self.sum = self.file.create_dataset(
|
|
50
|
+
name=SUM_PATH,
|
|
51
|
+
shape=(0,),
|
|
52
|
+
dtype=np.int64,
|
|
53
|
+
maxshape=(None,),
|
|
54
|
+
)
|
|
55
|
+
# Once datasets written, can switch the model to single writer multiple reader
|
|
56
|
+
self.file.swmr_mode = True
|
|
57
|
+
self.blob = generate_gaussian_blob(height, width) * np.iinfo(np.uint8).max
|
|
58
|
+
self.image_counter = 0
|
|
59
|
+
self.e = asyncio.Event()
|
|
60
|
+
|
|
61
|
+
def write_image_to_file(self, intensity: float):
|
|
62
|
+
data = np.floor(self.blob * intensity)
|
|
63
|
+
for dset, value in ((self.data, data), (self.sum, np.sum(data))):
|
|
64
|
+
dset.resize(self.image_counter + 1, axis=0)
|
|
65
|
+
dset[self.image_counter] = value
|
|
66
|
+
dset.flush()
|
|
67
|
+
self.image_counter += 1
|
|
68
|
+
self.e.set()
|
|
69
|
+
self.e.clear()
|
|
70
|
+
|
|
71
|
+
def close(self):
|
|
72
|
+
self.file.close()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PatternGenerator:
|
|
76
|
+
"""Generates pattern images in files."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, sleep=asyncio.sleep):
|
|
79
|
+
self._x = 0.0
|
|
80
|
+
self._y = 0.0
|
|
81
|
+
self._file: PatternFile | None = None
|
|
82
|
+
self.sleep = sleep
|
|
83
|
+
|
|
84
|
+
def set_x(self, x: float):
|
|
85
|
+
self._x = x
|
|
86
|
+
|
|
87
|
+
def set_y(self, y: float):
|
|
88
|
+
self._y = y
|
|
89
|
+
|
|
90
|
+
def generate_point(self, channel: int = 1, high_energy: bool = False) -> float:
|
|
91
|
+
"""Make a point between 0 and 1 based on x and y."""
|
|
92
|
+
offset = 100 if high_energy else 10
|
|
93
|
+
return generate_interesting_pattern(self._x, self._y, channel, offset)
|
|
94
|
+
|
|
95
|
+
def open_file(self, path: Path, width: int, height: int):
|
|
96
|
+
self._file = PatternFile(path, width, height)
|
|
97
|
+
|
|
98
|
+
def _get_file(self) -> PatternFile:
|
|
99
|
+
if not self._file:
|
|
100
|
+
raise RuntimeError("open_file not run")
|
|
101
|
+
return self._file
|
|
102
|
+
|
|
103
|
+
async def write_images_to_file(
|
|
104
|
+
self, exposure: float, period: float, number_of_frames: int
|
|
105
|
+
):
|
|
106
|
+
file = self._get_file()
|
|
107
|
+
start = time.monotonic()
|
|
108
|
+
for i in range(1, number_of_frames + 1):
|
|
109
|
+
deadline = start + i * period
|
|
110
|
+
timeout = deadline - time.monotonic()
|
|
111
|
+
await self.sleep(timeout)
|
|
112
|
+
intensity = self.generate_point() * exposure
|
|
113
|
+
file.write_image_to_file(intensity)
|
|
114
|
+
|
|
115
|
+
async def wait_for_next_index(self, timeout: float):
|
|
116
|
+
await asyncio.wait_for(self._get_file().e.wait(), timeout)
|
|
117
|
+
|
|
118
|
+
def get_last_index(self) -> int:
|
|
119
|
+
return self._get_file().image_counter
|
|
120
|
+
|
|
121
|
+
def close_file(self):
|
|
122
|
+
if self._file:
|
|
123
|
+
self._file.close()
|
|
124
|
+
self._file = None
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core import (
|
|
7
|
+
AsyncStatus,
|
|
8
|
+
DeviceVector,
|
|
9
|
+
SignalR,
|
|
10
|
+
StandardReadable,
|
|
11
|
+
StrictEnum,
|
|
12
|
+
gather_dict,
|
|
13
|
+
soft_signal_r_and_setter,
|
|
14
|
+
soft_signal_rw,
|
|
15
|
+
)
|
|
16
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
17
|
+
|
|
18
|
+
from ._pattern_generator import PatternGenerator
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EnergyMode(StrictEnum):
|
|
22
|
+
"""Energy mode for `SimPointDetector`."""
|
|
23
|
+
|
|
24
|
+
LOW = "Low Energy"
|
|
25
|
+
"""Low energy mode"""
|
|
26
|
+
|
|
27
|
+
HIGH = "High Energy"
|
|
28
|
+
"""High energy mode"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SimPointDetectorChannel(StandardReadable):
|
|
32
|
+
def __init__(self, value_signal: SignalR[int], name=""):
|
|
33
|
+
with self.add_children_as_readables(Format.HINTED_SIGNAL):
|
|
34
|
+
self.value = value_signal
|
|
35
|
+
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
36
|
+
self.mode = soft_signal_rw(EnergyMode)
|
|
37
|
+
super().__init__(name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SimPointDetector(StandardReadable):
|
|
41
|
+
"""Simalutes a point detector with multiple channels."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self, generator: PatternGenerator, num_channels: int = 3, name: str = ""
|
|
45
|
+
) -> None:
|
|
46
|
+
self._generator = generator
|
|
47
|
+
self.acquire_time = soft_signal_rw(float, 0.1)
|
|
48
|
+
self.acquiring, self._set_acquiring = soft_signal_r_and_setter(bool)
|
|
49
|
+
self._value_signals = dict(
|
|
50
|
+
soft_signal_r_and_setter(int) for _ in range(num_channels)
|
|
51
|
+
)
|
|
52
|
+
with self.add_children_as_readables():
|
|
53
|
+
self.channel = DeviceVector(
|
|
54
|
+
{
|
|
55
|
+
i + 1: SimPointDetectorChannel(value_signal)
|
|
56
|
+
for i, value_signal in enumerate(self._value_signals)
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
super().__init__(name=name)
|
|
60
|
+
|
|
61
|
+
async def _update_values(self, acquire_time: float):
|
|
62
|
+
# Get the modes
|
|
63
|
+
modes = await gather_dict(
|
|
64
|
+
{channel: channel.mode.get_value() for channel in self.channel.values()}
|
|
65
|
+
)
|
|
66
|
+
start = time.monotonic()
|
|
67
|
+
# Make an array of relative update times at 10Hz intervals
|
|
68
|
+
update_times = np.arange(0.1, acquire_time, 0.1)
|
|
69
|
+
# With the end position appended
|
|
70
|
+
update_times = np.concatenate((update_times, [acquire_time]))
|
|
71
|
+
for update_time in update_times:
|
|
72
|
+
# Calculate how long to wait to get there
|
|
73
|
+
relative_time = time.monotonic() - start
|
|
74
|
+
await asyncio.sleep(update_time - relative_time)
|
|
75
|
+
# Update the channel value
|
|
76
|
+
for i, channel in self.channel.items():
|
|
77
|
+
high_energy = modes[channel] == EnergyMode.HIGH
|
|
78
|
+
point = self._generator.generate_point(i, high_energy)
|
|
79
|
+
setter = self._value_signals[channel.value]
|
|
80
|
+
setter(int(point * 10000 * update_time))
|
|
81
|
+
|
|
82
|
+
@AsyncStatus.wrap
|
|
83
|
+
async def trigger(self):
|
|
84
|
+
for setter in self._value_signals.values():
|
|
85
|
+
setter(0)
|
|
86
|
+
await self._update_values(await self.acquire_time.get_value())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable
|
|
2
|
+
from ophyd_async.sim._pattern_generator import PatternGenerator
|
|
3
|
+
|
|
4
|
+
from ._motor import SimMotor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SimStage(StandardReadable):
|
|
8
|
+
"""A simulated sample stage with X and Y movables."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, pattern_generator: PatternGenerator, name="") -> None:
|
|
11
|
+
# Define some child Devices
|
|
12
|
+
with self.add_children_as_readables():
|
|
13
|
+
self.x = SimMotor(instant=False)
|
|
14
|
+
self.y = SimMotor(instant=False)
|
|
15
|
+
# Tell the pattern generator about the motor positions
|
|
16
|
+
self.x.user_readback.subscribe_value(pattern_generator.set_x)
|
|
17
|
+
self.y.user_readback.subscribe_value(pattern_generator.set_y)
|
|
18
|
+
# Set name of device and child devices
|
|
19
|
+
super().__init__(name=name)
|
ophyd_async/tango/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tango support for Signals, and Devices that use them."""
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ._base_device import TangoDevice, TangoPolling
|
|
1
|
+
from ._base_device import TangoDevice, TangoDeviceConnector, TangoPolling
|
|
2
2
|
from ._signal import (
|
|
3
3
|
infer_python_type,
|
|
4
4
|
infer_signal_type,
|
|
@@ -19,10 +19,12 @@ from ._tango_transport import (
|
|
|
19
19
|
get_tango_trl,
|
|
20
20
|
get_trl_descriptor,
|
|
21
21
|
)
|
|
22
|
+
from ._utils import DevStateEnum, get_device_trl_and_attr, get_full_attr_trl
|
|
22
23
|
|
|
23
24
|
__all__ = [
|
|
24
25
|
"AttributeProxy",
|
|
25
26
|
"CommandProxy",
|
|
27
|
+
"DevStateEnum",
|
|
26
28
|
"ensure_proper_executor",
|
|
27
29
|
"TangoSignalBackend",
|
|
28
30
|
"get_python_type",
|
|
@@ -39,4 +41,7 @@ __all__ = [
|
|
|
39
41
|
"TangoDevice",
|
|
40
42
|
"TangoReadable",
|
|
41
43
|
"TangoPolling",
|
|
44
|
+
"TangoDeviceConnector",
|
|
45
|
+
"get_device_trl_and_attr",
|
|
46
|
+
"get_full_attr_trl",
|
|
42
47
|
]
|
|
@@ -4,27 +4,23 @@ from dataclasses import dataclass
|
|
|
4
4
|
from typing import Any, Generic, TypeVar
|
|
5
5
|
|
|
6
6
|
from ophyd_async.core import Device, DeviceConnector, DeviceFiller, LazyMock
|
|
7
|
-
from tango import DeviceProxy
|
|
7
|
+
from tango import DeviceProxy
|
|
8
8
|
from tango.asyncio import DeviceProxy as AsyncDeviceProxy
|
|
9
9
|
|
|
10
10
|
from ._signal import TangoSignalBackend, infer_python_type, infer_signal_type
|
|
11
|
+
from ._utils import get_full_attr_trl
|
|
11
12
|
|
|
12
13
|
T = TypeVar("T")
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class TangoDevice(Device):
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
devices.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Tango resource locator, typically of the device server.
|
|
24
|
-
device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]]
|
|
25
|
-
Asynchronous or synchronous DeviceProxy object for the device. If not provided,
|
|
26
|
-
an asynchronous DeviceProxy object will be created using the trl and awaited
|
|
27
|
-
when the device is connected.
|
|
17
|
+
"""General class for TangoDevices.
|
|
18
|
+
|
|
19
|
+
Extends Device to provide attributes for Tango devices.
|
|
20
|
+
|
|
21
|
+
:param trl: Tango resource locator, typically of the device server.
|
|
22
|
+
An asynchronous DeviceProxy object will be created using the
|
|
23
|
+
trl and awaited when the device is connected.
|
|
28
24
|
"""
|
|
29
25
|
|
|
30
26
|
trl: str = ""
|
|
@@ -32,13 +28,15 @@ class TangoDevice(Device):
|
|
|
32
28
|
|
|
33
29
|
def __init__(
|
|
34
30
|
self,
|
|
35
|
-
trl: str | None
|
|
36
|
-
device_proxy: DeviceProxy | None = None,
|
|
31
|
+
trl: str | None,
|
|
37
32
|
support_events: bool = False,
|
|
38
33
|
name: str = "",
|
|
34
|
+
auto_fill_signals: bool = True,
|
|
39
35
|
) -> None:
|
|
40
36
|
connector = TangoDeviceConnector(
|
|
41
|
-
trl=trl,
|
|
37
|
+
trl=trl,
|
|
38
|
+
support_events=support_events,
|
|
39
|
+
auto_fill_signals=auto_fill_signals,
|
|
42
40
|
)
|
|
43
41
|
super().__init__(name=name, connector=connector)
|
|
44
42
|
|
|
@@ -74,12 +72,12 @@ class TangoDeviceConnector(DeviceConnector):
|
|
|
74
72
|
def __init__(
|
|
75
73
|
self,
|
|
76
74
|
trl: str | None,
|
|
77
|
-
device_proxy: DeviceProxy | None,
|
|
78
75
|
support_events: bool,
|
|
76
|
+
auto_fill_signals: bool = True,
|
|
79
77
|
) -> None:
|
|
80
78
|
self.trl = trl
|
|
81
|
-
self.proxy = device_proxy
|
|
82
79
|
self._support_events = support_events
|
|
80
|
+
self._auto_fill_signals = auto_fill_signals
|
|
83
81
|
|
|
84
82
|
def create_children_from_annotations(self, device: Device):
|
|
85
83
|
if not hasattr(self, "filler"):
|
|
@@ -87,7 +85,7 @@ class TangoDeviceConnector(DeviceConnector):
|
|
|
87
85
|
device=device,
|
|
88
86
|
signal_backend_factory=TangoSignalBackend,
|
|
89
87
|
device_connector_factory=lambda: TangoDeviceConnector(
|
|
90
|
-
None,
|
|
88
|
+
None, self._support_events
|
|
91
89
|
),
|
|
92
90
|
)
|
|
93
91
|
list(self.filler.create_devices_from_annotations(filled=False))
|
|
@@ -105,28 +103,38 @@ class TangoDeviceConnector(DeviceConnector):
|
|
|
105
103
|
return await super().connect_mock(device, mock)
|
|
106
104
|
|
|
107
105
|
async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
|
|
108
|
-
if self.trl
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
self.trl = self.proxy.name()
|
|
112
|
-
else:
|
|
113
|
-
raise TypeError("Neither proxy nor trl supplied")
|
|
114
|
-
|
|
106
|
+
if not self.trl:
|
|
107
|
+
raise RuntimeError(f"Could not created Device Proxy for TRL {self.trl}")
|
|
108
|
+
self.proxy = await AsyncDeviceProxy(self.trl)
|
|
115
109
|
children = sorted(
|
|
116
110
|
set()
|
|
117
111
|
.union(self.proxy.get_attribute_list())
|
|
118
112
|
.union(self.proxy.get_command_list())
|
|
119
113
|
)
|
|
114
|
+
|
|
115
|
+
children = [
|
|
116
|
+
child for child in children if child not in self.filler.ignored_signals
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
not_filled = {unfilled for unfilled, _ in device.children()}
|
|
120
|
+
|
|
121
|
+
# If auto_fill_signals is True, fill all children inferred from the device
|
|
122
|
+
# else fill only the children that are annotated
|
|
120
123
|
for name in children:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
if self._auto_fill_signals or name in not_filled:
|
|
125
|
+
# TODO: strip attribute name
|
|
126
|
+
full_trl = get_full_attr_trl(self.trl, name)
|
|
127
|
+
signal_type = await infer_signal_type(full_trl, self.proxy)
|
|
128
|
+
if signal_type:
|
|
129
|
+
backend = self.filler.fill_child_signal(name, signal_type)
|
|
130
|
+
# don't overlaod datatype if provided by annotation
|
|
131
|
+
if backend.datatype is None:
|
|
132
|
+
backend.datatype = await infer_python_type(full_trl, self.proxy)
|
|
133
|
+
backend.set_trl(full_trl)
|
|
134
|
+
|
|
128
135
|
# Check that all the requested children have been filled
|
|
129
136
|
self.filler.check_filled(f"{self.trl}: {children}")
|
|
137
|
+
|
|
130
138
|
# Set the name of the device to name all children
|
|
131
139
|
device.set_name(device.name)
|
|
132
140
|
return await super().connect_real(device, timeout, force_reconnect)
|