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