@@ -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