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,643 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from interpolatepy.tridiagonal_inv import solve_tridiagonal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Constants to replace magic numbers
|
|
8
|
+
MIN_POINTS = 2
|
|
9
|
+
MIN_SEGMENTS_FOR_UPPER_DIAG = 2
|
|
10
|
+
MIN_SEGMENTS_FOR_SECOND_ROW = 3
|
|
11
|
+
MIN_SEGMENTS_FOR_UPPER_DIAG_SECOND_ROW = 4
|
|
12
|
+
MIN_SEGMENTS_FOR_SECOND_ELEMENT = 4
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CubicSplineWithAcceleration1:
|
|
16
|
+
"""
|
|
17
|
+
Cubic spline trajectory planning with both velocity and acceleration constraints
|
|
18
|
+
at endpoints.
|
|
19
|
+
|
|
20
|
+
Implements the method described in section 4.4.4 of the paper, which adds
|
|
21
|
+
two extra points in the first and last segments to satisfy the acceleration
|
|
22
|
+
constraints.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
t_points : list of float or numpy.ndarray
|
|
27
|
+
Time points [t₀, t₂, t₃, ..., tₙ₋₂, tₙ]
|
|
28
|
+
q_points : list of float or numpy.ndarray
|
|
29
|
+
Position points [q₀, q₂, q₃, ..., qₙ₋₂, qₙ]
|
|
30
|
+
v0 : float, optional
|
|
31
|
+
Initial velocity at t₀. Default is 0.0
|
|
32
|
+
vn : float, optional
|
|
33
|
+
Final velocity at tₙ. Default is 0.0
|
|
34
|
+
a0 : float, optional
|
|
35
|
+
Initial acceleration at t₀. Default is 0.0
|
|
36
|
+
an : float, optional
|
|
37
|
+
Final acceleration at tₙ. Default is 0.0
|
|
38
|
+
debug : bool, optional
|
|
39
|
+
Whether to print debug information. Default is False
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
t_orig : numpy.ndarray
|
|
44
|
+
Original time points
|
|
45
|
+
q_orig : numpy.ndarray
|
|
46
|
+
Original position points
|
|
47
|
+
v0 : float
|
|
48
|
+
Initial velocity
|
|
49
|
+
vn : float
|
|
50
|
+
Final velocity
|
|
51
|
+
a0 : float
|
|
52
|
+
Initial acceleration
|
|
53
|
+
an : float
|
|
54
|
+
Final acceleration
|
|
55
|
+
debug : bool
|
|
56
|
+
Debug flag
|
|
57
|
+
n_orig : int
|
|
58
|
+
Number of original points
|
|
59
|
+
t : numpy.ndarray
|
|
60
|
+
Time points including extra points
|
|
61
|
+
q : numpy.ndarray
|
|
62
|
+
Position points including extra points
|
|
63
|
+
n : int
|
|
64
|
+
Total number of points including extras
|
|
65
|
+
T : numpy.ndarray
|
|
66
|
+
Time intervals between consecutive points
|
|
67
|
+
omega : numpy.ndarray
|
|
68
|
+
Acceleration values at each point
|
|
69
|
+
coeffs : numpy.ndarray
|
|
70
|
+
Polynomial coefficients for each segment
|
|
71
|
+
original_indices : list
|
|
72
|
+
Indices of original points in expanded arrays
|
|
73
|
+
|
|
74
|
+
Notes
|
|
75
|
+
-----
|
|
76
|
+
This implementation adds two extra points (at t₁ and tₙ₋₁) to the trajectory
|
|
77
|
+
to satisfy both velocity and acceleration constraints at the endpoints.
|
|
78
|
+
The extra points are placed at the midpoints of the first and last segments
|
|
79
|
+
of the original trajectory.
|
|
80
|
+
|
|
81
|
+
The acceleration (omega) values are computed by solving a tridiagonal system
|
|
82
|
+
that ensures C2 continuity throughout the trajectory.
|
|
83
|
+
|
|
84
|
+
Examples
|
|
85
|
+
--------
|
|
86
|
+
>>> import numpy as np
|
|
87
|
+
>>> # Define waypoints
|
|
88
|
+
>>> t_points = [0, 1, 2, 3]
|
|
89
|
+
>>> q_points = [0, 1, 0, 1]
|
|
90
|
+
>>> # Create spline with velocity and acceleration constraints
|
|
91
|
+
>>> spline = CubicSplineWithAcceleration1(t_points, q_points, v0=0.0, vn=0.0, a0=0.0, an=0.0)
|
|
92
|
+
>>> # Evaluate at specific time
|
|
93
|
+
>>> spline.evaluate(1.5)
|
|
94
|
+
>>> # Plot the trajectory
|
|
95
|
+
>>> spline.plot()
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__( # noqa: PLR0913, PLR0917
|
|
99
|
+
self,
|
|
100
|
+
t_points: list[float],
|
|
101
|
+
q_points: list[float],
|
|
102
|
+
v0: float = 0.0,
|
|
103
|
+
vn: float = 0.0,
|
|
104
|
+
a0: float = 0.0,
|
|
105
|
+
an: float = 0.0,
|
|
106
|
+
debug: bool = False,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Initialize the cubic spline with velocity and acceleration constraints.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
t_points : list of float or numpy.ndarray
|
|
114
|
+
Time points [t₀, t₂, t₃, ..., tₙ₋₂, tₙ]
|
|
115
|
+
q_points : list of float or numpy.ndarray
|
|
116
|
+
Position points [q₀, q₂, q₃, ..., qₙ₋₂, qₙ]
|
|
117
|
+
v0 : float, optional
|
|
118
|
+
Initial velocity at t₀. Default is 0.0
|
|
119
|
+
vn : float, optional
|
|
120
|
+
Final velocity at tₙ. Default is 0.0
|
|
121
|
+
a0 : float, optional
|
|
122
|
+
Initial acceleration at t₀. Default is 0.0
|
|
123
|
+
an : float, optional
|
|
124
|
+
Final acceleration at tₙ. Default is 0.0
|
|
125
|
+
debug : bool, optional
|
|
126
|
+
Whether to print debug information. Default is 0.0
|
|
127
|
+
|
|
128
|
+
Raises
|
|
129
|
+
------
|
|
130
|
+
ValueError
|
|
131
|
+
If time and position arrays have different lengths
|
|
132
|
+
If fewer than two points are provided
|
|
133
|
+
If time points are not strictly increasing
|
|
134
|
+
"""
|
|
135
|
+
# Validate inputs
|
|
136
|
+
if len(t_points) != len(q_points):
|
|
137
|
+
raise ValueError("Time and position arrays must have the same length")
|
|
138
|
+
|
|
139
|
+
if len(t_points) < MIN_POINTS:
|
|
140
|
+
raise ValueError("At least two points are required")
|
|
141
|
+
|
|
142
|
+
if not np.all(np.diff(t_points) > 0):
|
|
143
|
+
raise ValueError("Time points must be strictly increasing")
|
|
144
|
+
|
|
145
|
+
# Following paper's notation: original points are [q₀, q₂, q₃, ..., qₙ₋₂, qₙ]
|
|
146
|
+
# We will add extra points q₁ and qₙ₋₁
|
|
147
|
+
self.t_orig = np.array(t_points, dtype=float)
|
|
148
|
+
self.q_orig = np.array(q_points, dtype=float)
|
|
149
|
+
|
|
150
|
+
# Boundary conditions
|
|
151
|
+
self.v0 = float(v0) # Initial velocity
|
|
152
|
+
self.vn = float(vn) # Final velocity
|
|
153
|
+
self.a0 = float(a0) # Initial acceleration (ω₀)
|
|
154
|
+
self.an = float(an) # Final acceleration (ωₙ)
|
|
155
|
+
|
|
156
|
+
self.debug = debug
|
|
157
|
+
|
|
158
|
+
# Number of original points
|
|
159
|
+
self.n_orig = len(self.t_orig)
|
|
160
|
+
|
|
161
|
+
# Insert the two extra points
|
|
162
|
+
self.t, self.q = self._add_extra_points()
|
|
163
|
+
|
|
164
|
+
# Number of total points including extras
|
|
165
|
+
self.n = len(self.t)
|
|
166
|
+
|
|
167
|
+
# Compute time intervals
|
|
168
|
+
self.T = np.diff(self.t)
|
|
169
|
+
|
|
170
|
+
if self.debug:
|
|
171
|
+
print("Time interval length: ", self.T)
|
|
172
|
+
print("\n")
|
|
173
|
+
|
|
174
|
+
# Solve for the accelerations at all points
|
|
175
|
+
self.omega = self._solve_accelerations()
|
|
176
|
+
|
|
177
|
+
# Compute polynomial coefficients for each segment
|
|
178
|
+
self.coeffs = self._compute_coefficients()
|
|
179
|
+
|
|
180
|
+
# For plotting, keep track of original point indices
|
|
181
|
+
self.original_indices = self._get_original_indices()
|
|
182
|
+
|
|
183
|
+
def _add_extra_points(self) -> tuple[np.ndarray, np.ndarray]:
|
|
184
|
+
"""
|
|
185
|
+
Add two extra points at t₁ and tₙ₋₁ to satisfy acceleration constraints.
|
|
186
|
+
|
|
187
|
+
Following the paper, the time points are placed at midpoints:
|
|
188
|
+
t₁ = (t₀ + t₂)/2 and tₙ₋₁ = (tₙ₋₂ + tₙ)/2
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
tuple of numpy.ndarray
|
|
193
|
+
(t, q): New time and position arrays with extra points
|
|
194
|
+
|
|
195
|
+
Notes
|
|
196
|
+
-----
|
|
197
|
+
The q values for the extra points are initially set to zero and
|
|
198
|
+
will be properly computed after solving for the accelerations.
|
|
199
|
+
"""
|
|
200
|
+
# Create expanded arrays for time and position
|
|
201
|
+
t_new = np.zeros(self.n_orig + 2)
|
|
202
|
+
q_new = np.zeros(self.n_orig + 2)
|
|
203
|
+
|
|
204
|
+
# Set first and last points (same as original)
|
|
205
|
+
t_new[0] = self.t_orig[0] # t₀
|
|
206
|
+
t_new[-1] = self.t_orig[-1] # tₙ
|
|
207
|
+
q_new[0] = self.q_orig[0] # q₀
|
|
208
|
+
q_new[-1] = self.q_orig[-1] # qₙ
|
|
209
|
+
|
|
210
|
+
# Set the original interior points (from q₂ to qₙ₋₂)
|
|
211
|
+
t_new[2:-2] = self.t_orig[1:-1]
|
|
212
|
+
q_new[2:-2] = self.q_orig[1:-1]
|
|
213
|
+
|
|
214
|
+
# Add extra points at midpoints as suggested in the paper
|
|
215
|
+
t_new[1] = (self.t_orig[0] + self.t_orig[1]) / 2 # t₁
|
|
216
|
+
t_new[-2] = (self.t_orig[-2] + self.t_orig[-1]) / 2 # tₙ₋₁
|
|
217
|
+
|
|
218
|
+
if self.debug:
|
|
219
|
+
print("Original times: ", self.t_orig)
|
|
220
|
+
print("New times with extra points: ", t_new)
|
|
221
|
+
print("\n")
|
|
222
|
+
|
|
223
|
+
return t_new, q_new
|
|
224
|
+
|
|
225
|
+
def _solve_accelerations(self) -> np.ndarray:
|
|
226
|
+
"""
|
|
227
|
+
Solve for the accelerations by setting up and solving the linear system A ω = c.
|
|
228
|
+
|
|
229
|
+
Following the paper, we solve for interior accelerations [ω₁, ω₂, ..., ωₙ₋₁],
|
|
230
|
+
with ω₀ = a₀ and ωₙ = aₙ known from boundary conditions.
|
|
231
|
+
|
|
232
|
+
Uses the tridiagonal solver for improved efficiency.
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
numpy.ndarray
|
|
237
|
+
Array of accelerations [ω₀, ω₁, ..., ωₙ]
|
|
238
|
+
|
|
239
|
+
Notes
|
|
240
|
+
-----
|
|
241
|
+
This method sets up and solves the tridiagonal system described in equation (4.28)
|
|
242
|
+
of the paper. It also adjusts the positions of the extra points (q₁ and qₙ₋₁)
|
|
243
|
+
using equations (4.26) and (4.27) after the accelerations are computed.
|
|
244
|
+
|
|
245
|
+
The tridiagonal system has special structure for the first and last rows
|
|
246
|
+
to account for the boundary conditions. The system is of size (n-1) x (n-1)
|
|
247
|
+
where n is the number of segments.
|
|
248
|
+
"""
|
|
249
|
+
# Number of segments (number of points - 1)
|
|
250
|
+
n_segments = self.n - 1
|
|
251
|
+
|
|
252
|
+
# We need to solve for n_segments - 1 interior accelerations
|
|
253
|
+
# The system is (n_segments - 1) x (n_segments - 1)
|
|
254
|
+
|
|
255
|
+
# Preparing diagonal arrays
|
|
256
|
+
main_diag = np.zeros(n_segments - 1)
|
|
257
|
+
lower_diag = np.zeros(n_segments - 1) # First element not used
|
|
258
|
+
upper_diag = np.zeros(n_segments - 1) # Last element not used
|
|
259
|
+
|
|
260
|
+
# First element of main diagonal
|
|
261
|
+
main_diag[0] = 2 * self.T[1] + self.T[0] * (3 + self.T[0] / self.T[1])
|
|
262
|
+
if n_segments > MIN_SEGMENTS_FOR_UPPER_DIAG:
|
|
263
|
+
upper_diag[0] = self.T[1]
|
|
264
|
+
|
|
265
|
+
# Last element of main diagonal
|
|
266
|
+
main_diag[-1] = 2 * self.T[-2] + self.T[-1] * (3 + self.T[-1] / self.T[-2])
|
|
267
|
+
if n_segments > MIN_SEGMENTS_FOR_UPPER_DIAG:
|
|
268
|
+
lower_diag[-1] = self.T[-2]
|
|
269
|
+
|
|
270
|
+
if n_segments > MIN_SEGMENTS_FOR_SECOND_ROW:
|
|
271
|
+
# Second row (if it exists)
|
|
272
|
+
lower_diag[1] = self.T[1] - (self.T[0] ** 2 / self.T[1])
|
|
273
|
+
main_diag[1] = 2 * (self.T[1] + self.T[2])
|
|
274
|
+
if n_segments > MIN_SEGMENTS_FOR_UPPER_DIAG_SECOND_ROW:
|
|
275
|
+
upper_diag[1] = self.T[2]
|
|
276
|
+
|
|
277
|
+
# Second-to-last row (if it exists)
|
|
278
|
+
main_diag[-2] = 2 * (self.T[-3] + self.T[-2])
|
|
279
|
+
lower_diag[-2] = self.T[-3]
|
|
280
|
+
upper_diag[-2] = self.T[-2] - self.T[-1] ** 2 / self.T[-2]
|
|
281
|
+
|
|
282
|
+
# Middle rows (if any)
|
|
283
|
+
for i in range(2, n_segments - 3):
|
|
284
|
+
lower_diag[i] = self.T[i]
|
|
285
|
+
main_diag[i] = 2 * (self.T[i] + self.T[i + 1])
|
|
286
|
+
upper_diag[i] = self.T[i + 1]
|
|
287
|
+
|
|
288
|
+
# Construct vector c exactly as in equation (4.28)
|
|
289
|
+
c = np.zeros(n_segments - 1)
|
|
290
|
+
|
|
291
|
+
# First element
|
|
292
|
+
c[0] = 6 * (
|
|
293
|
+
(self.q[2] - self.q[0]) / self.T[1]
|
|
294
|
+
- self.v0 * (1 + self.T[0] / self.T[1])
|
|
295
|
+
- self.a0 * (1 / 2 + self.T[0] / (3 * self.T[1])) * self.T[0]
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Last element
|
|
299
|
+
c[-1] = 6 * (
|
|
300
|
+
(self.q[-3] - self.q[-1]) / self.T[-2]
|
|
301
|
+
+ self.vn * (1 + self.T[-1] / self.T[-2])
|
|
302
|
+
- self.an * (1 / 2 + self.T[-1] / (3 * self.T[-2])) * self.T[-1]
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if n_segments >= MIN_SEGMENTS_FOR_SECOND_ELEMENT:
|
|
306
|
+
# Second element (if there are at least 4 segments)
|
|
307
|
+
c[1] = 6 * (
|
|
308
|
+
(self.q[3] - self.q[2]) / self.T[2]
|
|
309
|
+
- (self.q[2] - self.q[0]) / self.T[1]
|
|
310
|
+
+ self.v0 * self.T[0] / self.T[1]
|
|
311
|
+
+ self.a0 * self.T[0] ** 2 / (3 * self.T[1])
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Second-to-last element (if there are at least 4 segments)
|
|
315
|
+
c[-2] = 6 * (
|
|
316
|
+
(self.q[-1] - self.q[-3]) / self.T[-2]
|
|
317
|
+
- (self.q[-3] - self.q[-4]) / self.T[-3]
|
|
318
|
+
- self.vn * self.T[-1] / self.T[-2]
|
|
319
|
+
+ self.an * self.T[-1] ** 2 / (3 * self.T[-2])
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Middle elements (if there are more than 4 segments)
|
|
323
|
+
for i in range(2, n_segments - 2):
|
|
324
|
+
if i == n_segments - 3:
|
|
325
|
+
continue # Skip since we've already handled the second-to-last element
|
|
326
|
+
c[i] = 6 * (
|
|
327
|
+
(self.q[i + 2] - self.q[i + 1]) / self.T[i + 1]
|
|
328
|
+
- (self.q[i + 1] - self.q[i]) / self.T[i]
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if self.debug:
|
|
332
|
+
print("Main diagonal:", main_diag)
|
|
333
|
+
print("Lower diagonal:", lower_diag)
|
|
334
|
+
print("Upper diagonal:", upper_diag)
|
|
335
|
+
print("Vector c:", c)
|
|
336
|
+
print("\n")
|
|
337
|
+
|
|
338
|
+
# Solve the system using the tridiagonal solver
|
|
339
|
+
interior_omega = solve_tridiagonal(lower_diag, main_diag, upper_diag, c)
|
|
340
|
+
|
|
341
|
+
# Complete accelerations vector with boundary values
|
|
342
|
+
omega = np.zeros(self.n)
|
|
343
|
+
omega[0] = self.a0 # ω₀ = a₀
|
|
344
|
+
omega[1:-1] = interior_omega # ω₁ to ωₙ₋₁
|
|
345
|
+
omega[-1] = self.an # ωₙ = aₙ
|
|
346
|
+
|
|
347
|
+
# Now adjust the positions of extra points using equations (4.26) and (4.27)
|
|
348
|
+
self.q[1] = (
|
|
349
|
+
self.q[0]
|
|
350
|
+
+ self.T[0] * self.v0
|
|
351
|
+
+ (self.T[0] ** 2 / 3) * self.a0
|
|
352
|
+
+ (self.T[0] ** 2 / 6) * omega[1]
|
|
353
|
+
)
|
|
354
|
+
self.q[-2] = (
|
|
355
|
+
self.q[-1]
|
|
356
|
+
- self.T[-1] * self.vn
|
|
357
|
+
+ (self.T[-1] ** 2 / 3) * self.an
|
|
358
|
+
+ (self.T[-1] ** 2 / 6) * omega[-2]
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if self.debug:
|
|
362
|
+
print("Computed accelerations:", omega)
|
|
363
|
+
print("Adjusted q₁:", self.q[1])
|
|
364
|
+
print("Adjusted qₙ₋₁:", self.q[-2])
|
|
365
|
+
print("\n")
|
|
366
|
+
|
|
367
|
+
return omega
|
|
368
|
+
|
|
369
|
+
def _compute_coefficients(self) -> np.ndarray:
|
|
370
|
+
"""
|
|
371
|
+
Compute the polynomial coefficients for each segment using equation (4.25).
|
|
372
|
+
|
|
373
|
+
For each segment k, computes [aₖ₀, aₖ₁, aₖ₂, aₖ₃] where:
|
|
374
|
+
aₖ₀ = qₖ
|
|
375
|
+
aₖ₁ = (qₖ₊₁-qₖ)/Tₖ - (Tₖ/6)(ωₖ₊₁+2ωₖ)
|
|
376
|
+
aₖ₂ = ωₖ/2
|
|
377
|
+
aₖ₃ = (ωₖ₊₁-ωₖ)/(6Tₖ)
|
|
378
|
+
|
|
379
|
+
Returns
|
|
380
|
+
-------
|
|
381
|
+
numpy.ndarray
|
|
382
|
+
Array of shape (n_segments, 4) with coefficients for each segment
|
|
383
|
+
|
|
384
|
+
Notes
|
|
385
|
+
-----
|
|
386
|
+
These coefficients define the cubic polynomial for each segment:
|
|
387
|
+
q(t) = aₖ₀ + aₖ₁(t-tₖ) + aₖ₂(t-tₖ)² + aₖ₃(t-tₖ)³
|
|
388
|
+
|
|
389
|
+
The coefficients are derived to ensure C2 continuity (continuity of position,
|
|
390
|
+
velocity, and acceleration) across segment boundaries.
|
|
391
|
+
"""
|
|
392
|
+
n_segments = self.n - 1
|
|
393
|
+
coeffs = np.zeros((n_segments, 4))
|
|
394
|
+
|
|
395
|
+
for k in range(n_segments):
|
|
396
|
+
# Equation (4.25):
|
|
397
|
+
# aₖ₀ = qₖ
|
|
398
|
+
coeffs[k, 0] = self.q[k]
|
|
399
|
+
|
|
400
|
+
# aₖ₁ = (qₖ₊₁-qₖ)/Tₖ - (Tₖ/6)(ωₖ₊₁+2ωₖ)
|
|
401
|
+
coeffs[k, 1] = (self.q[k + 1] - self.q[k]) / self.T[k] - (self.T[k] / 6) * (
|
|
402
|
+
self.omega[k + 1] + 2 * self.omega[k]
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# aₖ₂ = ωₖ/2
|
|
406
|
+
coeffs[k, 2] = self.omega[k] / 2
|
|
407
|
+
|
|
408
|
+
# aₖ₃ = (ωₖ₊₁-ωₖ)/(6Tₖ)
|
|
409
|
+
coeffs[k, 3] = (self.omega[k + 1] - self.omega[k]) / (6 * self.T[k])
|
|
410
|
+
|
|
411
|
+
if self.debug:
|
|
412
|
+
print("Polynomial coefficients:")
|
|
413
|
+
for k in range(n_segments):
|
|
414
|
+
print(f"Segment {k}: {coeffs[k]}")
|
|
415
|
+
|
|
416
|
+
return coeffs
|
|
417
|
+
|
|
418
|
+
def _get_original_indices(self) -> list[int]:
|
|
419
|
+
"""
|
|
420
|
+
Get the indices in the expanded array that correspond to original points.
|
|
421
|
+
|
|
422
|
+
Returns
|
|
423
|
+
-------
|
|
424
|
+
list of int
|
|
425
|
+
Indices of original points in the expanded arrays
|
|
426
|
+
|
|
427
|
+
Notes
|
|
428
|
+
-----
|
|
429
|
+
This is used primarily for visualization to distinguish between
|
|
430
|
+
original waypoints and extra points added to satisfy constraints.
|
|
431
|
+
"""
|
|
432
|
+
# First point
|
|
433
|
+
indices = [0]
|
|
434
|
+
|
|
435
|
+
# Interior original points - using list.extend instead of append in a loop
|
|
436
|
+
indices.extend([i + 1 for i in range(1, self.n_orig - 1)]) # +1 because we inserted q₁
|
|
437
|
+
|
|
438
|
+
# Last point
|
|
439
|
+
indices.append(self.n - 1)
|
|
440
|
+
|
|
441
|
+
return indices
|
|
442
|
+
|
|
443
|
+
def evaluate(self, t: float | list[float] | np.ndarray) -> float | np.ndarray:
|
|
444
|
+
"""
|
|
445
|
+
Evaluate the spline at time t.
|
|
446
|
+
|
|
447
|
+
Parameters
|
|
448
|
+
----------
|
|
449
|
+
t : float or array_like
|
|
450
|
+
Time point or array of time points
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
float or numpy.ndarray
|
|
455
|
+
Position(s) at the specified time(s)
|
|
456
|
+
|
|
457
|
+
Examples
|
|
458
|
+
--------
|
|
459
|
+
>>> # Evaluate at a single time point
|
|
460
|
+
>>> spline.evaluate(1.5)
|
|
461
|
+
>>> # Evaluate at multiple time points
|
|
462
|
+
>>> spline.evaluate(np.linspace(0, 3, 100))
|
|
463
|
+
"""
|
|
464
|
+
t_array = np.atleast_1d(t)
|
|
465
|
+
result = np.zeros_like(t_array, dtype=float)
|
|
466
|
+
|
|
467
|
+
for i, ti in enumerate(t_array):
|
|
468
|
+
# Find segment containing ti
|
|
469
|
+
if ti <= self.t[0]:
|
|
470
|
+
# Before start of trajectory
|
|
471
|
+
segment = 0
|
|
472
|
+
tau = 0
|
|
473
|
+
elif ti >= self.t[-1]:
|
|
474
|
+
# After end of trajectory
|
|
475
|
+
segment = self.n - 2
|
|
476
|
+
tau = self.T[segment]
|
|
477
|
+
else:
|
|
478
|
+
# Within trajectory
|
|
479
|
+
segment = np.searchsorted(self.t, ti, side="right") - 1
|
|
480
|
+
tau = ti - self.t[segment]
|
|
481
|
+
|
|
482
|
+
# Evaluate polynomial: aₖ₀ + aₖ₁(t-tₖ) + aₖ₂(t-tₖ)² + aₖ₃(t-tₖ)³
|
|
483
|
+
c = self.coeffs[segment]
|
|
484
|
+
result[i] = c[0] + c[1] * tau + c[2] * tau**2 + c[3] * tau**3
|
|
485
|
+
|
|
486
|
+
return result[0] if len(result) == 1 else result
|
|
487
|
+
|
|
488
|
+
def evaluate_velocity(self, t: float | list[float] | np.ndarray) -> float | np.ndarray:
|
|
489
|
+
"""
|
|
490
|
+
Evaluate the velocity at time t.
|
|
491
|
+
|
|
492
|
+
Parameters
|
|
493
|
+
----------
|
|
494
|
+
t : float or array_like
|
|
495
|
+
Time point or array of time points
|
|
496
|
+
|
|
497
|
+
Returns
|
|
498
|
+
-------
|
|
499
|
+
float or numpy.ndarray
|
|
500
|
+
Velocity at the specified time(s)
|
|
501
|
+
|
|
502
|
+
Examples
|
|
503
|
+
--------
|
|
504
|
+
>>> # Evaluate velocity at a single time point
|
|
505
|
+
>>> spline.evaluate_velocity(1.5)
|
|
506
|
+
>>> # Evaluate velocity at multiple time points
|
|
507
|
+
>>> spline.evaluate_velocity(np.linspace(0, 3, 100))
|
|
508
|
+
"""
|
|
509
|
+
t_array = np.atleast_1d(t)
|
|
510
|
+
result = np.zeros_like(t_array, dtype=float)
|
|
511
|
+
|
|
512
|
+
for i, ti in enumerate(t_array):
|
|
513
|
+
# Find segment containing ti
|
|
514
|
+
if ti <= self.t[0]:
|
|
515
|
+
segment = 0
|
|
516
|
+
tau = 0
|
|
517
|
+
elif ti >= self.t[-1]:
|
|
518
|
+
segment = self.n - 2
|
|
519
|
+
tau = self.T[segment]
|
|
520
|
+
else:
|
|
521
|
+
segment = np.searchsorted(self.t, ti, side="right") - 1
|
|
522
|
+
tau = ti - self.t[segment]
|
|
523
|
+
|
|
524
|
+
# Evaluate derivative: aₖ₁ + 2aₖ₂(t-tₖ) + 3aₖ₃(t-tₖ)²
|
|
525
|
+
c = self.coeffs[segment]
|
|
526
|
+
result[i] = c[1] + 2 * c[2] * tau + 3 * c[3] * tau**2
|
|
527
|
+
|
|
528
|
+
return result[0] if len(result) == 1 else result
|
|
529
|
+
|
|
530
|
+
def evaluate_acceleration(self, t: float | list[float] | np.ndarray) -> float | np.ndarray:
|
|
531
|
+
"""
|
|
532
|
+
Evaluate the acceleration at time t.
|
|
533
|
+
|
|
534
|
+
Parameters
|
|
535
|
+
----------
|
|
536
|
+
t : float or array_like
|
|
537
|
+
Time point or array of time points
|
|
538
|
+
|
|
539
|
+
Returns
|
|
540
|
+
-------
|
|
541
|
+
float or numpy.ndarray
|
|
542
|
+
Acceleration at the specified time(s)
|
|
543
|
+
|
|
544
|
+
Examples
|
|
545
|
+
--------
|
|
546
|
+
>>> # Evaluate acceleration at a single time point
|
|
547
|
+
>>> spline.evaluate_acceleration(1.5)
|
|
548
|
+
>>> # Evaluate acceleration at multiple time points
|
|
549
|
+
>>> spline.evaluate_acceleration(np.linspace(0, 3, 100))
|
|
550
|
+
"""
|
|
551
|
+
t_array = np.atleast_1d(t)
|
|
552
|
+
result = np.zeros_like(t_array, dtype=float)
|
|
553
|
+
|
|
554
|
+
for i, ti in enumerate(t_array):
|
|
555
|
+
# Find segment containing ti
|
|
556
|
+
if ti <= self.t[0]:
|
|
557
|
+
segment = 0
|
|
558
|
+
tau = 0
|
|
559
|
+
elif ti >= self.t[-1]:
|
|
560
|
+
segment = self.n - 2
|
|
561
|
+
tau = self.T[segment]
|
|
562
|
+
else:
|
|
563
|
+
segment = np.searchsorted(self.t, ti, side="right") - 1
|
|
564
|
+
tau = ti - self.t[segment]
|
|
565
|
+
|
|
566
|
+
# Evaluate second derivative: 2aₖ₂ + 6aₖ₃(t-tₖ)
|
|
567
|
+
c = self.coeffs[segment]
|
|
568
|
+
result[i] = 2 * c[2] + 6 * c[3] * tau
|
|
569
|
+
|
|
570
|
+
return result[0] if len(result) == 1 else result
|
|
571
|
+
|
|
572
|
+
def plot(self, num_points: int = 1000) -> None:
|
|
573
|
+
"""
|
|
574
|
+
Plot the spline trajectory with velocity and acceleration profiles.
|
|
575
|
+
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
num_points : int, optional
|
|
579
|
+
Number of points for smooth plotting. Default is 1000
|
|
580
|
+
|
|
581
|
+
Returns
|
|
582
|
+
-------
|
|
583
|
+
None
|
|
584
|
+
Displays the plot using matplotlib
|
|
585
|
+
|
|
586
|
+
Notes
|
|
587
|
+
-----
|
|
588
|
+
This method creates a figure with three subplots showing:
|
|
589
|
+
1. Position trajectory with original and extra waypoints
|
|
590
|
+
2. Velocity profile with initial and final velocities marked
|
|
591
|
+
3. Acceleration profile with initial and final accelerations marked
|
|
592
|
+
|
|
593
|
+
Original waypoints are shown as red circles, while extra points
|
|
594
|
+
are shown as green x-marks.
|
|
595
|
+
"""
|
|
596
|
+
# Generate evaluation points
|
|
597
|
+
t_eval = np.linspace(self.t[0], self.t[-1], num_points)
|
|
598
|
+
|
|
599
|
+
# Evaluate the spline and its derivatives
|
|
600
|
+
q = self.evaluate(t_eval)
|
|
601
|
+
v = self.evaluate_velocity(t_eval)
|
|
602
|
+
a = self.evaluate_acceleration(t_eval)
|
|
603
|
+
|
|
604
|
+
# Get original points for plotting
|
|
605
|
+
original_t = [self.t[i] for i in self.original_indices]
|
|
606
|
+
original_q = [self.q[i] for i in self.original_indices]
|
|
607
|
+
|
|
608
|
+
# Get extra points
|
|
609
|
+
extra_indices = [i for i in range(self.n) if i not in self.original_indices]
|
|
610
|
+
extra_t = [self.t[i] for i in extra_indices]
|
|
611
|
+
extra_q = [self.q[i] for i in extra_indices]
|
|
612
|
+
|
|
613
|
+
# Create a figure with three subplots
|
|
614
|
+
_fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 8), sharex=True)
|
|
615
|
+
|
|
616
|
+
# Position plot
|
|
617
|
+
ax1.plot(t_eval, q, "b-", linewidth=2, label="Spline")
|
|
618
|
+
ax1.plot(original_t, original_q, "ro", markersize=8, label="Original Points")
|
|
619
|
+
ax1.plot(extra_t, extra_q, "gx", markersize=8, label="Extra Points")
|
|
620
|
+
ax1.set_ylabel("Position")
|
|
621
|
+
ax1.grid(True)
|
|
622
|
+
ax1.legend()
|
|
623
|
+
ax1.set_title("Cubic Spline with Velocity and Acceleration Constraints")
|
|
624
|
+
|
|
625
|
+
# Velocity plot
|
|
626
|
+
ax2.plot(t_eval, v, "g-", linewidth=2)
|
|
627
|
+
ax2.plot(self.t[0], self.v0, "bo", markersize=6, label=f"Initial v: {self.v0}")
|
|
628
|
+
ax2.plot(self.t[-1], self.vn, "bo", markersize=6, label=f"Final v: {self.vn}")
|
|
629
|
+
ax2.set_ylabel("Velocity")
|
|
630
|
+
ax2.grid(True)
|
|
631
|
+
ax2.legend()
|
|
632
|
+
|
|
633
|
+
# Acceleration plot
|
|
634
|
+
ax3.plot(t_eval, a, "r-", linewidth=2)
|
|
635
|
+
ax3.plot(self.t[0], self.a0, "bo", markersize=6, label=f"Initial a: {self.a0}")
|
|
636
|
+
ax3.plot(self.t[-1], self.an, "bo", markersize=6, label=f"Final a: {self.an}")
|
|
637
|
+
ax3.set_ylabel("Acceleration")
|
|
638
|
+
ax3.set_xlabel("Time")
|
|
639
|
+
ax3.grid(True)
|
|
640
|
+
ax3.legend()
|
|
641
|
+
|
|
642
|
+
plt.tight_layout()
|
|
643
|
+
plt.show()
|