Skip to content

Commit 8ac4e81

Browse files
committed
cubic spline with ex
1 parent ef52e8a commit 8ac4e81

File tree

2 files changed

+358
-0
lines changed

2 files changed

+358
-0
lines changed

examples/cubic_spline_ex.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from interpolatepy.cubic_spline import CubicSpline
2+
3+
4+
# Example usage
5+
if __name__ == "__main__":
6+
# Define waypoints
7+
t_points = [0.0, 5.0, 7.0, 8.0, 10.0, 15.0, 18.0]
8+
q_points = [3.0, -2.0, -5.0, 0.0, 6.0, 12.0, 8.0]
9+
10+
# Create cubic spline with zero initial and final velocities
11+
spline = CubicSpline(t_points, q_points, v0=2.0, vn=-3.0, debug=False)
12+
13+
# Plot the trajectory
14+
spline.plot(1000)

interpolatepy/cubic_spline.py

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
4+
from interpolatepy.tridiagonal_inv import solve_tridiagonal
5+
6+
7+
class CubicSpline:
8+
"""
9+
Cubic spline trajectory planning implementation.
10+
11+
This class implements the cubic spline algorithm described in the document.
12+
It generates a smooth trajectory passing through specified waypoints with
13+
continuous velocity and acceleration profiles.
14+
"""
15+
16+
def __init__(
17+
self,
18+
t_points: list[float] | np.ndarray,
19+
q_points: list[float] | np.ndarray,
20+
v0: float = 0.0,
21+
vn: float = 0.0,
22+
debug: bool = False,
23+
) -> None:
24+
"""
25+
Initialize a cubic spline trajectory.
26+
27+
Args:
28+
t_points: List or array of time points (t0, t1, ..., tn)
29+
q_points: List or array of position points (q0, q1, ..., qn)
30+
v0: Initial velocity at t0 (default: 0.0)
31+
vn: Final velocity at tn (default: 0.0)
32+
debug: Whether to print debug information (default: False)
33+
"""
34+
# Ensure inputs are numpy arrays
35+
self.t_points = np.array(t_points, dtype=float)
36+
self.q_points = np.array(q_points, dtype=float)
37+
self.v0 = float(v0)
38+
self.vn = float(vn)
39+
self.debug = debug
40+
41+
# Check input validity
42+
if len(self.t_points) != len(self.q_points):
43+
raise ValueError("Time points and position points must have the same length")
44+
45+
if not np.all(np.diff(self.t_points) > 0):
46+
raise ValueError("Time points must be strictly increasing")
47+
48+
# Compute time intervals
49+
self.n = len(self.t_points) - 1
50+
self.t_intervals = np.diff(self.t_points)
51+
52+
# Compute intermediate velocities and coefficients
53+
self.velocities = self._compute_velocities()
54+
self.coefficients = self._compute_coefficients()
55+
56+
def _compute_velocities(self) -> np.ndarray:
57+
"""
58+
Compute the velocities at intermediate points by solving
59+
the tridiagonal system described in the document.
60+
61+
This method implements the mathematical formulation from pages 4-5 of the document,
62+
corresponding to equation (17). It sets up and solves the tridiagonal system A*v = c
63+
to find the intermediate velocities, which ensures C2 continuity of the spline.
64+
65+
Returns:
66+
Array of velocities [v0, v1, ..., vn]
67+
"""
68+
n = self.n
69+
t_intervals = self.t_intervals
70+
q = self.q_points
71+
72+
# Create the tridiagonal matrix A as shown in the document:
73+
# Matrix A has the following structure:
74+
# [2(T0+T1) T0 0 ... 0 ]
75+
# [T2 2(T1+T2) T1 0 ... ]
76+
# [0 T3 2(T2+T3) T2 ... ]
77+
# [... ... ... ... ... ]
78+
# [0 ... 0 Tn-2 2(Tn-3+Tn-2) Tn-3]
79+
# [0 ... 0 0 Tn-1 2(Tn-2+Tn-1)]
80+
81+
if n == 1:
82+
# Special case: only one segment
83+
# We know v0 and vn, so no need to solve system
84+
return np.array([self.v0, self.vn])
85+
86+
# Create the right-hand side vector c from equation (17) in the document
87+
# c_i = 3/(T_i*T_{i+1}) * [T_i^2*(q_{i+2}-q_{i+1}) + T_{i+1}^2*(q_{i+1}-q_i)]
88+
rhs = np.zeros(n - 1)
89+
90+
for i in range(n - 1):
91+
rhs[i] = (
92+
3
93+
/ (t_intervals[i] * t_intervals[i + 1])
94+
* (
95+
t_intervals[i] ** 2 * (q[i + 2] - q[i + 1])
96+
+ t_intervals[i + 1] ** 2 * (q[i + 1] - q[i])
97+
)
98+
)
99+
100+
# Adjust the right-hand side to account for known boundary velocities v0 and vn
101+
# These adjustments come from moving the terms with known velocities to the RHS
102+
if n > 1:
103+
# First equation: subtract T_1*v_0 from RHS
104+
rhs[0] -= t_intervals[1] * self.v0
105+
106+
# Last equation: subtract T_{n-2}*v_n from RHS
107+
rhs[-1] -= t_intervals[-2] * self.vn
108+
109+
# Solve the system for the intermediate velocities v1, ..., v(n-1)
110+
# Case n=2 means we have exactly one intermediate velocity to solve (1x1 system)
111+
if n == 2: # noqa: PLR2004
112+
# Special case: only one intermediate velocity to solve for
113+
# Simple division is sufficient (1x1 system)
114+
main_diag_value = 2 * (t_intervals[0] + t_intervals[1])
115+
v_intermediate = rhs / main_diag_value
116+
else:
117+
# Instead of building the full matrix, extract the diagonal elements for the
118+
# tridiagonal solver
119+
120+
# Main diagonal: 2(T_i + T_{i+1})
121+
main_diag = np.zeros(n - 1)
122+
for i in range(n - 1):
123+
main_diag[i] = 2 * (t_intervals[i] + t_intervals[i + 1])
124+
125+
# Lower diagonal: T_{i+1} (first element not used in solve_tridiagonal)
126+
lower_diag = np.zeros(n - 1)
127+
for i in range(1, n - 1):
128+
lower_diag[i] = t_intervals[i + 1]
129+
130+
# Upper diagonal: T_i (last element not used in solve_tridiagonal)
131+
upper_diag = np.zeros(n - 1)
132+
for i in range(n - 2):
133+
upper_diag[i] = t_intervals[i]
134+
135+
# Print debug information only if debug is enabled
136+
if self.debug:
137+
print("\nTridiagonal Matrix components:")
138+
print("Main diagonal:", main_diag)
139+
print("Lower diagonal:", lower_diag)
140+
print("Upper diagonal:", upper_diag)
141+
print("Right-hand side vector:", rhs)
142+
143+
# Solve the tridiagonal system using the Thomas algorithm
144+
v_intermediate = solve_tridiagonal(lower_diag, main_diag, upper_diag, rhs)
145+
146+
# Print the intermediate velocities if debug is enabled
147+
if self.debug:
148+
print("\nIntermediate velocities v_1 to v_{n-1}:")
149+
print(v_intermediate)
150+
151+
# Construct the full velocity array by including the known boundary velocities
152+
velocities = np.zeros(n + 1)
153+
velocities[0] = self.v0 # Initial velocity (given)
154+
velocities[1:-1] = v_intermediate # Intermediate velocities (computed)
155+
velocities[-1] = self.vn # Final velocity (given)
156+
157+
# Print the complete velocity vector if debug is enabled
158+
if self.debug:
159+
print("\nComplete velocity vector v:")
160+
print(velocities)
161+
162+
return velocities
163+
164+
def _compute_coefficients(self) -> np.ndarray:
165+
"""
166+
Compute the coefficients for each cubic polynomial segment.
167+
For each segment k, we compute:
168+
- ak0: constant term
169+
- ak1: coefficient of (t-tk)
170+
- ak2: coefficient of (t-tk)^2
171+
- ak3: coefficient of (t-tk)^3
172+
173+
Returns:
174+
Array of shape (n, 4) containing the coefficients
175+
"""
176+
n = self.n
177+
t_intervals = self.t_intervals
178+
q = self.q_points
179+
v = self.velocities
180+
181+
coeffs = np.zeros((n, 4))
182+
183+
for k in range(n):
184+
coeffs[k, 0] = q[k] # ak0 = qk
185+
coeffs[k, 1] = v[k] # ak1 = vk
186+
187+
# Compute ak2 and ak3
188+
coeffs[k, 2] = (1 / t_intervals[k]) * (
189+
(3 * (q[k + 1] - q[k]) / t_intervals[k]) - 2 * v[k] - v[k + 1]
190+
)
191+
coeffs[k, 3] = (1 / t_intervals[k] ** 2) * (
192+
(2 * (q[k] - q[k + 1]) / t_intervals[k]) + v[k] + v[k + 1]
193+
)
194+
195+
# Print detailed coefficient calculation if debug is enabled
196+
if self.debug:
197+
print(f"\nCoefficient calculation for segment {k}:")
198+
print(f" ak0 = {coeffs[k, 0]}")
199+
print(f" ak1 = {coeffs[k, 1]}")
200+
print(f" ak2 = {coeffs[k, 2]}")
201+
print(f" ak3 = {coeffs[k, 3]}")
202+
203+
return coeffs
204+
205+
def evaluate(self, t: float | np.ndarray) -> float | np.ndarray:
206+
"""
207+
Evaluate the spline at time t.
208+
209+
Args:
210+
t: Time point or array of time points
211+
212+
Returns:
213+
Position(s) at the specified time(s)
214+
"""
215+
t = np.atleast_1d(t)
216+
result = np.zeros_like(t)
217+
218+
for i, ti in enumerate(t):
219+
# Find the segment that contains ti
220+
if ti <= self.t_points[0]:
221+
# Before the start of the trajectory
222+
k = 0
223+
tau = 0
224+
elif ti >= self.t_points[-1]:
225+
# After the end of the trajectory
226+
k = self.n - 1
227+
tau = self.t_intervals[k]
228+
else:
229+
# Within the trajectory
230+
# Find the largest k such that t_k <= ti
231+
k = np.searchsorted(self.t_points, ti, side="right") - 1
232+
tau = ti - self.t_points[k]
233+
234+
# Evaluate the polynomial
235+
a = self.coefficients[k]
236+
result[i] = a[0] + a[1] * tau + a[2] * tau**2 + a[3] * tau**3
237+
238+
return result[0] if len(result) == 1 else result
239+
240+
def evaluate_velocity(self, t: float | np.ndarray) -> float | np.ndarray:
241+
"""
242+
Evaluate the velocity at time t.
243+
244+
Args:
245+
t: Time point or array of time points
246+
247+
Returns:
248+
Velocity at the specified time(s)
249+
"""
250+
t = np.atleast_1d(t)
251+
result = np.zeros_like(t)
252+
253+
for i, ti in enumerate(t):
254+
# Find the segment that contains ti
255+
if ti <= self.t_points[0]:
256+
# Before the start of the trajectory
257+
k = 0
258+
tau = 0
259+
elif ti >= self.t_points[-1]:
260+
# After the end of the trajectory
261+
k = self.n - 1
262+
tau = self.t_intervals[k]
263+
else:
264+
# Within the trajectory
265+
k = np.searchsorted(self.t_points, ti, side="right") - 1
266+
tau = ti - self.t_points[k]
267+
268+
# Evaluate the derivative of the polynomial
269+
a = self.coefficients[k]
270+
result[i] = a[1] + 2 * a[2] * tau + 3 * a[3] * tau**2
271+
272+
return result[0] if len(result) == 1 else result
273+
274+
def evaluate_acceleration(self, t: float | np.ndarray) -> float | np.ndarray:
275+
"""
276+
Evaluate the acceleration at time t.
277+
278+
Args:
279+
t: Time point or array of time points
280+
281+
Returns:
282+
Acceleration at the specified time(s)
283+
"""
284+
t = np.atleast_1d(t)
285+
result = np.zeros_like(t)
286+
287+
for i, ti in enumerate(t):
288+
# Find the segment that contains ti
289+
if ti <= self.t_points[0]:
290+
# Before the start of the trajectory
291+
k = 0
292+
tau = 0
293+
elif ti >= self.t_points[-1]:
294+
# After the end of the trajectory
295+
k = self.n - 1
296+
tau = self.t_intervals[k]
297+
else:
298+
# Within the trajectory
299+
k = np.searchsorted(self.t_points, ti, side="right") - 1
300+
tau = ti - self.t_points[k]
301+
302+
# Evaluate the second derivative of the polynomial
303+
a = self.coefficients[k]
304+
result[i] = 2 * a[2] + 6 * a[3] * tau
305+
306+
return result[0] if len(result) == 1 else result
307+
308+
def plot(self, num_points: int = 1000) -> None:
309+
"""
310+
Plot the spline trajectory along with its velocity and acceleration profiles.
311+
312+
Args:
313+
num_points: Number of points to use for plotting
314+
"""
315+
t_min, t_max = self.t_points[0], self.t_points[-1]
316+
t = np.linspace(t_min, t_max, num_points)
317+
318+
q = self.evaluate(t)
319+
v = self.evaluate_velocity(t)
320+
a = self.evaluate_acceleration(t)
321+
322+
_fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 8), sharex=True)
323+
324+
# Position plot
325+
ax1.plot(t, q, "b-", linewidth=2)
326+
ax1.plot(self.t_points, self.q_points, "ro", markersize=8)
327+
ax1.set_ylabel("Position")
328+
ax1.grid(True)
329+
ax1.set_title("Cubic Spline Trajectory")
330+
331+
# Velocity plot
332+
ax2.plot(t, v, "g-", linewidth=2)
333+
ax2.plot(self.t_points, self.velocities, "ro", markersize=6)
334+
ax2.set_ylabel("Velocity")
335+
ax2.grid(True)
336+
337+
# Acceleration plot
338+
ax3.plot(t, a, "r-", linewidth=2)
339+
ax3.set_ylabel("Acceleration")
340+
ax3.set_xlabel("Time")
341+
ax3.grid(True)
342+
343+
plt.tight_layout()
344+
plt.show()

0 commit comments

Comments
 (0)