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
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import numpy.typing as npt
|
|
10
|
+
from numpy import float64
|
|
11
|
+
from scanspec.core import Slice
|
|
12
|
+
from velocity_profile.velocityprofile import VelocityProfile
|
|
13
|
+
|
|
14
|
+
from ophyd_async.core import error_if_none
|
|
15
|
+
from ophyd_async.epics.motor import Motor
|
|
16
|
+
from ophyd_async.epics.pmac._utils import _PmacMotorInfo
|
|
17
|
+
|
|
18
|
+
MIN_TURNAROUND = 0.002
|
|
19
|
+
MIN_INTERVAL = 0.002
|
|
20
|
+
|
|
21
|
+
IndexType = TypeVar("IndexType", int, slice, npt.NDArray[np.int_], list[int])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UserProgram(IntEnum):
|
|
25
|
+
COLLECTION_WINDOW = 1 # Period within a collection window
|
|
26
|
+
GAP = 2 # Transition period between collection windows
|
|
27
|
+
END = 8 # Post-scan state
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PVT:
|
|
32
|
+
"""Represents a single position, velocity, and time point for multiple motors."""
|
|
33
|
+
|
|
34
|
+
position: dict[Motor, float64]
|
|
35
|
+
velocity: dict[Motor, float64]
|
|
36
|
+
time: float64
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def default(cls, motors: list[Motor]) -> PVT:
|
|
40
|
+
"""Initialise a PVT with zeros.
|
|
41
|
+
|
|
42
|
+
:param motors: List of motors
|
|
43
|
+
:returns: PVT instance with zero value placeholders
|
|
44
|
+
"""
|
|
45
|
+
return cls(
|
|
46
|
+
position={motor: np.float64(0.0) for motor in motors},
|
|
47
|
+
velocity={motor: np.float64(0.0) for motor in motors},
|
|
48
|
+
time=np.float64(0.0),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Trajectory:
|
|
54
|
+
positions: dict[Motor, npt.NDArray[np.float64]]
|
|
55
|
+
velocities: dict[Motor, npt.NDArray[np.float64]]
|
|
56
|
+
user_programs: npt.NDArray[np.int32]
|
|
57
|
+
durations: npt.NDArray[np.float64]
|
|
58
|
+
|
|
59
|
+
def __len__(self) -> int:
|
|
60
|
+
return len(self.user_programs)
|
|
61
|
+
|
|
62
|
+
def with_ramp_down(
|
|
63
|
+
self,
|
|
64
|
+
entry_pvt: PVT,
|
|
65
|
+
ramp_down_pos: dict[Motor, np.float64],
|
|
66
|
+
ramp_down_time: float,
|
|
67
|
+
ramp_down_velocity: float,
|
|
68
|
+
) -> Trajectory:
|
|
69
|
+
# Make room for additional two points to ramp down
|
|
70
|
+
# from a collection window upper to ramp down position
|
|
71
|
+
trajectory_length = len(self.user_programs)
|
|
72
|
+
total_length = trajectory_length + 2
|
|
73
|
+
motors = ramp_down_pos.keys()
|
|
74
|
+
|
|
75
|
+
positions = {motor: np.empty(total_length, float) for motor in motors}
|
|
76
|
+
velocities = {motor: np.empty(total_length, float) for motor in motors}
|
|
77
|
+
durations: npt.NDArray[np.float64] = np.empty(total_length, float)
|
|
78
|
+
user_programs: npt.NDArray[np.int32] = np.empty(total_length, int)
|
|
79
|
+
|
|
80
|
+
durations[:trajectory_length] = self.durations
|
|
81
|
+
durations[trajectory_length:] = [entry_pvt.time, ramp_down_time]
|
|
82
|
+
|
|
83
|
+
user_programs[:trajectory_length] = self.user_programs
|
|
84
|
+
user_programs[trajectory_length:] = [
|
|
85
|
+
UserProgram.COLLECTION_WINDOW,
|
|
86
|
+
UserProgram.END,
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
for motor in ramp_down_pos.keys():
|
|
90
|
+
positions[motor][:trajectory_length] = self.positions[motor]
|
|
91
|
+
positions[motor][trajectory_length:] = [
|
|
92
|
+
entry_pvt.position[motor],
|
|
93
|
+
ramp_down_pos[motor],
|
|
94
|
+
]
|
|
95
|
+
velocities[motor][:trajectory_length] = self.velocities[motor]
|
|
96
|
+
velocities[motor][trajectory_length:] = [
|
|
97
|
+
entry_pvt.velocity[motor],
|
|
98
|
+
ramp_down_velocity,
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
return Trajectory(
|
|
102
|
+
positions=positions,
|
|
103
|
+
velocities=velocities,
|
|
104
|
+
user_programs=user_programs,
|
|
105
|
+
durations=durations,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_slice(
|
|
110
|
+
cls,
|
|
111
|
+
slice: Slice[Motor],
|
|
112
|
+
motor_info: _PmacMotorInfo,
|
|
113
|
+
entry_pvt: PVT | None = None,
|
|
114
|
+
ramp_up_time: float | None = None,
|
|
115
|
+
) -> tuple[Trajectory, PVT]:
|
|
116
|
+
"""Parse a trajectory from a slice.
|
|
117
|
+
|
|
118
|
+
:param slice: Information about a series of scan frames along a number of axes
|
|
119
|
+
:param motor_info: Instance of _PmacMotorInfo
|
|
120
|
+
:param entry_pvt: PVT entering this trajectory
|
|
121
|
+
:param ramp_up_time: Time required to ramp up to speed
|
|
122
|
+
:returns Trajectory: Data class representing our parsed trajectory
|
|
123
|
+
"""
|
|
124
|
+
# List of indices into slice where gaps occur
|
|
125
|
+
gap_indices: list[int] = np.where(slice.gap)[0].tolist()
|
|
126
|
+
motors = slice.axes()
|
|
127
|
+
|
|
128
|
+
# Given a ramp up time is provided, we must be at a
|
|
129
|
+
# gap in the trajectory, that we can ramp up through
|
|
130
|
+
if (not gap_indices or gap_indices[0] != 0) and ramp_up_time:
|
|
131
|
+
raise RuntimeError(
|
|
132
|
+
"Slice must start with a gap, if ramp up time provided. "
|
|
133
|
+
f"Ramp up time given: {ramp_up_time}."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Given a ramp up time is provided, we need to construct the PVT
|
|
137
|
+
if (ramp_up_time is None) == (entry_pvt is None):
|
|
138
|
+
raise RuntimeError(
|
|
139
|
+
"Exactly one of ramp_up_time or entry_pvt must be provided."
|
|
140
|
+
f"Provided ramp up time: {ramp_up_time} and entry PVT: {entry_pvt}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Find start and end indices for collection windows
|
|
144
|
+
collection_windows = np.argwhere(
|
|
145
|
+
np.diff(~(slice.gap), prepend=False, append=False)
|
|
146
|
+
).reshape((-1, 2))
|
|
147
|
+
|
|
148
|
+
collection_window_iter = iter(collection_windows)
|
|
149
|
+
sub_traj_funcs = []
|
|
150
|
+
|
|
151
|
+
# Given we start at a collection window, insert it
|
|
152
|
+
if not gap_indices or gap_indices[0] != 0:
|
|
153
|
+
start, end = next(collection_window_iter)
|
|
154
|
+
sub_traj_funcs.append(
|
|
155
|
+
partial(
|
|
156
|
+
Trajectory.from_collection_window,
|
|
157
|
+
start,
|
|
158
|
+
end,
|
|
159
|
+
motors,
|
|
160
|
+
slice,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# For each gap, insert a gap, followed by a collection window
|
|
165
|
+
# given the distance to the next gap is greater than 1
|
|
166
|
+
for gap in gap_indices:
|
|
167
|
+
kwargs = {}
|
|
168
|
+
if gap == 0 and ramp_up_time:
|
|
169
|
+
kwargs["ramp_up_time"] = ramp_up_time
|
|
170
|
+
sub_traj_funcs.append(
|
|
171
|
+
partial(
|
|
172
|
+
Trajectory.from_gap,
|
|
173
|
+
motor_info,
|
|
174
|
+
gap,
|
|
175
|
+
motors,
|
|
176
|
+
slice,
|
|
177
|
+
**kwargs,
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
if gap != len(slice.gap) - 1 and not slice.gap[gap + 1]:
|
|
181
|
+
start, end = next(collection_window_iter)
|
|
182
|
+
sub_traj_funcs.append(
|
|
183
|
+
partial(
|
|
184
|
+
Trajectory.from_collection_window,
|
|
185
|
+
start,
|
|
186
|
+
end,
|
|
187
|
+
motors,
|
|
188
|
+
slice,
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
sub_trajectories: list[Trajectory] = []
|
|
193
|
+
# If no sub trajectories, initial frame is the end frame
|
|
194
|
+
exit_pvt = entry_pvt
|
|
195
|
+
for func in sub_traj_funcs:
|
|
196
|
+
# Build each sub trajectory, passing the last PVT
|
|
197
|
+
# to the next sub trajectory to build upon
|
|
198
|
+
# Explicitly defining initial and end PVTs
|
|
199
|
+
# to clearly show that the last PVT of a trajectory
|
|
200
|
+
# is passed as the first PVT of the next trajectory
|
|
201
|
+
traj, exit_pvt = func(entry_pvt)
|
|
202
|
+
entry_pvt = exit_pvt
|
|
203
|
+
sub_trajectories.append(traj)
|
|
204
|
+
|
|
205
|
+
# Combine sub trajectories
|
|
206
|
+
total_trajectory = Trajectory.from_trajectories(sub_trajectories, motors)
|
|
207
|
+
|
|
208
|
+
return total_trajectory, (exit_pvt or PVT.default(motors))
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def from_trajectories(
|
|
212
|
+
cls, sub_trajectories: list[Trajectory], motors: list[Motor]
|
|
213
|
+
) -> Trajectory:
|
|
214
|
+
"""Parse a trajectory from smaller strajectories.
|
|
215
|
+
|
|
216
|
+
:param sub_trajectories: List of trajectories to concatenate
|
|
217
|
+
:returns: Trajectory instance as concatenation of all sub trajectories
|
|
218
|
+
"""
|
|
219
|
+
# Initialise arrays to insert sub arrays into
|
|
220
|
+
total_points = sum(len(trajectory) for trajectory in sub_trajectories)
|
|
221
|
+
positions = {motor: np.empty(total_points, float) for motor in motors}
|
|
222
|
+
velocities = {motor: np.empty(total_points, float) for motor in motors}
|
|
223
|
+
durations: npt.NDArray[np.float64] = np.empty(total_points, float)
|
|
224
|
+
user_programs: npt.NDArray[np.int32] = np.empty(total_points, int)
|
|
225
|
+
|
|
226
|
+
# Keep track of where we are in overall trajectory
|
|
227
|
+
index_into_trajectory = 0
|
|
228
|
+
for trajectory in sub_trajectories:
|
|
229
|
+
# Insert sub trajectory arrays into overall trajectory arrays
|
|
230
|
+
durations[
|
|
231
|
+
index_into_trajectory : index_into_trajectory + len(trajectory)
|
|
232
|
+
] = trajectory.durations
|
|
233
|
+
user_programs[
|
|
234
|
+
index_into_trajectory : index_into_trajectory + len(trajectory)
|
|
235
|
+
] = trajectory.user_programs
|
|
236
|
+
for motor in motors:
|
|
237
|
+
positions[motor][
|
|
238
|
+
index_into_trajectory : index_into_trajectory + len(trajectory)
|
|
239
|
+
] = trajectory.positions[motor]
|
|
240
|
+
velocities[motor][
|
|
241
|
+
index_into_trajectory : index_into_trajectory + len(trajectory)
|
|
242
|
+
] = trajectory.velocities[motor]
|
|
243
|
+
# Update where we are in the overall trajectory
|
|
244
|
+
index_into_trajectory += len(trajectory)
|
|
245
|
+
|
|
246
|
+
trajectory = Trajectory(
|
|
247
|
+
positions=positions,
|
|
248
|
+
velocities=velocities,
|
|
249
|
+
durations=durations,
|
|
250
|
+
user_programs=user_programs,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return trajectory
|
|
254
|
+
|
|
255
|
+
@classmethod
|
|
256
|
+
def from_collection_window(
|
|
257
|
+
cls,
|
|
258
|
+
start: int,
|
|
259
|
+
end: int,
|
|
260
|
+
motors: list[Motor],
|
|
261
|
+
slice: Slice,
|
|
262
|
+
entry_pvt: PVT,
|
|
263
|
+
) -> tuple[Trajectory, PVT]:
|
|
264
|
+
"""Parse a trajectory from a collection window.
|
|
265
|
+
|
|
266
|
+
For all frames of the slice that fall within this window, this function will:
|
|
267
|
+
- Insert a sequence of lower → midpoint → lower → ... → midpoint points
|
|
268
|
+
until window ends
|
|
269
|
+
- Calculate and insert 3 point average velocities for these points, using the
|
|
270
|
+
entry PVT to blend with previous trajectories
|
|
271
|
+
|
|
272
|
+
:param start: Index into slice where collection window starts
|
|
273
|
+
:param end: Index into slice where collection window end
|
|
274
|
+
:param motors: List of motors involved in trajectory
|
|
275
|
+
:param slice: Information about a series of scan frames along a number of axes
|
|
276
|
+
:param entry_pvt: PVT entering this trajectory
|
|
277
|
+
:returns: Tuple of:
|
|
278
|
+
Trajectory instance encompassing collection window points
|
|
279
|
+
PVT at exit of collection window
|
|
280
|
+
"""
|
|
281
|
+
slice_duration = error_if_none(slice.duration, "Slice must have a duration")
|
|
282
|
+
half_durations = slice_duration / 2
|
|
283
|
+
if end > len(half_durations):
|
|
284
|
+
# Clamp collection window if no more frames
|
|
285
|
+
end = len(half_durations)
|
|
286
|
+
trajectory_size = 2 * len(half_durations[start:end])
|
|
287
|
+
positions = {motor: np.empty(trajectory_size, float) for motor in motors}
|
|
288
|
+
velocities = {motor: np.empty(trajectory_size, float) for motor in motors}
|
|
289
|
+
durations: npt.NDArray[np.float64] = np.empty(trajectory_size, float)
|
|
290
|
+
user_programs: npt.NDArray[np.int32] = np.empty(trajectory_size, int)
|
|
291
|
+
|
|
292
|
+
# Initialise exit PVT
|
|
293
|
+
exit_pvt = PVT.default(motors)
|
|
294
|
+
|
|
295
|
+
for motor in motors:
|
|
296
|
+
# Insert lower -> mid -> lower -> mid... positions
|
|
297
|
+
positions[motor][::2] = slice.lower[motor][start:end]
|
|
298
|
+
positions[motor][1::2] = slice.midpoints[motor][start:end]
|
|
299
|
+
|
|
300
|
+
# For velocities we will need the relative distances
|
|
301
|
+
mid_to_upper_velocities = (
|
|
302
|
+
slice.upper[motor][start:end] - slice.midpoints[motor][start:end]
|
|
303
|
+
) / half_durations[start:end]
|
|
304
|
+
lower_to_mid_velocities = (
|
|
305
|
+
slice.midpoints[motor][start:end] - slice.lower[motor][start:end]
|
|
306
|
+
) / half_durations[start:end]
|
|
307
|
+
|
|
308
|
+
# Smooth first lower point velocity with previous PVT
|
|
309
|
+
velocities[motor][0] = (
|
|
310
|
+
lower_to_mid_velocities[0] + entry_pvt.velocity[motor]
|
|
311
|
+
) / 2
|
|
312
|
+
|
|
313
|
+
# For the midpoints, we take the average of the
|
|
314
|
+
# lower -> mid and mid -> upper velocities of the same point
|
|
315
|
+
velocities[motor][1::2] = (
|
|
316
|
+
lower_to_mid_velocities + mid_to_upper_velocities
|
|
317
|
+
) / 2
|
|
318
|
+
|
|
319
|
+
# For the lower points, we need to take the average of the
|
|
320
|
+
# mid -> upper velocity of the previous point and
|
|
321
|
+
# lower -> mid velocity of the current point
|
|
322
|
+
velocities[motor][2::2] = (
|
|
323
|
+
mid_to_upper_velocities[:-1] + lower_to_mid_velocities[1:]
|
|
324
|
+
) / 2
|
|
325
|
+
|
|
326
|
+
# Exit PVT is the final upper point and its mid to upper velocity
|
|
327
|
+
exit_pvt.position[motor] = slice.upper[motor][end - 1]
|
|
328
|
+
exit_pvt.velocity[motor] = mid_to_upper_velocities[-1]
|
|
329
|
+
|
|
330
|
+
durations = np.repeat(
|
|
331
|
+
(half_durations[start:end]),
|
|
332
|
+
2,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
user_programs = np.repeat(UserProgram.COLLECTION_WINDOW, len(user_programs))
|
|
336
|
+
|
|
337
|
+
exit_pvt.time = half_durations[end - 1]
|
|
338
|
+
|
|
339
|
+
trajectory = Trajectory(
|
|
340
|
+
positions=positions,
|
|
341
|
+
velocities=velocities,
|
|
342
|
+
durations=durations,
|
|
343
|
+
user_programs=user_programs,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return trajectory, exit_pvt
|
|
347
|
+
|
|
348
|
+
@classmethod
|
|
349
|
+
def from_gap(
|
|
350
|
+
cls,
|
|
351
|
+
motor_info: _PmacMotorInfo,
|
|
352
|
+
gap: int,
|
|
353
|
+
motors: list[Motor],
|
|
354
|
+
slice: Slice,
|
|
355
|
+
entry_pvt: PVT,
|
|
356
|
+
ramp_up_time: float | None = None,
|
|
357
|
+
) -> tuple[Trajectory, PVT]:
|
|
358
|
+
"""Parse a trajectory from a gap.
|
|
359
|
+
|
|
360
|
+
This function will:
|
|
361
|
+
- Compute gap PVT points to bridge a previous and next collection window
|
|
362
|
+
- Insert the previous collecion windows upper point into the trajectory
|
|
363
|
+
- Insert the next collection windows first lower and midpoint
|
|
364
|
+
- Calculate a 2 point average velocity for the first lower and midpoint
|
|
365
|
+
- Produce an exit PVT of the next frames upper, midpoint-upper velocity, and
|
|
366
|
+
time
|
|
367
|
+
|
|
368
|
+
:param motor_info: Instance of _PmacMotorInfo
|
|
369
|
+
:param gap: Index into slice where gap must occur
|
|
370
|
+
:param motors: List of motors involved in trajectory
|
|
371
|
+
:param slice: Information about a series of scan frames along a number of axes
|
|
372
|
+
:param entry_pvt: PVT entering this trajectory
|
|
373
|
+
:returns: Tuple of:
|
|
374
|
+
Trajectory instance bridging previous and next collection windows with gap
|
|
375
|
+
PVT at the start of the next collection window
|
|
376
|
+
"""
|
|
377
|
+
slice_duration = error_if_none(slice.duration, "Slice must have a duration")
|
|
378
|
+
half_durations = slice_duration / 2
|
|
379
|
+
|
|
380
|
+
# Initialise exit PVT
|
|
381
|
+
exit_pvt = PVT.default(motors)
|
|
382
|
+
|
|
383
|
+
# Fill arrays for gap exit (start of next collection window)
|
|
384
|
+
end_positions = {motor: np.empty(2, dtype=np.float64) for motor in motors}
|
|
385
|
+
end_velocities = {motor: np.empty(2, dtype=np.float64) for motor in motors}
|
|
386
|
+
end_durations = np.empty(2, dtype=np.float64)
|
|
387
|
+
for motor in motors:
|
|
388
|
+
end_positions[motor][0] = slice.lower[motor][gap]
|
|
389
|
+
end_positions[motor][1] = slice.midpoints[motor][gap]
|
|
390
|
+
|
|
391
|
+
mid_to_upper_velocity = (
|
|
392
|
+
slice.upper[motor][gap] - slice.midpoints[motor][gap]
|
|
393
|
+
) / half_durations[gap]
|
|
394
|
+
lower_to_mid_velocity = (
|
|
395
|
+
slice.midpoints[motor][gap] - slice.lower[motor][gap]
|
|
396
|
+
) / half_durations[gap]
|
|
397
|
+
|
|
398
|
+
end_velocities[motor][0] = lower_to_mid_velocity
|
|
399
|
+
|
|
400
|
+
end_velocities[motor][1] = (
|
|
401
|
+
lower_to_mid_velocity + mid_to_upper_velocity
|
|
402
|
+
) / 2
|
|
403
|
+
end_durations[1] = half_durations[gap]
|
|
404
|
+
|
|
405
|
+
exit_pvt.position[motor] = slice.upper[motor][gap]
|
|
406
|
+
exit_pvt.velocity[motor] = mid_to_upper_velocity
|
|
407
|
+
|
|
408
|
+
exit_pvt.time = half_durations[gap]
|
|
409
|
+
|
|
410
|
+
# If we are ramping up, don't compute gap PVTs
|
|
411
|
+
if ramp_up_time:
|
|
412
|
+
end_durations[0] = ramp_up_time
|
|
413
|
+
return Trajectory(
|
|
414
|
+
positions=end_positions,
|
|
415
|
+
velocities=end_velocities,
|
|
416
|
+
durations=end_durations,
|
|
417
|
+
user_programs=np.array([1, 1], dtype=int),
|
|
418
|
+
), exit_pvt
|
|
419
|
+
|
|
420
|
+
entry_velocities = entry_pvt.velocity
|
|
421
|
+
exit_velocities = {
|
|
422
|
+
motor: (slice.midpoints[motor][gap] - slice.lower[motor][gap])
|
|
423
|
+
/ half_durations[gap]
|
|
424
|
+
for motor in motors
|
|
425
|
+
}
|
|
426
|
+
distances = {
|
|
427
|
+
motor: 0.0
|
|
428
|
+
if np.isclose(
|
|
429
|
+
(distance := slice.lower[motor][gap] - entry_pvt.position[motor]),
|
|
430
|
+
0.0,
|
|
431
|
+
atol=1e-12,
|
|
432
|
+
)
|
|
433
|
+
else distance
|
|
434
|
+
for motor in motors
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# Get velocity and time profiles across gap
|
|
438
|
+
time_arrays, velocity_arrays = _get_velocity_profile(
|
|
439
|
+
motors,
|
|
440
|
+
motor_info,
|
|
441
|
+
entry_velocities,
|
|
442
|
+
exit_velocities,
|
|
443
|
+
distances,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Calculate gap PVTs
|
|
447
|
+
gap_positions, gap_velocities, gap_durations = (
|
|
448
|
+
_calculate_profile_from_velocities(
|
|
449
|
+
motors,
|
|
450
|
+
entry_pvt,
|
|
451
|
+
time_arrays,
|
|
452
|
+
velocity_arrays,
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Initialise larger arrays for last collection windows
|
|
457
|
+
# final upper point, the gap points, and the next collection windows
|
|
458
|
+
# initial lower and midpoints
|
|
459
|
+
# gap_duration includes duration for next collection windows initial
|
|
460
|
+
# lower point, so array_size = len(gap_duration) + 2
|
|
461
|
+
positions = {
|
|
462
|
+
motor: np.empty(len(gap_durations) + 2, dtype=np.float64)
|
|
463
|
+
for motor in motors
|
|
464
|
+
}
|
|
465
|
+
velocities = {
|
|
466
|
+
motor: np.empty(len(gap_durations) + 2, dtype=np.float64)
|
|
467
|
+
for motor in motors
|
|
468
|
+
}
|
|
469
|
+
durations = np.empty(len(gap_durations) + 2, dtype=np.float64)
|
|
470
|
+
|
|
471
|
+
for motor in motors:
|
|
472
|
+
# Insert last collection windows upper point
|
|
473
|
+
positions[motor][0] = entry_pvt.position[motor]
|
|
474
|
+
velocities[motor][0] = entry_pvt.velocity[motor]
|
|
475
|
+
durations[0] = entry_pvt.time
|
|
476
|
+
|
|
477
|
+
# Insert gap information
|
|
478
|
+
positions[motor][1:-2] = gap_positions[motor]
|
|
479
|
+
velocities[motor][1:-2] = gap_velocities[motor]
|
|
480
|
+
durations[1:-1] = gap_durations
|
|
481
|
+
|
|
482
|
+
# Append first 2 points of next collection window
|
|
483
|
+
positions[motor][-2:] = end_positions[motor]
|
|
484
|
+
velocities[motor][-2:] = end_velocities[motor]
|
|
485
|
+
durations[-1] = end_durations[-1]
|
|
486
|
+
|
|
487
|
+
trajectory = Trajectory(
|
|
488
|
+
positions=positions,
|
|
489
|
+
velocities=velocities,
|
|
490
|
+
durations=durations,
|
|
491
|
+
user_programs=np.array(
|
|
492
|
+
[1] + [2] * (len(gap_durations) - 1) + [1, 1], dtype=int
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return trajectory, exit_pvt
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _get_velocity_profile(
|
|
500
|
+
motors: list[Motor],
|
|
501
|
+
motor_info: _PmacMotorInfo,
|
|
502
|
+
start_velocities: dict[Motor, np.float64],
|
|
503
|
+
end_velocities: dict[Motor, np.float64],
|
|
504
|
+
distances: dict[Motor, float],
|
|
505
|
+
) -> tuple[dict[Motor, npt.NDArray[np.float64]], dict[Motor, npt.NDArray[np.float64]]]:
|
|
506
|
+
"""Generate time and velocity profiles for motors across a gap.
|
|
507
|
+
|
|
508
|
+
For each motor, a `VelocityProfile` is constructed.
|
|
509
|
+
Profiles are iteratively recalculated to converge on a
|
|
510
|
+
consistent minimum total gap time across all motors.
|
|
511
|
+
|
|
512
|
+
This function will:
|
|
513
|
+
- Initialise with a minimum turnaround time (`MIN_TURNAROUND`).
|
|
514
|
+
- Build a velocity profile for each motor and determine the total
|
|
515
|
+
move time required.
|
|
516
|
+
- Update the minimum total gap time to the maximum of these totals.
|
|
517
|
+
- Repeat until all profiles agree on the same minimum time or an
|
|
518
|
+
iteration limit (i.e., 2) is reached.
|
|
519
|
+
|
|
520
|
+
:param motors: Sequence of motors involved in trajectory
|
|
521
|
+
:param motor_info: Instance of _PmacMotorInfo
|
|
522
|
+
:param start_velocities: Motor velocities at start of gap
|
|
523
|
+
:param end_velocities: Motor velocities at end of gap
|
|
524
|
+
:param distances: Motor distances required to travel accross gap
|
|
525
|
+
:raises ValueError: Cannot converge on common minimum time in 2 iterations
|
|
526
|
+
:returns tuple: A tuple containing:
|
|
527
|
+
dict: Motor's absolute timestamps of their velocity changes
|
|
528
|
+
dict: Motor's velocity changes
|
|
529
|
+
"""
|
|
530
|
+
profiles: dict[Motor, VelocityProfile] = {}
|
|
531
|
+
time_arrays = {}
|
|
532
|
+
velocity_arrays = {}
|
|
533
|
+
|
|
534
|
+
min_time = MIN_TURNAROUND
|
|
535
|
+
|
|
536
|
+
iterations = 2
|
|
537
|
+
|
|
538
|
+
while iterations > 0:
|
|
539
|
+
new_min_time = 0.0 # reset for this iteration
|
|
540
|
+
|
|
541
|
+
for motor in motors:
|
|
542
|
+
# Build profile for this motor
|
|
543
|
+
p = VelocityProfile(
|
|
544
|
+
start_velocities[motor],
|
|
545
|
+
end_velocities[motor],
|
|
546
|
+
distances[motor],
|
|
547
|
+
min_time,
|
|
548
|
+
motor_info.motor_acceleration_rate[motor],
|
|
549
|
+
motor_info.motor_max_velocity[motor],
|
|
550
|
+
0,
|
|
551
|
+
MIN_INTERVAL,
|
|
552
|
+
)
|
|
553
|
+
p.get_profile()
|
|
554
|
+
|
|
555
|
+
profiles[motor] = p
|
|
556
|
+
new_min_time = max(new_min_time, p.t_total)
|
|
557
|
+
|
|
558
|
+
# Check if all profiles have converged on min_time
|
|
559
|
+
if np.isclose(new_min_time, min_time):
|
|
560
|
+
for motor in motors:
|
|
561
|
+
time_arrays[motor], velocity_arrays[motor] = profiles[
|
|
562
|
+
motor
|
|
563
|
+
].make_arrays()
|
|
564
|
+
return time_arrays, velocity_arrays
|
|
565
|
+
else:
|
|
566
|
+
min_time = new_min_time
|
|
567
|
+
iterations -= 1 # Get profiles with new minimum turnaround
|
|
568
|
+
|
|
569
|
+
raise ValueError(
|
|
570
|
+
"Failed to get a consistent time when calculating velocity profiles."
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _calculate_profile_from_velocities(
|
|
575
|
+
motors: list[Motor],
|
|
576
|
+
entry_pvt: PVT,
|
|
577
|
+
time_arrays: dict[Motor, npt.NDArray[np.float64]],
|
|
578
|
+
velocity_arrays: dict[Motor, npt.NDArray[np.float64]],
|
|
579
|
+
) -> tuple[
|
|
580
|
+
dict[Motor, npt.NDArray[np.float64]],
|
|
581
|
+
dict[Motor, npt.NDArray[np.float64]],
|
|
582
|
+
list[float],
|
|
583
|
+
]:
|
|
584
|
+
"""Convert per-axis time/velocity profiles into aligned time/position profiles.
|
|
585
|
+
|
|
586
|
+
Given per-axis arrays of times and corresponding velocities,
|
|
587
|
+
this builds a single unified timeline containing all unique velocity change points
|
|
588
|
+
from every axis. It then steps through that timeline, and for each axis:
|
|
589
|
+
|
|
590
|
+
* If the current time matches one of the axis's own velocity change points,
|
|
591
|
+
use that known velocity.
|
|
592
|
+
* Otherwise, linearly interpolate between the axis's previous and next
|
|
593
|
+
known velocities, based on how far through that section we are.
|
|
594
|
+
|
|
595
|
+
At each unified time step, the velocity is integrated over the step duration
|
|
596
|
+
(using the trapezoidal rule) to update the axis's position. This produces
|
|
597
|
+
per-axis position and velocity arrays that are aligned to the same global
|
|
598
|
+
time grid.
|
|
599
|
+
|
|
600
|
+
Example:
|
|
601
|
+
combined_times = [0.1, 0.2, 0.3, 0.4]
|
|
602
|
+
axis_times[motor] = [0.1, 0.4]
|
|
603
|
+
velocity_array[motor] = [2, 5]
|
|
604
|
+
|
|
605
|
+
At 0.1 → known vel = 2 (use directly)
|
|
606
|
+
At 0.2 → 1/3 of the way to next vel → 3.0
|
|
607
|
+
At 0.3 → 2/3 of the way to next vel → 4.0
|
|
608
|
+
At 0.4 → known vel = 5 (use directly)
|
|
609
|
+
|
|
610
|
+
These instantaneous velocities are integrated over each Δt to yield
|
|
611
|
+
positions aligned with the global timeline.
|
|
612
|
+
|
|
613
|
+
:param motors: Sequence of motors involved in trajectory
|
|
614
|
+
:param slice: Information about a series of scan frames along a number of axes
|
|
615
|
+
:param gap: Index into slice where gap has occured
|
|
616
|
+
:param time_arrays: Motor's absolute timestamps of velocity changes
|
|
617
|
+
:param velocity_arrays: Motor's velocity changes
|
|
618
|
+
:returns GapSegment: Class representing a segment of a trajectory that is a gap
|
|
619
|
+
"""
|
|
620
|
+
# Combine all per-axis time points into a single sorted array of times
|
|
621
|
+
# This way we can evaluate each motor along the same timeline
|
|
622
|
+
# We know all axes positions at t=0, so we drop this point
|
|
623
|
+
combined_times = np.sort(np.unique(np.concatenate(list(time_arrays.values()))))[1:]
|
|
624
|
+
|
|
625
|
+
# We convert a list of t into a list of Δt
|
|
626
|
+
# We do this by substracting against previous cumulative time
|
|
627
|
+
time_intervals = np.diff(np.concatenate(([0.0], combined_times))).tolist()
|
|
628
|
+
|
|
629
|
+
# We also know all axes positions when t=t_final, so we drop this point
|
|
630
|
+
# However, we need the interval for the next collection window, so we store it
|
|
631
|
+
*time_intervals, final_interval = time_intervals
|
|
632
|
+
combined_times = combined_times[:-1]
|
|
633
|
+
num_intervals = len(time_intervals)
|
|
634
|
+
|
|
635
|
+
# Prepare dicts for the resulting position and velocity profiles over the gap
|
|
636
|
+
positions: dict[Motor, npt.NDArray[np.float64]] = {}
|
|
637
|
+
velocities: dict[Motor, npt.NDArray[np.float64]] = {}
|
|
638
|
+
|
|
639
|
+
# Loop over each motor and integrate its velocity profile over the unified times
|
|
640
|
+
for motor in motors:
|
|
641
|
+
axis_times = time_arrays[motor]
|
|
642
|
+
axis_velocities = velocity_arrays[motor]
|
|
643
|
+
axis_position = entry_pvt.position[
|
|
644
|
+
motor
|
|
645
|
+
] # start position at beginning of the gap
|
|
646
|
+
prev_interval_vel = axis_velocities[
|
|
647
|
+
0
|
|
648
|
+
] # last velocity seen from the previous global time interval
|
|
649
|
+
time_since_prev_axis_point = 0.0 # elapsed time since the last velocity point
|
|
650
|
+
axis_idx = 1 # index into this axis's velocity/time arrays
|
|
651
|
+
|
|
652
|
+
# Allocate output arrays for this motor with correct size
|
|
653
|
+
positions[motor] = np.empty(num_intervals, dtype=np.float64)
|
|
654
|
+
velocities[motor] = np.empty(num_intervals, dtype=np.float64)
|
|
655
|
+
|
|
656
|
+
# Step through each interval in the Δt list
|
|
657
|
+
for i, dt in enumerate(time_intervals):
|
|
658
|
+
next_vel = axis_velocities[axis_idx]
|
|
659
|
+
prev_vel = axis_velocities[axis_idx - 1]
|
|
660
|
+
axis_dt = axis_times[axis_idx] - axis_times[axis_idx - 1]
|
|
661
|
+
|
|
662
|
+
if np.isclose(combined_times[i], axis_times[axis_idx]):
|
|
663
|
+
# If the current combined time exactly matches this motor's
|
|
664
|
+
# next velocity change point, no interpolation is needed, so:
|
|
665
|
+
this_vel = next_vel
|
|
666
|
+
axis_idx += 1
|
|
667
|
+
time_since_prev_axis_point = 0.0
|
|
668
|
+
else:
|
|
669
|
+
# Otherwise, linearly interpolate velocity between the previous
|
|
670
|
+
# and next known velocity points for this motor
|
|
671
|
+
time_since_prev_axis_point += dt
|
|
672
|
+
# The fraction of the way we are from previous to next known velocities
|
|
673
|
+
# for this motor
|
|
674
|
+
frac = time_since_prev_axis_point / axis_dt
|
|
675
|
+
# Interpolate for our velocity
|
|
676
|
+
this_vel = prev_vel + frac * (next_vel - prev_vel)
|
|
677
|
+
|
|
678
|
+
# Integrate velocity over this interval to update position.
|
|
679
|
+
# Using the trapezoidal rule:
|
|
680
|
+
delta_pos = 0.5 * (prev_interval_vel + this_vel) * dt
|
|
681
|
+
axis_position += delta_pos
|
|
682
|
+
prev_interval_vel = this_vel # update for next loop
|
|
683
|
+
|
|
684
|
+
# Store the computed position and velocity for this interval
|
|
685
|
+
positions[motor][i] = axis_position
|
|
686
|
+
velocities[motor][i] = this_vel
|
|
687
|
+
|
|
688
|
+
return (
|
|
689
|
+
positions,
|
|
690
|
+
velocities,
|
|
691
|
+
time_intervals + [final_interval],
|
|
692
|
+
)
|