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.
@@ -0,0 +1,486 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+
4
+ from interpolatepy.tridiagonal_inv import solve_tridiagonal
5
+
6
+
7
+ class CubicSpline:
8
+ """
9
+ Cubic spline trajectory planning implementation.
10
+
11
+ This class implements the cubic spline algorithm described in the document.
12
+ It generates a smooth trajectory passing through specified waypoints with
13
+ continuous velocity and acceleration profiles.
14
+
15
+ Parameters
16
+ ----------
17
+ t_points : list or numpy.ndarray
18
+ List or array of time points (t0, t1, ..., tn)
19
+ q_points : list or numpy.ndarray
20
+ List or array of position points (q0, q1, ..., qn)
21
+ v0 : float, optional
22
+ Initial velocity at t0. Default is 0.0
23
+ vn : float, optional
24
+ Final velocity at tn. Default is 0.0
25
+ debug : bool, optional
26
+ Whether to print debug information. Default is False
27
+
28
+ Attributes
29
+ ----------
30
+ t_points : numpy.ndarray
31
+ Array of time points
32
+ q_points : numpy.ndarray
33
+ Array of position points
34
+ v0 : float
35
+ Initial velocity
36
+ vn : float
37
+ Final velocity
38
+ debug : bool
39
+ Debug flag
40
+ n : int
41
+ Number of segments (n = len(t_points) - 1)
42
+ t_intervals : numpy.ndarray
43
+ Time intervals between consecutive points
44
+ velocities : numpy.ndarray
45
+ Velocities at each waypoint
46
+ coefficients : numpy.ndarray
47
+ Polynomial coefficients for each segment
48
+
49
+ Notes
50
+ -----
51
+ The cubic spline ensures C2 continuity (continuous position, velocity, and
52
+ acceleration) across all waypoints.
53
+
54
+ Examples
55
+ --------
56
+ >>> import numpy as np
57
+ >>> # Define waypoints
58
+ >>> t_points = [0, 1, 2, 3]
59
+ >>> q_points = [0, 1, 0, 1]
60
+ >>> # Create spline
61
+ >>> spline = CubicSpline(t_points, q_points)
62
+ >>> # Evaluate at specific time
63
+ >>> spline.evaluate(1.5)
64
+ >>> # Plot the trajectory
65
+ >>> spline.plot()
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ t_points: list[float] | np.ndarray,
71
+ q_points: list[float] | np.ndarray,
72
+ v0: float = 0.0,
73
+ vn: float = 0.0,
74
+ debug: bool = False,
75
+ ) -> None:
76
+ """
77
+ Initialize a cubic spline trajectory.
78
+
79
+ Parameters
80
+ ----------
81
+ t_points : list or numpy.ndarray
82
+ List or array of time points (t0, t1, ..., tn)
83
+ q_points : list or numpy.ndarray
84
+ List or array of position points (q0, q1, ..., qn)
85
+ v0 : float, optional
86
+ Initial velocity at t0. Default is 0.0
87
+ vn : float, optional
88
+ Final velocity at tn. Default is 0.0
89
+ debug : bool, optional
90
+ Whether to print debug information. Default is False
91
+
92
+ Raises
93
+ ------
94
+ ValueError
95
+ If t_points and q_points have different lengths
96
+ If t_points are not strictly increasing
97
+ """
98
+ # Ensure inputs are numpy arrays
99
+ self.t_points = np.array(t_points, dtype=float)
100
+ self.q_points = np.array(q_points, dtype=float)
101
+ self.v0 = float(v0)
102
+ self.vn = float(vn)
103
+ self.debug = debug
104
+
105
+ # Check input validity
106
+ if len(self.t_points) != len(self.q_points):
107
+ raise ValueError("Time points and position points must have the same length")
108
+
109
+ if not np.all(np.diff(self.t_points) > 0):
110
+ raise ValueError("Time points must be strictly increasing")
111
+
112
+ # Compute time intervals
113
+ self.n = len(self.t_points) - 1
114
+ self.t_intervals = np.diff(self.t_points)
115
+
116
+ # Compute intermediate velocities and coefficients
117
+ self.velocities = self._compute_velocities()
118
+ self.coefficients = self._compute_coefficients()
119
+
120
+ def _compute_velocities(self) -> np.ndarray:
121
+ """
122
+ Compute the velocities at intermediate points by solving
123
+ the tridiagonal system described in the document.
124
+
125
+ This method implements the mathematical formulation from pages 4-5 of the document,
126
+ corresponding to equation (17). It sets up and solves the tridiagonal system A*v = c
127
+ to find the intermediate velocities, which ensures C2 continuity of the spline.
128
+
129
+ Returns
130
+ -------
131
+ numpy.ndarray
132
+ Array of velocities [v0, v1, ..., vn]
133
+
134
+ Notes
135
+ -----
136
+ The tridiagonal system is formed as follows:
137
+
138
+ Matrix A:
139
+ [2(T0+T1) T0 0 ... 0 ]
140
+ [T2 2(T1+T2) T1 0 ... ]
141
+ [0 T3 2(T2+T3) T2 ... ]
142
+ [... ... ... ... ... ]
143
+ [0 ... 0 Tn-2 2(Tn-3+Tn-2) Tn-3]
144
+ [0 ... 0 0 Tn-1 2(Tn-2+Tn-1)]
145
+
146
+ Vector c:
147
+ c_i = 3/(T_i*T_{i+1}) * [T_i^2*(q_{i+2}-q_{i+1}) + T_{i+1}^2*(q_{i+1}-q_i)]
148
+ """
149
+ n = self.n
150
+ t_intervals = self.t_intervals
151
+ q = self.q_points
152
+
153
+ # Create the tridiagonal matrix A as shown in the document:
154
+ # Matrix A has the following structure:
155
+ # [2(T0+T1) T0 0 ... 0 ]
156
+ # [T2 2(T1+T2) T1 0 ... ]
157
+ # [0 T3 2(T2+T3) T2 ... ]
158
+ # [... ... ... ... ... ]
159
+ # [0 ... 0 Tn-2 2(Tn-3+Tn-2) Tn-3]
160
+ # [0 ... 0 0 Tn-1 2(Tn-2+Tn-1)]
161
+
162
+ if n == 1:
163
+ # Special case: only one segment
164
+ # We know v0 and vn, so no need to solve system
165
+ return np.array([self.v0, self.vn])
166
+
167
+ # Create the right-hand side vector c from equation (17) in the document
168
+ # c_i = 3/(T_i*T_{i+1}) * [T_i^2*(q_{i+2}-q_{i+1}) + T_{i+1}^2*(q_{i+1}-q_i)]
169
+ rhs = np.zeros(n - 1)
170
+
171
+ for i in range(n - 1):
172
+ rhs[i] = (
173
+ 3
174
+ / (t_intervals[i] * t_intervals[i + 1])
175
+ * (
176
+ t_intervals[i] ** 2 * (q[i + 2] - q[i + 1])
177
+ + t_intervals[i + 1] ** 2 * (q[i + 1] - q[i])
178
+ )
179
+ )
180
+
181
+ # Adjust the right-hand side to account for known boundary velocities v0 and vn
182
+ # These adjustments come from moving the terms with known velocities to the RHS
183
+ if n > 1:
184
+ # First equation: subtract T_1*v_0 from RHS
185
+ rhs[0] -= t_intervals[1] * self.v0
186
+
187
+ # Last equation: subtract T_{n-2}*v_n from RHS
188
+ rhs[-1] -= t_intervals[-2] * self.vn
189
+
190
+ # Solve the system for the intermediate velocities v1, ..., v(n-1)
191
+ # Case n=2 means we have exactly one intermediate velocity to solve (1x1 system)
192
+ if n == 2: # noqa: PLR2004
193
+ # Special case: only one intermediate velocity to solve for
194
+ # Simple division is sufficient (1x1 system)
195
+ main_diag_value = 2 * (t_intervals[0] + t_intervals[1])
196
+ v_intermediate = rhs / main_diag_value
197
+ else:
198
+ # Instead of building the full matrix, extract the diagonal elements for the
199
+ # tridiagonal solver
200
+
201
+ # Main diagonal: 2(T_i + T_{i+1})
202
+ main_diag = np.zeros(n - 1)
203
+ for i in range(n - 1):
204
+ main_diag[i] = 2 * (t_intervals[i] + t_intervals[i + 1])
205
+
206
+ # Lower diagonal: T_{i+1} (first element not used in solve_tridiagonal)
207
+ lower_diag = np.zeros(n - 1)
208
+ for i in range(1, n - 1):
209
+ lower_diag[i] = t_intervals[i + 1]
210
+
211
+ # Upper diagonal: T_i (last element not used in solve_tridiagonal)
212
+ upper_diag = np.zeros(n - 1)
213
+ for i in range(n - 2):
214
+ upper_diag[i] = t_intervals[i]
215
+
216
+ # Print debug information only if debug is enabled
217
+ if self.debug:
218
+ print("\nTridiagonal Matrix components:")
219
+ print("Main diagonal:", main_diag)
220
+ print("Lower diagonal:", lower_diag)
221
+ print("Upper diagonal:", upper_diag)
222
+ print("Right-hand side vector:", rhs)
223
+
224
+ # Solve the tridiagonal system using the Thomas algorithm
225
+ v_intermediate = solve_tridiagonal(lower_diag, main_diag, upper_diag, rhs)
226
+
227
+ # Print the intermediate velocities if debug is enabled
228
+ if self.debug:
229
+ print("\nIntermediate velocities v_1 to v_{n-1}:")
230
+ print(v_intermediate)
231
+
232
+ # Construct the full velocity array by including the known boundary velocities
233
+ velocities = np.zeros(n + 1)
234
+ velocities[0] = self.v0 # Initial velocity (given)
235
+ velocities[1:-1] = v_intermediate # Intermediate velocities (computed)
236
+ velocities[-1] = self.vn # Final velocity (given)
237
+
238
+ # Print the complete velocity vector if debug is enabled
239
+ if self.debug:
240
+ print("\nComplete velocity vector v:")
241
+ print(velocities)
242
+
243
+ return velocities
244
+
245
+ def _compute_coefficients(self) -> np.ndarray:
246
+ """
247
+ Compute the coefficients for each cubic polynomial segment.
248
+
249
+ For each segment k, we compute:
250
+ - ak0: constant term
251
+ - ak1: coefficient of (t-tk)
252
+ - ak2: coefficient of (t-tk)^2
253
+ - ak3: coefficient of (t-tk)^3
254
+
255
+ Returns
256
+ -------
257
+ numpy.ndarray
258
+ Array of shape (n, 4) containing the coefficients for each segment
259
+ where n is the number of segments
260
+
261
+ Notes
262
+ -----
263
+ The cubic polynomial for segment k is defined as:
264
+ q(t) = ak0 + ak1*(t-tk) + ak2*(t-tk)^2 + ak3*(t-tk)^3
265
+
266
+ The coefficients are computed to ensure position, velocity, and
267
+ acceleration continuity at the waypoints.
268
+ """
269
+ n = self.n
270
+ t_intervals = self.t_intervals
271
+ q = self.q_points
272
+ v = self.velocities
273
+
274
+ coeffs = np.zeros((n, 4))
275
+
276
+ for k in range(n):
277
+ coeffs[k, 0] = q[k] # ak0 = qk
278
+ coeffs[k, 1] = v[k] # ak1 = vk
279
+
280
+ # Compute ak2 and ak3
281
+ coeffs[k, 2] = (1 / t_intervals[k]) * (
282
+ (3 * (q[k + 1] - q[k]) / t_intervals[k]) - 2 * v[k] - v[k + 1]
283
+ )
284
+ coeffs[k, 3] = (1 / t_intervals[k] ** 2) * (
285
+ (2 * (q[k] - q[k + 1]) / t_intervals[k]) + v[k] + v[k + 1]
286
+ )
287
+
288
+ # Print detailed coefficient calculation if debug is enabled
289
+ if self.debug:
290
+ print(f"\nCoefficient calculation for segment {k}:")
291
+ print(f" ak0 = {coeffs[k, 0]}")
292
+ print(f" ak1 = {coeffs[k, 1]}")
293
+ print(f" ak2 = {coeffs[k, 2]}")
294
+ print(f" ak3 = {coeffs[k, 3]}")
295
+
296
+ return coeffs
297
+
298
+ def evaluate(self, t: float | np.ndarray) -> float | np.ndarray:
299
+ """
300
+ Evaluate the spline at time t.
301
+
302
+ Parameters
303
+ ----------
304
+ t : float or numpy.ndarray
305
+ Time point or array of time points
306
+
307
+ Returns
308
+ -------
309
+ float or numpy.ndarray
310
+ Position(s) at the specified time(s)
311
+
312
+ Examples
313
+ --------
314
+ >>> # Evaluate at a single time point
315
+ >>> spline.evaluate(1.5)
316
+ >>> # Evaluate at multiple time points
317
+ >>> spline.evaluate(np.linspace(0, 3, 100))
318
+ """
319
+ t = np.atleast_1d(t)
320
+ result = np.zeros_like(t)
321
+
322
+ for i, ti in enumerate(t):
323
+ # Find the segment that contains ti
324
+ if ti <= self.t_points[0]:
325
+ # Before the start of the trajectory
326
+ k = 0
327
+ tau = 0
328
+ elif ti >= self.t_points[-1]:
329
+ # After the end of the trajectory
330
+ k = self.n - 1
331
+ tau = self.t_intervals[k]
332
+ else:
333
+ # Within the trajectory
334
+ # Find the largest k such that t_k <= ti
335
+ k = np.searchsorted(self.t_points, ti, side="right") - 1
336
+ tau = ti - self.t_points[k]
337
+
338
+ # Evaluate the polynomial
339
+ a = self.coefficients[k]
340
+ result[i] = a[0] + a[1] * tau + a[2] * tau**2 + a[3] * tau**3
341
+
342
+ return result[0] if len(result) == 1 else result
343
+
344
+ def evaluate_velocity(self, t: float | np.ndarray) -> float | np.ndarray:
345
+ """
346
+ Evaluate the velocity at time t.
347
+
348
+ Parameters
349
+ ----------
350
+ t : float or numpy.ndarray
351
+ Time point or array of time points
352
+
353
+ Returns
354
+ -------
355
+ float or numpy.ndarray
356
+ Velocity at the specified time(s)
357
+
358
+ Examples
359
+ --------
360
+ >>> # Evaluate velocity at a single time point
361
+ >>> spline.evaluate_velocity(1.5)
362
+ >>> # Evaluate velocity at multiple time points
363
+ >>> spline.evaluate_velocity(np.linspace(0, 3, 100))
364
+ """
365
+ t = np.atleast_1d(t)
366
+ result = np.zeros_like(t)
367
+
368
+ for i, ti in enumerate(t):
369
+ # Find the segment that contains ti
370
+ if ti <= self.t_points[0]:
371
+ # Before the start of the trajectory
372
+ k = 0
373
+ tau = 0
374
+ elif ti >= self.t_points[-1]:
375
+ # After the end of the trajectory
376
+ k = self.n - 1
377
+ tau = self.t_intervals[k]
378
+ else:
379
+ # Within the trajectory
380
+ k = np.searchsorted(self.t_points, ti, side="right") - 1
381
+ tau = ti - self.t_points[k]
382
+
383
+ # Evaluate the derivative of the polynomial
384
+ a = self.coefficients[k]
385
+ result[i] = a[1] + 2 * a[2] * tau + 3 * a[3] * tau**2
386
+
387
+ return result[0] if len(result) == 1 else result
388
+
389
+ def evaluate_acceleration(self, t: float | np.ndarray) -> float | np.ndarray:
390
+ """
391
+ Evaluate the acceleration at time t.
392
+
393
+ Parameters
394
+ ----------
395
+ t : float or numpy.ndarray
396
+ Time point or array of time points
397
+
398
+ Returns
399
+ -------
400
+ float or numpy.ndarray
401
+ Acceleration at the specified time(s)
402
+
403
+ Examples
404
+ --------
405
+ >>> # Evaluate acceleration at a single time point
406
+ >>> spline.evaluate_acceleration(1.5)
407
+ >>> # Evaluate acceleration at multiple time points
408
+ >>> spline.evaluate_acceleration(np.linspace(0, 3, 100))
409
+ """
410
+ t = np.atleast_1d(t)
411
+ result = np.zeros_like(t)
412
+
413
+ for i, ti in enumerate(t):
414
+ # Find the segment that contains ti
415
+ if ti <= self.t_points[0]:
416
+ # Before the start of the trajectory
417
+ k = 0
418
+ tau = 0
419
+ elif ti >= self.t_points[-1]:
420
+ # After the end of the trajectory
421
+ k = self.n - 1
422
+ tau = self.t_intervals[k]
423
+ else:
424
+ # Within the trajectory
425
+ k = np.searchsorted(self.t_points, ti, side="right") - 1
426
+ tau = ti - self.t_points[k]
427
+
428
+ # Evaluate the second derivative of the polynomial
429
+ a = self.coefficients[k]
430
+ result[i] = 2 * a[2] + 6 * a[3] * tau
431
+
432
+ return result[0] if len(result) == 1 else result
433
+
434
+ def plot(self, num_points: int = 1000) -> None:
435
+ """
436
+ Plot the spline trajectory along with its velocity and acceleration profiles.
437
+
438
+ Parameters
439
+ ----------
440
+ num_points : int, optional
441
+ Number of points to use for plotting. Default is 1000
442
+
443
+ Returns
444
+ -------
445
+ None
446
+ Displays the plot using matplotlib
447
+
448
+ Notes
449
+ -----
450
+ This method creates a figure with three subplots showing:
451
+ 1. Position trajectory with waypoints
452
+ 2. Velocity profile
453
+ 3. Acceleration profile
454
+
455
+ The original waypoints are marked with red circles on the position plot.
456
+ """
457
+ t_min, t_max = self.t_points[0], self.t_points[-1]
458
+ t = np.linspace(t_min, t_max, num_points)
459
+
460
+ q = self.evaluate(t)
461
+ v = self.evaluate_velocity(t)
462
+ a = self.evaluate_acceleration(t)
463
+
464
+ _fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 8), sharex=True)
465
+
466
+ # Position plot
467
+ ax1.plot(t, q, "b-", linewidth=2)
468
+ ax1.plot(self.t_points, self.q_points, "ro", markersize=8)
469
+ ax1.set_ylabel("Position")
470
+ ax1.grid(True)
471
+ ax1.set_title("Cubic Spline Trajectory")
472
+
473
+ # Velocity plot
474
+ ax2.plot(t, v, "g-", linewidth=2)
475
+ ax2.plot(self.t_points, self.velocities, "ro", markersize=6)
476
+ ax2.set_ylabel("Velocity")
477
+ ax2.grid(True)
478
+
479
+ # Acceleration plot
480
+ ax3.plot(t, a, "r-", linewidth=2)
481
+ ax3.set_ylabel("Acceleration")
482
+ ax3.set_xlabel("Time")
483
+ ax3.grid(True)
484
+
485
+ plt.tight_layout()
486
+ plt.show()