Skip to content

Commit 797d0ad

Browse files
committed
linear circular paths
1 parent f366a70 commit 797d0ad

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed

interpolatepy/simple_paths.py

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import numpy as np
2+
3+
4+
class LinearPath:
5+
def __init__(self, pi: np.ndarray, pf: np.ndarray) -> None:
6+
"""
7+
Initialize a linear path from point pi to point pf.
8+
9+
Parameters:
10+
pi (array-like): Initial point coordinates [x, y, z]
11+
pf (array-like): Final point coordinates [x, y, z]
12+
"""
13+
self.pi: np.ndarray = np.array(pi)
14+
self.pf: np.ndarray = np.array(pf)
15+
self.length: float = np.linalg.norm(self.pf - self.pi)
16+
17+
# Unit tangent vector (constant for linear path)
18+
if self.length > 0:
19+
self.tangent: np.ndarray = (self.pf - self.pi) / self.length
20+
else:
21+
self.tangent = np.zeros(3)
22+
23+
def position(self, s: float) -> np.ndarray:
24+
"""
25+
Calculate position at arc length s.
26+
27+
Parameters:
28+
s (float or array): Arc length parameter(s)
29+
30+
Returns:
31+
numpy.ndarray: Position vector(s)
32+
"""
33+
# Ensure s is within valid range
34+
s = np.clip(s, 0, self.length)
35+
36+
# Equation 4.34: p(s) = pi + (s/||pf-pi||)(pf-pi)
37+
return self.pi + (s / self.length) * (self.pf - self.pi) if self.length > 0 else self.pi
38+
39+
def velocity(self, _s: float | None = None) -> np.ndarray:
40+
"""
41+
Calculate first derivative with respect to arc length.
42+
For linear path, this is constant and doesn't depend on s.
43+
44+
Parameters:
45+
_s (float, optional): Arc length parameter (not used for linear path)
46+
47+
Returns:
48+
numpy.ndarray: Velocity (tangent) vector
49+
"""
50+
# Equation 4.35: dp/ds = (pf-pi)/||pf-pi||
51+
return self.tangent
52+
53+
@staticmethod
54+
def acceleration(_s: float | None = None) -> np.ndarray:
55+
"""
56+
Calculate second derivative with respect to arc length.
57+
For linear path, this is always zero.
58+
59+
Parameters:
60+
_s (float, optional): Arc length parameter (not used for linear path)
61+
62+
Returns:
63+
numpy.ndarray: Acceleration vector (always zero for linear path)
64+
"""
65+
# Equation 4.36: d²p/ds² = 0
66+
return np.zeros(3)
67+
68+
def evaluate_at(self, s_values: float | list[float] | np.ndarray) -> dict[str, np.ndarray]:
69+
"""
70+
Evaluate position, velocity, and acceleration at specific arc length values.
71+
72+
Parameters:
73+
s_values (float or array-like): Arc length parameter(s)
74+
75+
Returns:
76+
dict: Dictionary containing arrays for position, velocity, and acceleration
77+
Each array has shape (n, 3) where n is the number of s values
78+
"""
79+
# Convert scalar to array if needed
80+
s_values_arr: np.ndarray = (
81+
np.array([s_values]) if np.isscalar(s_values) else np.array(s_values)
82+
)
83+
84+
# Clip values to valid range
85+
s_clipped = np.clip(s_values_arr, 0, self.length)
86+
87+
# Initialize result arrays
88+
n = len(s_clipped)
89+
positions = np.zeros((n, 3))
90+
velocities = np.zeros((n, 3))
91+
accelerations = np.zeros((n, 3))
92+
93+
# Calculate positions for each s
94+
for i, s in enumerate(s_clipped):
95+
positions[i] = self.position(s)
96+
97+
# For linear path, velocity is constant and acceleration is zero
98+
velocities[:] = self.velocity()
99+
# accelerations already initialized to zeros
100+
101+
return {
102+
"position": positions,
103+
"velocity": velocities,
104+
"acceleration": accelerations,
105+
"s": s_clipped,
106+
}
107+
108+
def all_traj(self, num_points: int = 100) -> dict[str, np.ndarray]:
109+
"""
110+
Generate a complete trajectory along the entire linear path.
111+
112+
Parameters:
113+
num_points (int): Number of points to generate along the path
114+
115+
Returns:
116+
dict: Dictionary containing arrays for position, velocity, and acceleration
117+
Each array has shape (num_points, 3)
118+
"""
119+
# Generate evenly spaced points along the entire path
120+
s_values = np.linspace(0, self.length, num_points)
121+
122+
# Use evaluate_at to get the trajectory data
123+
return self.evaluate_at(s_values)
124+
125+
126+
class CircularPath:
127+
def __init__(self, r: np.ndarray, d: np.ndarray, pi: np.ndarray) -> None:
128+
"""
129+
Initialize a circular path.
130+
131+
Parameters:
132+
r (array-like): Unit vector of circle axis
133+
d (array-like): Position vector of a point on the circle axis
134+
pi (array-like): Position vector of a point on the circle
135+
"""
136+
self.r: np.ndarray = np.array(r)
137+
self.d: np.ndarray = np.array(d)
138+
self.pi: np.ndarray = np.array(pi)
139+
140+
# Normalize axis vector
141+
self.r /= np.linalg.norm(self.r)
142+
143+
# Compute delta vector
144+
delta = self.pi - self.d
145+
146+
# Check if pi is not on the axis
147+
if np.abs(np.dot(delta, self.r)) >= np.linalg.norm(delta):
148+
raise ValueError("The point pi must not be on the circle axis")
149+
150+
# Compute center (equation 4.37)
151+
self.center = self.d + np.dot(delta, self.r) * self.r
152+
153+
# Compute radius
154+
self.radius = np.linalg.norm(self.pi - self.center)
155+
156+
# Compute rotation matrix
157+
x_prime = (self.pi - self.center) / self.radius # Unit vector in x' direction
158+
z_prime = self.r # Unit vector in z' direction
159+
y_prime = np.cross(z_prime, x_prime) # Unit vector in y' direction
160+
161+
# Rotation matrix R = [x' y' z']
162+
self.R = np.column_stack((x_prime, y_prime, z_prime))
163+
164+
def position(self, s: float | np.ndarray) -> np.ndarray:
165+
"""
166+
Calculate position at arc length s.
167+
168+
Parameters:
169+
s (float or array): Arc length parameter(s)
170+
171+
Returns:
172+
numpy.ndarray: Position vector(s)
173+
"""
174+
if np.isscalar(s):
175+
# Position in local coordinate system (equation 4.38)
176+
p_prime = np.array(
177+
[self.radius * np.cos(s / self.radius), self.radius * np.sin(s / self.radius), 0]
178+
)
179+
180+
# Position in global coordinate system (equation 4.39)
181+
return self.center + self.R @ p_prime
182+
183+
# Ensure s is treated as an array/iterable
184+
s_array = np.asarray(s)
185+
positions = []
186+
for s_val in s_array:
187+
p_prime = np.array(
188+
[
189+
self.radius * np.cos(s_val / self.radius),
190+
self.radius * np.sin(s_val / self.radius),
191+
0,
192+
]
193+
)
194+
positions.append(self.center + self.R @ p_prime)
195+
return np.array(positions)
196+
197+
def velocity(self, s: float) -> np.ndarray:
198+
"""
199+
Calculate first derivative with respect to arc length.
200+
201+
Parameters:
202+
s (float): Arc length parameter
203+
204+
Returns:
205+
numpy.ndarray: Velocity (tangent) vector
206+
"""
207+
# Velocity in local coordinate system (equation 4.40)
208+
dp_prime_ds = np.array([-np.sin(s / self.radius), np.cos(s / self.radius), 0])
209+
210+
# Velocity in global coordinate system
211+
return self.R @ dp_prime_ds
212+
213+
def acceleration(self, s: float) -> np.ndarray:
214+
"""
215+
Calculate second derivative with respect to arc length.
216+
217+
Parameters:
218+
s (float): Arc length parameter
219+
220+
Returns:
221+
numpy.ndarray: Acceleration vector
222+
"""
223+
# Acceleration in local coordinate system (equation 4.41)
224+
d2p_prime_ds2 = np.array(
225+
[-np.cos(s / self.radius) / self.radius, -np.sin(s / self.radius) / self.radius, 0]
226+
)
227+
228+
# Acceleration in global coordinate system
229+
return self.R @ d2p_prime_ds2
230+
231+
def evaluate_at(self, s_values: float | list[float] | np.ndarray) -> dict[str, np.ndarray]:
232+
"""
233+
Evaluate position, velocity, and acceleration at specific arc length values.
234+
235+
Parameters:
236+
s_values (float or array-like): Arc length parameter(s)
237+
238+
Returns:
239+
dict: Dictionary containing arrays for position, velocity, and acceleration
240+
Each array has shape (n, 3) where n is the number of s values
241+
"""
242+
# Convert scalar to array if needed
243+
s_values_arr: np.ndarray = (
244+
np.array([s_values]) if np.isscalar(s_values) else np.array(s_values)
245+
)
246+
247+
# Initialize result arrays
248+
n = len(s_values_arr)
249+
positions = np.zeros((n, 3))
250+
velocities = np.zeros((n, 3))
251+
accelerations = np.zeros((n, 3))
252+
253+
# Calculate values for each s
254+
for i, s in enumerate(s_values_arr):
255+
positions[i] = self.position(s)
256+
velocities[i] = self.velocity(s)
257+
accelerations[i] = self.acceleration(s)
258+
259+
return {
260+
"position": positions,
261+
"velocity": velocities,
262+
"acceleration": accelerations,
263+
"s": s_values_arr,
264+
}
265+
266+
def all_traj(self, num_points: int = 100) -> dict[str, np.ndarray]:
267+
"""
268+
Generate a complete trajectory around the entire circular path.
269+
270+
Parameters:
271+
num_points (int): Number of points to generate around the circle
272+
273+
Returns:
274+
dict: Dictionary containing arrays for position, velocity, and acceleration
275+
Each array has shape (num_points, 3)
276+
"""
277+
# Generate evenly spaced points for a complete circle
278+
s_values = np.linspace(0, 2 * np.pi * self.radius, num_points)
279+
280+
# Use evaluate_at to get the trajectory data
281+
return self.evaluate_at(s_values)

0 commit comments

Comments
 (0)