Skip to content

Commit 84db297

Browse files
committed
Migrate to ND cubic spline interpolation for continuous yaw and eyaw calc
1 parent 2a7fb1c commit 84db297

File tree

2 files changed

+147
-30
lines changed

2 files changed

+147
-30
lines changed

f1tenth_gym/envs/track/cubic_spline.py

+141-23
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,98 @@ class CubicSpline2D:
2727
cubic spline for y coordinates.
2828
"""
2929

30-
def __init__(self, x, y):
31-
self.points = np.c_[x, y]
32-
if not np.all(self.points[-1] == self.points[0]):
30+
def __init__(self, x, y,
31+
psis: Optional[np.ndarray] = None,
32+
ks: Optional[np.ndarray] = None,
33+
vxs: Optional[np.ndarray] = None,
34+
axs: Optional[np.ndarray] = None,
35+
ss: Optional[np.ndarray] = None,
36+
):
37+
self.xs = x
38+
self.ys = y
39+
input_vals = [x, y, psis, ks, vxs, axs, ss]
40+
41+
# Only close the path if for the input values from the user,
42+
# the first and last points are not the same => the path is not closed
43+
# Otherwise, the constructed values can mess up the s calculation and closure
44+
need_closure = False
45+
for input_val in input_vals:
46+
if input_val is not None:
47+
if not (input_val[-1] == input_val[0]):
48+
need_closure = True
49+
break
50+
51+
def close_with_constructor(input_val, constructor, closed_path):
52+
'''
53+
If the input value is not None, return it.
54+
Otherwise, return the constructor, with closure if necessary.
55+
56+
Parameters
57+
----------
58+
input_val : np.ndarray | None
59+
The input value from the user.
60+
constructor : np.ndarray
61+
The constructor to use if the input value is None.
62+
closed_path : bool
63+
Indicator whether the orirignal path is closed.
64+
'''
65+
if input_val is not None:
66+
return input_val
67+
else:
68+
temp_ret = constructor
69+
if closed_path:
70+
temp_ret[-1] = temp_ret[0]
71+
return temp_ret
72+
73+
self.psis = close_with_constructor(psis, self._calc_yaw_from_xy(x, y), not need_closure)
74+
self.ks = close_with_constructor(ks, self._calc_kappa_from_xy(x, y), not need_closure)
75+
self.vxs = close_with_constructor(vxs, np.ones_like(x), not need_closure)
76+
self.axs = close_with_constructor(axs, np.zeros_like(x), not need_closure)
77+
self.ss = close_with_constructor(ss, self.__calc_s(x, y), not need_closure)
78+
psis_spline = close_with_constructor(psis, self._calc_yaw_from_xy(x, y), not need_closure)
79+
80+
# If yaw is provided, interpolate cosines and sines of yaw for continuity
81+
cosines_spline = np.cos(psis_spline)
82+
sines_spline = np.sin(psis_spline)
83+
84+
ks_spline = close_with_constructor(ks, self._calc_kappa_from_xy(x, y), not need_closure)
85+
vxs_spline = close_with_constructor(vxs, np.zeros_like(x), not need_closure)
86+
axs_spline = close_with_constructor(axs, np.zeros_like(x), not need_closure)
87+
88+
self.points = np.c_[self.xs, self.ys,
89+
cosines_spline, sines_spline,
90+
ks_spline, vxs_spline, axs_spline]
91+
92+
if need_closure:
3393
self.points = np.vstack(
3494
(self.points, self.points[0])
3595
) # Ensure the path is closed
36-
self.s = self.__calc_s(self.points[:, 0], self.points[:, 1])
96+
97+
if ss is not None:
98+
self.s = ss
99+
else:
100+
self.s = self.__calc_s(self.points[:, 0], self.points[:, 1])
101+
self.s_interval = (self.s[-1] - self.s[0]) / len(self.s)
102+
37103
# Use scipy CubicSpline to interpolate the points with periodic boundary conditions
38-
# This is necessary to ensure the path is continuous
104+
# This is necesaxsry to ensure the path is continuous
39105
self.spline = interpolate.CubicSpline(self.s, self.points, bc_type="periodic")
106+
self.spline_x = np.array(self.spline.x)
107+
self.spline_c = np.array(self.spline.c)
108+
109+
110+
def find_segment_for_s(self, x):
111+
# Find the segment of the spline that x is in
112+
return (x / (self.spline.x[-1] + self.s_interval) * (len(self.spline_x) - 1)).astype(int)
113+
114+
def predict_with_spline(self, point, segment, state_index=0):
115+
# A (4, 100) array, where the rows contain (x-x[i])**3, (x-x[i])**2 etc.
116+
# exp_x = (point - self.spline.x[[segment]])[None, :] ** np.arange(4)[::-1, None]
117+
exp_x = ((point - self.spline.x[segment % len(self.spline.x)]) ** np.arange(4)[::-1])[:, None]
118+
vec = self.spline.c[:, segment % self.spline.c.shape[1], state_index]
119+
# Sum over the rows of exp_x weighted by coefficients in the ith column of s.c
120+
point = vec.dot(exp_x)
121+
return np.asarray(point)
40122

41123
def __calc_s(self, x: np.ndarray, y: np.ndarray) -> np.ndarray:
42124
"""
@@ -60,6 +142,20 @@ def __calc_s(self, x: np.ndarray, y: np.ndarray) -> np.ndarray:
60142
s = [0]
61143
s.extend(np.cumsum(self.ds))
62144
return np.array(s)
145+
146+
def _calc_yaw_from_xy(self, x, y):
147+
dx_dt = np.gradient(x)
148+
dy_dt = np.gradient(y)
149+
heading = np.arctan2(dy_dt, dx_dt)
150+
return heading
151+
152+
def _calc_kappa_from_xy(self, x, y):
153+
dx_dt = np.gradient(x, 2)
154+
dy_dt = np.gradient(y, 2)
155+
d2x_dt2 = np.gradient(dx_dt, 2)
156+
d2y_dt2 = np.gradient(dy_dt, 2)
157+
curvature = -(d2x_dt2 * dy_dt - dx_dt * d2y_dt2) / (dx_dt * dx_dt + dy_dt * dy_dt)**1.5
158+
return curvature
63159

