Skip to content

Commit 1d1733b

Browse files
committed
Collisions use clean scans and account for lidar offset and vehicle geometry
1 parent edd74f5 commit 1d1733b

File tree

2 files changed

+147
-33
lines changed

2 files changed

+147
-33
lines changed

f1tenth_gym/envs/dynamic_models/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ class VehicleParameters:
5454
width: float = math.nan
5555
length: float = math.nan
5656

57+
# Collision body center offset from base_link (rear axle) in meters.
58+
# For symmetric overhang: x = wheelbase/2 = (lf + lr)/2
59+
collision_body_center_x: float = 0.0
60+
collision_body_center_y: float = 0.0
61+
5762
# Additional parameters for the multi-body model (defaulted to NaN)
5863
kappa_dot_max: float = math.nan
5964
kappa_dot_dot_max: float = math.nan
@@ -155,6 +160,7 @@ def to_array(self, model: "DynamicModel") -> np.ndarray:
155160
v_max=20.0,
156161
width=0.31,
157162
length=0.58,
163+
collision_body_center_x=(0.15875 + 0.17145) / 2, # wheelbase/2 = 0.1651m
158164
)
159165

160166
F1FIFTH_VEHICLE_PARAMETERS = VehicleParameters(
@@ -176,6 +182,7 @@ def to_array(self, model: "DynamicModel") -> np.ndarray:
176182
v_max=20.0,
177183
width=0.55,
178184
length=0.8,
185+
collision_body_center_x=(0.2725 + 0.2585) / 2, # wheelbase/2 = 0.2655m
179186
)
180187

