ophyd-async 0.13.2__py3-none-any.whl → 0.13.4__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/_table.py +8 -0
- ophyd_async/epics/adcore/_core_logic.py +3 -1
- ophyd_async/epics/motor.py +11 -2
- ophyd_async/epics/pmac/_pmac_io.py +8 -4
- ophyd_async/epics/pmac/_pmac_trajectory.py +144 -43
- ophyd_async/epics/pmac/_pmac_trajectory_generation.py +692 -0
- ophyd_async/epics/pmac/_utils.py +1 -681
- ophyd_async/fastcs/jungfrau/__init__.py +2 -1
- ophyd_async/fastcs/jungfrau/_controller.py +29 -11
- ophyd_async/fastcs/jungfrau/_utils.py +10 -2
- ophyd_async/fastcs/panda/__init__.py +10 -0
- ophyd_async/fastcs/panda/_block.py +14 -0
- ophyd_async/fastcs/panda/_trigger.py +123 -3
- {ophyd_async-0.13.2.dist-info → ophyd_async-0.13.4.dist-info}/METADATA +3 -2
- {ophyd_async-0.13.2.dist-info → ophyd_async-0.13.4.dist-info}/RECORD +20 -19
- {ophyd_async-0.13.2.dist-info → ophyd_async-0.13.4.dist-info}/WHEEL +0 -0
- {ophyd_async-0.13.2.dist-info → ophyd_async-0.13.4.dist-info}/licenses/LICENSE +0 -0
- {ophyd_async-0.13.2.dist-info → ophyd_async-0.13.4.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.4'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 13, 4)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
ophyd_async/core/__init__.py
CHANGED
|
@@ -78,7 +78,7 @@ from ._signal_backend import (
|
|
|
78
78
|
)
|
|
79
79
|
from ._soft_signal_backend import SoftSignalBackend
|
|
80
80
|
from ._status import AsyncStatus, WatchableAsyncStatus, completed_status
|
|
81
|
-
from ._table import Table
|
|
81
|
+
from ._table import Table, TableSubclass
|
|
82
82
|
from ._utils import (
|
|
83
83
|
CALCULATE_TIMEOUT,
|
|
84
84
|
DEFAULT_TIMEOUT,
|
|
@@ -221,4 +221,5 @@ __all__ = [
|
|
|
221
221
|
"EnableDisable",
|
|
222
222
|
"InOut",
|
|
223
223
|
"OnOff",
|
|
224
|
+
"TableSubclass",
|
|
224
225
|
]
|
ophyd_async/core/_table.py
CHANGED
|
@@ -13,6 +13,7 @@ TableSubclass = TypeVar("TableSubclass", bound="Table")
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _concat(value1, value2):
|
|
16
|
+
"""Concatenate two values, supports both NumPy arrays and generic types."""
|
|
16
17
|
if isinstance(value1, np.ndarray):
|
|
17
18
|
return np.concatenate((value1, value2))
|
|
18
19
|
else:
|
|
@@ -20,6 +21,8 @@ def _concat(value1, value2):
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _make_default_factory(dtype: np.dtype) -> Callable[[], np.ndarray]:
|
|
24
|
+
"""Creates a default factory, returns an empty Numpy array of a specified dtype."""
|
|
25
|
+
|
|
23
26
|
def numpy_array_default_factory() -> np.ndarray:
|
|
24
27
|
return np.array([], dtype)
|
|
25
28
|
|
|
@@ -91,6 +94,11 @@ class Table(ConfinedModel):
|
|
|
91
94
|
raise TypeError(f"Cannot use annotation {anno} in a Table")
|
|
92
95
|
cls.__annotations__[k] = new_anno
|
|
93
96
|
|
|
97
|
+
@classmethod
|
|
98
|
+
def empty(cls: type[TableSubclass]) -> TableSubclass:
|
|
99
|
+
"""Makes an empty table with zero length columns."""
|
|
100
|
+
return cls() # type: ignore
|
|
101
|
+
|
|
94
102
|
def __add__(self, right: TableSubclass) -> TableSubclass:
|
|
95
103
|
"""Concatenate the arrays in field values."""
|
|
96
104
|
cls = type(right)
|
|
@@ -34,9 +34,11 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
|
|
|
34
34
|
self,
|
|
35
35
|
driver: ADBaseIOT,
|
|
36
36
|
good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
|
|
37
|
+
image_mode: ADImageMode = ADImageMode.MULTIPLE,
|
|
37
38
|
) -> None:
|
|
38
39
|
self.driver: ADBaseIOT = driver
|
|
39
40
|
self.good_states = good_states
|
|
41
|
+
self.image_mode = image_mode
|
|
40
42
|
self.frame_timeout = DEFAULT_TIMEOUT
|
|
41
43
|
self._arm_status: AsyncStatus | None = None
|
|
42
44
|
|
|
@@ -52,7 +54,7 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
|
|
|
52
54
|
)
|
|
53
55
|
await asyncio.gather(
|
|
54
56
|
self.driver.num_images.set(trigger_info.total_number_of_exposures),
|
|
55
|
-
self.driver.image_mode.set(
|
|
57
|
+
self.driver.image_mode.set(self.image_mode),
|
|
56
58
|
)
|
|
57
59
|
|
|
58
60
|
async def arm(self):
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -107,18 +107,27 @@ class Motor(
|
|
|
107
107
|
self.user_readback.set_name(name)
|
|
108
108
|
|
|
109
109
|
async def check_motor_limit(self, abs_start_pos: float, abs_end_pos: float):
|
|
110
|
-
"""Check the
|
|
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
|
+
"""
|
|
111
115
|
motor_lower_limit, motor_upper_limit, egu = await asyncio.gather(
|
|
112
116
|
self.low_limit_travel.get_value(),
|
|
113
117
|
self.high_limit_travel.get_value(),
|
|
114
118
|
self.motor_egu.get_value(),
|
|
115
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
|
+
|
|
116
125
|
if (
|
|
117
126
|
not motor_upper_limit >= abs_start_pos >= motor_lower_limit
|
|
118
127
|
or not motor_upper_limit >= abs_end_pos >= motor_lower_limit
|
|
119
128
|
):
|
|
120
129
|
raise MotorLimitsException(
|
|
121
|
-
f"
|
|
130
|
+
f"{self.name} motor trajectory for requested fly/move is from "
|
|
122
131
|
f"{abs_start_pos}{egu} to "
|
|
123
132
|
f"{abs_end_pos}{egu} but motor limits are "
|
|
124
133
|
f"{motor_lower_limit}{egu} <= x <= {motor_upper_limit}{egu} "
|
|
@@ -4,7 +4,7 @@ import numpy as np
|
|
|
4
4
|
|
|
5
5
|
from ophyd_async.core import Array1D, Device, DeviceVector, StandardReadable
|
|
6
6
|
from ophyd_async.epics import motor
|
|
7
|
-
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
7
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
8
8
|
|
|
9
9
|
CS_LETTERS = "ABCUVWXYZ"
|
|
10
10
|
|
|
@@ -40,11 +40,15 @@ class PmacTrajectoryIO(StandardReadable):
|
|
|
40
40
|
for i, letter in enumerate(CS_LETTERS)
|
|
41
41
|
}
|
|
42
42
|
)
|
|
43
|
+
self.total_points = epics_signal_r(int, f"{prefix}TotalPoints_RBV")
|
|
43
44
|
self.points_to_build = epics_signal_rw(int, prefix + "ProfilePointsToBuild")
|
|
44
|
-
self.build_profile =
|
|
45
|
+
self.build_profile = epics_signal_x(prefix + "ProfileBuild")
|
|
46
|
+
self.append_profile = epics_signal_x(prefix + "ProfileAppend")
|
|
47
|
+
# This should be a SignalX, but because it is a Busy record, must
|
|
48
|
+
# be a SignalRW to be waited on in PmacTrajectoryTriggerLogic.
|
|
49
|
+
# TODO: Change record type to bo from busy (https://github.com/DiamondLightSource/pmac/issues/154)
|
|
45
50
|
self.execute_profile = epics_signal_rw(bool, prefix + "ProfileExecute")
|
|
46
|
-
self.
|
|
47
|
-
self.abort_profile = epics_signal_rw(bool, prefix + "ProfileAbort")
|
|
51
|
+
self.abort_profile = epics_signal_x(prefix + "ProfileAbort")
|
|
48
52
|
self.profile_cs_name = epics_signal_rw(str, prefix + "ProfileCsName")
|
|
49
53
|
self.calculate_velocities = epics_signal_rw(bool, prefix + "ProfileCalcVel")
|
|
50
54
|
|
|
@@ -1,100 +1,201 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
|
-
from scanspec.core import Path
|
|
5
|
+
from scanspec.core import Path, Slice
|
|
5
6
|
from scanspec.specs import Spec
|
|
6
7
|
|
|
7
8
|
from ophyd_async.core import (
|
|
8
9
|
DEFAULT_TIMEOUT,
|
|
10
|
+
AsyncStatus,
|
|
9
11
|
FlyerController,
|
|
12
|
+
error_if_none,
|
|
13
|
+
observe_value,
|
|
10
14
|
set_and_wait_for_value,
|
|
11
15
|
wait_for_value,
|
|
12
16
|
)
|
|
13
17
|
from ophyd_async.epics.motor import Motor
|
|
14
18
|
from ophyd_async.epics.pmac import PmacIO
|
|
15
19
|
from ophyd_async.epics.pmac._pmac_io import CS_LETTERS
|
|
20
|
+
from ophyd_async.epics.pmac._pmac_trajectory_generation import PVT, Trajectory
|
|
16
21
|
from ophyd_async.epics.pmac._utils import (
|
|
17
22
|
_PmacMotorInfo,
|
|
18
|
-
_Trajectory,
|
|
19
23
|
calculate_ramp_position_and_duration,
|
|
20
24
|
)
|
|
21
25
|
|
|
26
|
+
# PMAC durations are in milliseconds
|
|
27
|
+
# We must convert from scanspec durations (seconds) to milliseconds
|
|
28
|
+
# PMAC motion program multiples durations by 0.001
|
|
29
|
+
# (see https://github.com/DiamondLightSource/pmac/blob/afe81f8bb9179c3a20eff351f30bc6cfd1539ad9/pmacApp/pmc/trajectory_scan_code_ppmac.pmc#L241)
|
|
30
|
+
# Therefore, we must divide scanspec durations by 10e-6
|
|
31
|
+
TICK_S = 0.000001
|
|
32
|
+
SLICE_SIZE = 4000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class PmacPrepareContext:
|
|
37
|
+
path: Path
|
|
38
|
+
motor_info: _PmacMotorInfo
|
|
39
|
+
ramp_up_time: float
|
|
40
|
+
|
|
22
41
|
|
|
23
42
|
class PmacTrajectoryTriggerLogic(FlyerController):
|
|
24
43
|
def __init__(self, pmac: PmacIO) -> None:
|
|
25
44
|
self.pmac = pmac
|
|
26
|
-
self.
|
|
45
|
+
self._next_pvt: PVT | None
|
|
46
|
+
self._loaded: int = 0
|
|
47
|
+
self._trajectory_status: AsyncStatus | None = None
|
|
48
|
+
self._prepare_context: PmacPrepareContext | None = None
|
|
27
49
|
|
|
28
50
|
async def prepare(self, value: Spec[Motor]):
|
|
29
|
-
|
|
30
|
-
|
|
51
|
+
path = Path(value.calculate())
|
|
52
|
+
slice = path.consume(SLICE_SIZE)
|
|
53
|
+
path_length = len(path)
|
|
54
|
+
motors = slice.axes()
|
|
55
|
+
motor_info = await _PmacMotorInfo.from_motors(self.pmac, motors)
|
|
31
56
|
ramp_up_pos, ramp_up_time = calculate_ramp_position_and_duration(
|
|
32
57
|
slice, motor_info, True
|
|
33
58
|
)
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
self._prepare_context = PmacPrepareContext(
|
|
60
|
+
path=path, motor_info=motor_info, ramp_up_time=ramp_up_time
|
|
36
61
|
)
|
|
37
|
-
trajectory = _Trajectory.from_slice(slice, ramp_up_time, motor_info)
|
|
38
62
|
await asyncio.gather(
|
|
39
|
-
self._build_trajectory(
|
|
40
|
-
trajectory, motor_info, ramp_down_pos, ramp_down_time
|
|
41
|
-
),
|
|
63
|
+
self._build_trajectory(motor_info, slice, path_length, ramp_up_time),
|
|
42
64
|
self._move_to_start(motor_info, ramp_up_pos),
|
|
43
65
|
)
|
|
44
66
|
|
|
45
67
|
async def kickoff(self):
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
prepare_context = error_if_none(
|
|
69
|
+
self._prepare_context, "Cannot kickoff. Must call prepare first."
|
|
70
|
+
)
|
|
71
|
+
self._prepare_context = None
|
|
72
|
+
self._trajectory_status = self._execute_trajectory(
|
|
73
|
+
prepare_context.path, prepare_context.motor_info
|
|
74
|
+
)
|
|
75
|
+
# Wait for the ramp up to happen
|
|
76
|
+
await wait_for_value(
|
|
77
|
+
self.pmac.trajectory.total_points,
|
|
78
|
+
lambda v: v >= 1,
|
|
79
|
+
prepare_context.ramp_up_time + DEFAULT_TIMEOUT,
|
|
50
80
|
)
|
|
51
81
|
|
|
52
82
|
async def complete(self):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await wait_for_value(
|
|
56
|
-
self.pmac.trajectory.execute_profile,
|
|
57
|
-
False,
|
|
58
|
-
timeout=self.scantime + DEFAULT_TIMEOUT,
|
|
83
|
+
trajectory_status = error_if_none(
|
|
84
|
+
self._trajectory_status, "Cannot complete. Must call kickoff first."
|
|
59
85
|
)
|
|
86
|
+
await trajectory_status
|
|
87
|
+
# Reset trajectory status and number of loaded points
|
|
88
|
+
self._trajectory_status = None
|
|
89
|
+
self._loaded = 0
|
|
60
90
|
|
|
61
91
|
async def stop(self):
|
|
62
|
-
await self.pmac.trajectory.abort_profile.
|
|
92
|
+
await self.pmac.trajectory.abort_profile.trigger()
|
|
93
|
+
|
|
94
|
+
@AsyncStatus.wrap
|
|
95
|
+
async def _execute_trajectory(self, path: Path, motor_info: _PmacMotorInfo):
|
|
96
|
+
execute_status = self.pmac.trajectory.execute_profile.set(
|
|
97
|
+
True, wait=True, timeout=None
|
|
98
|
+
)
|
|
99
|
+
# We consume SLICE_SIZE from self.path and parse a trajectory
|
|
100
|
+
# containing at least 2 * SLICE_SIZE, as a gapless trajectory
|
|
101
|
+
# will contain 2 points per slice frame. If gaps are present,
|
|
102
|
+
# additional points are inserted, overfilling the buffer.
|
|
103
|
+
min_buffer_size = SLICE_SIZE * 2
|
|
104
|
+
async for current_point in observe_value(
|
|
105
|
+
self.pmac.trajectory.total_points,
|
|
106
|
+
done_status=execute_status,
|
|
107
|
+
timeout=DEFAULT_TIMEOUT,
|
|
108
|
+
):
|
|
109
|
+
# Ensure we maintain a minimum buffer size, if we have more points to append
|
|
110
|
+
if len(path) != 0 and self._loaded - current_point < min_buffer_size:
|
|
111
|
+
# We have less than SLICE_SIZE * 2 points in the buffer, so refill
|
|
112
|
+
next_slice = path.consume(SLICE_SIZE)
|
|
113
|
+
path_length = len(path)
|
|
114
|
+
await self._append_trajectory(next_slice, path_length, motor_info)
|
|
115
|
+
|
|
116
|
+
async def _append_trajectory(
|
|
117
|
+
self, slice: Slice, path_length: int, motor_info: _PmacMotorInfo
|
|
118
|
+
):
|
|
119
|
+
trajectory = await self._parse_trajectory(slice, path_length, motor_info)
|
|
120
|
+
await self._set_trajectory_arrays(trajectory, motor_info)
|
|
121
|
+
await self.pmac.trajectory.append_profile.trigger()
|
|
63
122
|
|
|
64
123
|
async def _build_trajectory(
|
|
65
124
|
self,
|
|
66
|
-
trajectory: _Trajectory,
|
|
67
125
|
motor_info: _PmacMotorInfo,
|
|
68
|
-
|
|
69
|
-
|
|
126
|
+
slice: Slice,
|
|
127
|
+
path_length: int,
|
|
128
|
+
ramp_up_time: float,
|
|
70
129
|
):
|
|
71
|
-
trajectory =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await self.pmac.trajectory.positions[number + 1].set(
|
|
79
|
-
trajectory.positions[motor]
|
|
80
|
-
)
|
|
81
|
-
await self.pmac.trajectory.velocities[number + 1].set(
|
|
82
|
-
trajectory.velocities[motor]
|
|
83
|
-
)
|
|
84
|
-
size += len(trajectory.positions[motor])
|
|
130
|
+
trajectory = await self._parse_trajectory(
|
|
131
|
+
slice, path_length, motor_info, ramp_up_time
|
|
132
|
+
)
|
|
133
|
+
use_axis = {
|
|
134
|
+
axis + 1: (axis in motor_info.motor_cs_index.values())
|
|
135
|
+
for axis in range(len(CS_LETTERS))
|
|
136
|
+
}
|
|
85
137
|
|
|
86
138
|
coros = [
|
|
87
139
|
self.pmac.trajectory.profile_cs_name.set(motor_info.cs_port),
|
|
88
|
-
self.pmac.trajectory.time_array.set(trajectory.durations),
|
|
89
|
-
self.pmac.trajectory.user_array.set(trajectory.user_programs),
|
|
90
|
-
self.pmac.trajectory.points_to_build.set(size),
|
|
91
140
|
self.pmac.trajectory.calculate_velocities.set(False),
|
|
141
|
+
self._set_trajectory_arrays(trajectory, motor_info),
|
|
92
142
|
] + [
|
|
93
143
|
self.pmac.trajectory.use_axis[number].set(use)
|
|
94
144
|
for number, use in use_axis.items()
|
|
95
145
|
]
|
|
146
|
+
|
|
147
|
+
await asyncio.gather(*coros)
|
|
148
|
+
await self.pmac.trajectory.build_profile.trigger()
|
|
149
|
+
|
|
150
|
+
async def _parse_trajectory(
|
|
151
|
+
self,
|
|
152
|
+
slice: Slice,
|
|
153
|
+
path_length: int,
|
|
154
|
+
motor_info: _PmacMotorInfo,
|
|
155
|
+
ramp_up_time: float | None = None,
|
|
156
|
+
) -> Trajectory:
|
|
157
|
+
trajectory, exit_pvt = Trajectory.from_slice(
|
|
158
|
+
slice,
|
|
159
|
+
motor_info,
|
|
160
|
+
None if ramp_up_time else self._next_pvt,
|
|
161
|
+
ramp_up_time=ramp_up_time,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if path_length == 0:
|
|
165
|
+
ramp_down_pos, ramp_down_time = calculate_ramp_position_and_duration(
|
|
166
|
+
slice, motor_info, False
|
|
167
|
+
)
|
|
168
|
+
trajectory = trajectory.with_ramp_down(
|
|
169
|
+
exit_pvt, ramp_down_pos, ramp_down_time, 0
|
|
170
|
+
)
|
|
171
|
+
self._next_pvt = exit_pvt
|
|
172
|
+
self._loaded += len(trajectory)
|
|
173
|
+
|
|
174
|
+
return trajectory
|
|
175
|
+
|
|
176
|
+
async def _set_trajectory_arrays(
|
|
177
|
+
self, trajectory: Trajectory, motor_info: _PmacMotorInfo
|
|
178
|
+
):
|
|
179
|
+
coros = []
|
|
180
|
+
for motor, number in motor_info.motor_cs_index.items():
|
|
181
|
+
coros.append(
|
|
182
|
+
self.pmac.trajectory.positions[number + 1].set(
|
|
183
|
+
trajectory.positions[motor]
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
coros.append(
|
|
187
|
+
self.pmac.trajectory.velocities[number + 1].set(
|
|
188
|
+
trajectory.velocities[motor]
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
coros.extend(
|
|
192
|
+
[
|
|
193
|
+
self.pmac.trajectory.time_array.set(trajectory.durations / TICK_S),
|
|
194
|
+
self.pmac.trajectory.user_array.set(trajectory.user_programs),
|
|
195
|
+
self.pmac.trajectory.points_to_build.set(len(trajectory.durations)),
|
|
196
|
+
]
|
|
197
|
+
)
|
|
96
198
|
await asyncio.gather(*coros)
|
|
97
|
-
await self.pmac.trajectory.build_profile.set(True)
|
|
98
199
|
|
|
99
200
|
async def _move_to_start(
|
|
100
201
|
self, motor_info: _PmacMotorInfo, ramp_up_position: dict[Motor, np.float64]
|