64160
def calc_position(self, s: float) -> np.ndarray:
65161
"""
@@ -78,7 +174,10 @@ def calc_position(self, s: float) -> np.ndarray:
78174
y : float | None
79175
y position for given s.
80176
"""
81-
return self.spline(s)
177+
segment = self.find_segment_for_s(s)
178+
x = self.predict_with_spline(s, segment, 0)[0]
179+
y = self.predict_with_spline(s, segment, 1)[0]
180+
return x,y
82181

83182
def calc_curvature(self, s: float) -> Optional[float]:
84183
"""
@@ -95,11 +194,29 @@ def calc_curvature(self, s: float) -> Optional[float]:
95194
k : float
96195
curvature for given s.
97196
"""
98-
dx, dy = self.spline(s, 1)
99-
ddx, ddy = self.spline(s, 2)
100-
k = (ddy * dx - ddx * dy) / ((dx**2 + dy**2) ** (3 / 2))
197+
segment = self.find_segment_for_s(s)
198+
k = self.predict_with_spline(s, segment, 4)[0]
101199
return k
102200

201+
def find_curvature(self, s: float) -> Optional[float]:
202+
"""
203+
Find curvature at the given s by the segment.
204+
205+
Parameters
206+
----------
207+
s : float
208+
distance from the start point. if `s` is outside the data point's
209+
range, return None.
210+
211+
Returns
212+
-------
213+
k : float
214+
curvature for given s.
215+
"""
216+
segment = self.find_segment_for_s(s)
217+
k = self.points[segment, 4]
218+
return k
219+
103220
def calc_yaw(self, s: float) -> Optional[float]:
104221
"""
105222
Calc yaw angle at the given s.
@@ -114,11 +231,11 @@ def calc_yaw(self, s: float) -> Optional[float]:
114231
yaw : float
115232
yaw angle (tangent vector) for given s.
116233
"""
117-
dx, dy = self.spline(s, 1)
118-
yaw = math.atan2(dy, dx)
119-
# Convert yaw to [0, 2pi]
120-
yaw = yaw % (2 * math.pi)
121-
234+
segment = self.find_segment_for_s(s)
235+
cos = self.predict_with_spline(s, segment, 2)[0]
236+
sin = self.predict_with_spline(s, segment, 3)[0]
237+
# yaw = (math.atan2(sin, cos) + 2 * math.pi) % (2 * math.pi)
238+
yaw = np.arctan2(sin, cos)
122239
return yaw
123240

