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,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
+ )
@@ -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