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,451 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+ from typing import ClassVar
4
+
5
+ import numpy as np
6
+
7
+
8
+ # Define constants for polynomial orders
9
+ ORDER_3 = 3
10
+ ORDER_5 = 5
11
+ ORDER_7 = 7
12
+
13
+
14
+ @dataclass
15
+ class BoundaryCondition:
16
+ """Class for storing boundary conditions for trajectory generation."""
17
+
18
+ position: float
19
+ velocity: float
20
+ acceleration: float = 0.0
21
+ jerk: float = 0.0
22
+
23
+
24
+ @dataclass
25
+ class TimeInterval:
26
+ """Class for storing time interval for trajectory generation."""
27
+
28
+ start: float
29
+ end: float
30
+
31
+
32
+ @dataclass
33
+ class TrajectoryParams:
34
+ """Class for storing parameters for multipoint trajectory generation."""
35
+
36
+ points: list[float]
37
+ times: list[float]
38
+ velocities: list[float] | None = None
39
+ accelerations: list[float] | None = None
40
+ jerks: list[float] | None = None
41
+ order: int = ORDER_3
42
+
43
+
44
+ class PolynomialTrajectory:
45
+ """
46
+ A class for generating polynomial trajectories with specified boundary conditions.
47
+
48
+ This class provides methods to create polynomial trajectories of different orders
49
+ (3rd, 5th, and 7th) with specified boundary conditions such as position, velocity,
50
+ acceleration, and jerk. It also supports creating trajectories through multiple points.
51
+
52
+ Methods
53
+ -------
54
+ order_3_trajectory
55
+ Generate a 3rd order polynomial trajectory with position and velocity constraints
56
+ order_5_trajectory
57
+ Generate a 5th order polynomial trajectory with position, velocity, and
58
+ acceleration constraints
59
+ order_7_trajectory
60
+ Generate a 7th order polynomial trajectory with position, velocity, acceleration,
61
+ and jerk constraints
62
+ heuristic_velocities
63
+ Compute intermediate velocities for a sequence of points
64
+ multipoint_trajectory
65
+ Generate a trajectory through a sequence of points with specified times
66
+ """
67
+
68
+ # Define the valid polynomial orders as class variables
69
+ VALID_ORDERS: ClassVar[tuple[int, ...]] = (ORDER_3, ORDER_5, ORDER_7)
70
+
71
+ @staticmethod
72
+ def order_3_trajectory(
73
+ initial: BoundaryCondition,
74
+ final: BoundaryCondition,
75
+ time: TimeInterval,
76
+ ) -> Callable[[float], tuple[float, float, float, float]]:
77
+ """
78
+ Generate a 3rd order polynomial trajectory with specified boundary conditions.
79
+
80
+ Parameters
81
+ ----------
82
+ initial : BoundaryCondition
83
+ Initial boundary conditions (position, velocity)
84
+ final : BoundaryCondition
85
+ Final boundary conditions (position, velocity)
86
+ time : TimeInterval
87
+ Time interval for the trajectory
88
+
89
+ Returns
90
+ -------
91
+ Callable[[float], tuple[float, float, float, float]]
92
+ Function that computes position, velocity, acceleration, and jerk at time t
93
+ """
94
+ t_diff = time.end - time.start
95
+ h = final.position - initial.position
96
+
97
+ # Coefficients as defined in equation (2.2)
98
+ a0 = initial.position
99
+ a1 = initial.velocity
100
+ a2 = (3 * h - (2 * initial.velocity + final.velocity) * t_diff) / (t_diff**2)
101
+ a3 = (-2 * h + (initial.velocity + final.velocity) * t_diff) / (t_diff**3)
102
+
103
+ def trajectory(t: float) -> tuple[float, float, float, float]:
104
+ # Ensure t is within bounds
105
+ t = np.clip(t, time.start, time.end)
106
+
107
+ # Time relative to t_start
108
+ tau = t - time.start
109
+
110
+ # Position
111
+ q = a0 + a1 * tau + a2 * tau**2 + a3 * tau**3
112
+
113
+ # Velocity
114
+ qd = a1 + 2 * a2 * tau + 3 * a3 * tau**2
115
+
116
+ # Acceleration
117
+ qdd = 2 * a2 + 6 * a3 * tau
118
+
119
+ # Jerk
120
+ qddd = 6 * a3
121
+
122
+ return q, qd, qdd, qddd
123
+
124
+ return trajectory
125
+
126
+ @staticmethod
127
+ def order_5_trajectory(
128
+ initial: BoundaryCondition,
129
+ final: BoundaryCondition,
130
+ time: TimeInterval,
131
+ ) -> Callable[[float], tuple[float, float, float, float]]:
132
+ """
133
+ Generate a 5th order polynomial trajectory with specified boundary conditions.
134
+
135
+ Parameters
136
+ ----------
137
+ initial : BoundaryCondition
138
+ Initial boundary conditions (position, velocity, acceleration)
139
+ final : BoundaryCondition
140
+ Final boundary conditions (position, velocity, acceleration)
141
+ time : TimeInterval
142
+ Time interval for the trajectory
143
+
144
+ Returns
145
+ -------
146
+ Callable[[float], tuple[float, float, float, float]]
147
+ Function that computes position, velocity, acceleration, and jerk at time t
148
+ """
149
+ t_diff = time.end - time.start
150
+ h = final.position - initial.position
151
+
152
+ # Coefficients as defined in equation (2.5)
153
+ a0 = initial.position
154
+ a1 = initial.velocity
155
+ a2 = initial.acceleration / 2
156
+ a3 = (1 / (2 * t_diff**3)) * (
157
+ 20 * h
158
+ - (8 * final.velocity + 12 * initial.velocity) * t_diff
159
+ - (3 * initial.acceleration - final.acceleration) * t_diff**2
160
+ )
161
+ a4 = (1 / (2 * t_diff**4)) * (
162
+ -30 * h
163
+ + (14 * final.velocity + 16 * initial.velocity) * t_diff
164
+ + (3 * initial.acceleration - 2 * final.acceleration) * t_diff**2
165
+ )
166
+ a5 = (1 / (2 * t_diff**5)) * (
167
+ 12 * h
168
+ - 6 * (final.velocity + initial.velocity) * t_diff
169
+ + (final.acceleration - initial.acceleration) * t_diff**2
170
+ )
171
+
172
+ def trajectory(t: float) -> tuple[float, float, float, float]:
173
+ # Ensure t is within bounds
174
+ t = np.clip(t, time.start, time.end)
175
+
176
+ # Time relative to t_start
177
+ tau = t - time.start
178
+
179
+ # Position
180
+ q = a0 + a1 * tau + a2 * tau**2 + a3 * tau**3 + a4 * tau**4 + a5 * tau**5
181
+
182
+ # Velocity
183
+ qd = a1 + 2 * a2 * tau + 3 * a3 * tau**2 + 4 * a4 * tau**3 + 5 * a5 * tau**4
184
+
185
+ # Acceleration
186
+ qdd = 2 * a2 + 6 * a3 * tau + 12 * a4 * tau**2 + 20 * a5 * tau**3
187
+
188
+ # Jerk
189
+ qddd = 6 * a3 + 24 * a4 * tau + 60 * a5 * tau**2
190
+
191
+ return q, qd, qdd, qddd
192
+
193
+ return trajectory
194
+
195
+ @staticmethod
196
+ def order_7_trajectory(
197
+ initial: BoundaryCondition,
198
+ final: BoundaryCondition,
199
+ time: TimeInterval,
200
+ ) -> Callable[[float], tuple[float, float, float, float]]:
201
+ """
202
+ Generate a 7th order polynomial trajectory with specified boundary conditions.
203
+
204
+ Parameters
205
+ ----------
206
+ initial : BoundaryCondition
207
+ Initial boundary conditions (position, velocity, acceleration, jerk)
208
+ final : BoundaryCondition
209
+ Final boundary conditions (position, velocity, acceleration, jerk)
210
+ time : TimeInterval
211
+ Time interval for the trajectory
212
+
213
+ Returns
214
+ -------
215
+ Callable[[float], tuple[float, float, float, float]]
216
+ Function that computes position, velocity, acceleration, and jerk at time t
217
+ """
218
+ t_diff = time.end - time.start
219
+ h = final.position - initial.position
220
+
221
+ # Coefficients for 7th order polynomial
222
+ a0 = initial.position
223
+ a1 = initial.velocity
224
+ a2 = initial.acceleration / 2
225
+ a3 = initial.jerk / 6
226
+ a4 = (
227
+ 210 * h
228
+ - t_diff
229
+ * (
230
+ (30 * initial.acceleration - 15 * final.acceleration) * t_diff
231
+ + (4 * initial.jerk + final.jerk) * t_diff**2
232
+ + 120 * initial.velocity
233
+ + 90 * final.velocity
234
+ )
235
+ ) / (6 * t_diff**4)
236
+ a5 = (
237
+ -168 * h
238
+ + t_diff
239
+ * (
240
+ (20 * initial.acceleration - 14 * final.acceleration) * t_diff
241
+ + (2 * initial.jerk + final.jerk) * t_diff**2
242
+ + 90 * initial.velocity
243
+ + 78 * final.velocity
244
+ )
245
+ ) / (2 * t_diff**5)
246
+ a6 = (
247
+ 420 * h
248
+ - t_diff
249
+ * (
250
+ (45 * initial.acceleration - 39 * final.acceleration) * t_diff
251
+ + (4 * initial.jerk + 3 * final.jerk) * t_diff**2
252
+ + 216 * initial.velocity
253
+ + 204 * final.velocity
254
+ )
255
+ ) / (6 * t_diff**6)
256
+ a7 = (
257
+ -120 * h
258
+ + t_diff
259
+ * (
260
+ (12 * initial.acceleration - 12 * final.acceleration) * t_diff
261
+ + (initial.jerk + final.jerk) * t_diff**2
262
+ + 60 * initial.velocity
263
+ + 60 * final.velocity
264
+ )
265
+ ) / (6 * t_diff**7)
266
+
267
+ def trajectory(t: float) -> tuple[float, float, float, float]:
268
+ # Ensure t is within bounds
269
+ t = np.clip(t, time.start, time.end)
270
+
271
+ # Time relative to t_start
272
+ tau = t - time.start
273
+
274
+ # Position
275
+ q = (
276
+ a0
277
+ + a1 * tau
278
+ + a2 * tau**2
279
+ + a3 * tau**3
280
+ + a4 * tau**4
281
+ + a5 * tau**5
282
+ + a6 * tau**6
283
+ + a7 * tau**7
284
+ )
285
+
286
+ # Velocity
287
+ qd = (
288
+ a1
289
+ + 2 * a2 * tau
290
+ + 3 * a3 * tau**2
291
+ + 4 * a4 * tau**3
292
+ + 5 * a5 * tau**4
293
+ + 6 * a6 * tau**5
294
+ + 7 * a7 * tau**6
295
+ )
296
+
297
+ # Acceleration
298
+ qdd = (
299
+ 2 * a2
300
+ + 6 * a3 * tau
301
+ + 12 * a4 * tau**2
302
+ + 20 * a5 * tau**3
303
+ + 30 * a6 * tau**4
304
+ + 42 * a7 * tau**5
305
+ )
306
+
307
+ # Jerk
308
+ qddd = 6 * a3 + 24 * a4 * tau + 60 * a5 * tau**2 + 120 * a6 * tau**3 + 210 * a7 * tau**4
309
+
310
+ return q, qd, qdd, qddd
311
+
312
+ return trajectory
313
+
314
+ @staticmethod
315
+ def heuristic_velocities(points: list[float], times: list[float]) -> list[float]:
316
+ """
317
+ Compute intermediate velocities for a sequence of points using the heuristic rule.
318
+
319
+ The heuristic rule sets the velocity at each intermediate point to the average of
320
+ the slopes of the adjacent segments, unless the slopes have different signs, in which
321
+ case the velocity is set to zero.
322
+
323
+ Parameters
324
+ ----------
325
+ points : list[float]
326
+ List of position points [q0, q1, ..., qn]
327
+ times : list[float]
328
+ List of time points [t0, t1, ..., tn]
329
+
330
+ Returns
331
+ -------
332
+ list[float]
333
+ List of velocities [v0, v1, ..., vn]
334
+ """
335
+ n = len(points)
336
+ velocities = [0.0] * n # Initialize with zeros
337
+
338
+ # Compute the slopes between consecutive points
339
+ slopes = [(points[i] - points[i - 1]) / (times[i] - times[i - 1]) for i in range(1, n)]
340
+
341
+ # First and last velocities are set to 0 by default
342
+ velocities[0] = 0.0
343
+ velocities[n - 1] = 0.0
344
+
345
+ # Compute intermediate velocities using the heuristic rule
346
+ for i in range(1, n - 1):
347
+ if np.sign(slopes[i - 1]) != np.sign(slopes[i]):
348
+ velocities[i] = 0.0
349
+ else:
350
+ velocities[i] = 0.5 * (slopes[i - 1] + slopes[i])
351
+
352
+ return velocities
353
+
354
+ @classmethod
355
+ def multipoint_trajectory(
356
+ cls: type["PolynomialTrajectory"],
357
+ params: TrajectoryParams,
358
+ ) -> Callable[[float], tuple[float, float, float, float]]:
359
+ """
360
+ Generate a trajectory through a sequence of points with specified times.
361
+
362
+ Parameters
363
+ ----------
364
+ params : TrajectoryParams
365
+ Parameters for trajectory generation including points, times, and optional
366
+ velocities, accelerations, jerks, and polynomial order.
367
+
368
+ Returns
369
+ -------
370
+ Callable[[float], tuple[float, float, float, float]]
371
+ Function that computes trajectory at time t
372
+
373
+ Raises
374
+ ------
375
+ ValueError
376
+ If number of points and times are not the same, or if order is not
377
+ one of the valid polynomial orders.
378
+ """
379
+ n = len(params.points)
380
+
381
+ if n != len(params.times):
382
+ raise ValueError("Number of points and times must be the same")
383
+
384
+ if params.order not in cls.VALID_ORDERS:
385
+ valid_orders_str = ", ".join(str(order) for order in cls.VALID_ORDERS)
386
+ raise ValueError(f"Order must be one of: {valid_orders_str}")
387
+
388
+ # If velocities are not provided, compute using heuristic rule
389
+ vel = params.velocities
390
+ if vel is None:
391
+ vel = cls.heuristic_velocities(params.points, params.times)
392
+
393
+ # If accelerations are not provided, set to zeros
394
+ acc = params.accelerations
395
+ if acc is None and params.order in {ORDER_5, ORDER_7}:
396
+ acc = [0.0] * n
397
+
398
+ # If jerks are not provided, set to zeros
399
+ jrk = params.jerks
400
+ if jrk is None and params.order == ORDER_7:
401
+ jrk = [0.0] * n
402
+
403
+ # Create a list of segment trajectories
404
+ segments = []
405
+
406
+ for i in range(n - 1):
407
+ # Create time interval for this segment
408
+ time_interval = TimeInterval(params.times[i], params.times[i + 1])
409
+
410
+ if params.order == ORDER_3:
411
+ # 3rd order trajectory
412
+ initial = BoundaryCondition(params.points[i], vel[i])
413
+ final = BoundaryCondition(params.points[i + 1], vel[i + 1])
414
+ segment = cls.order_3_trajectory(initial, final, time_interval)
415
+ elif params.order == ORDER_5 and acc is not None:
416
+ # 5th order trajectory
417
+ initial = BoundaryCondition(params.points[i], vel[i], acc[i])
418
+ final = BoundaryCondition(params.points[i + 1], vel[i + 1], acc[i + 1])
419
+ segment = cls.order_5_trajectory(initial, final, time_interval)
420
+ elif params.order == ORDER_7 and acc is not None and jrk is not None:
421
+ # 7th order trajectory
422
+ initial = BoundaryCondition(params.points[i], vel[i], acc[i], jrk[i])
423
+ final = BoundaryCondition(params.points[i + 1], vel[i + 1], acc[i + 1], jrk[i + 1])
424
+ segment = cls.order_7_trajectory(initial, final, time_interval)
425
+
426
+ segments.append((segment, params.times[i], params.times[i + 1]))
427
+
428
+ def trajectory(t: float) -> tuple[float, float, float, float]:
429
+ # Handle boundary cases first for efficiency
430
+ if t < params.times[0]:
431
+ return segments[0][0](params.times[0])
432
+ if t > params.times[-1]:
433
+ return segments[-1][0](params.times[-1])
434
+
435
+ # Binary search to find the appropriate segment
436
+ left, right = 0, len(segments) - 1
437
+
438
+ while left <= right:
439
+ mid = (left + right) // 2
440
+ t_start, t_end = segments[mid][1], segments[mid][2]
441
+
442
+ if t_start <= t <= t_end:
443
+ return segments[mid][0](t)
444
+ if t < t_start:
445
+ right = mid - 1
446
+ else: # t > t_end
447
+ left = mid + 1
448
+
449
+ raise ValueError(f"No segment found for time {t}")
450
+
451
+ return trajectory