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,611 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CubicSmoothingSpline:
|
|
6
|
+
"""
|
|
7
|
+
Cubic smoothing spline trajectory planning with control over the smoothness
|
|
8
|
+
versus waypoint accuracy trade-off.
|
|
9
|
+
|
|
10
|
+
This class implements cubic smoothing splines for trajectory generation as
|
|
11
|
+
described in section 4.4.5 of the textbook. The algorithm minimizes a weighted
|
|
12
|
+
sum of waypoint error and acceleration magnitude, allowing the user to control
|
|
13
|
+
the trade-off between path smoothness and waypoint accuracy through the
|
|
14
|
+
parameter μ.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
t_points : list[float] or array_like
|
|
19
|
+
Time points [t₀, t₁, t₂, ..., tₙ] for the spline knots.
|
|
20
|
+
q_points : list[float] or array_like
|
|
21
|
+
Position points [q₀, q₁, q₂, ..., qₙ] at each time point.
|
|
22
|
+
mu : float, optional
|
|
23
|
+
Trade-off parameter between accuracy (μ=1) and smoothness (μ=0).
|
|
24
|
+
Must be in range (0, 1]. Default is 0.5.
|
|
25
|
+
weights : list[float] or array_like, optional
|
|
26
|
+
Individual point weights [w₀, w₁, ..., wₙ]. Higher values enforce
|
|
27
|
+
closer approximation at corresponding points. Default is None
|
|
28
|
+
(equal weights of 1.0 for all points).
|
|
29
|
+
v0 : float, optional
|
|
30
|
+
Initial velocity constraint at t₀. Default is 0.0.
|
|
31
|
+
vn : float, optional
|
|
32
|
+
Final velocity constraint at tₙ. Default is 0.0.
|
|
33
|
+
debug : bool, optional
|
|
34
|
+
Whether to print debug information. Default is False.
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
t : ndarray
|
|
39
|
+
Time points array.
|
|
40
|
+
q : ndarray
|
|
41
|
+
Original position points array.
|
|
42
|
+
s : ndarray
|
|
43
|
+
Approximated position points array.
|
|
44
|
+
mu : float
|
|
45
|
+
Smoothing parameter value.
|
|
46
|
+
lambd : float
|
|
47
|
+
Lambda parameter derived from μ: λ = (1-μ)/(6μ).
|
|
48
|
+
omega : ndarray
|
|
49
|
+
Acceleration values at each time point.
|
|
50
|
+
coeffs : ndarray
|
|
51
|
+
Polynomial coefficients for each segment.
|
|
52
|
+
|
|
53
|
+
Raises
|
|
54
|
+
------
|
|
55
|
+
ValueError
|
|
56
|
+
If time and position arrays have different lengths.
|
|
57
|
+
If fewer than 2 points are provided.
|
|
58
|
+
If time points are not strictly increasing.
|
|
59
|
+
If parameter μ is outside the valid range (0, 1].
|
|
60
|
+
If weights array length doesn't match time and position arrays.
|
|
61
|
+
|
|
62
|
+
Notes
|
|
63
|
+
-----
|
|
64
|
+
For μ=1, the spline performs exact interpolation.
|
|
65
|
+
For μ approaching 0, the spline becomes increasingly smooth.
|
|
66
|
+
Setting weight to infinity for a point forces exact interpolation at that point.
|
|
67
|
+
|
|
68
|
+
References
|
|
69
|
+
----------
|
|
70
|
+
The implementation follows section 4.4.5 of the robotics textbook
|
|
71
|
+
describing cubic smoothing splines for trajectory generation.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Constants to replace magic numbers
|
|
75
|
+
MIN_POINTS_REQUIRED = 2
|
|
76
|
+
HIGH_CONDITION_THRESHOLD = 1e12
|
|
77
|
+
REGULARIZATION_FACTOR = 1e-8
|
|
78
|
+
|
|
79
|
+
def __init__( # noqa: PLR0913, PLR0917
|
|
80
|
+
self,
|
|
81
|
+
t_points: list[float],
|
|
82
|
+
q_points: list[float],
|
|
83
|
+
mu: float = 0.5,
|
|
84
|
+
weights: list[float] | None = None,
|
|
85
|
+
v0: float = 0.0,
|
|
86
|
+
vn: float = 0.0,
|
|
87
|
+
debug: bool = False,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Initialize the cubic smoothing spline.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
t_points : list[float] or array_like
|
|
95
|
+
Time points [t₀, t₁, t₂, ..., tₙ] for the spline knots.
|
|
96
|
+
q_points : list[float] or array_like
|
|
97
|
+
Position points [q₀, q₁, q₂, ..., qₙ] at each time point.
|
|
98
|
+
mu : float, optional
|
|
99
|
+
Trade-off parameter between accuracy (μ=1) and smoothness (μ=0).
|
|
100
|
+
Must be in range (0, 1]. Default is 0.5.
|
|
101
|
+
weights : list[float] or array_like, optional
|
|
102
|
+
Individual point weights [w₀, w₁, ..., wₙ]. Higher values enforce
|
|
103
|
+
closer approximation at corresponding points. Default is None
|
|
104
|
+
(equal weights of 1.0 for all points).
|
|
105
|
+
v0 : float, optional
|
|
106
|
+
Initial velocity constraint at t₀. Default is 0.0.
|
|
107
|
+
vn : float, optional
|
|
108
|
+
Final velocity constraint at tₙ. Default is 0.0.
|
|
109
|
+
debug : bool, optional
|
|
110
|
+
Whether to print debug information. Default is False.
|
|
111
|
+
|
|
112
|
+
Raises
|
|
113
|
+
------
|
|
114
|
+
ValueError
|
|
115
|
+
If time and position arrays have different lengths.
|
|
116
|
+
If fewer than 2 points are provided.
|
|
117
|
+
If time points are not strictly increasing.
|
|
118
|
+
If parameter μ is outside the valid range (0, 1].
|
|
119
|
+
If weights array length doesn't match time and position arrays.
|
|
120
|
+
"""
|
|
121
|
+
# Validate inputs
|
|
122
|
+
if len(t_points) != len(q_points):
|
|
123
|
+
raise ValueError("Time and position arrays must have the same length")
|
|
124
|
+
|
|
125
|
+
if len(t_points) < self.MIN_POINTS_REQUIRED:
|
|
126
|
+
raise ValueError("At least two points are required")
|
|
127
|
+
|
|
128
|
+
if not np.all(np.diff(t_points) > 0):
|
|
129
|
+
raise ValueError("Time points must be strictly increasing")
|
|
130
|
+
|
|
131
|
+
# Corrected validation for mu - should be (0, 1] not [0, 1]
|
|
132
|
+
if mu <= 0.0 or mu > 1.0:
|
|
133
|
+
raise ValueError("Parameter μ must be in range (0, 1]")
|
|
134
|
+
|
|
135
|
+
# Store parameters
|
|
136
|
+
self.t = np.array(t_points, dtype=float)
|
|
137
|
+
self.q = np.array(q_points, dtype=float)
|
|
138
|
+
self.mu = float(mu)
|
|
139
|
+
self.v0 = float(v0)
|
|
140
|
+
self.vn = float(vn)
|
|
141
|
+
self.debug = debug
|
|
142
|
+
|
|
143
|
+
# Number of points
|
|
144
|
+
self.n = len(self.t)
|
|
145
|
+
|
|
146
|
+
# Compute lambda parameter: λ = (1-μ)/(6μ)
|
|
147
|
+
# Add small epsilon to avoid division by zero if mu is very close to 0
|
|
148
|
+
self.lambd = (1 - self.mu) / (6 * self.mu + 1e-10) if self.mu < 1 else 0
|
|
149
|
+
|
|
150
|
+
# Compute time intervals
|
|
151
|
+
self.time_intervals = np.diff(self.t)
|
|
152
|
+
|
|
153
|
+
# Initialize weights or use default (equal weights)
|
|
154
|
+
if weights is None:
|
|
155
|
+
# Default: all points have equal weight of 1
|
|
156
|
+
self.w = np.ones(self.n)
|
|
157
|
+
else:
|
|
158
|
+
if len(weights) != self.n:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
"Weights array must have the same length as time and position arrays"
|
|
161
|
+
)
|
|
162
|
+
self.w = np.array(weights, dtype=float)
|
|
163
|
+
|
|
164
|
+
# Handle infinite weights (fixed points)
|
|
165
|
+
self.w_inv = np.zeros(self.n)
|
|
166
|
+
finite_mask = np.isfinite(self.w)
|
|
167
|
+
self.w_inv[finite_mask] = 1.0 / self.w[finite_mask]
|
|
168
|
+
|
|
169
|
+
if self.debug:
|
|
170
|
+
print("Smoothing parameter μ:", self.mu)
|
|
171
|
+
print("Lambda λ:", self.lambd)
|
|
172
|
+
print("Weights:", self.w)
|
|
173
|
+
print("Inverse weights:", self.w_inv)
|
|
174
|
+
|
|
175
|
+
# Construct the matrices
|
|
176
|
+
self.a_matrix, self.c_matrix = self._construct_matrices()
|
|
177
|
+
|
|
178
|
+
# Solve the system to get accelerations
|
|
179
|
+
self.omega = self._solve_system()
|
|
180
|
+
|
|
181
|
+
# Compute the approximated positions
|
|
182
|
+
self.s = self._compute_positions()
|
|
183
|
+
|
|
184
|
+
# Compute polynomial coefficients
|
|
185
|
+
self.coeffs = self._compute_coefficients()
|
|
186
|
+
|
|
187
|
+
if self.debug:
|
|
188
|
+
print("Original points:", self.q)
|
|
189
|
+
print("Approximated points:", self.s)
|
|
190
|
+
print("Accelerations:", self.omega)
|
|
191
|
+
print("Maximum position error:", np.max(np.abs(self.q - self.s)))
|
|
192
|
+
|
|
193
|
+
def _construct_matrices(self) -> tuple[np.ndarray, np.ndarray]:
|
|
194
|
+
"""
|
|
195
|
+
Construct matrices A and C for the linear system.
|
|
196
|
+
|
|
197
|
+
Builds the tridiagonal matrix A and the matrix C necessary for solving
|
|
198
|
+
the cubic smoothing spline system of equations.
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
tuple[np.ndarray, np.ndarray]
|
|
203
|
+
a_matrix : ndarray
|
|
204
|
+
Tridiagonal matrix relating accelerations, shape (n, n).
|
|
205
|
+
c_matrix : ndarray
|
|
206
|
+
Matrix relating positions to accelerations, shape (n, n).
|
|
207
|
+
|
|
208
|
+
Notes
|
|
209
|
+
-----
|
|
210
|
+
A matrix is constructed according to equation 4.23 in the textbook.
|
|
211
|
+
C matrix is constructed according to equation 4.34 for the smoothing spline.
|
|
212
|
+
"""
|
|
213
|
+
n = self.n
|
|
214
|
+
time_intervals = self.time_intervals
|
|
215
|
+
|
|
216
|
+
# Construct A matrix (equation 4.23)
|
|
217
|
+
# A is symmetric and tridiagonal
|
|
218
|
+
a_matrix = np.zeros((n, n))
|
|
219
|
+
|
|
220
|
+
# Fill main diagonal
|
|
221
|
+
a_matrix[0, 0] = 2 * time_intervals[0]
|
|
222
|
+
a_matrix[n - 1, n - 1] = 2 * time_intervals[n - 2]
|
|
223
|
+
|
|
224
|
+
for i in range(1, n - 1):
|
|
225
|
+
a_matrix[i, i] = 2 * (time_intervals[i - 1] + time_intervals[i])
|
|
226
|
+
|
|
227
|
+
# Fill upper and lower diagonals
|
|
228
|
+
for i in range(n - 1):
|
|
229
|
+
a_matrix[i, i + 1] = time_intervals[i]
|
|
230
|
+
a_matrix[i + 1, i] = time_intervals[i] # Symmetric
|
|
231
|
+
|
|
232
|
+
# Construct C matrix (equation 4.34) for the smoothing spline
|
|
233
|
+
# This matrix relates positions to accelerations
|
|
234
|
+
c_matrix = np.zeros((n, n))
|
|
235
|
+
|
|
236
|
+
# First row with initial velocity constraint
|
|
237
|
+
c_matrix[0, 0] = -6 / time_intervals[0]
|
|
238
|
+
c_matrix[0, 1] = 6 / time_intervals[0]
|
|
239
|
+
|
|
240
|
+
# Last row with final velocity constraint
|
|
241
|
+
c_matrix[n - 1, n - 2] = 6 / time_intervals[n - 2]
|
|
242
|
+
c_matrix[n - 1, n - 1] = -6 / time_intervals[n - 2]
|
|
243
|
+
|
|
244
|
+
# Interior rows
|
|
245
|
+
for i in range(1, n - 1):
|
|
246
|
+
c_matrix[i, i - 1] = 6 / time_intervals[i - 1]
|
|
247
|
+
c_matrix[i, i] = -(6 / time_intervals[i - 1] + 6 / time_intervals[i])
|
|
248
|
+
c_matrix[i, i + 1] = 6 / time_intervals[i]
|
|
249
|
+
|
|
250
|
+
if self.debug:
|
|
251
|
+
print("Matrix A:\n", a_matrix)
|
|
252
|
+
print("Matrix C:\n", c_matrix)
|
|
253
|
+
|
|
254
|
+
return a_matrix, c_matrix
|
|
255
|
+
|
|
256
|
+
def _solve_system(self) -> np.ndarray:
|
|
257
|
+
"""
|
|
258
|
+
Solve the linear system to find the accelerations.
|
|
259
|
+
|
|
260
|
+
Solves either the pure interpolation system (μ=1) or the smoothing
|
|
261
|
+
spline system (0<μ<1) to determine the acceleration values at each
|
|
262
|
+
time point.
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
np.ndarray
|
|
267
|
+
Vector of accelerations ω at each time point.
|
|
268
|
+
|
|
269
|
+
Notes
|
|
270
|
+
-----
|
|
271
|
+
For pure interpolation (μ=1): Aω = c (equation 4.22)
|
|
272
|
+
For smoothing spline (0<μ<1): (A + λCW⁻¹Cᵀ)ω = Cq (equation 4.35)
|
|
273
|
+
|
|
274
|
+
If the system is poorly conditioned, a small regularization term
|
|
275
|
+
is added to improve numerical stability.
|
|
276
|
+
"""
|
|
277
|
+
n = self.n
|
|
278
|
+
|
|
279
|
+
# For pure interpolation (μ = 1): Aω = c (equation 4.22)
|
|
280
|
+
if self.mu == 1.0:
|
|
281
|
+
# Construct the vector c according to equation (4.24)
|
|
282
|
+
c = np.zeros(n)
|
|
283
|
+
|
|
284
|
+
# First element - with initial velocity constraint
|
|
285
|
+
c[0] = 6 * ((self.q[1] - self.q[0]) / self.time_intervals[0] - self.v0)
|
|
286
|
+
|
|
287
|
+
# Last element - with final velocity constraint
|
|
288
|
+
c[n - 1] = 6 * (self.vn - (self.q[n - 1] - self.q[n - 2]) / self.time_intervals[n - 2])
|
|
289
|
+
|
|
290
|
+
# Interior elements
|
|
291
|
+
for i in range(1, n - 1):
|
|
292
|
+
c[i] = 6 * (
|
|
293
|
+
(self.q[i + 1] - self.q[i]) / self.time_intervals[i]
|
|
294
|
+
- (self.q[i] - self.q[i - 1]) / self.time_intervals[i - 1]
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if self.debug:
|
|
298
|
+
print("Vector c (pure interpolation):", c)
|
|
299
|
+
|
|
300
|
+
# Solve Aω = c using a more robust solver
|
|
301
|
+
# Use solve instead of inv for better numerical stability
|
|
302
|
+
return np.linalg.solve(self.a_matrix, c)
|
|
303
|
+
|
|
304
|
+
# For smoothing spline (0 < μ < 1): (A + λCW⁻¹Cᵀ)ω = Cq (equation 4.35)
|
|
305
|
+
# Right hand side: Cq
|
|
306
|
+
rhs = self.c_matrix @ self.q
|
|
307
|
+
|
|
308
|
+
# Left hand side: (A + λCW⁻¹Cᵀ)
|
|
309
|
+
# Create w_inv as a diagonal matrix
|
|
310
|
+
w_inv_diag = np.diag(self.w_inv)
|
|
311
|
+
|
|
312
|
+
# Compute the matrix product more carefully
|
|
313
|
+
c_w_inv_ct = self.c_matrix @ w_inv_diag @ self.c_matrix.T
|
|
314
|
+
|
|
315
|
+
# Make sure c_w_inv_ct is symmetric (could be slightly asymmetric due to numerical issues)
|
|
316
|
+
c_w_inv_ct = (c_w_inv_ct + c_w_inv_ct.T) / 2
|
|
317
|
+
|
|
318
|
+
system_matrix = self.a_matrix + self.lambd * c_w_inv_ct
|
|
319
|
+
|
|
320
|
+
# Ensure system_matrix is symmetric for better numerical stability
|
|
321
|
+
system_matrix = (system_matrix + system_matrix.T) / 2
|
|
322
|
+
|
|
323
|
+
if self.debug:
|
|
324
|
+
print("System matrix (smoothing):\n", system_matrix)
|
|
325
|
+
print("RHS vector:", rhs)
|
|
326
|
+
print("Condition number:", np.linalg.cond(system_matrix))
|
|
327
|
+
|
|
328
|
+
# Add small regularization if the matrix is poorly conditioned
|
|
329
|
+
if np.linalg.cond(system_matrix) > self.HIGH_CONDITION_THRESHOLD:
|
|
330
|
+
system_matrix += np.eye(n) * self.REGULARIZATION_FACTOR
|
|
331
|
+
if self.debug:
|
|
332
|
+
print("Added regularization. New condition number:", np.linalg.cond(system_matrix))
|
|
333
|
+
|
|
334
|
+
# Solve the system
|
|
335
|
+
try:
|
|
336
|
+
omega = np.linalg.solve(system_matrix, rhs)
|
|
337
|
+
except np.linalg.LinAlgError:
|
|
338
|
+
print("Warning: Linear system is singular or poorly conditioned.")
|
|
339
|
+
# Use least squares to find a solution
|
|
340
|
+
omega, _residuals, _rank, _s = np.linalg.lstsq(system_matrix, rhs, rcond=None)
|
|
341
|
+
|
|
342
|
+
return omega
|
|
343
|
+
|
|
344
|
+
def _compute_positions(self) -> np.ndarray:
|
|
345
|
+
"""
|
|
346
|
+
Compute the approximated positions using equation (4.36).
|
|
347
|
+
|
|
348
|
+
For pure interpolation (μ=1), returns exact positions.
|
|
349
|
+
For smoothing spline (0<μ<1), computes approximated positions.
|
|
350
|
+
|
|
351
|
+
Returns
|
|
352
|
+
-------
|
|
353
|
+
np.ndarray
|
|
354
|
+
Vector of approximated positions s.
|
|
355
|
+
|
|
356
|
+
Notes
|
|
357
|
+
-----
|
|
358
|
+
For pure interpolation (μ=1): s = q (exact fit)
|
|
359
|
+
For smoothing spline (0<μ<1): s = q - λW⁻¹Cᵀω (equation 4.36)
|
|
360
|
+
"""
|
|
361
|
+
# For pure interpolation (μ = 1), s = q (exact fit)
|
|
362
|
+
if self.mu == 1.0:
|
|
363
|
+
return self.q.copy()
|
|
364
|
+
|
|
365
|
+
# For smoothing spline (0 < μ < 1), s = q - λW⁻¹Cᵀω (equation 4.36)
|
|
366
|
+
# Compute W⁻¹Cᵀω more explicitly to avoid potential issues
|
|
367
|
+
ct_omega = self.c_matrix.T @ self.omega
|
|
368
|
+
adjustment = self.lambd * (self.w_inv * ct_omega)
|
|
369
|
+
s = self.q - adjustment
|
|
370
|
+
|
|
371
|
+
if self.debug:
|
|
372
|
+
print(f"Computed s: {s} for mu: {self.mu}")
|
|
373
|
+
return s
|
|
374
|
+
|
|
375
|
+
def _compute_coefficients(self) -> np.ndarray:
|
|
376
|
+
"""
|
|
377
|
+
Compute polynomial coefficients for each segment.
|
|
378
|
+
|
|
379
|
+
For each segment k from 0 to n-2, computes the cubic polynomial
|
|
380
|
+
coefficients that define the spline.
|
|
381
|
+
|
|
382
|
+
Returns
|
|
383
|
+
-------
|
|
384
|
+
np.ndarray
|
|
385
|
+
Array of shape (n-1, 4) with coefficients [a₀, a₁, a₂, a₃]
|
|
386
|
+
for each segment.
|
|
387
|
+
|
|
388
|
+
Notes
|
|
389
|
+
-----
|
|
390
|
+
For each segment k from 0 to n-2, computes:
|
|
391
|
+
a₀ = s(tₖ)
|
|
392
|
+
a₁ = (s(tₖ₊₁) - s(tₖ))/Tₖ - (Tₖ/6)·(ωₖ₊₁ + 2ωₖ)
|
|
393
|
+
a₂ = ωₖ/2
|
|
394
|
+
a₃ = (ωₖ₊₁ - ωₖ)/(6·Tₖ)
|
|
395
|
+
|
|
396
|
+
The resulting polynomial for segment k is:
|
|
397
|
+
p(τ) = a₀ + a₁·τ + a₂·τ² + a₃·τ³
|
|
398
|
+
where τ = t - tₖ is the local time within the segment.
|
|
399
|
+
"""
|
|
400
|
+
n_segments = self.n - 1
|
|
401
|
+
coeffs = np.zeros((n_segments, 4))
|
|
402
|
+
|
|
403
|
+
for k in range(n_segments):
|
|
404
|
+
# Position coefficient
|
|
405
|
+
coeffs[k, 0] = self.s[k]
|
|
406
|
+
|
|
407
|
+
# Velocity coefficient - corrected formula from standard cubic spline theory
|
|
408
|
+
coeffs[k, 1] = (self.s[k + 1] - self.s[k]) / self.time_intervals[k] - (
|
|
409
|
+
self.time_intervals[k] / 6
|
|
410
|
+
) * (self.omega[k + 1] + 2 * self.omega[k])
|
|
411
|
+
|
|
412
|
+
# Acceleration coefficient
|
|
413
|
+
coeffs[k, 2] = self.omega[k] / 2
|
|
414
|
+
|
|
415
|
+
# Jerk coefficient
|
|
416
|
+
coeffs[k, 3] = (self.omega[k + 1] - self.omega[k]) / (6 * self.time_intervals[k])
|
|
417
|
+
|
|
418
|
+
if self.debug:
|
|
419
|
+
print("Polynomial coefficients:")
|
|
420
|
+
for k in range(n_segments):
|
|
421
|
+
print(f"Segment {k}: {coeffs[k]}")
|
|
422
|
+
|
|
423
|
+
return coeffs
|
|
424
|
+
|
|
425
|
+
def evaluate(self, t: float | list[float] | np.ndarray) -> float | np.ndarray:
|
|
426
|
+
"""
|
|
427
|
+
Evaluate the spline at time t.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
t : float or list[float] or np.ndarray
|
|
432
|
+
Time point or array of time points at which to evaluate the spline.
|
|
433
|
+
|
|
434
|
+
Returns
|
|
435
|
+
-------
|
|
436
|
+
float or np.ndarray
|
|
437
|
+
Position(s) at the specified time(s). Returns a scalar if input
|
|
438
|
+
is a scalar, otherwise returns an array matching the shape of input.
|
|
439
|
+
|
|
440
|
+
Notes
|
|
441
|
+
-----
|
|
442
|
+
For t < t₀ or t > tₙ, the function extrapolates using the first or
|
|
443
|
+
last segment respectively.
|
|
444
|
+
"""
|
|
445
|
+
t_array = np.atleast_1d(t)
|
|
446
|
+
result = np.zeros_like(t_array, dtype=float)
|
|
447
|
+
|
|
448
|
+
for i, ti in enumerate(t_array):
|
|
449
|
+
# Find segment containing ti
|
|
450
|
+
if ti <= self.t[0]:
|
|
451
|
+
# Improved handling for times before start of trajectory
|
|
452
|
+
# Extrapolate using the first segment
|
|
453
|
+
segment = 0
|
|
454
|
+
tau = ti - self.t[segment] # Can be negative for extrapolation
|
|
455
|
+
elif ti >= self.t[-1]:
|
|
456
|
+
# Improved handling for times after end of trajectory
|
|
457
|
+
# Extrapolate using the last segment
|
|
458
|
+
segment = self.n - 2
|
|
459
|
+
tau = ti - self.t[segment] # Will be > T[segment] for extrapolation
|
|
460
|
+
else:
|
|
461
|
+
# Within trajectory - find the correct segment
|
|
462
|
+
segment = np.searchsorted(self.t, ti, side="right") - 1
|
|
463
|
+
tau = ti - self.t[segment]
|
|
464
|
+
|
|
465
|
+
# Evaluate polynomial: a₀ + a₁τ + a₂τ² + a₃τ³
|
|
466
|
+
c = self.coeffs[segment]
|
|
467
|
+
result[i] = c[0] + c[1] * tau + c[2] * tau**2 + c[3] * tau**3
|
|
468
|
+
|
|
469
|
+
return result[0] if len(t_array) == 1 else result
|
|
470
|
+
|
|
471
|
+
def evaluate_velocity(self, t: float | list[float] | np.ndarray) -> float | np.ndarray:
|
|
472
|
+
"""
|
|
473
|
+
Evaluate the velocity at time t.
|
|
474
|
+
|
|
475
|
+
Parameters
|
|
476
|
+
----------
|
|
477
|
+
t : float or list[float] or np.ndarray
|
|
478
|
+
Time point or array of time points at which to evaluate velocity.
|
|
479
|
+
|
|
480
|
+
Returns
|
|
481
|
+
-------
|
|
482
|
+
float or np.ndarray
|
|
483
|
+
Velocity at the specified time(s). Returns a scalar if input
|
|
484
|
+
is a scalar, otherwise returns an array matching the shape of input.
|
|
485
|
+
|
|
486
|
+
Notes
|
|
487
|
+
-----
|
|
488
|
+
Computes the first derivative of the spline at the given time(s).
|
|
489
|
+
For t < t₀ or t > tₙ, the function extrapolates using the first or
|
|
490
|
+
last segment respectively.
|
|
491
|
+
"""
|
|
492
|
+
t_array = np.atleast_1d(t)
|
|
493
|
+
result = np.zeros_like(t_array, dtype=float)
|
|
494
|
+
|
|
495
|
+
for i, ti in enumerate(t_array):
|
|
496
|
+
# Find segment containing ti
|
|
497
|
+
if ti <= self.t[0]:
|
|
498
|
+
segment = 0
|
|
499
|
+
tau = ti - self.t[segment] # Corrected extrapolation
|
|
500
|
+
elif ti >= self.t[-1]:
|
|
501
|
+
segment = self.n - 2
|
|
502
|
+
tau = ti - self.t[segment] # Corrected extrapolation
|
|
503
|
+
else:
|
|
504
|
+
segment = np.searchsorted(self.t, ti, side="right") - 1
|
|
505
|
+
tau = ti - self.t[segment]
|
|
506
|
+
|
|
507
|
+
# Evaluate first derivative: a₁ + 2a₂τ + 3a₃τ²
|
|
508
|
+
c = self.coeffs[segment]
|
|
509
|
+
result[i] = c[1] + 2 * c[2] * tau + 3 * c[3] * tau**2
|
|
510
|
+
|
|
511
|
+
return result[0] if len(t_array) == 1 else result
|
|
512
|
+
|
|
513
|
+
def evaluate_acceleration(self, t: float | list[float] | np.ndarray) -> float | np.ndarray:
|
|
514
|
+
"""
|
|
515
|
+
Evaluate the acceleration at time t.
|
|
516
|
+
|
|
517
|
+
Parameters
|
|
518
|
+
----------
|
|
519
|
+
t : float or list[float] or np.ndarray
|
|
520
|
+
Time point or array of time points at which to evaluate acceleration.
|
|
521
|
+
|
|
522
|
+
Returns
|
|
523
|
+
-------
|
|
524
|
+
float or np.ndarray
|
|
525
|
+
Acceleration at the specified time(s). Returns a scalar if input
|
|
526
|
+
is a scalar, otherwise returns an array matching the shape of input.
|
|
527
|
+
|
|
528
|
+
Notes
|
|
529
|
+
-----
|
|
530
|
+
Computes the second derivative of the spline at the given time(s).
|
|
531
|
+
For t < t₀ or t > tₙ, the function extrapolates using the first or
|
|
532
|
+
last segment respectively.
|
|
533
|
+
"""
|
|
534
|
+
t_array = np.atleast_1d(t)
|
|
535
|
+
result = np.zeros_like(t_array, dtype=float)
|
|
536
|
+
|
|
537
|
+
for i, ti in enumerate(t_array):
|
|
538
|
+
# Find segment containing ti
|
|
539
|
+
if ti <= self.t[0]:
|
|
540
|
+
segment = 0
|
|
541
|
+
tau = ti - self.t[segment] # Corrected extrapolation
|
|
542
|
+
elif ti >= self.t[-1]:
|
|
543
|
+
segment = self.n - 2
|
|
544
|
+
tau = ti - self.t[segment] # Corrected extrapolation
|
|
545
|
+
else:
|
|
546
|
+
segment = np.searchsorted(self.t, ti, side="right") - 1
|
|
547
|
+
tau = ti - self.t[segment]
|
|
548
|
+
|
|
549
|
+
# Evaluate second derivative: 2a₂ + 6a₃τ
|
|
550
|
+
c = self.coeffs[segment]
|
|
551
|
+
result[i] = 2 * c[2] + 6 * c[3] * tau
|
|
552
|
+
|
|
553
|
+
return result[0] if len(t_array) == 1 else result
|
|
554
|
+
|
|
555
|
+
def plot(self, num_points: int = 1000) -> None:
|
|
556
|
+
"""
|
|
557
|
+
Plot the spline trajectory with velocity and acceleration profiles.
|
|
558
|
+
|
|
559
|
+
Creates a three-panel figure showing position, velocity, and acceleration
|
|
560
|
+
profiles over time.
|
|
561
|
+
|
|
562
|
+
Parameters
|
|
563
|
+
----------
|
|
564
|
+
num_points : int, optional
|
|
565
|
+
Number of points for smooth plotting. Default is 1000.
|
|
566
|
+
|
|
567
|
+
Returns
|
|
568
|
+
-------
|
|
569
|
+
None
|
|
570
|
+
Displays the plot using matplotlib's show() function.
|
|
571
|
+
|
|
572
|
+
Notes
|
|
573
|
+
-----
|
|
574
|
+
The plot includes:
|
|
575
|
+
- Top panel: position trajectory with original waypoints and approximated points
|
|
576
|
+
- Middle panel: velocity profile
|
|
577
|
+
- Bottom panel: acceleration profile
|
|
578
|
+
"""
|
|
579
|
+
# Generate evaluation points
|
|
580
|
+
t_eval = np.linspace(self.t[0], self.t[-1], num_points)
|
|
581
|
+
|
|
582
|
+
# Evaluate the spline and its derivatives
|
|
583
|
+
q = self.evaluate(t_eval)
|
|
584
|
+
v = self.evaluate_velocity(t_eval)
|
|
585
|
+
a = self.evaluate_acceleration(t_eval)
|
|
586
|
+
|
|
587
|
+
# Create a figure with three subplots
|
|
588
|
+
_fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 8), sharex=True)
|
|
589
|
+
|
|
590
|
+
# Position plot
|
|
591
|
+
ax1.plot(t_eval, q, "b-", linewidth=2, label="Smoothing Spline")
|
|
592
|
+
ax1.plot(self.t, self.q, "ro", markersize=8, label="Original Waypoints")
|
|
593
|
+
ax1.plot(self.t, self.s, "gx", markersize=6, label="Approximated Points")
|
|
594
|
+
ax1.set_ylabel("Position")
|
|
595
|
+
ax1.grid(True)
|
|
596
|
+
ax1.legend()
|
|
597
|
+
ax1.set_title(f"Cubic Smoothing Spline (μ={self.mu:.2f})")
|
|
598
|
+
|
|
599
|
+
# Velocity plot
|
|
600
|
+
ax2.plot(t_eval, v, "g-", linewidth=2)
|
|
601
|
+
ax2.set_ylabel("Velocity")
|
|
602
|
+
ax2.grid(True)
|
|
603
|
+
|
|
604
|
+
# Acceleration plot
|
|
605
|
+
ax3.plot(t_eval, a, "r-", linewidth=2)
|
|
606
|
+
ax3.set_ylabel("Acceleration")
|
|
607
|
+
ax3.set_xlabel("Time")
|
|
608
|
+
ax3.grid(True)
|
|
609
|
+
|
|
610
|
+
plt.tight_layout()
|
|
611
|
+
plt.show()
|