ophyd-async 0.13.3__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/pmac/_pmac_io.py +8 -4
- ophyd_async/epics/pmac/_pmac_trajectory.py +144 -41
- 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.3.dist-info → ophyd_async-0.13.4.dist-info}/METADATA +1 -1
- {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.4.dist-info}/RECORD +19 -18
- {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.4.dist-info}/WHEEL +0 -0
- {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.4.dist-info}/licenses/LICENSE +0 -0
- {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.4.dist-info}/top_level.txt +0 -0
ophyd_async/epics/pmac/_utils.py
CHANGED
|
@@ -3,16 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from collections.abc import Sequence
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from enum import IntEnum
|
|
7
|
-
from typing import Protocol
|
|
8
6
|
|
|
9
7
|
import numpy as np
|
|
10
|
-
import numpy.typing as npt
|
|
11
|
-
from numpy import float64
|
|
12
8
|
from scanspec.core import Slice
|
|
13
|
-
from velocity_profile.velocityprofile import VelocityProfile
|
|
14
9
|
|
|
15
|
-
from ophyd_async.core import
|
|
10
|
+
from ophyd_async.core import gather_dict
|
|
16
11
|
from ophyd_async.epics.motor import Motor
|
|
17
12
|
|
|
18
13
|
from ._pmac_io import CS_LETTERS, PmacIO
|
|
@@ -27,407 +22,6 @@ MIN_TURNAROUND = 0.002
|
|
|
27
22
|
MIN_INTERVAL = 0.002
|
|
28
23
|
|
|
29
24
|
|
|
30
|
-
class UserProgram(IntEnum):
|
|
31
|
-
COLLECTION_WINDOW = 1 # Period within a collection window
|
|
32
|
-
GAP = 2 # Transition period between collection windows
|
|
33
|
-
END = 8 # Post-scan state
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class FillableSegment(Protocol):
|
|
37
|
-
"""Protocol for trajectory segments that can insert their data into a trajectory.
|
|
38
|
-
|
|
39
|
-
A 'FillableSegment' represents a contiguous portion of a trajectory
|
|
40
|
-
that knows how to:
|
|
41
|
-
- Insert its motor positions and velocities into a '_Trajectory'.
|
|
42
|
-
- Insert its durations and associated user programs into a '_Trajectory'.
|
|
43
|
-
- Report its own length.
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
def insert_positions_and_velocities_into_trajectory(
|
|
47
|
-
self,
|
|
48
|
-
index_into_trajectory: int,
|
|
49
|
-
trajectory: _Trajectory,
|
|
50
|
-
motor: Motor,
|
|
51
|
-
) -> None:
|
|
52
|
-
"""Insert segment's positions and velocities for a given motor.
|
|
53
|
-
|
|
54
|
-
:param index_into_trajectory: Index into the trajectory this segment is inserted
|
|
55
|
-
:param trajectory: Instance of '_Trajectory' that will be populated
|
|
56
|
-
:param motor: Motor we are populating '_Trajectory' for
|
|
57
|
-
"""
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
def insert_durations_and_user_programs_into_trajectory(
|
|
61
|
-
self,
|
|
62
|
-
index_into_trajectory: int,
|
|
63
|
-
trajectory: _Trajectory,
|
|
64
|
-
) -> None:
|
|
65
|
-
"""Insert segment's durations and user programs.
|
|
66
|
-
|
|
67
|
-
:param index_into_trajectory: Index into the trajectory this segment is inserted
|
|
68
|
-
:param trajectory: Instance of '_Trajectory' that will be populated
|
|
69
|
-
"""
|
|
70
|
-
pass
|
|
71
|
-
|
|
72
|
-
def __len__(self) -> int:
|
|
73
|
-
"""Return the number of trajectory points in this segment."""
|
|
74
|
-
...
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class GapSegment:
|
|
78
|
-
def __init__(
|
|
79
|
-
self,
|
|
80
|
-
positions: dict[Motor, np.ndarray],
|
|
81
|
-
velocities: dict[Motor, np.ndarray],
|
|
82
|
-
duration: list[float],
|
|
83
|
-
):
|
|
84
|
-
"""Represents a gap between collection windows in a trajectory.
|
|
85
|
-
|
|
86
|
-
:param positions: Motor to gap positions
|
|
87
|
-
:param velocities: Motor to gap velocities
|
|
88
|
-
:param duration: List of time required to achieve gap point
|
|
89
|
-
"""
|
|
90
|
-
self.positions = positions
|
|
91
|
-
self.velocities = velocities
|
|
92
|
-
self.duration = duration
|
|
93
|
-
|
|
94
|
-
def __len__(self):
|
|
95
|
-
# Gap length is the number of per-axis position points
|
|
96
|
-
# This number is identical for all motors
|
|
97
|
-
# as all motors follow a unified timeline through a gap
|
|
98
|
-
return next(iter(self.positions.values())).shape[0]
|
|
99
|
-
|
|
100
|
-
def insert_positions_and_velocities_into_trajectory(
|
|
101
|
-
self,
|
|
102
|
-
index_into_trajectory: int,
|
|
103
|
-
trajectory: _Trajectory,
|
|
104
|
-
motor: Motor,
|
|
105
|
-
):
|
|
106
|
-
"""Inserts gap positions and velocities into the trajectory.
|
|
107
|
-
|
|
108
|
-
This function will populate a '_Trajectory' with the current 'GapSegment's
|
|
109
|
-
pre-computed positions and velocities for a given motor.
|
|
110
|
-
"""
|
|
111
|
-
num_gap_points = self.__len__()
|
|
112
|
-
# Update how many gap points we've added so far
|
|
113
|
-
# Insert gap points into end of collection window
|
|
114
|
-
trajectory.positions[motor][
|
|
115
|
-
index_into_trajectory : index_into_trajectory + num_gap_points
|
|
116
|
-
] = self.positions[motor]
|
|
117
|
-
trajectory.velocities[motor][
|
|
118
|
-
index_into_trajectory : index_into_trajectory + num_gap_points
|
|
119
|
-
] = self.velocities[motor]
|
|
120
|
-
|
|
121
|
-
def insert_durations_and_user_programs_into_trajectory(
|
|
122
|
-
self,
|
|
123
|
-
index_into_trajectory: int,
|
|
124
|
-
trajectory: _Trajectory,
|
|
125
|
-
) -> None:
|
|
126
|
-
"""Inserts gap durations and user programs into the trajectory.
|
|
127
|
-
|
|
128
|
-
This function will populate a '_Trajectory' with the current 'GapSegment's
|
|
129
|
-
pre-computed durations and set user programs to 'UserProgram.GAP'.
|
|
130
|
-
"""
|
|
131
|
-
num_gap_points = self.__len__()
|
|
132
|
-
# We append an extra duration (i.e., num_gap_points + 1)
|
|
133
|
-
# This is because we need to insert the duration it takes
|
|
134
|
-
# to get from the final gap point to the next collection window point
|
|
135
|
-
# This duration is calculated alongside gaps so is inserted here for
|
|
136
|
-
# the next collection window
|
|
137
|
-
trajectory.durations[
|
|
138
|
-
index_into_trajectory : index_into_trajectory + num_gap_points + 1
|
|
139
|
-
] = (np.array(self.duration) / TICK_S).astype(int)
|
|
140
|
-
|
|
141
|
-
trajectory.user_programs[
|
|
142
|
-
index_into_trajectory : index_into_trajectory + num_gap_points
|
|
143
|
-
] = UserProgram.GAP
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
class CollectionWindow:
|
|
147
|
-
def __init__(
|
|
148
|
-
self, start: int, end: int, slice: Slice, half_durations: npt.NDArray[float64]
|
|
149
|
-
):
|
|
150
|
-
"""Represents a collection window in a trajectory.
|
|
151
|
-
|
|
152
|
-
:param start: Index into slice where this collection window starts
|
|
153
|
-
:param end: Index into slice where this collection window ends
|
|
154
|
-
:param slice: Information about a series of scan frames along a number of axes
|
|
155
|
-
:param half_durations: Array of half the time required to get to a frame
|
|
156
|
-
"""
|
|
157
|
-
self.start = start
|
|
158
|
-
self.end = end
|
|
159
|
-
self.slice = slice
|
|
160
|
-
self.half_durations = half_durations
|
|
161
|
-
|
|
162
|
-
def __len__(self):
|
|
163
|
-
return ((self.end - self.start) * 2) + 1
|
|
164
|
-
|
|
165
|
-
def insert_positions_and_velocities_into_trajectory(
|
|
166
|
-
self,
|
|
167
|
-
index_into_trajectory: int,
|
|
168
|
-
trajectory: _Trajectory,
|
|
169
|
-
motor: Motor,
|
|
170
|
-
):
|
|
171
|
-
"""Inserts collection window positions and velocities into the trajectory.
|
|
172
|
-
|
|
173
|
-
For all frames of the slice that fall within this window, this function will:
|
|
174
|
-
- Insert an initial lower point to the trajectory
|
|
175
|
-
- Insert a sequence of midpoint → upper → midpoint → ... → upper points
|
|
176
|
-
until window ends
|
|
177
|
-
- Calculate and insert a 2 point average velocity from the initial
|
|
178
|
-
lower → midpoint (for the first velocity) and from the final
|
|
179
|
-
midpoint → upper (for last velocity)
|
|
180
|
-
- Calculate and insert 3 point average velocities from intermediate
|
|
181
|
-
points, as these points have a previous and next point to use in the
|
|
182
|
-
calculation
|
|
183
|
-
"""
|
|
184
|
-
window_start_idx = index_into_trajectory
|
|
185
|
-
window_end_idx = index_into_trajectory + self.__len__()
|
|
186
|
-
|
|
187
|
-
# Lower bound at the segment start
|
|
188
|
-
trajectory.positions[motor][window_start_idx] = self.slice.lower[motor][
|
|
189
|
-
self.start
|
|
190
|
-
]
|
|
191
|
-
|
|
192
|
-
# Fill mids into odd slots, uppers into even slots
|
|
193
|
-
trajectory.positions[motor][window_start_idx + 1 : window_end_idx : 2] = (
|
|
194
|
-
self.slice.midpoints[motor][self.start : self.end]
|
|
195
|
-
)
|
|
196
|
-
trajectory.positions[motor][window_start_idx + 2 : window_end_idx : 2] = (
|
|
197
|
-
self.slice.upper[motor][self.start : self.end]
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
# For velocities we will need the relative distances
|
|
201
|
-
mid_to_upper_velocities = (
|
|
202
|
-
self.slice.upper[motor][self.start : self.end]
|
|
203
|
-
- self.slice.midpoints[motor][self.start : self.end]
|
|
204
|
-
) / self.half_durations[self.start : self.end]
|
|
205
|
-
lower_to_mid_velocities = (
|
|
206
|
-
self.slice.midpoints[motor][self.start : self.end]
|
|
207
|
-
- self.slice.lower[motor][self.start : self.end]
|
|
208
|
-
) / self.half_durations[self.start : self.end]
|
|
209
|
-
|
|
210
|
-
# First velocity is the lower -> mid of first point
|
|
211
|
-
trajectory.velocities[motor][window_start_idx] = lower_to_mid_velocities[0]
|
|
212
|
-
|
|
213
|
-
# For the midpoints, we take the average of the
|
|
214
|
-
# lower -> mid and mid -> upper velocities of the same point
|
|
215
|
-
trajectory.velocities[motor][window_start_idx + 1 : window_end_idx : 2] = (
|
|
216
|
-
lower_to_mid_velocities + mid_to_upper_velocities
|
|
217
|
-
) / 2
|
|
218
|
-
|
|
219
|
-
# For the upper points, we need to take the average of the
|
|
220
|
-
# mid -> upper velocity of the previous point and
|
|
221
|
-
# lower -> mid velocity of the current point
|
|
222
|
-
trajectory.velocities[motor][
|
|
223
|
-
window_start_idx + 2 : (window_end_idx) - 1 : 2
|
|
224
|
-
] = (mid_to_upper_velocities[:-1] + lower_to_mid_velocities[1:]) / 2
|
|
225
|
-
|
|
226
|
-
# For the last velocity take the mid to upper velocity
|
|
227
|
-
trajectory.velocities[motor][window_end_idx - 1] = mid_to_upper_velocities[-1]
|
|
228
|
-
|
|
229
|
-
def insert_durations_and_user_programs_into_trajectory(
|
|
230
|
-
self,
|
|
231
|
-
index_into_trajectory: int,
|
|
232
|
-
trajectory: _Trajectory,
|
|
233
|
-
) -> None:
|
|
234
|
-
"""Inserts collection window durations and user programs into the trajectory.
|
|
235
|
-
|
|
236
|
-
For all frames from the slice that fall within this window, this function will:
|
|
237
|
-
- Insert a half duration for each midpoint and upper in the window,
|
|
238
|
-
where a full duration is the duration of the corresponding slice frame
|
|
239
|
-
- Insert user programs into the window as 'UserProgram.COLLECTION_WINDOW'
|
|
240
|
-
|
|
241
|
-
Note: this function will not insert the first duration of the window,
|
|
242
|
-
as this must be filled in by preceding ramp up duration or a gap segment
|
|
243
|
-
"""
|
|
244
|
-
window_start_idx = index_into_trajectory
|
|
245
|
-
window_end_idx = index_into_trajectory + self.__len__()
|
|
246
|
-
|
|
247
|
-
trajectory.durations[window_start_idx + 1 : window_end_idx] = np.repeat(
|
|
248
|
-
(self.half_durations[self.start : self.end] / TICK_S).astype(int),
|
|
249
|
-
2,
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
trajectory.user_programs[window_start_idx:window_end_idx] = (
|
|
253
|
-
UserProgram.COLLECTION_WINDOW
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
@dataclass
|
|
258
|
-
class _Trajectory:
|
|
259
|
-
positions: dict[Motor, np.ndarray]
|
|
260
|
-
velocities: dict[Motor, np.ndarray]
|
|
261
|
-
user_programs: npt.NDArray[np.int32]
|
|
262
|
-
durations: npt.NDArray[np.float64]
|
|
263
|
-
|
|
264
|
-
@classmethod
|
|
265
|
-
def from_slice(
|
|
266
|
-
cls, slice: Slice[Motor], ramp_up_time: float, motor_info: _PmacMotorInfo
|
|
267
|
-
) -> _Trajectory:
|
|
268
|
-
"""Parse a trajectory from a slice.
|
|
269
|
-
|
|
270
|
-
:param slice: Information about a series of scan frames along a number of axes
|
|
271
|
-
:param ramp_up_duration: Time required to ramp up to speed
|
|
272
|
-
:param ramp_down: Booleon representing if we ramp down or not
|
|
273
|
-
:returns Trajectory: Data class representing our parsed trajectory
|
|
274
|
-
:raises RuntimeError: Slice must a duration array
|
|
275
|
-
"""
|
|
276
|
-
slice_duration = error_if_none(slice.duration, "Slice must have a duration")
|
|
277
|
-
half_durations = slice_duration / 2
|
|
278
|
-
|
|
279
|
-
scan_size = len(slice)
|
|
280
|
-
# List of indices of the first frame of a collection window, after a gap
|
|
281
|
-
collection_window_idxs: list[int] = np.where(slice.gap)[0].tolist()
|
|
282
|
-
motors = slice.axes()
|
|
283
|
-
|
|
284
|
-
# Precompute gaps
|
|
285
|
-
# We structure our trajectory as a list of collection windows and gap segments
|
|
286
|
-
segments: list[FillableSegment] = []
|
|
287
|
-
total_gap_points = 0
|
|
288
|
-
for collection_start_idx, collection_end_idx in zip(
|
|
289
|
-
collection_window_idxs[:-1], collection_window_idxs[1:], strict=False
|
|
290
|
-
):
|
|
291
|
-
# Add a collection window that we will fill later
|
|
292
|
-
segments.append(
|
|
293
|
-
CollectionWindow(
|
|
294
|
-
start=collection_start_idx,
|
|
295
|
-
end=collection_end_idx,
|
|
296
|
-
slice=slice,
|
|
297
|
-
half_durations=half_durations,
|
|
298
|
-
)
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
# Now we precompute our gaps
|
|
302
|
-
# Get entry velocities, exit velocities, and distances across gap
|
|
303
|
-
entry_velocities, exit_velocities, distances = (
|
|
304
|
-
_get_entry_and_exit_velocities(
|
|
305
|
-
motors, slice, half_durations, collection_end_idx
|
|
306
|
-
)
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
# Get velocity and time profiles across gap
|
|
310
|
-
time_arrays, velocity_arrays = _get_velocity_profile(
|
|
311
|
-
motors, motor_info, entry_velocities, exit_velocities, distances
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
gap_segment = _calculate_profile_from_velocities(
|
|
315
|
-
motors,
|
|
316
|
-
slice,
|
|
317
|
-
collection_end_idx,
|
|
318
|
-
time_arrays,
|
|
319
|
-
velocity_arrays,
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
# Add a gap segment
|
|
323
|
-
segments.append(gap_segment)
|
|
324
|
-
|
|
325
|
-
total_gap_points += len(gap_segment)
|
|
326
|
-
|
|
327
|
-
# "collection_windows" does not include final window,
|
|
328
|
-
# as no subsequeny gap to mark its termination
|
|
329
|
-
# So, we add it here, ending it at the end of the slice
|
|
330
|
-
segments.append(
|
|
331
|
-
CollectionWindow(
|
|
332
|
-
collection_window_idxs[-1], len(slice), slice, half_durations
|
|
333
|
-
)
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
positions: dict[Motor, npt.NDArray[np.float64]] = {}
|
|
337
|
-
velocities: dict[Motor, npt.NDArray[np.float64]] = {}
|
|
338
|
-
|
|
339
|
-
# Initialise arrays
|
|
340
|
-
# Trajectory size calculated from 2 points per frame (midpoint and upper)
|
|
341
|
-
# Plus an initial lower point at the start of every collection window
|
|
342
|
-
# Plus PVT points added between collection windows (gaps)
|
|
343
|
-
trajectory_size = (
|
|
344
|
-
2 * scan_size + len(collection_window_idxs)
|
|
345
|
-
) + total_gap_points
|
|
346
|
-
positions = {motor: np.empty(trajectory_size, float) for motor in motors}
|
|
347
|
-
velocities = {motor: np.empty(trajectory_size, float) for motor in motors}
|
|
348
|
-
durations: npt.NDArray[np.float64] = np.empty(trajectory_size, float)
|
|
349
|
-
user_programs: npt.NDArray[np.int32] = np.empty(trajectory_size, int)
|
|
350
|
-
# Ramp up time for start of collection window
|
|
351
|
-
durations[0] = int(ramp_up_time / TICK_S)
|
|
352
|
-
|
|
353
|
-
# Pass initialised arrays into a _Trajectory, that we fill later on
|
|
354
|
-
trajectory = cls(
|
|
355
|
-
positions=positions,
|
|
356
|
-
velocities=velocities,
|
|
357
|
-
durations=durations,
|
|
358
|
-
user_programs=user_programs,
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
# Fill trajectory
|
|
362
|
-
# Start by filling durations and user_programs once
|
|
363
|
-
# as this is identical for all motors of a trajectory
|
|
364
|
-
# Index keeps track of where we are in the trajectory
|
|
365
|
-
index_into_trajectory = 0
|
|
366
|
-
# Iterate over collection windows or gaps
|
|
367
|
-
for segment in segments:
|
|
368
|
-
# This inserts slice or gap durations and user_programs
|
|
369
|
-
# into the output trajectory
|
|
370
|
-
segment.insert_durations_and_user_programs_into_trajectory(
|
|
371
|
-
index_into_trajectory=index_into_trajectory,
|
|
372
|
-
trajectory=trajectory,
|
|
373
|
-
)
|
|
374
|
-
# The length of each segment moves the trajectory index
|
|
375
|
-
index_into_trajectory += len(segment)
|
|
376
|
-
# Now fill positions and velocities for each motor
|
|
377
|
-
for motor in motors:
|
|
378
|
-
# Reset index for positions and velocities, for each motor
|
|
379
|
-
index_into_trajectory = 0
|
|
380
|
-
for segment in segments:
|
|
381
|
-
# This inserts slice or gap positions and velocites
|
|
382
|
-
# into the output trajectory
|
|
383
|
-
segment.insert_positions_and_velocities_into_trajectory(
|
|
384
|
-
index_into_trajectory=index_into_trajectory,
|
|
385
|
-
trajectory=trajectory,
|
|
386
|
-
motor=motor,
|
|
387
|
-
)
|
|
388
|
-
index_into_trajectory += len(segment)
|
|
389
|
-
|
|
390
|
-
# Check that calculated velocities do not exceed motor's max velocity
|
|
391
|
-
velocities_above_limit_mask = (
|
|
392
|
-
np.abs(trajectory.velocities[motor])
|
|
393
|
-
- motor_info.motor_max_velocity[motor]
|
|
394
|
-
) / motor_info.motor_max_velocity[motor] >= 1e-6
|
|
395
|
-
if velocities_above_limit_mask.any():
|
|
396
|
-
# Velocities above motor max velocity
|
|
397
|
-
bad_velocities = trajectory.velocities[motor][
|
|
398
|
-
velocities_above_limit_mask
|
|
399
|
-
]
|
|
400
|
-
# Indices in trajectory above motor max velocity
|
|
401
|
-
# np.nonzero returns tuple, but as only one mask passed, we need index 0
|
|
402
|
-
indices_to_bad_velocities = np.nonzero(velocities_above_limit_mask)[0]
|
|
403
|
-
raise ValueError(
|
|
404
|
-
f"{motor.name} velocity exceeds motor's max velocity of "
|
|
405
|
-
f"{motor_info.motor_max_velocity[motor]} "
|
|
406
|
-
f"at trajectory indices {indices_to_bad_velocities.tolist()}: "
|
|
407
|
-
f"{bad_velocities}"
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
return trajectory
|
|
411
|
-
|
|
412
|
-
def append_ramp_down(
|
|
413
|
-
self,
|
|
414
|
-
ramp_down_pos: dict[Motor, np.float64],
|
|
415
|
-
ramp_down_time: float,
|
|
416
|
-
ramp_down_velocity: float,
|
|
417
|
-
) -> _Trajectory:
|
|
418
|
-
self.durations = np.append(self.durations, [int(ramp_down_time / TICK_S)])
|
|
419
|
-
self.user_programs = np.append(self.user_programs, UserProgram.END)
|
|
420
|
-
for motor in ramp_down_pos.keys():
|
|
421
|
-
self.positions[motor] = np.append(
|
|
422
|
-
self.positions[motor], [ramp_down_pos[motor]]
|
|
423
|
-
)
|
|
424
|
-
self.velocities[motor] = np.append(
|
|
425
|
-
self.velocities[motor], [ramp_down_velocity]
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
return self
|
|
429
|
-
|
|
430
|
-
|
|
431
25
|
@dataclass
|
|
432
26
|
class _PmacMotorInfo:
|
|
433
27
|
cs_port: str
|
|
@@ -571,277 +165,3 @@ def calculate_ramp_position_and_duration(
|
|
|
571
165
|
motor_to_ramp_position[axis] = ref_pos + sign * displacement
|
|
572
166
|
|
|
573
167
|
return (motor_to_ramp_position, max_ramp_time)
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
def _get_velocity_profile(
|
|
577
|
-
motors: list[Motor],
|
|
578
|
-
motor_info: _PmacMotorInfo,
|
|
579
|
-
start_velocities: dict[Motor, np.float64],
|
|
580
|
-
end_velocities: dict[Motor, np.float64],
|
|
581
|
-
distances: dict[Motor, float],
|
|
582
|
-
) -> tuple[dict[Motor, npt.NDArray[np.float64]], dict[Motor, npt.NDArray[np.float64]]]:
|
|
583
|
-
"""Generate time and velocity profiles for motors across a gap.
|
|
584
|
-
|
|
585
|
-
For each motor, a `VelocityProfile` is constructed.
|
|
586
|
-
Profiles are iteratively recalculated to converge on a
|
|
587
|
-
consistent minimum total gap time across all motors.
|
|
588
|
-
|
|
589
|
-
This function will:
|
|
590
|
-
- Initialise with a minimum turnaround time (`MIN_TURNAROUND`).
|
|
591
|
-
- Build a velocity profile for each motor and determine the total
|
|
592
|
-
move time required.
|
|
593
|
-
- Update the minimum total gap time to the maximum of these totals.
|
|
594
|
-
- Repeat until all profiles agree on the same minimum time or an
|
|
595
|
-
iteration limit (i.e., 2) is reached.
|
|
596
|
-
|
|
597
|
-
:param motors: Sequence of motors involved in trajectory
|
|
598
|
-
:param motor_info: Instance of _PmacMotorInfo
|
|
599
|
-
:param start_velocities: Motor velocities at start of gap
|
|
600
|
-
:param end_velocities: Motor velocities at end of gap
|
|
601
|
-
:param distances: Motor distances required to travel accross gap
|
|
602
|
-
:raises ValueError: Cannot converge on common minimum time in 2 iterations
|
|
603
|
-
:returns tuple: A tuple containing:
|
|
604
|
-
dict: Motor's absolute timestamps of their velocity changes
|
|
605
|
-
dict: Motor's velocity changes
|
|
606
|
-
"""
|
|
607
|
-
profiles: dict[Motor, VelocityProfile] = {}
|
|
608
|
-
time_arrays = {}
|
|
609
|
-
velocity_arrays = {}
|
|
610
|
-
|
|
611
|
-
min_time = MIN_TURNAROUND
|
|
612
|
-
iterations = 2
|
|
613
|
-
|
|
614
|
-
while iterations > 0:
|
|
615
|
-
new_min_time = 0.0 # reset for this iteration
|
|
616
|
-
|
|
617
|
-
for motor in motors:
|
|
618
|
-
# Build profile for this motor
|
|
619
|
-
p = VelocityProfile(
|
|
620
|
-
start_velocities[motor],
|
|
621
|
-
end_velocities[motor],
|
|
622
|
-
distances[motor],
|
|
623
|
-
min_time,
|
|
624
|
-
motor_info.motor_acceleration_rate[motor],
|
|
625
|
-
motor_info.motor_max_velocity[motor],
|
|
626
|
-
0,
|
|
627
|
-
MIN_INTERVAL,
|
|
628
|
-
)
|
|
629
|
-
p.get_profile()
|
|
630
|
-
|
|
631
|
-
profiles[motor] = p
|
|
632
|
-
new_min_time = max(new_min_time, p.t_total)
|
|
633
|
-
|
|
634
|
-
# Check if all profiles have converged on min_time
|
|
635
|
-
if np.isclose(new_min_time, min_time):
|
|
636
|
-
for motor in motors:
|
|
637
|
-
time_arrays[motor], velocity_arrays[motor] = profiles[
|
|
638
|
-
motor
|
|
639
|
-
].make_arrays()
|
|
640
|
-
return time_arrays, velocity_arrays
|
|
641
|
-
else:
|
|
642
|
-
min_time = new_min_time
|
|
643
|
-
iterations -= 1 # Get profiles with new minimum turnaround
|
|
644
|
-
|
|
645
|
-
raise ValueError(
|
|
646
|
-
"Failed to get a consistent time when calculating velocity profiles."
|
|
647
|
-
)
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
def _get_entry_and_exit_velocities(
|
|
651
|
-
motors: list[Motor],
|
|
652
|
-
slice: Slice,
|
|
653
|
-
half_durations: npt.NDArray[float64],
|
|
654
|
-
gap: int,
|
|
655
|
-
) -> tuple[
|
|
656
|
-
dict[Motor, np.float64],
|
|
657
|
-
dict[Motor, np.float64],
|
|
658
|
-
dict[Motor, float],
|
|
659
|
-
]:
|
|
660
|
-
"""Compute motor entry and exit velocities across a gap.
|
|
661
|
-
|
|
662
|
-
For each motor, this function:
|
|
663
|
-
- Calculates the midpoint velocity before and after the gap
|
|
664
|
-
- Uses midpoint distances (lower → midpoint and midpoint → upper) to
|
|
665
|
-
calculate the velocity just before entering the gap (entry velocity)
|
|
666
|
-
and just after exiting the gap (exit velocity).
|
|
667
|
-
- Computes the travel distance across the gap from the upper point of
|
|
668
|
-
the preceding frame to the lower point of the following frame.
|
|
669
|
-
|
|
670
|
-
:param motors: Sequence of motors involved in trajectory
|
|
671
|
-
:param slice: Information about a series of scan frames along a number of axes
|
|
672
|
-
:param half_durations: Array of half the time required to get to a frame
|
|
673
|
-
:param gap: Index into the slice where gap has occured
|
|
674
|
-
:returns tuple: A tuple containing:
|
|
675
|
-
dict: Motor to entry velocity into gap
|
|
676
|
-
dict: Motor to exit velocity out of gap
|
|
677
|
-
dict: Motor to distance to travel across gap
|
|
678
|
-
"""
|
|
679
|
-
entry_velocities: dict[Motor, np.float64] = {}
|
|
680
|
-
exit_velocities: dict[Motor, np.float64] = {}
|
|
681
|
-
distances: dict[Motor, float] = {}
|
|
682
|
-
for motor in motors:
|
|
683
|
-
# x
|
|
684
|
-
# x x
|
|
685
|
-
# x x
|
|
686
|
-
# vl vlm vm vmu vu
|
|
687
|
-
# Given distances from Frame, lower, midpoint, upper, calculate
|
|
688
|
-
# vl for entry into gap (i.e., gap-1) and vu for exit out of gap
|
|
689
|
-
entry_lower_upper_distance = (
|
|
690
|
-
slice.upper[motor][gap - 1] - slice.lower[motor][gap - 1]
|
|
691
|
-
)
|
|
692
|
-
exit_lower_upper_distance = slice.upper[motor][gap] - slice.lower[motor][gap]
|
|
693
|
-
|
|
694
|
-
entry_midpoint_velocity = entry_lower_upper_distance / (
|
|
695
|
-
2 * half_durations[gap - 1]
|
|
696
|
-
)
|
|
697
|
-
exit_midpoint_velocity = exit_lower_upper_distance / (2 * half_durations[gap])
|
|
698
|
-
|
|
699
|
-
# For entry, halfway point is vlm
|
|
700
|
-
# so calculate lower to midpoint distance (i.e., dlm)
|
|
701
|
-
lower_midpoint_distance = (
|
|
702
|
-
slice.midpoints[motor][gap - 1] - slice.lower[motor][gap - 1]
|
|
703
|
-
)
|
|
704
|
-
# For exit, halfway point is vmu
|
|
705
|
-
# so calculate midpoint to upper distance (i.e., dmu)
|
|
706
|
-
midpoint_upper_distance = slice.upper[motor][gap] - slice.midpoints[motor][gap]
|
|
707
|
-
|
|
708
|
-
# Extrapolate to get our entry or exit velocity
|
|
709
|
-
# For example:
|
|
710
|
-
# (vl + vm) / 2 = vlm
|
|
711
|
-
# so vl = 2 * vlm - vm
|
|
712
|
-
# where vlm = dlm / (t/2)
|
|
713
|
-
# Therefore, velocity from point just before gap
|
|
714
|
-
entry_velocities[motor] = (
|
|
715
|
-
2 * (lower_midpoint_distance / half_durations[gap - 1])
|
|
716
|
-
- entry_midpoint_velocity
|
|
717
|
-
)
|
|
718
|
-
|
|
719
|
-
# Velocity from point just after gap
|
|
720
|
-
exit_velocities[motor] = (
|
|
721
|
-
2 * (midpoint_upper_distance / half_durations[gap]) - exit_midpoint_velocity
|
|
722
|
-
)
|
|
723
|
-
|
|
724
|
-
# Travel distance across gap
|
|
725
|
-
distances[motor] = slice.lower[motor][gap] - slice.upper[motor][gap - 1]
|
|
726
|
-
if np.isclose(distances[motor], 0.0, atol=1e-12):
|
|
727
|
-
distances[motor] = 0.0
|
|
728
|
-
|
|
729
|
-
return entry_velocities, exit_velocities, distances
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
def _calculate_profile_from_velocities(
|
|
733
|
-
motors: list[Motor],
|
|
734
|
-
slice: Slice,
|
|
735
|
-
gap: int,
|
|
736
|
-
time_arrays: dict[Motor, npt.NDArray[np.float64]],
|
|
737
|
-
velocity_arrays: dict[Motor, npt.NDArray[np.float64]],
|
|
738
|
-
) -> GapSegment:
|
|
739
|
-
"""Convert per-axis time/velocity profiles into aligned time/position profiles.
|
|
740
|
-
|
|
741
|
-
Given per-axis arrays of times and corresponding velocities,
|
|
742
|
-
this builds a single unified timeline containing all unique velocity change points
|
|
743
|
-
from every axis. It then steps through that timeline, and for each axis:
|
|
744
|
-
|
|
745
|
-
* If the current time matches one of the axis's own velocity change points,
|
|
746
|
-
use that known velocity.
|
|
747
|
-
* Otherwise, linearly interpolate between the axis's previous and next
|
|
748
|
-
known velocities, based on how far through that section we are.
|
|
749
|
-
|
|
750
|
-
At each unified time step, the velocity is integrated over the step duration
|
|
751
|
-
(using the trapezoidal rule) to update the axis's position. This produces
|
|
752
|
-
per-axis position and velocity arrays that are aligned to the same global
|
|
753
|
-
time grid.
|
|
754
|
-
|
|
755
|
-
Example:
|
|
756
|
-
combined_times = [0.1, 0.2, 0.3, 0.4]
|
|
757
|
-
axis_times[motor] = [0.1, 0.4]
|
|
758
|
-
velocity_array[motor] = [2, 5]
|
|
759
|
-
|
|
760
|
-
At 0.1 → known vel = 2 (use directly)
|
|
761
|
-
At 0.2 → 1/3 of the way to next vel → 3.0
|
|
762
|
-
At 0.3 → 2/3 of the way to next vel → 4.0
|
|
763
|
-
At 0.4 → known vel = 5 (use directly)
|
|
764
|
-
|
|
765
|
-
These instantaneous velocities are integrated over each Δt to yield
|
|
766
|
-
positions aligned with the global timeline.
|
|
767
|
-
|
|
768
|
-
:param motors: Sequence of motors involved in trajectory
|
|
769
|
-
:param slice: Information about a series of scan frames along a number of axes
|
|
770
|
-
:param gap: Index into slice where gap has occured
|
|
771
|
-
:param time_arrays: Motor's absolute timestamps of velocity changes
|
|
772
|
-
:param velocity_arrays: Motor's velocity changes
|
|
773
|
-
:returns GapSegment: Class representing a segment of a trajectory that is a gap
|
|
774
|
-
"""
|
|
775
|
-
# Combine all per-axis time points into a single sorted array of times
|
|
776
|
-
# This way we can evaluate each motor along the same timeline
|
|
777
|
-
# We know all axes positions at t=0, so we drop this point
|
|
778
|
-
combined_times = np.sort(np.unique(np.concatenate(list(time_arrays.values()))))[1:]
|
|
779
|
-
|
|
780
|
-
# We convert a list of t into a list of Δt
|
|
781
|
-
# We do this by substracting against previous cumulative time
|
|
782
|
-
time_intervals = np.diff(np.concatenate(([0.0], combined_times))).tolist()
|
|
783
|
-
|
|
784
|
-
# We also know all axes positions when t=t_final, so we drop this point
|
|
785
|
-
# However, we need the interval for the next collection window, so we store it
|
|
786
|
-
*time_intervals, final_interval = time_intervals
|
|
787
|
-
combined_times = combined_times[:-1]
|
|
788
|
-
num_intervals = len(time_intervals)
|
|
789
|
-
|
|
790
|
-
# Prepare dicts for the resulting position and velocity profiles over the gap
|
|
791
|
-
positions: dict[Motor, npt.NDArray[np.float64]] = {}
|
|
792
|
-
velocities: dict[Motor, npt.NDArray[np.float64]] = {}
|
|
793
|
-
|
|
794
|
-
# Loop over each motor and integrate its velocity profile over the unified times
|
|
795
|
-
for motor in motors:
|
|
796
|
-
axis_times = time_arrays[motor]
|
|
797
|
-
axis_velocities = velocity_arrays[motor]
|
|
798
|
-
axis_position = slice.upper[motor][
|
|
799
|
-
gap - 1
|
|
800
|
-
] # start position at beginning of the gap
|
|
801
|
-
prev_interval_vel = axis_velocities[
|
|
802
|
-
0
|
|
803
|
-
] # last velocity seen from the previous global time interval
|
|
804
|
-
time_since_prev_axis_point = 0.0 # elapsed time since the last velocity point
|
|
805
|
-
axis_idx = 1 # index into this axis's velocity/time arrays
|
|
806
|
-
|
|
807
|
-
# Allocate output arrays for this motor with correct size
|
|
808
|
-
positions[motor] = np.empty(num_intervals, dtype=np.float64)
|
|
809
|
-
velocities[motor] = np.empty(num_intervals, dtype=np.float64)
|
|
810
|
-
|
|
811
|
-
# Step through each interval in the Δt list
|
|
812
|
-
for i, dt in enumerate(time_intervals):
|
|
813
|
-
next_vel = axis_velocities[axis_idx]
|
|
814
|
-
prev_vel = axis_velocities[axis_idx - 1]
|
|
815
|
-
axis_dt = axis_times[axis_idx] - axis_times[axis_idx - 1]
|
|
816
|
-
|
|
817
|
-
if np.isclose(combined_times[i], axis_times[axis_idx]):
|
|
818
|
-
# If the current combined time exactly matches this motor's
|
|
819
|
-
# next velocity change point, no interpolation is needed, so:
|
|
820
|
-
this_vel = next_vel
|
|
821
|
-
axis_idx += 1
|
|
822
|
-
time_since_prev_axis_point = 0.0
|
|
823
|
-
else:
|
|
824
|
-
# Otherwise, linearly interpolate velocity between the previous
|
|
825
|
-
# and next known velocity points for this motor
|
|
826
|
-
time_since_prev_axis_point += dt
|
|
827
|
-
# The fraction of the way we are from previous to next known velocities
|
|
828
|
-
# for this motor
|
|
829
|
-
frac = time_since_prev_axis_point / axis_dt
|
|
830
|
-
# Interpolate for our velocity
|
|
831
|
-
this_vel = prev_vel + frac * (next_vel - prev_vel)
|
|
832
|
-
|
|
833
|
-
# Integrate velocity over this interval to update position.
|
|
834
|
-
# Using the trapezoidal rule:
|
|
835
|
-
delta_pos = 0.5 * (prev_interval_vel + this_vel) * dt
|
|
836
|
-
axis_position += delta_pos
|
|
837
|
-
prev_interval_vel = this_vel # update for next loop
|
|
838
|
-
|
|
839
|
-
# Store the computed position and velocity for this interval
|
|
840
|
-
positions[motor][i] = axis_position
|
|
841
|
-
velocities[motor][i] = this_vel
|
|
842
|
-
|
|
843
|
-
return GapSegment(
|
|
844
|
-
positions=positions,
|
|
845
|
-
velocities=velocities,
|
|
846
|
-
duration=time_intervals + [final_interval],
|
|
847
|
-
)
|