InterpolatePy 1.0.0__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.
- interpolatepy/__init__.py +8 -0
- interpolatepy/b_spline.py +737 -0
- interpolatepy/b_spline_approx.py +544 -0
- interpolatepy/b_spline_cubic.py +444 -0
- interpolatepy/b_spline_interpolate.py +515 -0
- interpolatepy/b_spline_smooth.py +639 -0
- interpolatepy/c_s_smoot_search.py +177 -0
- interpolatepy/c_s_smoothing.py +611 -0
- interpolatepy/c_s_with_acc1.py +643 -0
- interpolatepy/c_s_with_acc2.py +494 -0
- interpolatepy/cubic_spline.py +486 -0
- interpolatepy/double_s.py +580 -0
- interpolatepy/frenet_frame.py +245 -0
- interpolatepy/linear.py +107 -0
- interpolatepy/polynomials.py +451 -0
- interpolatepy/simple_paths.py +281 -0
- interpolatepy/trapezoidal.py +613 -0
- interpolatepy/tridiagonal_inv.py +96 -0
- interpolatepy/version.py +5 -0
- interpolatepy-1.0.0.dist-info/METADATA +415 -0
- interpolatepy-1.0.0.dist-info/RECORD +24 -0
- interpolatepy-1.0.0.dist-info/WHEEL +5 -0
- interpolatepy-1.0.0.dist-info/licenses/LICENSE +21 -0
- interpolatepy-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import NamedTuple
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Constants to avoid magic numbers
|
|
9
|
+
EPSILON = 1e-6 # Small value to avoid division by zero
|
|
10
|
+
MIN_GAMMA = 0.01 # Lower bound for gamma in binary search
|
|
11
|
+
MAX_ITERATIONS = 50 # Maximum iterations for binary search
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class TrajectoryBounds:
|
|
16
|
+
"""Bounds for trajectory planning.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
v_bound : float
|
|
21
|
+
Velocity bound (absolute value will be used for both min/max).
|
|
22
|
+
a_bound : float
|
|
23
|
+
Acceleration bound (absolute value will be used for both min/max).
|
|
24
|
+
j_bound : float
|
|
25
|
+
Jerk bound (absolute value will be used for both min/max).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
v_bound: float
|
|
29
|
+
a_bound: float
|
|
30
|
+
j_bound: float
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
"""Validate the bounds."""
|
|
34
|
+
if not all(isinstance(x, int | float) for x in [self.v_bound, self.a_bound, self.j_bound]):
|
|
35
|
+
raise TypeError("All bounds must be numeric values")
|
|
36
|
+
|
|
37
|
+
if self.v_bound <= 0 or self.a_bound <= 0 or self.j_bound <= 0:
|
|
38
|
+
raise ValueError("Bounds must be positive values")
|
|
39
|
+
|
|
40
|
+
# Convert to absolute values
|
|
41
|
+
self.v_bound = abs(self.v_bound)
|
|
42
|
+
self.a_bound = abs(self.a_bound)
|
|
43
|
+
self.j_bound = abs(self.j_bound)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StateParams(NamedTuple):
|
|
47
|
+
"""Parameters representing position and velocity state.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
q_0 : float
|
|
52
|
+
Start position.
|
|
53
|
+
q_1 : float
|
|
54
|
+
End position.
|
|
55
|
+
v_0 : float
|
|
56
|
+
Velocity at start of trajectory.
|
|
57
|
+
v_1 : float
|
|
58
|
+
Velocity at end of trajectory.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
q_0: float
|
|
62
|
+
q_1: float
|
|
63
|
+
v_0: float
|
|
64
|
+
v_1: float
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DoubleSTrajectory:
|
|
68
|
+
"""
|
|
69
|
+
Double S-Trajectory Planner Class
|
|
70
|
+
|
|
71
|
+
This class implements a trajectory planning algorithm that generates smooth motion
|
|
72
|
+
profiles with bounded velocity, acceleration, and jerk (double S-trajectory).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, state_params: StateParams, bounds: TrajectoryBounds) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Initialize the trajectory planner.
|
|
78
|
+
|
|
79
|
+
Parameters:
|
|
80
|
+
-----------
|
|
81
|
+
state_params : StateParams
|
|
82
|
+
Start and end states for trajectory planning
|
|
83
|
+
bounds : TrajectoryBounds
|
|
84
|
+
Velocity, acceleration, and jerk bounds for trajectory planning
|
|
85
|
+
"""
|
|
86
|
+
# Input validation for state params
|
|
87
|
+
if not all(
|
|
88
|
+
isinstance(x, int | float)
|
|
89
|
+
for x in [state_params.q_0, state_params.q_1, state_params.v_0, state_params.v_1]
|
|
90
|
+
):
|
|
91
|
+
raise TypeError("All state parameters must be numeric values")
|
|
92
|
+
|
|
93
|
+
# Store initial parameters
|
|
94
|
+
self.state = state_params
|
|
95
|
+
self.bounds = bounds
|
|
96
|
+
|
|
97
|
+
# Check if initial or final velocities exceed bounds
|
|
98
|
+
if abs(state_params.v_0) > bounds.v_bound or abs(state_params.v_1) > bounds.v_bound:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Initial or final velocities exceed the velocity bound of {bounds.v_bound}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Initialize trajectory parameters
|
|
104
|
+
self.T = 0.0 # Total trajectory duration
|
|
105
|
+
self.Ta = 0.0 # Acceleration phase duration
|
|
106
|
+
self.Tv = 0.0 # Constant velocity phase duration
|
|
107
|
+
self.Td = 0.0 # Deceleration phase duration
|
|
108
|
+
self.Tj_1 = 0.0 # Jerk time in acceleration phase
|
|
109
|
+
self.Tj_2 = 0.0 # Jerk time in deceleration phase
|
|
110
|
+
self.a_lim_a = 0.0 # Acceleration limit during acceleration phase
|
|
111
|
+
self.a_lim_d = 0.0 # Acceleration limit during deceleration phase
|
|
112
|
+
self.v_lim = 0.0 # Velocity limit during constant velocity phase
|
|
113
|
+
self.sigma = 1.0 # Direction coefficient
|
|
114
|
+
|
|
115
|
+
# Transformed parameters
|
|
116
|
+
self.q_0_transformed = 0.0
|
|
117
|
+
self.q_1_transformed = 0.0
|
|
118
|
+
self.v_0_transformed = 0.0
|
|
119
|
+
self.v_1_transformed = 0.0
|
|
120
|
+
self.v_max = 0.0
|
|
121
|
+
self.v_min = 0.0
|
|
122
|
+
self.a_max = 0.0
|
|
123
|
+
self.a_min = 0.0
|
|
124
|
+
self.j_max = 0.0
|
|
125
|
+
self.j_min = 0.0
|
|
126
|
+
|
|
127
|
+
# Plan the trajectory
|
|
128
|
+
self._plan_trajectory()
|
|
129
|
+
|
|
130
|
+
def _plan_trajectory(self) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Plan the trajectory based on the initialized parameters.
|
|
133
|
+
This internal method computes all necessary trajectory parameters.
|
|
134
|
+
"""
|
|
135
|
+
qd_0, qd_1 = self.state.q_0, self.state.q_1
|
|
136
|
+
vd_0, vd_1 = self.state.v_0, self.state.v_1
|
|
137
|
+
|
|
138
|
+
# BLOCK 1: Given the initial conditions compute direction and transform parameters
|
|
139
|
+
# If positions are equal, handle specially
|
|
140
|
+
if np.isclose(qd_1, qd_0):
|
|
141
|
+
# For equal positions, create a minimal trajectory to match velocities if needed
|
|
142
|
+
if np.isclose(vd_1, vd_0):
|
|
143
|
+
# If velocities also match, return a static trajectory
|
|
144
|
+
self.T = 0.0
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# Different velocities but same position requires a special trajectory
|
|
148
|
+
t_min = abs(vd_1 - vd_0) / self.bounds.a_bound
|
|
149
|
+
self.T = max(t_min * 1.5, 0.1) # Ensure minimum duration
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# Normal case - different positions
|
|
153
|
+
self.sigma = np.sign(qd_1 - qd_0)
|
|
154
|
+
|
|
155
|
+
# Transform parameters based on direction
|
|
156
|
+
self.q_0_transformed = self.sigma * qd_0
|
|
157
|
+
self.q_1_transformed = self.sigma * qd_1
|
|
158
|
+
self.v_0_transformed = self.sigma * vd_0
|
|
159
|
+
self.v_1_transformed = self.sigma * vd_1
|
|
160
|
+
|
|
161
|
+
# Set limits based on direction
|
|
162
|
+
self.v_max = (self.sigma + 1) / 2 * self.bounds.v_bound + (self.sigma - 1) / 2 * (
|
|
163
|
+
-self.bounds.v_bound
|
|
164
|
+
)
|
|
165
|
+
self.v_min = (self.sigma + 1) / 2 * (-self.bounds.v_bound) + (
|
|
166
|
+
self.sigma - 1
|
|
167
|
+
) / 2 * self.bounds.v_bound
|
|
168
|
+
self.a_max = (self.sigma + 1) / 2 * self.bounds.a_bound + (self.sigma - 1) / 2 * (
|
|
169
|
+
-self.bounds.a_bound
|
|
170
|
+
)
|
|
171
|
+
self.a_min = (self.sigma + 1) / 2 * (-self.bounds.a_bound) + (
|
|
172
|
+
self.sigma - 1
|
|
173
|
+
) / 2 * self.bounds.a_bound
|
|
174
|
+
self.j_max = (self.sigma + 1) / 2 * self.bounds.j_bound + (self.sigma - 1) / 2 * (
|
|
175
|
+
-self.bounds.j_bound
|
|
176
|
+
)
|
|
177
|
+
self.j_min = (self.sigma + 1) / 2 * (-self.bounds.j_bound) + (
|
|
178
|
+
self.sigma - 1
|
|
179
|
+
) / 2 * self.bounds.j_bound
|
|
180
|
+
|
|
181
|
+
# Compute time intervals assuming v_max and a_max are reached
|
|
182
|
+
|
|
183
|
+
# Acceleration part
|
|
184
|
+
if ((self.v_max - self.v_0_transformed) * self.j_max) < self.a_max**2:
|
|
185
|
+
self.Tj_1 = np.sqrt(
|
|
186
|
+
max((self.v_max - self.v_0_transformed) / self.j_max, 0)
|
|
187
|
+
) # Prevent negative sqrt
|
|
188
|
+
self.Ta = 2 * self.Tj_1
|
|
189
|
+
else:
|
|
190
|
+
self.Tj_1 = self.a_max / self.j_max
|
|
191
|
+
self.Ta = self.Tj_1 + (self.v_max - self.v_0_transformed) / self.a_max
|
|
192
|
+
|
|
193
|
+
# Deceleration part
|
|
194
|
+
if ((self.v_max - self.v_1_transformed) * self.j_max) < self.a_max**2:
|
|
195
|
+
self.Tj_2 = np.sqrt(
|
|
196
|
+
max((self.v_max - self.v_1_transformed) / self.j_max, 0)
|
|
197
|
+
) # Prevent negative sqrt
|
|
198
|
+
self.Td = 2 * self.Tj_2
|
|
199
|
+
else:
|
|
200
|
+
self.Tj_2 = self.a_max / self.j_max
|
|
201
|
+
self.Td = self.Tj_2 + (self.v_max - self.v_1_transformed) / self.a_max
|
|
202
|
+
|
|
203
|
+
# Determine the time duration of the constant velocity phase
|
|
204
|
+
if abs(self.v_max) < EPSILON: # Avoid division by zero
|
|
205
|
+
self.Tv = 0
|
|
206
|
+
else:
|
|
207
|
+
self.Tv = (
|
|
208
|
+
(self.q_1_transformed - self.q_0_transformed) / self.v_max
|
|
209
|
+
- self.Ta / 2 * (1 + self.v_0_transformed / self.v_max)
|
|
210
|
+
- self.Td / 2 * (1 + self.v_1_transformed / self.v_max)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Check if Tv < 0 (v_max is not reached)
|
|
214
|
+
if self.Tv < 0:
|
|
215
|
+
# Set Tv = 0 (to prevent computation errors)
|
|
216
|
+
self.Tv = 0
|
|
217
|
+
|
|
218
|
+
# Iterate to find appropriate acceleration constraints using binary search
|
|
219
|
+
gamma_high = 1.0
|
|
220
|
+
gamma_low = MIN_GAMMA
|
|
221
|
+
gamma_mid = 0.5
|
|
222
|
+
iteration = 0
|
|
223
|
+
|
|
224
|
+
while iteration < MAX_ITERATIONS:
|
|
225
|
+
iteration += 1
|
|
226
|
+
gamma_mid = (gamma_high + gamma_low) / 2
|
|
227
|
+
|
|
228
|
+
# Test with current gamma
|
|
229
|
+
a_max_test = gamma_mid * self.bounds.a_bound
|
|
230
|
+
a_min_test = -gamma_mid * self.bounds.a_bound
|
|
231
|
+
|
|
232
|
+
# Recalculate time intervals
|
|
233
|
+
tj = a_max_test / self.j_max
|
|
234
|
+
delta = (
|
|
235
|
+
a_max_test**4 / self.j_max**2
|
|
236
|
+
+ 2 * (self.v_0_transformed**2 + self.v_1_transformed**2)
|
|
237
|
+
+ a_max_test
|
|
238
|
+
* (
|
|
239
|
+
4 * (self.q_1_transformed - self.q_0_transformed)
|
|
240
|
+
- 2
|
|
241
|
+
* a_max_test
|
|
242
|
+
/ self.j_max
|
|
243
|
+
* (self.v_0_transformed + self.v_1_transformed)
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Check if delta is negative (no solution with current gamma)
|
|
248
|
+
if delta < 0:
|
|
249
|
+
gamma_high = gamma_mid
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
ta = (a_max_test**2 / self.j_max - 2 * self.v_0_transformed + np.sqrt(delta)) / (
|
|
253
|
+
2 * a_max_test
|
|
254
|
+
)
|
|
255
|
+
td = (a_max_test**2 / self.j_max - 2 * self.v_1_transformed + np.sqrt(delta)) / (
|
|
256
|
+
2 * a_max_test
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if ta < 0:
|
|
260
|
+
if (
|
|
261
|
+
abs(self.v_1_transformed + self.v_0_transformed) < EPSILON
|
|
262
|
+
): # Avoid division by zero
|
|
263
|
+
ta = 0
|
|
264
|
+
td = 0
|
|
265
|
+
tj_1 = 0
|
|
266
|
+
tj_2 = 0
|
|
267
|
+
break
|
|
268
|
+
ta = 0
|
|
269
|
+
td = (
|
|
270
|
+
2
|
|
271
|
+
* (self.q_1_transformed - self.q_0_transformed)
|
|
272
|
+
/ (self.v_1_transformed + self.v_0_transformed)
|
|
273
|
+
)
|
|
274
|
+
tj_2_arg = self.j_max * (self.q_1_transformed - self.q_0_transformed) - np.sqrt(
|
|
275
|
+
self.j_max
|
|
276
|
+
* (
|
|
277
|
+
self.j_max * (self.q_1_transformed - self.q_0_transformed) ** 2
|
|
278
|
+
+ (self.v_1_transformed + self.v_0_transformed) ** 2
|
|
279
|
+
* (self.v_1_transformed - self.v_0_transformed)
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
tj_2 = (
|
|
283
|
+
tj_2_arg / (self.j_max * (self.v_1_transformed + self.v_0_transformed))
|
|
284
|
+
if abs(tj_2_arg) > EPSILON
|
|
285
|
+
else 0
|
|
286
|
+
)
|
|
287
|
+
elif td < 0:
|
|
288
|
+
if (
|
|
289
|
+
abs(self.v_1_transformed + self.v_0_transformed) < EPSILON
|
|
290
|
+
): # Avoid division by zero
|
|
291
|
+
ta = 0
|
|
292
|
+
td = 0
|
|
293
|
+
tj_1 = 0
|
|
294
|
+
tj_2 = 0
|
|
295
|
+
break
|
|
296
|
+
td = 0
|
|
297
|
+
ta = (
|
|
298
|
+
2
|
|
299
|
+
* (self.q_1_transformed - self.q_0_transformed)
|
|
300
|
+
/ (self.v_1_transformed + self.v_0_transformed)
|
|
301
|
+
)
|
|
302
|
+
tj_1_arg = self.j_max * (self.q_1_transformed - self.q_0_transformed) - np.sqrt(
|
|
303
|
+
self.j_max
|
|
304
|
+
* (
|
|
305
|
+
self.j_max * (self.q_1_transformed - self.q_0_transformed) ** 2
|
|
306
|
+
- (self.v_1_transformed + self.v_0_transformed) ** 2
|
|
307
|
+
* (self.v_1_transformed - self.v_0_transformed)
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
tj_1 = (
|
|
311
|
+
tj_1_arg / (self.j_max * (self.v_1_transformed + self.v_0_transformed))
|
|
312
|
+
if abs(tj_1_arg) > EPSILON
|
|
313
|
+
else 0
|
|
314
|
+
)
|
|
315
|
+
elif (ta > 2 * tj) and (td > 2 * tj):
|
|
316
|
+
# Valid solution found
|
|
317
|
+
self.a_max = a_max_test
|
|
318
|
+
self.a_min = a_min_test
|
|
319
|
+
self.Tj_1 = tj
|
|
320
|
+
self.Tj_2 = tj
|
|
321
|
+
self.Ta = ta
|
|
322
|
+
self.Td = td
|
|
323
|
+
break
|
|
324
|
+
else:
|
|
325
|
+
# Need to reduce gamma further
|
|
326
|
+
gamma_high = gamma_mid
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
# Check if solution is valid
|
|
330
|
+
if tj_1 >= 0 and tj_2 >= 0 and ta >= 0 and td >= 0:
|
|
331
|
+
self.a_max = a_max_test
|
|
332
|
+
self.a_min = a_min_test
|
|
333
|
+
self.Tj_1 = tj_1
|
|
334
|
+
self.Tj_2 = tj_2
|
|
335
|
+
self.Ta = ta
|
|
336
|
+
self.Td = td
|
|
337
|
+
break
|
|
338
|
+
gamma_high = gamma_mid
|
|
339
|
+
|
|
340
|
+
# Compute trajectory parameters
|
|
341
|
+
self.a_lim_a = self.j_max * self.Tj_1
|
|
342
|
+
self.a_lim_d = -self.j_max * self.Tj_2
|
|
343
|
+
|
|
344
|
+
# Ensure we don't divide by zero or have negative time periods
|
|
345
|
+
self.Ta = max(self.Ta, 0)
|
|
346
|
+
self.Td = max(self.Td, 0)
|
|
347
|
+
self.Tv = max(self.Tv, 0)
|
|
348
|
+
self.Tj_1 = max(self.Tj_1, 0)
|
|
349
|
+
self.Tj_2 = max(self.Tj_2, 0)
|
|
350
|
+
|
|
351
|
+
# Calculate v_lim safely
|
|
352
|
+
if self.Ta <= self.Tj_1:
|
|
353
|
+
self.v_lim = self.v_0_transformed + self.j_max * self.Ta**2 / 2
|
|
354
|
+
else:
|
|
355
|
+
self.v_lim = self.v_0_transformed + (self.Ta - self.Tj_1) * self.a_lim_a
|
|
356
|
+
|
|
357
|
+
# Total trajectory time
|
|
358
|
+
self.T = self.Ta + self.Tv + self.Td
|
|
359
|
+
|
|
360
|
+
# Round final time to discrete ticks (in milliseconds)
|
|
361
|
+
self.T = round(self.T * 1000) / 1000
|
|
362
|
+
|
|
363
|
+
def evaluate(self, t: float | np.ndarray) -> tuple[float | np.ndarray, ...]:
|
|
364
|
+
"""
|
|
365
|
+
Evaluate the double-S trajectory at time t.
|
|
366
|
+
|
|
367
|
+
Parameters:
|
|
368
|
+
-----------
|
|
369
|
+
t : float or ndarray
|
|
370
|
+
Time(s) at which to evaluate the trajectory
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
--------
|
|
374
|
+
q : float or ndarray
|
|
375
|
+
Position at time t
|
|
376
|
+
qp : float or ndarray
|
|
377
|
+
Velocity at time t
|
|
378
|
+
qpp : float or ndarray
|
|
379
|
+
Acceleration at time t
|
|
380
|
+
qppp : float or ndarray
|
|
381
|
+
Jerk at time t
|
|
382
|
+
"""
|
|
383
|
+
# Handle array input
|
|
384
|
+
if isinstance(t, list | np.ndarray):
|
|
385
|
+
# Preallocate arrays for efficiency
|
|
386
|
+
q = np.zeros_like(t, dtype=float)
|
|
387
|
+
qp = np.zeros_like(t, dtype=float)
|
|
388
|
+
qpp = np.zeros_like(t, dtype=float)
|
|
389
|
+
qppp = np.zeros_like(t, dtype=float)
|
|
390
|
+
|
|
391
|
+
# Compute for each time point
|
|
392
|
+
for i, t_i in enumerate(t):
|
|
393
|
+
q[i], qp[i], qpp[i], qppp[i] = self.evaluate(float(t_i))
|
|
394
|
+
return q, qp, qpp, qppp
|
|
395
|
+
|
|
396
|
+
# Special case for equal positions with equal velocities
|
|
397
|
+
if np.isclose(self.state.q_0, self.state.q_1) and np.isclose(
|
|
398
|
+
self.state.v_0, self.state.v_1
|
|
399
|
+
):
|
|
400
|
+
return self.state.q_0, self.state.v_0, 0.0, 0.0
|
|
401
|
+
|
|
402
|
+
# Special case for equal positions with different velocities
|
|
403
|
+
if np.isclose(self.state.q_0, self.state.q_1) and not np.isclose(
|
|
404
|
+
self.state.v_0, self.state.v_1
|
|
405
|
+
):
|
|
406
|
+
t_norm = min(t / self.T, 1.0)
|
|
407
|
+
qp = self.state.v_0 + t_norm * (self.state.v_1 - self.state.v_0)
|
|
408
|
+
|
|
409
|
+
phase = 2 * np.pi * t_norm
|
|
410
|
+
amplitude = (self.state.v_1 - self.state.v_0) * self.T / (2 * np.pi)
|
|
411
|
+
q = self.state.q_0 + amplitude * np.sin(phase)
|
|
412
|
+
|
|
413
|
+
qpp = (self.state.v_1 - self.state.v_0) / self.T + amplitude * (
|
|
414
|
+
2 * np.pi / self.T
|
|
415
|
+
) * np.cos(phase)
|
|
416
|
+
qppp = -amplitude * (2 * np.pi / self.T) ** 2 * np.sin(phase)
|
|
417
|
+
|
|
418
|
+
return q, qp, qpp, qppp
|
|
419
|
+
|
|
420
|
+
# Ensure t is within bounds [0, T]
|
|
421
|
+
t = np.clip(t, 0, self.T)
|
|
422
|
+
|
|
423
|
+
# Handle zero or near-zero duration trajectory
|
|
424
|
+
if self.T < EPSILON:
|
|
425
|
+
return self.state.q_1, self.state.v_1, 0, 0
|
|
426
|
+
|
|
427
|
+
# Use transformed coordinates for calculation
|
|
428
|
+
q_0 = self.q_0_transformed
|
|
429
|
+
q_1 = self.q_1_transformed
|
|
430
|
+
|
|
431
|
+
# ACCELERATION PHASE
|
|
432
|
+
if t <= self.Tj_1 and self.Tj_1 > 0:
|
|
433
|
+
# t in [0, Tj_1]
|
|
434
|
+
q = q_0 + self.v_0_transformed * t + self.j_max * t**3 / 6
|
|
435
|
+
qp = self.v_0_transformed + self.j_max * (t**2) / 2
|
|
436
|
+
qpp = self.j_max * t
|
|
437
|
+
qppp = self.j_max
|
|
438
|
+
|
|
439
|
+
elif t <= (self.Ta - self.Tj_1) and self.Ta > self.Tj_1:
|
|
440
|
+
# t in [Tj_1, Ta - Tj_1]
|
|
441
|
+
q = (
|
|
442
|
+
q_0
|
|
443
|
+
+ self.v_0_transformed * t
|
|
444
|
+
+ self.a_lim_a / 6 * (3 * t**2 - 3 * self.Tj_1 * t + self.Tj_1**2)
|
|
445
|
+
)
|
|
446
|
+
qp = self.v_0_transformed + self.a_lim_a * (t - self.Tj_1 / 2)
|
|
447
|
+
qpp = self.a_lim_a
|
|
448
|
+
qppp = 0
|
|
449
|
+
|
|
450
|
+
elif t <= self.Ta and self.Ta > 0:
|
|
451
|
+
# t in [Ta-Tj_1, Ta]
|
|
452
|
+
q = (
|
|
453
|
+
q_0
|
|
454
|
+
+ (self.v_lim + self.v_0_transformed) * self.Ta / 2
|
|
455
|
+
- self.v_lim * (self.Ta - t)
|
|
456
|
+
- self.j_min * (self.Ta - t) ** 3 / 6
|
|
457
|
+
)
|
|
458
|
+
qp = self.v_lim + self.j_min * (self.Ta - t) ** 2 / 2
|
|
459
|
+
qpp = -self.j_min * (self.Ta - t)
|
|
460
|
+
qppp = self.j_min
|
|
461
|
+
|
|
462
|
+
# CONSTANT VELOCITY PHASE
|
|
463
|
+
elif t <= (self.Ta + self.Tv) and self.Tv > 0:
|
|
464
|
+
# t in [Ta, Ta+Tv]
|
|
465
|
+
q = q_0 + (self.v_lim + self.v_0_transformed) * self.Ta / 2 + self.v_lim * (t - self.Ta)
|
|
466
|
+
qp = self.v_lim
|
|
467
|
+
qpp = 0
|
|
468
|
+
qppp = 0
|
|
469
|
+
|
|
470
|
+
# DECELERATION PHASE
|
|
471
|
+
elif t <= (self.Ta + self.Tv + self.Tj_2) and self.Tj_2 > 0:
|
|
472
|
+
# t in [Ta+Tv, Ta+Tv+Tj_2]
|
|
473
|
+
q = (
|
|
474
|
+
q_1
|
|
475
|
+
- (self.v_lim + self.v_1_transformed) * self.Td / 2
|
|
476
|
+
+ self.v_lim * (t - self.T + self.Td)
|
|
477
|
+
- self.j_max * (t - self.T + self.Td) ** 3 / 6
|
|
478
|
+
)
|
|
479
|
+
qp = self.v_lim - self.j_max * (t - self.T + self.Td) ** 2 / 2
|
|
480
|
+
qpp = -self.j_max * (t - self.T + self.Td)
|
|
481
|
+
qppp = -self.j_max
|
|
482
|
+
|
|
483
|
+
elif t <= (self.Ta + self.Tv + (self.Td - self.Tj_2)) and self.Td > self.Tj_2:
|
|
484
|
+
# t in [Ta+Tv+Tj_2, Ta+Tv+(Td-Tj_2)]
|
|
485
|
+
q = (
|
|
486
|
+
q_1
|
|
487
|
+
- (self.v_lim + self.v_1_transformed) * self.Td / 2
|
|
488
|
+
+ self.v_lim * (t - self.T + self.Td)
|
|
489
|
+
+ self.a_lim_d
|
|
490
|
+
/ 6
|
|
491
|
+
* (
|
|
492
|
+
3 * (t - self.T + self.Td) ** 2
|
|
493
|
+
- 3 * self.Tj_2 * (t - self.T + self.Td)
|
|
494
|
+
+ self.Tj_2**2
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
qp = self.v_lim + self.a_lim_d * (t - self.T + self.Td - self.Tj_2 / 2)
|
|
498
|
+
qpp = self.a_lim_d
|
|
499
|
+
qppp = 0
|
|
500
|
+
|
|
501
|
+
elif t <= self.T and self.Td > 0:
|
|
502
|
+
# t in [Ta+Tv+(Td-Tj_2), T]
|
|
503
|
+
q = q_1 - self.v_1_transformed * (self.T - t) - self.j_max * (self.T - t) ** 3 / 6
|
|
504
|
+
qp = self.v_1_transformed + self.j_max * (self.T - t) ** 2 / 2
|
|
505
|
+
qpp = -self.j_max * (self.T - t)
|
|
506
|
+
qppp = self.j_max
|
|
507
|
+
|
|
508
|
+
else:
|
|
509
|
+
# After end of trajectory or for empty phases
|
|
510
|
+
q = q_1
|
|
511
|
+
qp = self.v_1_transformed
|
|
512
|
+
qpp = 0
|
|
513
|
+
qppp = 0
|
|
514
|
+
|
|
515
|
+
# Transform back using sigma
|
|
516
|
+
q = self.sigma * q
|
|
517
|
+
qp = self.sigma * qp
|
|
518
|
+
qpp = self.sigma * qpp
|
|
519
|
+
qppp = self.sigma * qppp
|
|
520
|
+
|
|
521
|
+
return q, qp, qpp, qppp
|
|
522
|
+
|
|
523
|
+
def get_duration(self) -> float:
|
|
524
|
+
"""
|
|
525
|
+
Returns the total duration of the trajectory.
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
--------
|
|
529
|
+
T : float
|
|
530
|
+
Total trajectory duration in seconds
|
|
531
|
+
"""
|
|
532
|
+
return self.T
|
|
533
|
+
|
|
534
|
+
def get_phase_durations(self) -> dict[str, float]:
|
|
535
|
+
"""
|
|
536
|
+
Returns the durations of each phase in the trajectory.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
--------
|
|
540
|
+
phases : dict
|
|
541
|
+
Dictionary containing the durations of each phase
|
|
542
|
+
"""
|
|
543
|
+
return {
|
|
544
|
+
"total": self.T,
|
|
545
|
+
"acceleration": self.Ta,
|
|
546
|
+
"constant_velocity": self.Tv,
|
|
547
|
+
"deceleration": self.Td,
|
|
548
|
+
"jerk_acceleration": self.Tj_1,
|
|
549
|
+
"jerk_deceleration": self.Tj_2,
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
@staticmethod
|
|
553
|
+
def create_trajectory(
|
|
554
|
+
state_params: StateParams, bounds: TrajectoryBounds
|
|
555
|
+
) -> tuple[Callable[[float | np.ndarray], tuple[float | np.ndarray, ...]], float]:
|
|
556
|
+
"""
|
|
557
|
+
Static factory method to create a trajectory function and return its duration.
|
|
558
|
+
This method provides an interface similar to the original function-based API.
|
|
559
|
+
|
|
560
|
+
Parameters:
|
|
561
|
+
-----------
|
|
562
|
+
state_params : StateParams
|
|
563
|
+
Start and end states for trajectory planning
|
|
564
|
+
bounds : TrajectoryBounds
|
|
565
|
+
Velocity, acceleration, and jerk bounds for trajectory planning
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
--------
|
|
569
|
+
trajectory_function : Callable
|
|
570
|
+
A function that takes time t as input and returns position, velocity,
|
|
571
|
+
acceleration, and jerk at that time
|
|
572
|
+
T : float
|
|
573
|
+
Total trajectory duration
|
|
574
|
+
"""
|
|
575
|
+
planner = DoubleSTrajectory(state_params, bounds)
|
|
576
|
+
|
|
577
|
+
def trajectory(t: float | np.ndarray) -> tuple[float | np.ndarray, ...]:
|
|
578
|
+
return planner.evaluate(t)
|
|
579
|
+
|
|
580
|
+
return trajectory, planner.get_duration()
|