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.
- interpolatepy/__init__.py +8 -0
- interpolatepy/b_spline.py +737 -0
- interpolatepy/b_spline_approx.py +544 -0
- interpolatepy/b_spline_cubic.py +444 -0
- interpolatepy/b_spline_interpolate.py +515 -0
- interpolatepy/b_spline_smooth.py +639 -0
- interpolatepy/c_s_smoot_search.py +177 -0
- interpolatepy/c_s_smoothing.py +611 -0
- interpolatepy/c_s_with_acc1.py +643 -0
- interpolatepy/c_s_with_acc2.py +494 -0
- interpolatepy/cubic_spline.py +486 -0
- interpolatepy/double_s.py +580 -0
- interpolatepy/frenet_frame.py +245 -0
- interpolatepy/linear.py +107 -0
- interpolatepy/polynomials.py +451 -0
- interpolatepy/simple_paths.py +281 -0
- interpolatepy/trapezoidal.py +613 -0
- interpolatepy/tridiagonal_inv.py +96 -0
- interpolatepy/version.py +5 -0
- interpolatepy-1.0.0.dist-info/METADATA +415 -0
- interpolatepy-1.0.0.dist-info/RECORD +24 -0
- interpolatepy-1.0.0.dist-info/WHEEL +5 -0
- interpolatepy-1.0.0.dist-info/licenses/LICENSE +21 -0
- interpolatepy-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core functions for computing and visualizing Frenet frames along parametric curves.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from matplotlib.axes import Axes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
EPS = 1e-10
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def compute_trajectory_frames(
|
|
15
|
+
position_func: Callable[[float], tuple[np.ndarray, np.ndarray, np.ndarray]],
|
|
16
|
+
u_values: np.ndarray,
|
|
17
|
+
tool_orientation: float | tuple[float, float, float] | None = None,
|
|
18
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
19
|
+
"""
|
|
20
|
+
Compute Frenet frames along a parametric curve and optionally apply tool orientation.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
position_func : callable
|
|
25
|
+
A function that takes a parameter u and returns:
|
|
26
|
+
- position p(u)
|
|
27
|
+
- first derivative dp/du
|
|
28
|
+
- second derivative d2p/du2
|
|
29
|
+
u_values : ndarray
|
|
30
|
+
Parameter values at which to compute the frames.
|
|
31
|
+
tool_orientation : float or tuple, optional
|
|
32
|
+
If None: Returns the Frenet frames without modification.
|
|
33
|
+
If float: Angle in radians to rotate around the binormal axis (legacy mode).
|
|
34
|
+
If tuple: (roll, pitch, yaw) angles in radians representing RPY rotations.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
points : ndarray
|
|
39
|
+
Points on the curve with shape (len(u_values), 3).
|
|
40
|
+
frames : ndarray
|
|
41
|
+
Frames at each point [et, en, eb] with shape (len(u_values), 3, 3),
|
|
42
|
+
either Frenet frames or tool frames.
|
|
43
|
+
"""
|
|
44
|
+
n_points = len(u_values)
|
|
45
|
+
points = np.zeros((n_points, 3))
|
|
46
|
+
frenet_frames = np.zeros((n_points, 3, 3))
|
|
47
|
+
|
|
48
|
+
for i, u in enumerate(u_values):
|
|
49
|
+
# Get position and derivatives from the provided function
|
|
50
|
+
p, dp_du, d2p_du2 = position_func(u)
|
|
51
|
+
|
|
52
|
+
# Store the position
|
|
53
|
+
points[i] = p
|
|
54
|
+
|
|
55
|
+
# Compute tangent vector (normalized first derivative)
|
|
56
|
+
dp_du_norm = np.linalg.norm(dp_du)
|
|
57
|
+
et = dp_du / dp_du_norm
|
|
58
|
+
|
|
59
|
+
# Compute the derivative of tangent vector with respect to u
|
|
60
|
+
# When parameter is not arc length, this formula applies (from footnote 11)
|
|
61
|
+
det_du = d2p_du2 / dp_du_norm - (dp_du * np.dot(dp_du, d2p_du2)) / (dp_du_norm**3)
|
|
62
|
+
|
|
63
|
+
# Compute normal vector (normalized derivative of tangent)
|
|
64
|
+
det_du_norm = np.linalg.norm(det_du)
|
|
65
|
+
|
|
66
|
+
if det_du_norm < EPS:
|
|
67
|
+
# Handle case of zero or near-zero curvature by selecting any perpendicular vector
|
|
68
|
+
if abs(et[0]) > abs(et[1]):
|
|
69
|
+
en = np.array([et[2], 0, -et[0]])
|
|
70
|
+
en /= np.linalg.norm(en)
|
|
71
|
+
else:
|
|
72
|
+
en = np.array([0, et[2], -et[1]])
|
|
73
|
+
en /= np.linalg.norm(en)
|
|
74
|
+
else:
|
|
75
|
+
en = det_du / det_du_norm
|
|
76
|
+
|
|
77
|
+
# Compute binormal vector (cross product) to complete the right-handed frame
|
|
78
|
+
eb = np.cross(et, en)
|
|
79
|
+
|
|
80
|
+
# Store the Frenet frame
|
|
81
|
+
frenet_frames[i, :, 0] = et
|
|
82
|
+
frenet_frames[i, :, 1] = en
|
|
83
|
+
frenet_frames[i, :, 2] = eb
|
|
84
|
+
|
|
85
|
+
# If tool orientation is specified, apply it to the Frenet frames
|
|
86
|
+
if tool_orientation is not None:
|
|
87
|
+
# Create final frames array
|
|
88
|
+
tool_frames = np.zeros_like(frenet_frames)
|
|
89
|
+
|
|
90
|
+
# Generate the appropriate rotation matrix based on the input type
|
|
91
|
+
if isinstance(tool_orientation, int | float):
|
|
92
|
+
# Legacy mode: simple rotation about binormal axis
|
|
93
|
+
alpha = tool_orientation
|
|
94
|
+
r_tool = np.array(
|
|
95
|
+
[[np.cos(alpha), -np.sin(alpha), 0], [np.sin(alpha), np.cos(alpha), 0], [0, 0, 1]]
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
# RPY angles (roll, pitch, yaw) in XYZ order
|
|
99
|
+
roll, pitch, yaw = tool_orientation
|
|
100
|
+
|
|
101
|
+
# X rotation (roll)
|
|
102
|
+
r_x = np.array(
|
|
103
|
+
[[1, 0, 0], [0, np.cos(roll), -np.sin(roll)], [0, np.sin(roll), np.cos(roll)]]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Y rotation (pitch)
|
|
107
|
+
r_y = np.array(
|
|
108
|
+
[[np.cos(pitch), 0, np.sin(pitch)], [0, 1, 0], [-np.sin(pitch), 0, np.cos(pitch)]]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Z rotation (yaw)
|
|
112
|
+
r_z = np.array(
|
|
113
|
+
[[np.cos(yaw), -np.sin(yaw), 0], [np.sin(yaw), np.cos(yaw), 0], [0, 0, 1]]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Combined rotation matrix in XYZ order
|
|
117
|
+
r_tool = np.dot(r_z, np.dot(r_y, r_x))
|
|
118
|
+
|
|
119
|
+
# Apply rotation to each Frenet frame
|
|
120
|
+
for i in range(n_points):
|
|
121
|
+
tool_frames[i] = np.dot(r_tool, frenet_frames[i])
|
|
122
|
+
|
|
123
|
+
return points, tool_frames
|
|
124
|
+
return points, frenet_frames
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def helicoidal_trajectory_with_derivatives(
|
|
128
|
+
u: float, r: float = 2.0, d: float = 0.5
|
|
129
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
130
|
+
"""
|
|
131
|
+
Helicoidal trajectory function returning position and derivatives.
|
|
132
|
+
This implements equation 8.7 from the textbook.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
u : float
|
|
137
|
+
Parameter value.
|
|
138
|
+
r : float, optional
|
|
139
|
+
Radius of the helix, by default 2.0.
|
|
140
|
+
d : float, optional
|
|
141
|
+
Pitch parameter, by default 0.5.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
p : ndarray
|
|
146
|
+
Position vector.
|
|
147
|
+
dp_du : ndarray
|
|
148
|
+
First derivative vector.
|
|
149
|
+
d2p_du2 : ndarray
|
|
150
|
+
Second derivative vector.
|
|
151
|
+
"""
|
|
152
|
+
# Position (equation 8.7)
|
|
153
|
+
p = np.array([r * np.cos(u), r * np.sin(u), d * u])
|
|
154
|
+
|
|
155
|
+
# First derivative dp/du
|
|
156
|
+
dp_du = np.array([-r * np.sin(u), r * np.cos(u), d])
|
|
157
|
+
|
|
158
|
+
# Second derivative d2p/du2
|
|
159
|
+
d2p_du2 = np.array([-r * np.cos(u), -r * np.sin(u), 0])
|
|
160
|
+
|
|
161
|
+
return p, dp_du, d2p_du2
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def circular_trajectory_with_derivatives(
|
|
165
|
+
u: float, r: float = 2.0
|
|
166
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
167
|
+
"""
|
|
168
|
+
Circular trajectory function returning position and derivatives.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
u : float
|
|
173
|
+
Parameter value.
|
|
174
|
+
r : float, optional
|
|
175
|
+
Radius of the circle, by default 2.0.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
p : ndarray
|
|
180
|
+
Position vector.
|
|
181
|
+
dp_du : ndarray
|
|
182
|
+
First derivative vector.
|
|
183
|
+
d2p_du2 : ndarray
|
|
184
|
+
Second derivative vector.
|
|
185
|
+
"""
|
|
186
|
+
# Position
|
|
187
|
+
p = np.array([r * np.cos(u), r * np.sin(u), 0])
|
|
188
|
+
|
|
189
|
+
# First derivative dp/du
|
|
190
|
+
dp_du = np.array([-r * np.sin(u), r * np.cos(u), 0])
|
|
191
|
+
|
|
192
|
+
# Second derivative d2p/du2
|
|
193
|
+
d2p_du2 = np.array([-r * np.cos(u), -r * np.sin(u), 0])
|
|
194
|
+
|
|
195
|
+
return p, dp_du, d2p_du2
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def plot_frames( # noqa: PLR0913, PLR0917
|
|
199
|
+
ax: Axes,
|
|
200
|
+
points: np.ndarray,
|
|
201
|
+
frames: np.ndarray,
|
|
202
|
+
scale: float = 0.5,
|
|
203
|
+
skip: int = 5,
|
|
204
|
+
colors: list[str] | None = None,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Plot the trajectory and frames.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
----------
|
|
211
|
+
ax : matplotlib.axes.Axes
|
|
212
|
+
The 3D axes to plot on.
|
|
213
|
+
points : ndarray
|
|
214
|
+
Points on the trajectory with shape (n, 3).
|
|
215
|
+
frames : ndarray
|
|
216
|
+
Frames at each point [et, en, eb] with shape (n, 3, 3).
|
|
217
|
+
scale : float, optional
|
|
218
|
+
Scale factor for the frame vectors, by default 0.5.
|
|
219
|
+
skip : int, optional
|
|
220
|
+
Number of frames to skip between plotted frames, by default 5.
|
|
221
|
+
colors : list of str, optional
|
|
222
|
+
Colors for the three vectors, by default ['r', 'g', 'b'].
|
|
223
|
+
"""
|
|
224
|
+
# Plot the trajectory
|
|
225
|
+
if colors is None:
|
|
226
|
+
colors = ["r", "g", "b"]
|
|
227
|
+
ax.plot(points[:, 0], points[:, 1], points[:, 2], "k-")
|
|
228
|
+
|
|
229
|
+
# Plot selected frames
|
|
230
|
+
for i in range(0, len(points), skip):
|
|
231
|
+
p = points[i]
|
|
232
|
+
|
|
233
|
+
# Plot the three vectors of the frame
|
|
234
|
+
for j in range(3):
|
|
235
|
+
ax.quiver(
|
|
236
|
+
p[0],
|
|
237
|
+
p[1],
|
|
238
|
+
p[2],
|
|
239
|
+
frames[i, 0, j],
|
|
240
|
+
frames[i, 1, j],
|
|
241
|
+
frames[i, 2, j],
|
|
242
|
+
color=colors[j],
|
|
243
|
+
length=scale,
|
|
244
|
+
normalize=True,
|
|
245
|
+
)
|
interpolatepy/linear.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def linear_traj(
|
|
5
|
+
p0: float | list[float] | np.ndarray,
|
|
6
|
+
p1: float | list[float] | np.ndarray,
|
|
7
|
+
t0: float,
|
|
8
|
+
t1: float,
|
|
9
|
+
time_array: np.ndarray,
|
|
10
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
11
|
+
"""
|
|
12
|
+
Generate points along a linear trajectory using NumPy vectorization.
|
|
13
|
+
|
|
14
|
+
This function computes positions, velocities, and accelerations for points
|
|
15
|
+
along a linear trajectory between starting position p0 and ending position p1.
|
|
16
|
+
The trajectory is calculated for each time point in time_array.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
p0 : float or list[float] or np.ndarray
|
|
21
|
+
Starting position. Can be a scalar for 1D motion or an array/list for
|
|
22
|
+
multi-dimensional motion.
|
|
23
|
+
p1 : float or list[float] or np.ndarray
|
|
24
|
+
Ending position. Must have the same dimensionality as p0.
|
|
25
|
+
t0 : float
|
|
26
|
+
Start time of the trajectory.
|
|
27
|
+
t1 : float
|
|
28
|
+
End time of the trajectory.
|
|
29
|
+
time_array : np.ndarray
|
|
30
|
+
Array of time points at which to calculate the trajectory.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
positions : np.ndarray
|
|
35
|
+
Array of positions at each time point. For scalar inputs, shape is (len(time_array),).
|
|
36
|
+
For vector inputs, shape is (len(time_array), dim) where dim is the dimension of p0/p1.
|
|
37
|
+
velocities : np.ndarray
|
|
38
|
+
Constant velocity at each time point, with the same shape as positions.
|
|
39
|
+
accelerations : np.ndarray
|
|
40
|
+
Zero acceleration at each time point, with the same shape as positions.
|
|
41
|
+
|
|
42
|
+
Examples
|
|
43
|
+
--------
|
|
44
|
+
Scalar positions (1D motion):
|
|
45
|
+
|
|
46
|
+
>>> import numpy as np
|
|
47
|
+
>>> times = np.linspace(0, 1, 5)
|
|
48
|
+
>>> pos, vel, acc = linear_traj(0, 1, 0, 1, times)
|
|
49
|
+
>>> print(pos)
|
|
50
|
+
[0. 0.25 0.5 0.75 1. ]
|
|
51
|
+
>>> print(vel)
|
|
52
|
+
[1. 1. 1. 1. 1.]
|
|
53
|
+
>>> print(acc)
|
|
54
|
+
[0. 0. 0. 0. 0.]
|
|
55
|
+
|
|
56
|
+
Vector positions (2D motion):
|
|
57
|
+
|
|
58
|
+
>>> import numpy as np
|
|
59
|
+
>>> times = np.linspace(0, 2, 3)
|
|
60
|
+
>>> p0 = [0, 0] # Start at origin
|
|
61
|
+
>>> p1 = [4, 6] # End at point (4, 6)
|
|
62
|
+
>>> pos, vel, acc = linear_traj(p0, p1, 0, 2, times)
|
|
63
|
+
>>> print(pos)
|
|
64
|
+
[[0. 0.]
|
|
65
|
+
[2. 3.]
|
|
66
|
+
[4. 6.]]
|
|
67
|
+
>>> print(vel)
|
|
68
|
+
[[2. 3.]
|
|
69
|
+
[2. 3.]
|
|
70
|
+
[2. 3.]]
|
|
71
|
+
|
|
72
|
+
Notes
|
|
73
|
+
-----
|
|
74
|
+
- This function implements linear interpolation with constant velocity and
|
|
75
|
+
zero acceleration.
|
|
76
|
+
- For vector inputs (multi-dimensional motion), proper broadcasting is applied
|
|
77
|
+
to ensure correct calculation across all dimensions.
|
|
78
|
+
- The function handles both scalar and vector inputs automatically:
|
|
79
|
+
* For scalar inputs: outputs have shape (len(time_array),)
|
|
80
|
+
* For vector inputs: outputs have shape (len(time_array), dim)
|
|
81
|
+
- Time points outside the range [t0, t1] will still produce valid positions
|
|
82
|
+
by extrapolating the linear trajectory.
|
|
83
|
+
- The velocity is always constant and equal to (p1 - p0) / (t1 - t0).
|
|
84
|
+
- The acceleration is always zero.
|
|
85
|
+
"""
|
|
86
|
+
# Convert inputs to numpy arrays if they aren't already
|
|
87
|
+
p0 = np.array(p0)
|
|
88
|
+
p1 = np.array(p1)
|
|
89
|
+
time_array = np.array(time_array)
|
|
90
|
+
|
|
91
|
+
# Calculate coefficients
|
|
92
|
+
a0 = p0
|
|
93
|
+
a1 = (p1 - p0) / (t1 - t0)
|
|
94
|
+
|
|
95
|
+
# Handle broadcasting differently based on whether positions are scalar or vector
|
|
96
|
+
if np.isscalar(p0) or p0.ndim == 0:
|
|
97
|
+
positions = a0 + a1 * (time_array - t0)
|
|
98
|
+
velocities = np.ones_like(time_array) * a1
|
|
99
|
+
accelerations = np.zeros_like(time_array)
|
|
100
|
+
else:
|
|
101
|
+
# Vector case - reshape for proper broadcasting
|
|
102
|
+
time_offset = (time_array - t0).reshape(-1, 1)
|
|
103
|
+
positions = a0 + a1 * time_offset
|
|
104
|
+
velocities = np.tile(a1, (len(time_array), 1))
|
|
105
|
+
accelerations = np.zeros((len(time_array), len(a0)))
|
|
106
|
+
|
|
107
|
+
return positions, velocities, accelerations
|