Skip to content

Commit f15e59f

Browse files
committed
implemented a quintic hermite spline trajectory planner to output a C2 continuous trajectory
1 parent f3d667c commit f15e59f

1 file changed

Lines changed: 173 additions & 0 deletions

File tree

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# File: GEMstack/onboard/planning/yield_spline_planner.py
2+
3+
from typing import List, Tuple
4+
import numpy as np
5+
6+
from ..component import Component
7+
from ...state import AllState, Path, Trajectory
8+
9+
10+
class QuinticHermiteSplinePlanner:
11+
"""
12+
Core quintic-Hermite engine: given coarse 2D or 3D waypoints
13+
(x,y[,heading]) builds a C2-continuous spline and samples it at fixed Δt.
14+
"""
15+
def __init__(self, v_des: float = 1.0, dt: float = 0.02):
16+
self.v_des = v_des
17+
self.dt = dt
18+
19+
def _compute_headings(self, pts: np.ndarray) -> np.ndarray:
20+
"""
21+
If pts.shape[1] == 3, assume pts[:,2] already contains heading ψ.
22+
Otherwise fall back to finite-difference approximation.
23+
"""
24+
n, d = pts.shape
25+
if d == 3:
26+
# user-provided headings
27+
return pts[:, 2].copy()
28+
29+
# approximate by central differences
30+
headings = np.zeros(n)
31+
for i in range(n):
32+
if i == 0:
33+
delta = pts[1] - pts[0]
34+
elif i == n - 1:
35+
delta = pts[-1] - pts[-2]
36+
else:
37+
delta = pts[i+1] - pts[i-1]
38+
headings[i] = np.arctan2(delta[1], delta[0])
39+
return headings
40+
41+
def build(self,
42+
waypoints: List[List[float]]
43+
) -> Tuple[np.ndarray, np.ndarray]:
44+
"""
45+
waypoints: list of [x,y] or [x,y,ψ] entries
46+
returns (pts_out, t_out), each as an np.ndarray
47+
"""
48+
W = np.array(waypoints, float) # shape = (n,2) or (n,3)
49+
headings = self._compute_headings(W)
50+
tangents = np.stack([np.cos(headings),
51+
np.sin(headings)], axis=1) * self.v_des
52+
53+
pts_out = []
54+
t_out = []
55+
t_accum = 0.0
56+
57+
M = np.array([[1, 1, 1],
58+
[3, 4, 5],
59+
[6, 12, 20]], float)
60+
61+
# build one quintic segment between each adjacent pair
62+
for i in range(len(W) - 1):
63+
p0, p1 = W[i,0:2], W[i+1,0:2]
64+
m0, m1 = tangents[i], tangents[i+1]
65+
66+
L = np.linalg.norm(p1 - p0)
67+
T = (L / self.v_des) if self.v_des > 0 else 0.0
68+
69+
# Hermite coefficients a0..a5
70+
a0 = p0
71+
a1 = m0 * T
72+
a2 = np.zeros(2)
73+
74+
RHS = np.vstack([
75+
p1 - (a0 + a1 + a2),
76+
m1 * T - ( a1 + 2*a2),
77+
np.zeros(2) - ( 2*a2)
78+
]) # shape = (3,2)
79+
80+
# solve for a3,a4,a5
81+
a3, a4, a5 = np.linalg.solve(M, RHS)
82+
83+
# sample
84+
if T > 0:
85+
t_samples = np.arange(0.0, T, self.dt)
86+
else:
87+
t_samples = np.array([0.0])
88+
89+
for tt in t_samples:
90+
s = tt / T if T > 0 else 0.0
91+
p = (a0
92+
+ a1 * s
93+
+ a2 * s**2
94+
+ a3 * s**3
95+
+ a4 * s**4
96+
+ a5 * s**5)
97+
pts_out.append(p.tolist())
98+
t_out.append(t_accum + tt)
99+
100+
t_accum += T
101+
102+
# append very last waypoint
103+
pts_out.append(W[-1,0:2].tolist())
104+
t_out.append(t_accum)
105+
106+
return np.array(pts_out), np.array(t_out)
107+
108+
109+
class SplinePlanner(Component):
110+
"""Follows route by smoothing coarse waypoints into a quintic spline."""
111+
def __init__(self):
112+
super().__init__()
113+
self.route_progress = None
114+
self.t_last = None
115+
116+
# how far ahead to plan (m), and sampling speed & dt
117+
self.lookahead_dist = 10.0
118+
self.v_des = 2.0
119+
self.dt = 0.02
120+
121+
# the spline engine
122+
self._spline = QuinticHermiteSplinePlanner(self.v_des, self.dt)
123+
124+
def state_inputs(self):
125+
return ['all']
126+
127+
def state_outputs(self):
128+
return ['trajectory']
129+
130+
def rate(self):
131+
return 10.0 # Hz
132+
133+
def update(self, state: AllState) -> Trajectory:
134+
t = state.t
135+
if self.t_last is None:
136+
self.t_last = t
137+
138+
# keep route_progress up to date
139+
veh = state.vehicle
140+
curr = np.array([veh.pose.x, veh.pose.y])
141+
142+
if self.route_progress is None:
143+
self.route_progress = 0.0
144+
_, new_param = state.route.closest_point_local(
145+
curr.tolist(),
146+
(self.route_progress - 5.0,
147+
self.route_progress + 5.0)
148+
)
149+
self.route_progress = new_param
150+
151+
# extract a look-ahead segment from the route
152+
seg: Path = state.route.trim(
153+
self.route_progress,
154+
self.route_progress + self.lookahead_dist
155+
)
156+
157+
# pull out the raw waypoints (may be [x,y] or [x,y,ψ])
158+
pts_raw: List[List[float]] = [
159+
list(pt) for pt in seg.points
160+
]
161+
162+
# build the quintic spline
163+
spline_pts, spline_times = self._spline.build(pts_raw)
164+
165+
# wrap in GEMstack Trajectory
166+
traj = Trajectory(
167+
frame = seg.frame,
168+
points = spline_pts.tolist(),
169+
times = spline_times.tolist()
170+
)
171+
172+
self.t_last = t
173+
return traj

0 commit comments

Comments
 (0)