124241
def calc_arclength(
@@ -145,15 +262,15 @@ def calc_arclength(
145262
"""
146263

147264
def distance_to_spline(s):
148-
x_eval, y_eval = self.spline(s)[0]
265+
x_eval, y_eval = self.spline(s)[0, :2]
149266
return np.sqrt((x - x_eval) ** 2 + (y - y_eval) ** 2)
150267

151268
output = so.fmin(distance_to_spline, s_guess, full_output=True, disp=False)
152269
closest_s = float(output[0][0])
153270
absolute_distance = output[1]
154271
return closest_s, absolute_distance
155272

156-
def calc_arclength_inaccurate(self, x: float, y: float) -> tuple[float, float]:
273+
def calc_arclength_inaccurate(self, x: float, y: float, s_inds=None) -> tuple[float, float]:
157274
"""
158275
Fast calculation of arclength for a given point (x, y) on the trajectory.
159276
Less accuarate and less smooth than calc_arclength but much faster.
@@ -173,15 +290,16 @@ def calc_arclength_inaccurate(self, x: float, y: float) -> tuple[float, float]:
173290
ey : float
174291
lateral deviation for given x, y.
175292
"""
293+
if s_inds is None:
294+
s_inds = np.arange(self.points.shape[0])
176295
_, ey, t, min_dist_segment = nearest_point_on_trajectory(
177-
np.array([x, y]), self.points
296+
np.array([x, y]).astype(np.float32), self.points[s_inds, :2]
178297
)
179-
# s = s at closest_point + t
298+
min_dist_segment_s_ind = s_inds[min_dist_segment]
180299
s = float(
181-
self.s[min_dist_segment]
182-
+ t * (self.s[min_dist_segment + 1] - self.s[min_dist_segment])
300+
self.s[min_dist_segment_s_ind]
301+
+ t * (self.s[min_dist_segment_s_ind + 1] - self.s[min_dist_segment_s_ind])
183302
)
184-
185303
return s, ey
186304

187305
def _calc_tangent(self, s: float) -> np.ndarray:
@@ -199,7 +317,7 @@ def _calc_tangent(self, s: float) -> np.ndarray:
199317
tangent : float
200318
tangent vector for given s.
201319
"""
202-
dx, dy = self.spline(s, 1)
320+
dx, dy = self.spline(s, 1)[:2]
203321
tangent = np.array([dx, dy])
204322
return tangent
205323

@@ -218,6 +336,6 @@ def _calc_normal(self, s: float) -> np.ndarray:
218336
normal : float
219337
normal vector for given s.
220338
"""
221-
dx, dy = self.spline(s, 1)
339+
dx, dy = self.spline(s, 1)[:2]
222340
normal = np.array([-dy, dx])
223341
return normal

f1tenth_gym/envs/track/track.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -355,20 +355,19 @@ def cartesian_to_frenet(self, x, y, phi, use_raceline=False, s_guess=0):
355355
ephi: heading deviation
356356
"""
357357
line = self.raceline if use_raceline else self.centerline
358-
s, ey = line.spline.calc_arclength_inaccurate(x, y)
358+
# s, ey = line.spline.calc_arclength_inaccurate(x, y) # inaccurate, but much faster
359+
s, ey = line.spline.calc_arclength(x, y, s_guess)
359360
# Wrap around
360361
s = s % line.spline.s[-1]
361362

362363
# Use the normal to calculate the signed lateral deviation
363-
normal = line.spline._calc_normal(s)
364+
yaw = line.spline.calc_yaw(s)
365+
normal = np.asarray([-np.sin(yaw), np.cos(yaw)])
364366
x_eval, y_eval = line.spline.calc_position(s)
365367
dx = x - x_eval
366368
dy = y - y_eval
367369
distance_sign = np.sign(np.dot([dx, dy], normal))
368370
ey = ey * distance_sign
369371

370-
ephi = phi - line.spline.calc_yaw(s)
371-
# ephi is unbouded, so we need to wrap it to [-pi, pi]
372-
ephi = (ephi + np.pi) % (2 * np.pi) - np.pi
373-
374-
return s, ey, ephi
372+
phi = phi - yaw
373+
return s, ey, np.arctan2(np.sin(phi), np.cos(phi))

0 commit comments

Comments
 (0)