ophyd-async 0.13.1__py3-none-any.whl → 0.13.3__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 +2 -1
- ophyd_async/core/_signal.py +7 -0
- ophyd_async/epics/core/_epics_connector.py +14 -1
- ophyd_async/epics/motor.py +30 -16
- ophyd_async/epics/{eiger → odin}/_odin_io.py +5 -3
- ophyd_async/epics/pmac/__init__.py +2 -0
- ophyd_async/epics/pmac/_pmac_io.py +2 -2
- ophyd_async/epics/pmac/_pmac_trajectory.py +114 -0
- ophyd_async/epics/pmac/_utils.py +671 -55
- ophyd_async/fastcs/eiger/_eiger.py +1 -1
- ophyd_async/fastcs/jungfrau/__init__.py +29 -0
- ophyd_async/fastcs/jungfrau/_controller.py +139 -0
- ophyd_async/fastcs/jungfrau/_jungfrau.py +30 -0
- ophyd_async/fastcs/jungfrau/_signals.py +94 -0
- ophyd_async/fastcs/jungfrau/_utils.py +79 -0
- ophyd_async/sim/_motor.py +11 -3
- ophyd_async/sim/_point_detector.py +6 -3
- ophyd_async/sim/_stage.py +14 -3
- {ophyd_async-0.13.1.dist-info → ophyd_async-0.13.3.dist-info}/METADATA +4 -2
- {ophyd_async-0.13.1.dist-info → ophyd_async-0.13.3.dist-info}/RECORD +25 -19
- /ophyd_async/epics/{eiger → odin}/__init__.py +0 -0
- {ophyd_async-0.13.1.dist-info → ophyd_async-0.13.3.dist-info}/WHEEL +0 -0
- {ophyd_async-0.13.1.dist-info → ophyd_async-0.13.3.dist-info}/licenses/LICENSE +0 -0
- {ophyd_async-0.13.1.dist-info → ophyd_async-0.13.3.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.13.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 13,
|
|
31
|
+
__version__ = version = '0.13.3'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 13, 3)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
ophyd_async/core/__init__.py
CHANGED
|
@@ -6,7 +6,7 @@ from ._derived_signal import (
|
|
|
6
6
|
derived_signal_rw,
|
|
7
7
|
derived_signal_w,
|
|
8
8
|
)
|
|
9
|
-
from ._derived_signal_backend import Transform
|
|
9
|
+
from ._derived_signal_backend import Transform, merge_gathered_dicts
|
|
10
10
|
from ._detector import (
|
|
11
11
|
DetectorController,
|
|
12
12
|
DetectorTrigger,
|
|
@@ -212,6 +212,7 @@ __all__ = [
|
|
|
212
212
|
"derived_signal_w",
|
|
213
213
|
"Transform",
|
|
214
214
|
"DerivedSignalFactory",
|
|
215
|
+
"merge_gathered_dicts",
|
|
215
216
|
# Back compat - delete before 1.0
|
|
216
217
|
"ConfigSignal",
|
|
217
218
|
"HintedSignal",
|
ophyd_async/core/_signal.py
CHANGED
|
@@ -121,6 +121,13 @@ class _SignalCache(Generic[SignalDatatypeT]):
|
|
|
121
121
|
self._valid = asyncio.Event()
|
|
122
122
|
self._reading: Reading[SignalDatatypeT] | None = None
|
|
123
123
|
self.backend: SignalBackend[SignalDatatypeT] = backend
|
|
124
|
+
try:
|
|
125
|
+
asyncio.get_running_loop()
|
|
126
|
+
except RuntimeError as e:
|
|
127
|
+
raise RuntimeError(
|
|
128
|
+
"Need a running event loop to subscribe to a signal, "
|
|
129
|
+
"are you trying to run subscribe outside a plan?"
|
|
130
|
+
) from e
|
|
124
131
|
signal.log.debug(f"Making subscription on source {signal.source}")
|
|
125
132
|
backend.set_callback(self._callback)
|
|
126
133
|
|
|
@@ -10,7 +10,20 @@ from ._signal import EpicsSignalBackend, get_signal_backend_type, split_protocol
|
|
|
10
10
|
|
|
11
11
|
@dataclass
|
|
12
12
|
class PvSuffix:
|
|
13
|
-
"""Define the PV suffix to be appended to the device prefix.
|
|
13
|
+
"""Define the PV suffix to be appended to the device prefix.
|
|
14
|
+
|
|
15
|
+
For a SignalRW:
|
|
16
|
+
- If you use the same "Suffix" for the read and write PV then use PvSuffix("Suffix")
|
|
17
|
+
- If you have "Suffix" for the write PV and "Suffix_RBV" for the read PV then use
|
|
18
|
+
PvSuffix.rbv("Suffix")
|
|
19
|
+
- If you have "WriteSuffix" for the write PV and "ReadSuffix" for the read PV then
|
|
20
|
+
you use PvSuffix(read_suffix="ReadSuffix", write_suffix="WriteSuffix")
|
|
21
|
+
|
|
22
|
+
For a SignalR:
|
|
23
|
+
- If you have "Suffix" for the read PV then use PvSuffix("Suffix")
|
|
24
|
+
- If you have "Suffix_RBV" for the read PV then use PvSuffix("Suffix_RBV"), do not
|
|
25
|
+
use PvSuffix.rbv as that will try to connect to multiple PVs
|
|
26
|
+
"""
|
|
14
27
|
|
|
15
28
|
read_suffix: str
|
|
16
29
|
write_suffix: str | None = None
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -106,6 +106,33 @@ class Motor(
|
|
|
106
106
|
# Readback should be named the same as its parent in read()
|
|
107
107
|
self.user_readback.set_name(name)
|
|
108
108
|
|
|
109
|
+
async def check_motor_limit(self, abs_start_pos: float, abs_end_pos: float):
|
|
110
|
+
"""Check the positions are within limits.
|
|
111
|
+
|
|
112
|
+
Will raise a MotorLimitsException if the given absolute positions will be
|
|
113
|
+
outside the motor soft limits.
|
|
114
|
+
"""
|
|
115
|
+
motor_lower_limit, motor_upper_limit, egu = await asyncio.gather(
|
|
116
|
+
self.low_limit_travel.get_value(),
|
|
117
|
+
self.high_limit_travel.get_value(),
|
|
118
|
+
self.motor_egu.get_value(),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# EPICS motor record treats limits of 0, 0 as no limit
|
|
122
|
+
if motor_lower_limit == 0 and motor_upper_limit == 0:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
not motor_upper_limit >= abs_start_pos >= motor_lower_limit
|
|
127
|
+
or not motor_upper_limit >= abs_end_pos >= motor_lower_limit
|
|
128
|
+
):
|
|
129
|
+
raise MotorLimitsException(
|
|
130
|
+
f"{self.name} motor trajectory for requested fly/move is from "
|
|
131
|
+
f"{abs_start_pos}{egu} to "
|
|
132
|
+
f"{abs_end_pos}{egu} but motor limits are "
|
|
133
|
+
f"{motor_lower_limit}{egu} <= x <= {motor_upper_limit}{egu} "
|
|
134
|
+
)
|
|
135
|
+
|
|
109
136
|
@AsyncStatus.wrap
|
|
110
137
|
async def prepare(self, value: FlyMotorInfo):
|
|
111
138
|
"""Move to the beginning of a suitable run-up distance ready for a fly scan."""
|
|
@@ -126,22 +153,7 @@ class Motor(
|
|
|
126
153
|
ramp_up_start_pos = value.ramp_up_start_pos(acceleration_time)
|
|
127
154
|
ramp_down_end_pos = value.ramp_down_end_pos(acceleration_time)
|
|
128
155
|
|
|
129
|
-
|
|
130
|
-
self.low_limit_travel.get_value(),
|
|
131
|
-
self.high_limit_travel.get_value(),
|
|
132
|
-
self.motor_egu.get_value(),
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
not motor_upper_limit >= ramp_up_start_pos >= motor_lower_limit
|
|
137
|
-
or not motor_upper_limit >= ramp_down_end_pos >= motor_lower_limit
|
|
138
|
-
):
|
|
139
|
-
raise MotorLimitsException(
|
|
140
|
-
f"Motor trajectory for requested fly is from "
|
|
141
|
-
f"{ramp_up_start_pos}{egu} to "
|
|
142
|
-
f"{ramp_down_end_pos}{egu} but motor limits are "
|
|
143
|
-
f"{motor_lower_limit}{egu} <= x <= {motor_upper_limit}{egu} "
|
|
144
|
-
)
|
|
156
|
+
await self.check_motor_limit(ramp_up_start_pos, ramp_down_end_pos)
|
|
145
157
|
|
|
146
158
|
# move to prepare position at maximum velocity
|
|
147
159
|
await self.velocity.set(abs(max_speed))
|
|
@@ -199,6 +211,8 @@ class Motor(
|
|
|
199
211
|
msg = "Mover has zero velocity"
|
|
200
212
|
raise ValueError(msg) from error
|
|
201
213
|
|
|
214
|
+
await self.check_motor_limit(old_position, new_position)
|
|
215
|
+
|
|
202
216
|
move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
|
|
203
217
|
async for current_position in observe_value(
|
|
204
218
|
self.user_readback, done_status=move_status
|
|
@@ -90,11 +90,11 @@ class OdinWriter(DetectorWriter):
|
|
|
90
90
|
self,
|
|
91
91
|
path_provider: PathProvider,
|
|
92
92
|
odin_driver: Odin,
|
|
93
|
-
|
|
93
|
+
detector_bit_depth: SignalR[int],
|
|
94
94
|
) -> None:
|
|
95
95
|
self._drv = odin_driver
|
|
96
96
|
self._path_provider = path_provider
|
|
97
|
-
self.
|
|
97
|
+
self._detector_bit_depth = Reference(detector_bit_depth)
|
|
98
98
|
self._capture_status: AsyncStatus | None = None
|
|
99
99
|
super().__init__()
|
|
100
100
|
|
|
@@ -103,7 +103,9 @@ class OdinWriter(DetectorWriter):
|
|
|
103
103
|
self._exposures_per_event = exposures_per_event
|
|
104
104
|
|
|
105
105
|
await asyncio.gather(
|
|
106
|
-
self._drv.data_type.set(
|
|
106
|
+
self._drv.data_type.set(
|
|
107
|
+
f"UInt{await self._detector_bit_depth().get_value()}"
|
|
108
|
+
),
|
|
107
109
|
self._drv.num_to_capture.set(0),
|
|
108
110
|
self._drv.file_path.set(str(info.directory_path)),
|
|
109
111
|
self._drv.file_name.set(info.filename),
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from ._pmac_io import PmacAxisAssignmentIO, PmacCoordIO, PmacIO, PmacTrajectoryIO
|
|
2
|
+
from ._pmac_trajectory import PmacTrajectoryTriggerLogic
|
|
2
3
|
|
|
3
4
|
__all__ = [
|
|
4
5
|
"PmacAxisAssignmentIO",
|
|
5
6
|
"PmacCoordIO",
|
|
6
7
|
"PmacIO",
|
|
7
8
|
"PmacTrajectoryIO",
|
|
9
|
+
"PmacTrajectoryTriggerLogic",
|
|
8
10
|
]
|
|
@@ -72,7 +72,7 @@ class PmacCoordIO(Device):
|
|
|
72
72
|
self.cs_port = epics_signal_r(str, f"{prefix}Port")
|
|
73
73
|
self.cs_axis_setpoint = DeviceVector(
|
|
74
74
|
{
|
|
75
|
-
i + 1: epics_signal_rw(
|
|
75
|
+
i + 1: epics_signal_rw(float, f"{prefix}M{i + 1}:DirectDemand")
|
|
76
76
|
for i in range(len(CS_LETTERS))
|
|
77
77
|
}
|
|
78
78
|
)
|
|
@@ -93,7 +93,7 @@ class PmacIO(Device):
|
|
|
93
93
|
|
|
94
94
|
self.assignment = DeviceVector(
|
|
95
95
|
{
|
|
96
|
-
i: PmacAxisAssignmentIO(motor_prefix)
|
|
96
|
+
i: PmacAxisAssignmentIO(motor_prefix + ":")
|
|
97
97
|
for i, motor_prefix in enumerate(motor_prefixes)
|
|
98
98
|
}
|
|
99
99
|
)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scanspec.core import Path
|
|
5
|
+
from scanspec.specs import Spec
|
|
6
|
+
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
DEFAULT_TIMEOUT,
|
|
9
|
+
FlyerController,
|
|
10
|
+
set_and_wait_for_value,
|
|
11
|
+
wait_for_value,
|
|
12
|
+
)
|
|
13
|
+
from ophyd_async.epics.motor import Motor
|
|
14
|
+
from ophyd_async.epics.pmac import PmacIO
|
|
15
|
+
from ophyd_async.epics.pmac._pmac_io import CS_LETTERS
|
|
16
|
+
from ophyd_async.epics.pmac._utils import (
|
|
17
|
+
_PmacMotorInfo,
|
|
18
|
+
_Trajectory,
|
|
19
|
+
calculate_ramp_position_and_duration,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PmacTrajectoryTriggerLogic(FlyerController):
|
|
24
|
+
def __init__(self, pmac: PmacIO) -> None:
|
|
25
|
+
self.pmac = pmac
|
|
26
|
+
self.scantime: float | None = None
|
|
27
|
+
|
|
28
|
+
async def prepare(self, value: Spec[Motor]):
|
|
29
|
+
slice = Path(value.calculate()).consume()
|
|
30
|
+
motor_info = await _PmacMotorInfo.from_motors(self.pmac, slice.axes())
|
|
31
|
+
ramp_up_pos, ramp_up_time = calculate_ramp_position_and_duration(
|
|
32
|
+
slice, motor_info, True
|
|
33
|
+
)
|
|
34
|
+
ramp_down_pos, ramp_down_time = calculate_ramp_position_and_duration(
|
|
35
|
+
slice, motor_info, False
|
|
36
|
+
)
|
|
37
|
+
trajectory = _Trajectory.from_slice(slice, ramp_up_time, motor_info)
|
|
38
|
+
await asyncio.gather(
|
|
39
|
+
self._build_trajectory(
|
|
40
|
+
trajectory, motor_info, ramp_down_pos, ramp_down_time
|
|
41
|
+
),
|
|
42
|
+
self._move_to_start(motor_info, ramp_up_pos),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
async def kickoff(self):
|
|
46
|
+
if not self.scantime:
|
|
47
|
+
raise RuntimeError("Cannot kickoff. Must call prepare first.")
|
|
48
|
+
self.status = await self.pmac.trajectory.execute_profile.set(
|
|
49
|
+
True, timeout=self.scantime + 1
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def complete(self):
|
|
53
|
+
if not self.scantime:
|
|
54
|
+
raise RuntimeError("Cannot complete. Must call prepare first.")
|
|
55
|
+
await wait_for_value(
|
|
56
|
+
self.pmac.trajectory.execute_profile,
|
|
57
|
+
False,
|
|
58
|
+
timeout=self.scantime + DEFAULT_TIMEOUT,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def stop(self):
|
|
62
|
+
await self.pmac.trajectory.abort_profile.set(True)
|
|
63
|
+
|
|
64
|
+
async def _build_trajectory(
|
|
65
|
+
self,
|
|
66
|
+
trajectory: _Trajectory,
|
|
67
|
+
motor_info: _PmacMotorInfo,
|
|
68
|
+
ramp_down_pos: dict[Motor, np.float64],
|
|
69
|
+
ramp_down_time: float,
|
|
70
|
+
):
|
|
71
|
+
trajectory = trajectory.append_ramp_down(ramp_down_pos, ramp_down_time, 0)
|
|
72
|
+
self.scantime = np.sum(trajectory.durations)
|
|
73
|
+
use_axis = {axis + 1: False for axis in range(len(CS_LETTERS))}
|
|
74
|
+
|
|
75
|
+
for motor, number in motor_info.motor_cs_index.items():
|
|
76
|
+
use_axis[number + 1] = True
|
|
77
|
+
await self.pmac.trajectory.positions[number + 1].set(
|
|
78
|
+
trajectory.positions[motor]
|
|
79
|
+
)
|
|
80
|
+
await self.pmac.trajectory.velocities[number + 1].set(
|
|
81
|
+
trajectory.velocities[motor]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
coros = [
|
|
85
|
+
self.pmac.trajectory.profile_cs_name.set(motor_info.cs_port),
|
|
86
|
+
self.pmac.trajectory.time_array.set(trajectory.durations),
|
|
87
|
+
self.pmac.trajectory.user_array.set(trajectory.user_programs),
|
|
88
|
+
self.pmac.trajectory.points_to_build.set(len(trajectory.durations)),
|
|
89
|
+
self.pmac.trajectory.calculate_velocities.set(False),
|
|
90
|
+
] + [
|
|
91
|
+
self.pmac.trajectory.use_axis[number].set(use)
|
|
92
|
+
for number, use in use_axis.items()
|
|
93
|
+
]
|
|
94
|
+
await asyncio.gather(*coros)
|
|
95
|
+
await self.pmac.trajectory.build_profile.set(True)
|
|
96
|
+
|
|
97
|
+
async def _move_to_start(
|
|
98
|
+
self, motor_info: _PmacMotorInfo, ramp_up_position: dict[Motor, np.float64]
|
|
99
|
+
):
|
|
100
|
+
coord = self.pmac.coord[motor_info.cs_number]
|
|
101
|
+
coros = []
|
|
102
|
+
await coord.defer_moves.set(True)
|
|
103
|
+
for motor, position in ramp_up_position.items():
|
|
104
|
+
coros.append(
|
|
105
|
+
set_and_wait_for_value(
|
|
106
|
+
coord.cs_axis_setpoint[motor_info.motor_cs_index[motor] + 1],
|
|
107
|
+
position,
|
|
108
|
+
set_timeout=10,
|
|
109
|
+
wait_for_set_completion=False,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
statuses = await asyncio.gather(*coros)
|
|
113
|
+
await coord.defer_moves.set(False)
|
|
114
|
+
await asyncio.gather(*statuses)
|