Source code for highway_env.envs.random_road_env

import warnings
from itertools import chain
from typing import cast

import numpy as np

from highway_env.envs.common.abstract import AbstractEnv, Observation
from highway_env.envs.common.action import Action, action_factory
from highway_env.envs.common.observation import (
    DictObservation,
    NavigationObservation,
    observation_factory,
)
from highway_env.envs.common.warnings import HighwayEnvExperimentalWarning
from highway_env.road.generation.engine.gen_utils import Lane
from highway_env.road.generation.generator import generate_random_lanes
from highway_env.road.generation.spatial_hash import (
    get_proximal_lanes_wrt_gridpoint,
    point_to_gridpoint,
)
from highway_env.road.lane import PolyLane
from highway_env.road.partitioned_road import PartitionedRoadNetwork
from highway_env.road.road import LineType, Road
from highway_env.vehicle.objects import Landmark, RoadObject


class ParkingSpot(Landmark):
    LENGTH = 7.0
    WIDTH = 3.0


[docs] class RandomRoadEnv(AbstractEnv): """ A navigation, negotiation, and parking environment set on a procedurally generated road network. The goal of an agent is to get to a parking spot as soon as possible without crashing into a curb or other vehicle. """ def __init__( self, config: dict | None = None, render_mode: str | None = None ) -> None: super().__init__(config=config, render_mode=render_mode) self.lanes = [] self.vehicle_parked = False # TODO: remove warning after environment is stable warnings.warn( HighwayEnvExperimentalWarning.template % self.__class__, HighwayEnvExperimentalWarning, stacklevel=2, )
[docs] @classmethod def default_config(cls) -> dict: """ - **max_timesteps**: number of policy timesteps before truncation - **curb_collision_reward**: one-time penalty after hitting lane border - **car_collision_reward**: one-time penalty after hitting another vehicle or object - **parking_reward**: one-time reward after parking in the goal parking spot - **parking_score_threshold**: determines the threshold of proximity to be parked - **parking_score_weights**: specifies how much position, velocity, and alignment matter - **route_following_reward_scalar**: determines the reward/penalty gained by traveling towards/away from the next waypoint - **timestep_reward**: step/living penalty - **parking_seed**: pseudorandom seed for determining the placement of parking spots within a generated road network - **generation_params**: custom parameters to be passed for generation - **preloaded_lanes**: prevents generation of a new road network by providing an already existing one - **lane_partition_gridsize**: the size of the grids when partitioning lanes for proximal checks. A lower value can reduce the number of unnecessary checks in dense networks. """ config = super().default_config() config.update( { "observation": { "type": "DictObservation", "observation_configs": { "lane_lidar": {"type": "LaneLidarObservation"}, "navigation": {"type": "NavigationObservation"}, "relative_goal": {"type": "RelativeGoalObservation"}, "lidar": {"type": "LidarObservation"}, }, }, "action": {"type": "ContinuousAction"}, "screen_width": 1200, "screen_height": 700, "max_timesteps": 1000, "curb_collision_reward": -10, "car_collision_reward": -20, "parking_reward": 10, "parking_score_threshold": 0.7, "parking_score_weights": [0.5, 1, 3], "route_following_reward_scalar": 0.1, "timestep_reward": -0.01, "parking_seed": 0, "generation_params": None, "preloaded_lanes": None, "lane_partition_gridsize": 30, } ) return config
[docs] def define_spaces(self) -> None: self.observation_type = observation_factory(self, self.config["observation"]) self.action_type = action_factory(self, self.config["action"]) self.observation_space = self.observation_type.space() self.action_space = self.action_type.space()
def _reset(self) -> None: self.lanes = self._make_road() assert self.road is not None parking_rng = np.random.default_rng(self.config["parking_seed"]) self.create_parking_spots( num_spots=2, spot_width=3, spot_height=6, rng=parking_rng ) spawn_spot = self.road.objects[0] self.vehicle = self.action_type.vehicle_class( self.road, spawn_spot.position, spawn_spot.heading, 0.0 ) self.vehicle.goal = self.road.objects[1] self.road.vehicles.append(self.vehicle) self.vehicle_parked = False def _reward(self, action: Action) -> float: """ Rewards: * Curb collision penalty * Vehicle-vehicle collision penalty * Parking reward (one-time) * Timestep punishment * Route-following reward """ # Collision collided_with_curb = self.detect_object_lane_collision(self.vehicle) collided_with_car = self.vehicle.crashed if collided_with_curb or collided_with_car: self.vehicle.crashed = True total_timestep_punishment_left = min( (self.config["max_timesteps"] + 1 - self.time) * self.config["timestep_reward"], 0, ) if collided_with_curb: return ( self.config["curb_collision_reward"] + total_timestep_punishment_left ) if collided_with_car: return ( self.config["car_collision_reward"] + total_timestep_punishment_left ) # Parking parking_score = self.compute_parking_score() if parking_score < self.config["parking_score_threshold"]: self.vehicle_parked = True return self.config["parking_reward"] # Route-following reward_earned = self.config["timestep_reward"] if self.config["route_following_reward_scalar"] != 0: navigation_observation = None if isinstance(self.observation_type, DictObservation): for obs in self.observation_type.observation_types.values(): if isinstance(obs, NavigationObservation): navigation_observation = obs elif isinstance(self.observation_type, NavigationObservation): navigation_observation = self.observation_type assert ( navigation_observation is not None ), "NavigationObservation must be included as an observation if route_following_reward_scalar is nonzero" waypoint_vector = navigation_observation.waypoint - self.vehicle.position route_following_score = ( self.config["route_following_reward_scalar"] * np.dot(waypoint_vector, self.vehicle.velocity) / np.linalg.norm(waypoint_vector) ) # print("Route following score:", route_following_score) reward_earned += route_following_score return reward_earned def _is_terminated(self) -> bool: """ Termination occurs either by collision or by successfully parking """ return self.vehicle_parked or self.vehicle.crashed def _is_truncated(self) -> bool: return self.time > self.config["max_timesteps"] def _info(self, obs: Observation, action: Action | None = None) -> dict: info = super()._info(obs, action) info["parked"] = self.vehicle_parked return info def compute_parking_score(self, p: float = 0.5) -> float: # We do not use our RelativeGoalObservation to compute reward. # Instead we use something similar to compute_reward in ParkingEnv # Lower parking score = better position_diff = np.linalg.norm( self.vehicle.position - self.vehicle.goal.position ) velocity_diff = np.linalg.norm(self.vehicle.velocity) alignment_penalty = 1 - abs( np.cos(self.vehicle.heading - self.vehicle.goal.heading) ) # 0 when perfectly aligned (forward or backward), 1 when sideways components = np.array( [ position_diff, velocity_diff, alignment_penalty, ] ) weights = np.array(self.config["parking_score_weights"]) return np.power(np.dot(np.abs(components), weights), p) def _make_road(self) -> list[Lane]: if self.config["preloaded_lanes"] is None: try: lanes = generate_random_lanes( self.np_random, self.config["generation_params"] ) except Exception as e: raise RuntimeError( "Fatal error encountered when generating road network." "If this issue persists, try a different seed." f"\n\tOriginal error: {e}" ) from e else: lanes = self.config["preloaded_lanes"] net = PartitionedRoadNetwork( partition_gridsize=self.config["lane_partition_gridsize"] ) for lane in lanes: real_lane = PolyLane( lane_points=lane.points, left_boundary_points=lane.left_points, right_boundary_points=lane.right_points, line_types=(LineType.CONTINUOUS, LineType.CONTINUOUS), ) net.add_lane_bidirectional(lane.start, lane.end, real_lane) self.road = Road(net) return lanes
[docs] def create_parking_spots( self, num_spots: int, spot_width: float, spot_height: float, rng: np.random.Generator, ) -> bool: """ :param num_spots: number of parking spots to generate :param spot_width: width of parking spot [must be less than the lane_width] :param spot_height: length of parking spot [must be less than forward_speed] :param rng: random number generator :return: whether or not there was enough space to generate the specified number of spots """ assert self.road is not None curb_spot_offset = 0.1 # segment_index: {lane_id, side, pt_id (1-(len-2))} segment_indices = [] for lane_id, lane in enumerate(self.lanes): for side in ["left_points", "right_points"]: for pt_id in range(1, len(getattr(lane, side)) - 2): segment_indices.append( {"lane_id": lane_id, "side": side, "pt_id": pt_id} ) rng.shuffle(segment_indices) num_parking_spots = 0 segment_indices_i = 0 while num_parking_spots < num_spots and segment_indices_i < len( segment_indices ): segment_index = segment_indices[segment_indices_i] lane_id = cast(int, segment_index["lane_id"]) side = cast(str, segment_index["side"]) pt_id = cast(int, segment_index["pt_id"]) lane = self.lanes[lane_id] lane_side = getattr(lane, side) pt0 = lane_side[pt_id] pt1 = lane_side[pt_id + 1] # We will attempt to place a parking spot parallel # to our lane segment # Requirement 1: This segment must be long enough # to encompass the parking spot seg_dist = np.linalg.norm(pt0 - pt1) if seg_dist < spot_height: segment_indices_i += 1 continue # Computing geometry for new parking spot vec = pt1 - pt0 vec /= np.linalg.norm(vec) if side == "right_points": perp_vec = np.array([vec[1], -vec[0]]) else: perp_vec = np.array([-vec[1], vec[0]]) center = (pt0 + pt1) / 2 + (perp_vec * (curb_spot_offset + spot_width / 2)) heading = np.atan2(vec[1], vec[0]) new_parking_spot = ParkingSpot(self.road, center, heading) self.road.objects.append(new_parking_spot) num_parking_spots += 1 # Requirement 2: The rectangular parking space should not # intersect with any other lane if self.detect_object_lane_collision(new_parking_spot): self.road.objects.remove(new_parking_spot) num_parking_spots -= 1 segment_indices_i += 1 continue # Requirement 3: The rectangular parking space should not # intersect with any other already existing parking spot collision_detected = False for other_object in self.road.objects: if other_object is not new_parking_spot: collision_detected, _, _ = new_parking_spot._is_colliding( other_object, 0 ) if collision_detected: break if collision_detected: self.road.objects.remove(new_parking_spot) num_parking_spots -= 1 segment_indices_i += 1 continue segment_indices_i += 1 if num_parking_spots < num_spots: print( "INSUFFICIENT SPOTS FOUND;" f" {num_parking_spots} / {num_spots} parking spots generated" ) return False return True
def detect_object_lane_collision(self, obj: RoadObject) -> bool: assert self.road is not None and isinstance( self.road.network, PartitionedRoadNetwork ) gridpoints = set() for pt in obj.polygon(): gridpoints.add(point_to_gridpoint(pt, self.road.network.partition_gridsize)) proximal_lanes = set() for gpt in gridpoints: proximal_lanes.update( get_proximal_lanes_wrt_gridpoint(self.road.network.grid_to_lanes, gpt) ) for lane_index in proximal_lanes: lane = cast(PolyLane, self.road.network.get_lane(lane_index)) left_pairs = zip(lane.left_boundary_points, lane.left_boundary_points[1:]) right_pairs = zip( lane.right_boundary_points, lane.right_boundary_points[1:] ) for p0, p1 in chain(left_pairs, right_pairs): if obj.intersects_with_line(p0, p1): return True return False