181188
FULLSCALE_VEHICLE_PARAMETERS = VehicleParameters(
@@ -197,6 +204,7 @@ def to_array(self, model: "DynamicModel") -> np.ndarray:
197204
v_max=45.8,
198205
width=1.674,
199206
length=4.298,
207+
collision_body_center_x=(0.88392 + 1.50876) / 2, # wheelbase/2 = 1.1963m
200208
kappa_dot_max=0.4,
201209
kappa_dot_dot_max=20.0,
202210
j_max=10_000.0,

f1tenth_gym/envs/simulator.py

Lines changed: 139 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,13 @@ def scan_num_beams(self) -> int:
292292
# ------------------------------------------------------------------
293293

294294
def _build_scan_cache(self, simulator: ScanSimulator2D, vehicle_params: VehicleParameters) -> ScanCache:
295+
"""Build precomputed scan geometry for collision detection.
296+
297+
Computes side_distances as the distance from the LiDAR position to the
298+
vehicle body edge for each beam angle. This correctly accounts for the
299+
LiDAR offset (base_link_to_lidar_tf) and collision body center offset
300+
(collision_body_center_x/y) from the vehicle parameters.
301+
"""
295302
num_beams = simulator.num_beams
296303
angles = np.zeros(num_beams, dtype=np.float32)
297304
cosines = np.zeros(num_beams, dtype=np.float32)
@@ -302,36 +309,40 @@ def _build_scan_cache(self, simulator: ScanSimulator2D, vehicle_params: VehicleP
302309
if not np.isfinite(half_length) or not np.isfinite(half_width):
303310
raise ValueError("Vehicle length and width must be finite to build LiDAR cache")
304311

312+
# Get LiDAR offset from base_link
313+
lidar_tf = self.config.lidar_config.base_link_to_lidar_tf
314+
lidar_dx, lidar_dy, lidar_dtheta = lidar_tf
315+
316+
# Get collision body center offset from base_link
317+
body_dx = vehicle_params.collision_body_center_x
318+
body_dy = vehicle_params.collision_body_center_y
319+
320+
# Compute LiDAR position relative to collision body center
321+
# (the collision body is centered at (body_dx, body_dy) from base_link)
322+
lidar_x_in_body = lidar_dx - body_dx
323+
lidar_y_in_body = lidar_dy - body_dy
324+
305325
increment = simulator.get_increment()
306326
angle_min = simulator.angle_min
327+
307328
for idx in range(num_beams):
308-
angle = angle_min + idx * increment
309-
angles[idx] = angle
310-
cosines[idx] = math.cos(angle)
311-
sin_angle = math.sin(angle)
312-
cos_angle = cosines[idx]
313-
if math.isclose(angle, 0.0, abs_tol=1e-9):
314-
side_distances[idx] = half_length
315-
elif 0.0 < angle < math.pi / 2:
316-
side_distances[idx] = min(
317-
half_width / max(sin_angle, 1e-9),
318-
half_length / max(cos_angle, 1e-9),
319-
)
320-
elif math.pi / 2 <= angle <= math.pi:
321-
side_distances[idx] = min(
322-
half_width / max(math.cos(angle - math.pi / 2), 1e-9),
323-
half_length / max(math.sin(angle - math.pi / 2), 1e-9),
324-
)
325-
elif -math.pi / 2 < angle < 0.0:
326-
side_distances[idx] = min(
327-
half_width / max(math.sin(-angle), 1e-9),
328-
half_length / max(math.cos(-angle), 1e-9),
329-
)
330-
else:
331-
side_distances[idx] = min(
332-
half_width / max(math.cos(-angle - math.pi / 2), 1e-9),
333-
half_length / max(math.sin(-angle - math.pi / 2), 1e-9),
334-
)
329+
# Beam angle relative to vehicle heading
330+
beam_angle = angle_min + idx * increment
331+
angles[idx] = beam_angle
332+
cosines[idx] = math.cos(beam_angle)
333+
334+
# Ray angle accounts for LiDAR yaw offset
335+
ray_angle = beam_angle + lidar_dtheta
336+
dir_cos = math.cos(ray_angle)
337+
dir_sin = math.sin(ray_angle)
338+
339+
# Compute distance from LiDAR to collision body edge
340+
side_distances[idx] = self._ray_to_rect_distance(
341+
lidar_x_in_body, lidar_y_in_body,
342+
dir_cos, dir_sin,
343+
half_length, half_width,
344+
)
345+
335346
return ScanCache(angles=angles, cosines=cosines, side_distances=side_distances)
336347

337348

@@ -347,15 +358,100 @@ def _lidar_pose_from_base(self, pose: np.ndarray) -> np.ndarray:
347358
scan_theta = pose[2] + dtheta
348359
return np.array([scan_x, scan_y, scan_theta], dtype=pose.dtype)
349360

361+
def _collision_pose_from_base(self, pose: np.ndarray) -> np.ndarray:
362+
"""Transform pose from base_link to collision body center.
363+
364+
Args:
365+
pose: Base link pose (x, y, theta).
366+
367+
Returns:
368+
Collision body center pose in world frame.
369+
"""
370+
dx = self.vehicle_params.collision_body_center_x
371+
dy = self.vehicle_params.collision_body_center_y
372+
if dx == 0.0 and dy == 0.0:
373+
return pose
374+
cos_yaw = math.cos(pose[2])
375+
sin_yaw = math.sin(pose[2])
376+
body_x = pose[0] + dx * cos_yaw - dy * sin_yaw
377+
body_y = pose[1] + dx * sin_yaw + dy * cos_yaw
378+
return np.array([body_x, body_y, pose[2]], dtype=pose.dtype)
379+
380+
@staticmethod
381+
def _ray_to_rect_distance(
382+
origin_x: float,
383+
origin_y: float,
384+
dir_cos: float,
385+
dir_sin: float,
386+
half_length: float,
387+
half_width: float,
388+
) -> float:
389+
"""Compute distance from a point to the rectangle boundary along a ray.
390+
391+
Args:
392+
origin_x, origin_y: Ray origin point (e.g., LiDAR position in body frame).
393+
dir_cos, dir_sin: Ray direction as (cos(angle), sin(angle)).
394+
half_length: Half of vehicle length (x extent from center).
395+
half_width: Half of vehicle width (y extent from center).
396+
397+
Returns:
398+
Distance from origin to rectangle edge along the ray direction.
399+
Returns 0.0 if origin is outside the rectangle.
400+
"""
401+
x_min, x_max = -half_length, half_length
402+
y_min, y_max = -half_width, half_width
403+
404+
eps = 1e-9
405+
if not (x_min - eps <= origin_x <= x_max + eps and
406+
y_min - eps <= origin_y <= y_max + eps):
407+
return 0.0
408+
409+
min_t = float('inf')
410+
411+
if abs(dir_cos) > eps:
412+
t = (x_max - origin_x) / dir_cos
413+
if t > eps:
414+
y_intersect = origin_y + t * dir_sin
415+
if y_min - eps <= y_intersect <= y_max + eps:
416+
min_t = min(min_t, t)
417+
418+
t = (x_min - origin_x) / dir_cos
419+
if t > eps:
420+
y_intersect = origin_y + t * dir_sin
421+
if y_min - eps <= y_intersect <= y_max + eps:
422+
min_t = min(min_t, t)
423+
424+
if abs(dir_sin) > eps:
425+
t = (y_max - origin_y) / dir_sin
426+
if t > eps:
427+
x_intersect = origin_x + t * dir_cos
428+
if x_min - eps <= x_intersect <= x_max + eps:
429+
min_t = min(min_t, t)
430+
431+
t = (y_min - origin_y) / dir_sin
432+
if t > eps:
433+
x_intersect = origin_x + t * dir_cos
434+
if x_min - eps <= x_intersect <= x_max + eps:
435+
min_t = min(min_t, t)
436+
437+
if min_t == float('inf'):
438+
return 0.0
439+
440+
return float(min_t)
441+
350442

351443
def _update_scans(self) -> None:
352444
for agent_idx, simulator in enumerate(self.scan_sims):
353445
pose = self.state.poses[agent_idx]
354446
scan_pose = self._lidar_pose_from_base(pose)
355-
scan = simulator.scan(scan_pose, self.scan_rngs[agent_idx])
447+
448+
# Get noise-free scan for collision detection
449+
scan_clean = simulator.scan(scan_pose, rng=None)
356450
cache = self.scan_cache[agent_idx]
451+
452+
# Collision check uses noise-free scan
357453
in_collision = check_ttc_jit(
358-
scan,
454+
scan_clean,
359455
self.state.standard_state[agent_idx, 3],
360456
cache.angles,
361457
cache.cosines,
@@ -368,25 +464,35 @@ def _update_scans(self) -> None:
368464
else:
369465
self.state.collisions[agent_idx] = 0.0
370466

467+
# Ray cast against other agents (also noise-free)
371468
origin = scan_pose.astype(np.float64)
372-
adjusted_scan = scan
469+
adjusted_scan = scan_clean
373470
for opp_idx in range(self.num_agents):
374471
if opp_idx == agent_idx:
375472
continue
376473
opp_pose = self.state.poses[opp_idx]
474+
opp_collision_pose = self._collision_pose_from_base(opp_pose)
377475
opp_vertices = get_vertices(
378-
np.array([opp_pose[0], opp_pose[1], opp_pose[2]], dtype=np.float64),
476+
np.array([opp_collision_pose[0], opp_collision_pose[1], opp_collision_pose[2]], dtype=np.float64),
379477
self.vehicle_params.length,
380478
self.vehicle_params.width,
381479
)
382480
adjusted_scan = ray_cast(origin, adjusted_scan, cache.angles, opp_vertices)
383-
self.state.scans[agent_idx] = adjusted_scan.astype(np.float32)
481+
482+
# Add noise for observation output only
483+
noisy_scan = adjusted_scan + self.scan_rngs[agent_idx].normal(
484+
0.0, simulator.std_dev, size=simulator.num_beams
485+
)
486+
self.state.scans[agent_idx] = np.clip(
487+
noisy_scan, simulator.min_range, simulator.max_range
488+
).astype(np.float32)
384489

385490
def _update_agent_collisions(self) -> None:
386491
for agent_idx in range(self.num_agents):
387492
pose = self.state.poses[agent_idx]
493+
collision_pose = self._collision_pose_from_base(pose)
388494
self.agent_vertices[agent_idx] = get_vertices(
389-
np.array([pose[0], pose[1], pose[2]], dtype=np.float64),
495+
np.array([collision_pose[0], collision_pose[1], collision_pose[2]], dtype=np.float64),
390496
self.vehicle_params.length,
391497
self.vehicle_params.width,
392498
)

0 commit comments

Comments
 (0)