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,494 @@
1
+ from dataclasses import dataclass
2
+
3
+ import numpy as np
4
+
5
+ from interpolatepy.cubic_spline import CubicSpline
6
+
7
+
8
+ @dataclass
9
+ class SplineParameters:
10
+ """
11
+ Container for spline initialization parameters.
12
+
13
+ Parameters
14
+ ----------
15
+ v0 : float, default=0.0
16
+ Initial velocity constraint at the start of the spline.
17
+ vn : float, default=0.0
18
+ Final velocity constraint at the end of the spline.
19
+ a0 : float, optional
20
+ Initial acceleration constraint at the start of the spline.
21
+ If provided, the first segment will use a quintic polynomial.
22
+ an : float, optional
23
+ Final acceleration constraint at the end of the spline.
24
+ If provided, the last segment will use a quintic polynomial.
25
+ debug : bool, default=False
26
+ If True, prints detailed information about the spline calculation.
27
+ """
28
+
29
+ v0: float = 0.0
30
+ vn: float = 0.0
31
+ a0: float | None = None
32
+ an: float | None = None
33
+ debug: bool = False
34
+
35
+
36
+ class CubicSplineWithAcceleration2(CubicSpline):
37
+ """
38
+ Cubic spline trajectory planning with initial and final acceleration constraints.
39
+
40
+ This class extends CubicSpline to handle initial and final acceleration constraints
41
+ by using 5th degree polynomials for the first and last segments, as mentioned in
42
+ section 4.4.4 of the paper.
43
+
44
+ The spline consists of cubic polynomial segments for interior segments and
45
+ optional quintic polynomial segments for the first and/or last segment when
46
+ acceleration constraints are specified.
47
+
48
+ Parameters
49
+ ----------
50
+ t_points : array_like
51
+ Array of time points (t0, t1, ..., tn).
52
+ q_points : array_like
53
+ Array of position points (q0, q1, ..., qn).
54
+ params : SplineParameters, optional
55
+ Spline parameters including initial/final velocities and accelerations.
56
+ If None, default parameters will be used.
57
+
58
+ Attributes
59
+ ----------
60
+ t_points : ndarray
61
+ Array of time points.
62
+ q_points : ndarray
63
+ Array of position points.
64
+ velocities : ndarray
65
+ Array of velocities at each point.
66
+ t_intervals : ndarray
67
+ Array of time intervals between consecutive points.
68
+ n : int
69
+ Number of segments.
70
+ coefficients : ndarray
71
+ Array of coefficients for each cubic segment.
72
+ a0 : float or None
73
+ Initial acceleration constraint.
74
+ an : float or None
75
+ Final acceleration constraint.
76
+ quintic_coeffs : dict
77
+ Dictionary containing quintic coefficients for first and/or last segments.
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ t_points: list[float] | np.ndarray,
83
+ q_points: list[float] | np.ndarray,
84
+ params: SplineParameters | None = None,
85
+ ) -> None:
86
+ """
87
+ Initialize a cubic spline trajectory with optional initial and final accelerations.
88
+
89
+ Parameters
90
+ ----------
91
+ t_points : array_like
92
+ Array of time points (t0, t1, ..., tn).
93
+ q_points : array_like
94
+ Array of position points (q0, q1, ..., qn).
95
+ params : SplineParameters, optional
96
+ Spline parameters including initial/final velocities and accelerations.
97
+ If None, default parameters will be used.
98
+ """
99
+ # Set default parameters if not provided
100
+ if params is None:
101
+ params = SplineParameters()
102
+
103
+ # Initialize the parent class to compute the basic cubic spline
104
+ super().__init__(t_points, q_points, params.v0, params.vn, params.debug)
105
+
106
+ # Store the acceleration constraints
107
+ self.a0 = params.a0
108
+ self.an = params.an
109
+
110
+ # Replace the first and/or last segment with a 5th degree polynomial if needed
111
+ if params.a0 is not None:
112
+ self._replace_first_segment_with_quintic()
113
+
114
+ if params.an is not None:
115
+ self._replace_last_segment_with_quintic()
116
+
117
+ def _replace_first_segment_with_quintic(self) -> None:
118
+ """
119
+ Replace the first segment with a 5th degree polynomial to satisfy
120
+ initial acceleration constraint.
121
+
122
+ This method computes the coefficients of a quintic polynomial for the first
123
+ segment to satisfy the position, velocity, and acceleration constraints at
124
+ both endpoints of the segment.
125
+
126
+ The quintic polynomial has the form:
127
+ p(tau) = b0 + b1*tau + b2*tau^2 + b3*tau^3 + b4*tau^4 + b5*tau^5
128
+
129
+ With constraints:
130
+ p(0) = q0, p'(0) = v0, p''(0) = a0
131
+ p(T) = q1, p'(T) = v1, p''(T) = a1
132
+
133
+ Where T is the duration of the first segment.
134
+
135
+ The coefficients are stored in self.quintic_coeffs["first"].
136
+ """
137
+ # Get the time points and positions for the first segment
138
+ t0, t1 = self.t_points[0], self.t_points[1]
139
+ q0, q1 = self.q_points[0], self.q_points[1]
140
+ v0, v1 = self.velocities[0], self.velocities[1]
141
+ a0 = self.a0
142
+
143
+ # Calculate the acceleration at the end of the first segment
144
+ # For a cubic polynomial a(t) = 2*a2 + 6*a3*t
145
+ a1 = 2 * self.coefficients[0, 2] + 6 * self.coefficients[0, 3] * self.t_intervals[0]
146
+
147
+ # Compute the coefficients of the quintic polynomial
148
+ # p(tau) = b0 + b1*tau + b2*tau^2 + b3*tau^3 + b4*tau^4 + b5*tau^5
149
+ # with constraints:
150
+ # p(0) = q0, p'(0) = v0, p''(0) = a0
151
+ # p(T) = q1, p'(T) = v1, p''(T) = a1
152
+ # where T = t1 - t0
153
+
154
+ t_interval = t1 - t0
155
+
156
+ # Set up the system of equations
157
+ a_matrix = np.array(
158
+ [
159
+ [1, 0, 0, 0, 0, 0], # p(0) = q0
160
+ [0, 1, 0, 0, 0, 0], # p'(0) = v0
161
+ [0, 0, 2, 0, 0, 0], # p''(0) = a0
162
+ [
163
+ 1,
164
+ t_interval,
165
+ t_interval**2,
166
+ t_interval**3,
167
+ t_interval**4,
168
+ t_interval**5,
169
+ ], # p(T) = q1
170
+ [
171
+ 0,
172
+ 1,
173
+ 2 * t_interval,
174
+ 3 * t_interval**2,
175
+ 4 * t_interval**3,
176
+ 5 * t_interval**4,
177
+ ], # p'(T) = v1
178
+ [0, 0, 2, 6 * t_interval, 12 * t_interval**2, 20 * t_interval**3], # p''(T) = a1
179
+ ]
180
+ )
181
+
182
+ b = np.array([q0, v0, a0, q1, v1, a1])
183
+
184
+ # Solve for the coefficients
185
+ quintic_coeffs = np.linalg.solve(a_matrix, b)
186
+
187
+ # Store the quintic coefficients for later use
188
+ if not hasattr(self, "quintic_coeffs"):
189
+ self.quintic_coeffs = {}
190
+
191
+ self.quintic_coeffs["first"] = quintic_coeffs
192
+
193
+ if self.debug:
194
+ print("\nReplaced first segment with quintic polynomial:")
195
+ print(f" b0 = {quintic_coeffs[0]}")
196
+ print(f" b1 = {quintic_coeffs[1]}")
197
+ print(f" b2 = {quintic_coeffs[2]}")
198
+ print(f" b3 = {quintic_coeffs[3]}")
199
+ print(f" b4 = {quintic_coeffs[4]}")
200
+ print(f" b5 = {quintic_coeffs[5]}")
201
+
202
+ def _replace_last_segment_with_quintic(self) -> None:
203
+ """
204
+ Replace the last segment with a 5th degree polynomial to satisfy
205
+ final acceleration constraint.
206
+
207
+ This method computes the coefficients of a quintic polynomial for the last
208
+ segment to satisfy the position, velocity, and acceleration constraints at
209
+ both endpoints of the segment.
210
+
211
+ The quintic polynomial has the form:
212
+ p(tau) = b0 + b1*tau + b2*tau^2 + b3*tau^3 + b4*tau^4 + b5*tau^5
213
+
214
+ With constraints:
215
+ p(0) = qn_1, p'(0) = vn_1, p''(0) = an_1
216
+ p(T) = qn, p'(T) = vn, p''(T) = an
217
+
218
+ Where T is the duration of the last segment.
219
+
220
+ The coefficients are stored in self.quintic_coeffs["last"].
221
+ """
222
+ # Get the time points and positions for the last segment
223
+ tn_1, tn = self.t_points[-2], self.t_points[-1]
224
+ qn_1, qn = self.q_points[-2], self.q_points[-1]
225
+ vn_1, vn = self.velocities[-2], self.velocities[-1]
226
+ an = self.an
227
+
228
+ # Calculate the acceleration at the start of the last segment
229
+ # For a cubic polynomial a(t) = 2*a2
230
+ an_1 = 2 * self.coefficients[-1, 2]
231
+
232
+ # Compute the coefficients of the quintic polynomial
233
+ # p(tau) = b0 + b1*tau + b2*tau^2 + b3*tau^3 + b4*tau^4 + b5*tau^5
234
+ # with constraints:
235
+ # p(0) = qn_1, p'(0) = vn_1, p''(0) = an_1
236
+ # p(T) = qn, p'(T) = vn, p''(T) = an
237
+ # where T = tn - tn_1
238
+
239
+ t_interval = tn - tn_1
240
+
241
+ # Set up the system of equations
242
+ a_matrix = np.array(
243
+ [
244
+ [1, 0, 0, 0, 0, 0], # p(0) = qn_1
245
+ [0, 1, 0, 0, 0, 0], # p'(0) = vn_1
246
+ [0, 0, 2, 0, 0, 0], # p''(0) = an_1
247
+ [
248
+ 1,
249
+ t_interval,
250
+ t_interval**2,
251
+ t_interval**3,
252
+ t_interval**4,
253
+ t_interval**5,
254
+ ], # p(T) = qn
255
+ [
256
+ 0,
257
+ 1,
258
+ 2 * t_interval,
259
+ 3 * t_interval**2,
260
+ 4 * t_interval**3,
261
+ 5 * t_interval**4,
262
+ ], # p'(T) = vn
263
+ [0, 0, 2, 6 * t_interval, 12 * t_interval**2, 20 * t_interval**3], # p''(T) = an
264
+ ]
265
+ )
266
+
267
+ b = np.array([qn_1, vn_1, an_1, qn, vn, an])
268
+
269
+ # Solve for the coefficients
270
+ quintic_coeffs = np.linalg.solve(a_matrix, b)
271
+
272
+ # Store the quintic coefficients for later use
273
+ if not hasattr(self, "quintic_coeffs"):
274
+ self.quintic_coeffs = {}
275
+
276
+ self.quintic_coeffs["last"] = quintic_coeffs
277
+
278
+ if self.debug:
279
+ print("\nReplaced last segment with quintic polynomial:")
280
+ print(f" b0 = {quintic_coeffs[0]}")
281
+ print(f" b1 = {quintic_coeffs[1]}")
282
+ print(f" b2 = {quintic_coeffs[2]}")
283
+ print(f" b3 = {quintic_coeffs[3]}")
284
+ print(f" b4 = {quintic_coeffs[4]}")
285
+ print(f" b5 = {quintic_coeffs[5]}")
286
+
287
+ def evaluate(self, t: float | np.ndarray) -> float | np.ndarray:
288
+ """
289
+ Evaluate the spline at time t.
290
+
291
+ Parameters
292
+ ----------
293
+ t : float or ndarray
294
+ Time point or array of time points at which to evaluate the spline.
295
+
296
+ Returns
297
+ -------
298
+ float or ndarray
299
+ Position(s) at the specified time(s). Returns a float if a single time
300
+ point is provided, or an ndarray if an array of time points is provided.
301
+
302
+ Notes
303
+ -----
304
+ For time values outside the spline range:
305
+ - If t < t_points[0], returns the position at t_points[0]
306
+ - If t > t_points[-1], returns the position at t_points[-1]
307
+
308
+ For the first and last segments, quintic polynomials are used if acceleration
309
+ constraints were specified. For all other segments, cubic polynomials are used.
310
+ """
311
+ t = np.atleast_1d(t)
312
+ result = np.zeros_like(t)
313
+
314
+ for i, ti in enumerate(t):
315
+ # Find the segment that contains ti
316
+ if ti <= self.t_points[0]:
317
+ # Before the start of the trajectory
318
+ k = 0
319
+ tau = 0
320
+ elif ti >= self.t_points[-1]:
321
+ # After the end of the trajectory
322
+ k = self.n - 1
323
+ tau = self.t_intervals[k]
324
+ else:
325
+ # Within the trajectory
326
+ # Find the largest k such that t_k <= ti
327
+ k = np.searchsorted(self.t_points, ti, side="right") - 1
328
+ tau = ti - self.t_points[k]
329
+
330
+ # Check if this segment uses a quintic polynomial
331
+ if k == 0 and hasattr(self, "quintic_coeffs") and "first" in self.quintic_coeffs:
332
+ # Use the quintic polynomial for the first segment
333
+ b = self.quintic_coeffs["first"]
334
+ result[i] = (
335
+ b[0]
336
+ + b[1] * tau
337
+ + b[2] * tau**2
338
+ + b[3] * tau**3
339
+ + b[4] * tau**4
340
+ + b[5] * tau**5
341
+ )
342
+ elif (
343
+ k == self.n - 1
344
+ and hasattr(self, "quintic_coeffs")
345
+ and "last" in self.quintic_coeffs
346
+ ):
347
+ # Use the quintic polynomial for the last segment
348
+ b = self.quintic_coeffs["last"]
349
+ result[i] = (
350
+ b[0]
351
+ + b[1] * tau
352
+ + b[2] * tau**2
353
+ + b[3] * tau**3
354
+ + b[4] * tau**4
355
+ + b[5] * tau**5
356
+ )
357
+ else:
358
+ # Use the cubic polynomial for other segments
359
+ a = self.coefficients[k]
360
+ result[i] = a[0] + a[1] * tau + a[2] * tau**2 + a[3] * tau**3
361
+
362
+ return result[0] if len(result) == 1 else result
363
+
364
+ def evaluate_velocity(self, t: float | np.ndarray) -> float | np.ndarray:
365
+ """
366
+ Evaluate the velocity at time t.
367
+
368
+ Parameters
369
+ ----------
370
+ t : float or ndarray
371
+ Time point or array of time points at which to evaluate the velocity.
372
+
373
+ Returns
374
+ -------
375
+ float or ndarray
376
+ Velocity at the specified time(s). Returns a float if a single time
377
+ point is provided, or an ndarray if an array of time points is provided.
378
+
379
+ Notes
380
+ -----
381
+ For time values outside the spline range:
382
+ - If t < t_points[0], returns the velocity at t_points[0]
383
+ - If t > t_points[-1], returns the velocity at t_points[-1]
384
+
385
+ For the first and last segments, derivatives of quintic polynomials are used
386
+ if acceleration constraints were specified. For all other segments, derivatives
387
+ of cubic polynomials are used.
388
+ """
389
+ t = np.atleast_1d(t)
390
+ result = np.zeros_like(t)
391
+
392
+ for i, ti in enumerate(t):
393
+ # Find the segment that contains ti
394
+ if ti <= self.t_points[0]:
395
+ k = 0
396
+ tau = 0
397
+ elif ti >= self.t_points[-1]:
398
+ k = self.n - 1
399
+ tau = self.t_intervals[k]
400
+ else:
401
+ k = np.searchsorted(self.t_points, ti, side="right") - 1
402
+ tau = ti - self.t_points[k]
403
+
404
+ # Check if this segment uses a quintic polynomial
405
+ if k == 0 and hasattr(self, "quintic_coeffs") and "first" in self.quintic_coeffs:
406
+ # Use the derivative of the quintic polynomial for the first segment
407
+ b = self.quintic_coeffs["first"]
408
+ result[i] = (
409
+ b[1]
410
+ + 2 * b[2] * tau
411
+ + 3 * b[3] * tau**2
412
+ + 4 * b[4] * tau**3
413
+ + 5 * b[5] * tau**4
414
+ )
415
+ elif (
416
+ k == self.n - 1
417
+ and hasattr(self, "quintic_coeffs")
418
+ and "last" in self.quintic_coeffs
419
+ ):
420
+ # Use the derivative of the quintic polynomial for the last segment
421
+ b = self.quintic_coeffs["last"]
422
+ result[i] = (
423
+ b[1]
424
+ + 2 * b[2] * tau
425
+ + 3 * b[3] * tau**2
426
+ + 4 * b[4] * tau**3
427
+ + 5 * b[5] * tau**4
428
+ )
429
+ else:
430
+ # Use the derivative of the cubic polynomial for other segments
431
+ a = self.coefficients[k]
432
+ result[i] = a[1] + 2 * a[2] * tau + 3 * a[3] * tau**2
433
+
434
+ return result[0] if len(result) == 1 else result
435
+
436
+ def evaluate_acceleration(self, t: float | np.ndarray) -> float | np.ndarray:
437
+ """
438
+ Evaluate the acceleration at time t.
439
+
440
+ Parameters
441
+ ----------
442
+ t : float or ndarray
443
+ Time point or array of time points at which to evaluate the acceleration.
444
+
445
+ Returns
446
+ -------
447
+ float or ndarray
448
+ Acceleration at the specified time(s). Returns a float if a single time
449
+ point is provided, or an ndarray if an array of time points is provided.
450
+
451
+ Notes
452
+ -----
453
+ For time values outside the spline range:
454
+ - If t < t_points[0], returns the acceleration at t_points[0]
455
+ - If t > t_points[-1], returns the acceleration at t_points[-1]
456
+
457
+ For the first and last segments, second derivatives of quintic polynomials are
458
+ used if acceleration constraints were specified. For all other segments, second
459
+ derivatives of cubic polynomials are used.
460
+ """
461
+ t = np.atleast_1d(t)
462
+ result = np.zeros_like(t)
463
+
464
+ for i, ti in enumerate(t):
465
+ # Find the segment that contains ti
466
+ if ti <= self.t_points[0]:
467
+ k = 0
468
+ tau = 0
469
+ elif ti >= self.t_points[-1]:
470
+ k = self.n - 1
471
+ tau = self.t_intervals[k]
472
+ else:
473
+ k = np.searchsorted(self.t_points, ti, side="right") - 1
474
+ tau = ti - self.t_points[k]
475
+
476
+ # Check if this segment uses a quintic polynomial
477
+ if k == 0 and hasattr(self, "quintic_coeffs") and "first" in self.quintic_coeffs:
478
+ # Use the second derivative of the quintic polynomial for the first segment
479
+ b = self.quintic_coeffs["first"]
480
+ result[i] = 2 * b[2] + 6 * b[3] * tau + 12 * b[4] * tau**2 + 20 * b[5] * tau**3
481
+ elif (
482
+ k == self.n - 1
483
+ and hasattr(self, "quintic_coeffs")
484
+ and "last" in self.quintic_coeffs
485
+ ):
486
+ # Use the second derivative of the quintic polynomial for the last segment
487
+ b = self.quintic_coeffs["last"]
488
+ result[i] = 2 * b[2] + 6 * b[3] * tau + 12 * b[4] * tau**2 + 20 * b[5] * tau**3
489
+ else:
490
+ # Use the second derivative of the cubic polynomial for other segments
491
+ a = self.coefficients[k]
492
+ result[i] = 2 * a[2] + 6 * a[3] * tau
493
+
494
+ return result[0] if len(result) == 1 else result