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,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()