From 83c4a326a4a925a22b03a5c4703f288d10570fa0 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Tue, 7 Feb 2023 17:06:25 -0500 Subject: [PATCH 01/28] Create initial implementation with map loading --- smarts/core/argoverse_map.py | 84 ++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 smarts/core/argoverse_map.py diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py new file mode 100644 index 0000000000..b38ea9dc95 --- /dev/null +++ b/smarts/core/argoverse_map.py @@ -0,0 +1,84 @@ +import logging +from pathlib import Path +from typing import Optional +from smarts.core.coordinates import BoundingBox +from smarts.core.road_map import RoadMap +from smarts.sstudio.types import MapSpec + +from av2.map.map_api import ArgoverseStaticMap +import av2.geometry.polyline_utils as polyline_utils +import av2.rendering.vector as vector_plotting_utils + + +class ArgoverseRoadMap(RoadMap): + """A road map for an Argoverse 2 scenario.""" + + def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): + self._log = logging.getLogger(self.__class__.__name__) + self._avm = avm + self._argoverse_scenario_id = avm.log_id + self._map_spec = map_spec + # self._surfaces = dict() + self._lanes = dict() + self._roads = dict() + # self._features = dict() + # self._waypoints_cache = SumoRoadNetwork._WaypointsCache() + # self._lanepoints = None + # if map_spec.lanepoint_spacing is not None: + # assert map_spec.lanepoint_spacing > 0 + # # XXX: this should be last here since LanePoints() calls road_network methods immediately + # self._lanepoints = LanePoints.from_sumo(self, spacing=map_spec.lanepoint_spacing) + + @classmethod + def from_spec(cls, map_spec: MapSpec): + """Generate a road map from the given specification.""" + scenario_dir = Path(map_spec.source) + scenario_id = scenario_dir.stem + map_path = scenario_dir / f"log_map_archive_{scenario_id}.json" + + if not map_path.exists(): + logging.warning(f"Map not found: {map_path}") + return None + + avm = ArgoverseStaticMap.from_json(map_path) + assert avm.log_id == scenario_id, "Loaded map ID does not match expected ID" + return cls(map_spec, avm) + + @property + def source(self) -> str: + """Path to the directory containing the map JSON file.""" + return self._map_spec.source + + @property + def bounding_box(self) -> Optional[BoundingBox]: + """The minimum bounding box that contains the map geometry. May return `None` to indicate + the map is unbounded. + """ + raise NotImplementedError() + + def is_same_map(self, map_spec) -> bool: + """Check if the MapSpec Object source points to the same RoadMap instance as the current""" + raise NotImplementedError + + def to_glb(self, glb_dir: str): + """Build a glb file for camera rendering and envision""" + raise NotImplementedError() + + def lane_by_id(self, lane_id: str) -> RoadMap.Lane: + lane = self._lanes.get(lane_id) + assert lane, f"ArgoverseMap got request for unknown lane_id: '{lane_id}'" + return lane + + def road_by_id(self, road_id: str) -> RoadMap.Road: + road = self._roads.get(road_id) + assert road, f"ArgoverseMap got request for unknown road_id: '{road_id}'" + return road + + +if __name__ == "__main__": + dataset_dir = "/home/saul/argoverse/train" + scenario_id = "0000b0f9-99f9-4a1f-a231-5be9e4c523f7" + source = str(Path(dataset_dir) / scenario_id) + spec = MapSpec(source=source) + map = ArgoverseRoadMap.from_spec(spec) + assert map.source == source From f92f68886652967a6c5ed3eb1a781bb8bf7d8a2e Mon Sep 17 00:00:00 2001 From: Saul Field Date: Wed, 8 Feb 2023 15:17:58 -0500 Subject: [PATCH 02/28] Map GLB generation --- scenarios/argoverse/scenario.py | 22 +++++ setup.cfg | 2 + smarts/core/argoverse_map.py | 148 ++++++++++++++++++++++++----- smarts/core/default_map_builder.py | 20 +++- smarts/core/utils/geometry.py | 56 +---------- smarts/core/utils/glb.py | 119 +++++++++++++++++++++++ 6 files changed, 282 insertions(+), 85 deletions(-) create mode 100644 scenarios/argoverse/scenario.py create mode 100644 smarts/core/utils/glb.py diff --git a/scenarios/argoverse/scenario.py b/scenarios/argoverse/scenario.py new file mode 100644 index 0000000000..3826776f12 --- /dev/null +++ b/scenarios/argoverse/scenario.py @@ -0,0 +1,22 @@ +from pathlib import Path + +from smarts.sstudio import gen_scenario +from smarts.sstudio import types as t + +dataset_path = None + +# traffic_histories = [ +# t.TrafficHistoryDataset( +# name=f"Argoverse", +# source_type="Argoverse", +# input_path=dataset_path, +# ) +# ] + +gen_scenario( + t.Scenario( + map_spec=t.MapSpec(source=f"{dataset_path}"), + # traffic_histories=traffic_histories, + ), + output_dir=Path(__file__).parent, +) diff --git a/setup.cfg b/setup.cfg index 7999cb55bd..4277d06b7d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -117,6 +117,8 @@ waymo = waymo-open-dataset-tf-2-4-0 opendrive = opendrive2lanelet>=1.2.1 +argoverse = + av2>=0.2.1 [aliases] test=pytest \ No newline at end of file diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index b38ea9dc95..f50c5398f1 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -1,16 +1,24 @@ +from functools import lru_cache import logging +import math from pathlib import Path -from typing import Optional -from smarts.core.coordinates import BoundingBox +from cached_property import cached_property +import time +from typing import Dict, List, Optional, Sequence, Tuple + +import numpy as np + +from smarts.core.coordinates import BoundingBox, Point, Pose from smarts.core.road_map import RoadMap +from smarts.core.utils.glb import make_map_glb from smarts.sstudio.types import MapSpec - from av2.map.map_api import ArgoverseStaticMap import av2.geometry.polyline_utils as polyline_utils import av2.rendering.vector as vector_plotting_utils +from shapely.geometry import Polygon -class ArgoverseRoadMap(RoadMap): +class ArgoverseMap(RoadMap): """A road map for an Argoverse 2 scenario.""" def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): @@ -18,10 +26,11 @@ def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): self._avm = avm self._argoverse_scenario_id = avm.log_id self._map_spec = map_spec - # self._surfaces = dict() - self._lanes = dict() - self._roads = dict() - # self._features = dict() + self._surfaces = dict() + self._lanes: Dict[str, ArgoverseMap.Lane] = dict() + self._roads: Dict[str, ArgoverseMap.Road] = dict() + self._features = dict() + self._load_map_data() # self._waypoints_cache = SumoRoadNetwork._WaypointsCache() # self._lanepoints = None # if map_spec.lanepoint_spacing is not None: @@ -44,41 +53,128 @@ def from_spec(cls, map_spec: MapSpec): assert avm.log_id == scenario_id, "Loaded map ID does not match expected ID" return cls(map_spec, avm) + def _load_map_data(self): + start = time.time() + + for lane_seg in self._avm.get_scenario_lane_segments(): + road_id = f"road-{lane_seg.id}" + lane_id = f"lane-{lane_seg.id}" + road = ArgoverseMap.Road(road_id) + lane = ArgoverseMap.Lane(lane_id, road, lane_seg.polygon_boundary[:, :2]) + self._roads[road_id] = road + self._lanes[lane_id] = lane + + end = time.time() + elapsed = round((end - start) * 1000.0, 3) + self._log.info(f"Loading Argoverse map took: {elapsed} ms") + @property def source(self) -> str: """Path to the directory containing the map JSON file.""" return self._map_spec.source - @property + @cached_property def bounding_box(self) -> Optional[BoundingBox]: - """The minimum bounding box that contains the map geometry. May return `None` to indicate - the map is unbounded. - """ - raise NotImplementedError() + xs, ys = np.array([]), np.array([]) + for lane_seg in self._avm.get_scenario_lane_segments(): + xs = np.concatenate((xs, lane_seg.polygon_boundary[:, 0])) + ys = np.concatenate((ys, lane_seg.polygon_boundary[:, 1])) + + return BoundingBox( + min_pt=Point(x=np.min(xs), y=np.min(ys)), + max_pt=Point(x=np.max(xs), y=np.max(ys)), + ) def is_same_map(self, map_spec) -> bool: """Check if the MapSpec Object source points to the same RoadMap instance as the current""" - raise NotImplementedError - - def to_glb(self, glb_dir: str): - """Build a glb file for camera rendering and envision""" raise NotImplementedError() + def _compute_lane_polygons(self): + polygons = [] + for lane_id, lane in self._lanes.items(): + metadata = { + "road_id": lane.road.road_id, + "lane_id": lane_id, + # "lane_index": lane.index, TODO + } + polygons.append((lane.shape(), metadata)) + return polygons + + def to_glb(self, glb_dir): + polygons = self._compute_lane_polygons() + # lane_dividers, edge_dividers = self._compute_traffic_dividers() + map_glb = make_map_glb(polygons, self.bounding_box, [], []) + map_glb.write_glb(Path(glb_dir) / "map.glb") + + # road_lines_glb = self._make_road_line_glb(edge_dividers) + # road_lines_glb.write_glb(Path(glb_dir) / "road_lines.glb") + + # lane_lines_glb = self._make_road_line_glb(lane_dividers) + # lane_lines_glb.write_glb(Path(glb_dir) / "lane_lines.glb") + + class Surface(RoadMap.Surface): + def __init__(self, surface_id: str): + self._surface_id = surface_id + + @property + def surface_id(self) -> str: + return self._surface_id + + @property + def is_drivable(self) -> bool: + return True + + def surface_by_id(self, surface_id: str) -> RoadMap.Surface: + return self._surfaces.get(surface_id) + + class Lane(RoadMap.Lane, Surface): + def __init__(self, lane_id: str, road: RoadMap.Road, polygon): + super().__init__(lane_id) + self._lane_id = lane_id + self._road = road + self._polygon = polygon + + def __hash__(self) -> int: + return hash(self.lane_id) + + @property + def lane_id(self) -> str: + return self._lane_id + + @property + def road(self) -> RoadMap.Road: + return self._road + + @cached_property + def speed_limit(self) -> Optional[float]: + raise NotImplementedError() + + @cached_property + def length(self) -> float: + raise NotImplementedError() + + @lru_cache(maxsize=4) + def shape(self, buffer_width: float = 0.0, default_width: Optional[float] = None) -> Polygon: + return Polygon(self._polygon) + def lane_by_id(self, lane_id: str) -> RoadMap.Lane: lane = self._lanes.get(lane_id) assert lane, f"ArgoverseMap got request for unknown lane_id: '{lane_id}'" return lane + class Road(RoadMap.Road, Surface): + def __init__(self, road_id: str): + super().__init__(road_id) + self._road_id = road_id + + def __hash__(self) -> int: + return hash(self.road_id) + + @property + def road_id(self) -> str: + return self._road_id + def road_by_id(self, road_id: str) -> RoadMap.Road: road = self._roads.get(road_id) assert road, f"ArgoverseMap got request for unknown road_id: '{road_id}'" return road - - -if __name__ == "__main__": - dataset_dir = "/home/saul/argoverse/train" - scenario_id = "0000b0f9-99f9-4a1f-a231-5be9e4c523f7" - source = str(Path(dataset_dir) / scenario_id) - spec = MapSpec(source=source) - map = ArgoverseRoadMap.from_spec(spec) - assert map.source == source diff --git a/smarts/core/default_map_builder.py b/smarts/core/default_map_builder.py index 20f785ff9e..dba71d6aa3 100644 --- a/smarts/core/default_map_builder.py +++ b/smarts/core/default_map_builder.py @@ -55,6 +55,7 @@ def _clear_cache(): _SUMO_MAP = 1 _OPENDRIVE_MAP = 2 _WAYMO_MAP = 3 +_ARGOVERSE_MAP = 4 def find_mapfile_in_dir(map_dir: str) -> Tuple[int, str]: @@ -78,6 +79,9 @@ def find_mapfile_in_dir(map_dir: str) -> Tuple[int, str]: elif ".tfrecord" in f: map_type = _WAYMO_MAP map_path = os.path.join(map_dir, f) + elif "log_map_archive" in f: + map_type = _ARGOVERSE_MAP + map_path = os.path.join(map_dir, f) return map_type, map_path @@ -132,17 +136,25 @@ def get_road_map(map_spec) -> Tuple[Optional[RoadMap], Optional[str]]: except (ImportError, ModuleNotFoundError): print(sys.exc_info()) print( - "You may not have installed the [waymo] dependencies required to build and use WaymoMap Scenario. Install them first using the command `pip install -e .[waymo]` at the source directory." + "You may not have installed the [waymo] dependencies required to build and use WaymoMap scenarios. Install them first using the command `pip install -e .[waymo]` at the source directory." ) return None, None map_class = WaymoMap + elif map_type == _ARGOVERSE_MAP: + try: + from smarts.core.argoverse_map import ArgoverseMap # pytype: disable=import-error + except (ImportError, ModuleNotFoundError): + print(sys.exc_info()) + print( + "You may not have installed the [argoverse] dependencies required to build and use Argoverse scenarios. Install them first using the command `pip install -e .[argoverse]` at the source directory." + ) + return None, None + map_class = ArgoverseMap else: return None, None if _existing_map: - if isinstance(_existing_map.obj, map_class) and _existing_map.obj.is_same_map( - map_spec - ): + if isinstance(_existing_map.obj, map_class) and _existing_map.obj.is_same_map(map_spec): return _existing_map.obj, _existing_map.map_hash _clear_cache() diff --git a/smarts/core/utils/geometry.py b/smarts/core/utils/geometry.py index b254b1a1d1..96fd573450 100644 --- a/smarts/core/utils/geometry.py +++ b/smarts/core/utils/geometry.py @@ -18,11 +18,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import math -from typing import Any, Dict, List, Tuple - -import numpy as np -import trimesh from shapely.geometry import LineString, MultiPolygon, Polygon from shapely.geometry.base import CAP_STYLE, JOIN_STYLE from shapely.ops import triangulate @@ -48,53 +43,4 @@ def buffered_shape(shape, width: float = 1.0) -> Polygon: def triangulate_polygon(polygon: Polygon): """Attempts to convert a polygon into triangles.""" # XXX: shapely.ops.triangulate current creates a convex fill of triangles. - return [ - tri_face - for tri_face in triangulate(polygon) - if tri_face.centroid.within(polygon) - ] - - -def generate_meshes_from_polygons( - polygons: List[Tuple[Polygon, Dict[str, Any]]] -) -> List[trimesh.Trimesh]: - """Creates a mesh out of a list of polygons.""" - meshes = [] - - # Trimesh's API require a list of vertices and a list of faces, where each - # face contains three indexes into the vertices list. Ideally, the vertices - # are all unique and the faces list references the same indexes as needed. - # TODO: Batch the polygon processing. - for poly, metadata in polygons: - vertices, faces = [], [] - point_dict = dict() - current_point_index = 0 - - # Collect all the points on the shape to reduce checks by 3 times - for x, y in poly.exterior.coords: - p = (x, y, 0) - if p not in point_dict: - vertices.append(p) - point_dict[p] = current_point_index - current_point_index += 1 - triangles = triangulate_polygon(poly) - for triangle in triangles: - face = np.array( - [point_dict.get((x, y, 0), -1) for x, y in triangle.exterior.coords] - ) - # Add face if not invalid - if -1 not in face: - faces.append(face) - - if not vertices or not faces: - continue - - mesh = trimesh.Trimesh(vertices=vertices, faces=faces, metadata=metadata) - - # Trimesh doesn't support a coordinate-system="z-up" configuration, so we - # have to apply the transformation manually. - mesh.apply_transform( - trimesh.transformations.rotation_matrix(math.pi / 2, [-1, 0, 0]) - ) - meshes.append(mesh) - return meshes + return [tri_face for tri_face in triangulate(polygon) if tri_face.centroid.within(polygon)] diff --git a/smarts/core/utils/glb.py b/smarts/core/utils/glb.py new file mode 100644 index 0000000000..7b2d51ebc5 --- /dev/null +++ b/smarts/core/utils/glb.py @@ -0,0 +1,119 @@ +import numpy as np +from trimesh.exchange import gltf +import math +from typing import Any, Dict, List, Tuple + +import numpy as np +import trimesh +from shapely.geometry import Polygon +from smarts.core.coordinates import BoundingBox + +from smarts.core.utils.geometry import triangulate_polygon + + +def _convert_camera(camera): + result = { + "name": camera.name, + "type": "perspective", + "perspective": { + "aspectRatio": camera.fov[0] / camera.fov[1], + "yfov": np.radians(camera.fov[1]), + "znear": float(camera.z_near), + # HACK: The trimesh gltf export doesn't include a zfar which Panda3D GLB + # loader expects. Here we override to make loading possible. + "zfar": float(camera.z_near + 100), + }, + } + return result + + +gltf._convert_camera = _convert_camera + + +class GLBData: + """Convenience class for writing GLB files.""" + + def __init__(self, bytes_): + self._bytes = bytes_ + + def write_glb(self, output_path: str): + """Generate a geometry file.""" + with open(output_path, "wb") as f: + f.write(self._bytes) + + +def _generate_meshes_from_polygons(polygons: List[Tuple[Polygon, Dict[str, Any]]]) -> List[trimesh.Trimesh]: + """Creates a mesh out of a list of polygons.""" + meshes = [] + + # Trimesh's API require a list of vertices and a list of faces, where each + # face contains three indexes into the vertices list. Ideally, the vertices + # are all unique and the faces list references the same indexes as needed. + # TODO: Batch the polygon processing. + for poly, metadata in polygons: + vertices, faces = [], [] + point_dict = dict() + current_point_index = 0 + + # Collect all the points on the shape to reduce checks by 3 times + for x, y in poly.exterior.coords: + p = (x, y, 0) + if p not in point_dict: + vertices.append(p) + point_dict[p] = current_point_index + current_point_index += 1 + triangles = triangulate_polygon(poly) + for triangle in triangles: + face = np.array([point_dict.get((x, y, 0), -1) for x, y in triangle.exterior.coords]) + # Add face if not invalid + if -1 not in face: + faces.append(face) + + if not vertices or not faces: + continue + + mesh = trimesh.Trimesh(vertices=vertices, faces=faces, metadata=metadata) + + # Trimesh doesn't support a coordinate-system="z-up" configuration, so we + # have to apply the transformation manually. + mesh.apply_transform(trimesh.transformations.rotation_matrix(math.pi / 2, [-1, 0, 0])) + meshes.append(mesh) + return meshes + + +def make_map_glb(polygons, bbox: BoundingBox, lane_dividers, edge_dividers) -> GLBData: + scene = trimesh.Scene() + + # Attach additional information for rendering as metadata in the map glb + metadata = { + "bounding_box": ( + bbox.min_pt.x, + bbox.min_pt.y, + bbox.max_pt.x, + bbox.max_pt.y, + ), + "lane_dividers": lane_dividers, + "edge_dividers": edge_dividers, + } + + meshes = _generate_meshes_from_polygons(polygons) + for mesh in meshes: + mesh.visual = trimesh.visual.TextureVisuals(material=trimesh.visual.material.PBRMaterial()) + + road_id = mesh.metadata["road_id"] + lane_id = mesh.metadata.get("lane_id") + name = f"{road_id}" + if lane_id is not None: + name += f"-{lane_id}" + scene.add_geometry(mesh, name, extras=mesh.metadata) + return GLBData(gltf.export_glb(scene, extras=metadata, include_normals=True)) + + +def make_road_line_glb(lines: List[List[Tuple[float, float]]]) -> GLBData: + scene = trimesh.Scene() + for line_pts in lines: + vertices = [(*pt, 0.1) for pt in line_pts] + point_cloud = trimesh.PointCloud(vertices=vertices) + point_cloud.apply_transform(trimesh.transformations.rotation_matrix(math.pi / 2, [-1, 0, 0])) + scene.add_geometry(point_cloud) + return GLBData(gltf.export_glb(scene)) From e13b998c37da83bbcf1d9df639202d0766200f3c Mon Sep 17 00:00:00 2001 From: Saul Field Date: Fri, 10 Feb 2023 15:02:07 -0500 Subject: [PATCH 03/28] Infer road lanes --- smarts/core/argoverse_map.py | 187 +++++++++++++++++++++++++++++++++-- smarts/core/road_map.py | 58 +++-------- 2 files changed, 194 insertions(+), 51 deletions(-) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index f50c5398f1..d2bf45456a 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -13,6 +13,7 @@ from smarts.core.utils.glb import make_map_glb from smarts.sstudio.types import MapSpec from av2.map.map_api import ArgoverseStaticMap +from av2.map.lane_segment import LaneMarkType import av2.geometry.polyline_utils as polyline_utils import av2.rendering.vector as vector_plotting_utils from shapely.geometry import Polygon @@ -23,6 +24,7 @@ class ArgoverseMap(RoadMap): def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): self._log = logging.getLogger(self.__class__.__name__) + self._log.setLevel(logging.INFO) self._avm = avm self._argoverse_scenario_id = avm.log_id self._map_spec = map_spec @@ -56,11 +58,55 @@ def from_spec(cls, map_spec: MapSpec): def _load_map_data(self): start = time.time() + all_ids = set(self._avm.get_scenario_lane_segment_ids()) + processed_ids = set() for lane_seg in self._avm.get_scenario_lane_segments(): + # If this is a rightmost lane, create a road with its neighbours + if lane_seg.right_neighbor_id is None: + neighbours: List[int] = [] + cur_seg = lane_seg + while True: + left = cur_seg.left_lane_marking.mark_type + if cur_seg.left_neighbor_id is not None and left == LaneMarkType.DASHED_WHITE: + # There is a valid lane to the left, so add it and continue + neighbours.append(cur_seg.left_neighbor_id) + cur_seg = self._avm.vector_lane_segments[cur_seg.left_neighbor_id] + else: + break # This is the leftmost lane in the road, so stop + + # Create the lane objects + road_id = "road" + lanes = [] + for index, seg_id in enumerate([lane_seg.id] + neighbours): + road_id += f"-{seg_id}" + lane_id = f"lane-{seg_id}" + seg = self._avm.vector_lane_segments[seg_id] + lane = ArgoverseMap.Lane(lane_id, seg.polygon_boundary[:, :2], index) + assert lane_id not in self._lanes + self._lanes[lane_id] = lane + processed_ids.add(seg_id) + lanes.append(lane) + + # Create the road and fill in references + road = ArgoverseMap.Road(road_id, lanes) + assert road_id not in self._roads + self._roads[road_id] = road + for lane in lanes: + lane._road = road + + # Create lanes for the remaining lane segments, each with their own road + remaining_ids = all_ids - processed_ids + for seg_id in remaining_ids: + lane_seg = self._avm.vector_lane_segments[seg_id] road_id = f"road-{lane_seg.id}" lane_id = f"lane-{lane_seg.id}" - road = ArgoverseMap.Road(road_id) - lane = ArgoverseMap.Lane(lane_id, road, lane_seg.polygon_boundary[:, :2]) + + lane = ArgoverseMap.Lane(lane_id, lane_seg.polygon_boundary[:, :2], 0) + road = ArgoverseMap.Road(road_id, [lane]) + lane._road = road + + assert road_id not in self._roads + assert lane_id not in self._lanes self._roads[road_id] = road self._lanes[lane_id] = lane @@ -103,6 +149,7 @@ def _compute_lane_polygons(self): def to_glb(self, glb_dir): polygons = self._compute_lane_polygons() # lane_dividers, edge_dividers = self._compute_traffic_dividers() + map_glb = make_map_glb(polygons, self.bounding_box, [], []) map_glb.write_glb(Path(glb_dir) / "map.glb") @@ -128,11 +175,12 @@ def surface_by_id(self, surface_id: str) -> RoadMap.Surface: return self._surfaces.get(surface_id) class Lane(RoadMap.Lane, Surface): - def __init__(self, lane_id: str, road: RoadMap.Road, polygon): + def __init__(self, lane_id: str, polygon, index: int): super().__init__(lane_id) self._lane_id = lane_id - self._road = road self._polygon = polygon + self._index = index + self._road = None def __hash__(self) -> int: return hash(self.lane_id) @@ -145,12 +193,72 @@ def lane_id(self) -> str: def road(self) -> RoadMap.Road: return self._road - @cached_property + @property def speed_limit(self) -> Optional[float]: - raise NotImplementedError() + return None - @cached_property + @property def length(self) -> float: + """The length of this lane.""" + raise NotImplementedError() + + @property + def in_junction(self) -> bool: + """If this lane is a part of a junction (usually an intersection.)""" + raise NotImplementedError() + + @property + def index(self) -> int: + """when not in_junction, 0 is outer / right-most (relative to lane heading) lane on road. + otherwise, index scheme is implementation-dependent, but must be deterministic.""" + # TAI: UK roads + raise NotImplementedError() + + @property + def lanes_in_same_direction(self) -> List[RoadMap.Lane]: + """returns all other lanes on this road where traffic goes + in the same direction. it is currently assumed these will be + adjacent to one another. In junctions, diverging lanes + should not be included.""" + raise NotImplementedError() + + @property + def lane_to_left(self) -> Tuple[RoadMap.Lane, bool]: + """Note: left is defined as 90 degrees clockwise relative to the lane heading. + (I.e., positive `t` in the RefLine coordinate system.) + Second result is True if lane is in the same direction as this one + In junctions, diverging lanes should not be included.""" + raise NotImplementedError() + + @property + def lane_to_right(self) -> Tuple[RoadMap.Lane, bool]: + """Note: right is defined as 90 degrees counter-clockwise relative to the lane heading. + (I.e., negative `t` in the RefLine coordinate system.) + Second result is True if lane is in the same direction as this one. + In junctions, diverging lanes should not be included.""" + raise NotImplementedError() + + @property + def incoming_lanes(self) -> List[RoadMap.Lane]: + """Lanes leading into this lane.""" + raise NotImplementedError() + + @property + def outgoing_lanes(self) -> List[RoadMap.Lane]: + """Lanes leading out of this lane.""" + raise NotImplementedError() + + def oncoming_lanes_at_offset(self, offset: float) -> List[RoadMap.Lane]: + """Returns a list of nearby lanes at offset that are (roughly) + parallel to this one but go in the opposite direction.""" + raise NotImplementedError() + + @property + def foes(self) -> List[RoadMap.Lane]: + """All lanes that in some ways intersect with (cross) this one, + including those that have the same outgoing lane as this one, + and so might require right-of-way rules. This should only + ever happen in junctions.""" raise NotImplementedError() @lru_cache(maxsize=4) @@ -163,9 +271,10 @@ def lane_by_id(self, lane_id: str) -> RoadMap.Lane: return lane class Road(RoadMap.Road, Surface): - def __init__(self, road_id: str): + def __init__(self, road_id: str, lanes: List[RoadMap.Lane]): super().__init__(road_id) self._road_id = road_id + self._lanes = lanes def __hash__(self) -> int: return hash(self.road_id) @@ -174,6 +283,68 @@ def __hash__(self) -> int: def road_id(self) -> str: return self._road_id + @property + def type(self) -> int: + """The type of this road.""" + raise NotImplementedError() + + @property + def type_as_str(self) -> str: + """The type of this road.""" + raise NotImplementedError() + + @property + def composite_road(self) -> RoadMap.Road: + """Return an abstract Road composed of one or more RoadMap.Road segments + (including this one) that has been inferred to correspond to one continuous + real-world road. May return same object as self.""" + return self + + @property + def is_composite(self) -> bool: + """Returns True if this Road object was inferred + and composed out of subordinate Road objects.""" + return False + + @property + def is_junction(self) -> bool: + """Note that a junction can be an intersection ('+') or a 'T', 'Y', 'L', etc.""" + raise NotImplementedError() + + @property + def length(self) -> float: + """The length of this road.""" + raise NotImplementedError() + + @property + def incoming_roads(self) -> List[RoadMap.Road]: + """All roads that lead into this road.""" + raise NotImplementedError() + + @property + def outgoing_roads(self) -> List[RoadMap.Road]: + """All roads that lead out of this road.""" + raise NotImplementedError() + + def oncoming_roads_at_point(self, point: Point) -> List[RoadMap.Road]: + """Returns a list of nearby roads to point that are (roughly) + parallel to this one but have lanes that go in the opposite direction.""" + raise NotImplementedError() + + @property + def parallel_roads(self) -> List[RoadMap.Road]: + """Returns roads that start and end at the same + point as this one.""" + raise NotImplementedError() + + @property + def lanes(self) -> List[RoadMap.Lane]: + return self._lanes + + def lane_at_index(self, index: int) -> RoadMap.Lane: + """Gets the lane with the given index.""" + raise NotImplementedError() + def road_by_id(self, road_id: str) -> RoadMap.Road: road = self._roads.get(road_id) assert road, f"ArgoverseMap got request for unknown road_id: '{road_id}'" diff --git a/smarts/core/road_map.py b/smarts/core/road_map.py index 37867c3d4d..d306c53e30 100644 --- a/smarts/core/road_map.py +++ b/smarts/core/road_map.py @@ -100,9 +100,7 @@ def feature_by_id(self, feature_id: str) -> RoadMap.Feature: """Find a feature in this road map that has the given identifier.""" raise NotImplementedError() - def dynamic_features_near( - self, point: Point, radius: float - ) -> List[Tuple[RoadMap.Feature, float]]: + def dynamic_features_near(self, point: Point, radius: float) -> List[Tuple[RoadMap.Feature, float]]: """Find features within radius meters of the given point.""" result = [] for feat in self.dynamic_features: @@ -111,9 +109,7 @@ def dynamic_features_near( result.append((feat, dist)) return result - def nearest_surfaces( - self, point: Point, radius: Optional[float] = None - ) -> List[Tuple[RoadMap.Surface, float]]: + def nearest_surfaces(self, point: Point, radius: Optional[float] = None) -> List[Tuple[RoadMap.Surface, float]]: """Find surfaces (lanes, roads, etc.) on this road map that are near the given point.""" raise NotImplementedError() @@ -124,9 +120,7 @@ def nearest_lanes( Returns a list of tuples of lane and distance, sorted by distance.""" raise NotImplementedError() - def nearest_lane( - self, point: Point, radius: Optional[float] = None, include_junctions=True - ) -> RoadMap.Lane: + def nearest_lane(self, point: Point, radius: Optional[float] = None, include_junctions=True) -> RoadMap.Lane: """Find the nearest lane on this road map to the given point.""" nearest_lanes = self.nearest_lanes(point, radius, include_junctions) return nearest_lanes[0][0] if nearest_lanes else None @@ -227,9 +221,7 @@ def features_near(self, pose: Pose, radius: float) -> List[RoadMap.Feature]: """The features on this surface near the given pose.""" raise NotImplementedError() - def shape( - self, buffer_width: float = 0.0, default_width: Optional[float] = None - ) -> Polygon: + def shape(self, buffer_width: float = 0.0, default_width: Optional[float] = None) -> Polygon: """Returns a convex polygon representing this surface, buffered by buffered_width (which must be non-negative), where buffer_width is a buffer around the perimeter of the polygon. In some situations, it may be desirable to also specify a `default_width`, in which case the returned polygon should have a convex shape where the @@ -373,9 +365,7 @@ def width_at_offset(self, offset: float) -> Tuple[float, float]: a width estimate with no confidence.""" raise NotImplementedError() - def project_along( - self, start_offset: float, distance: float - ) -> Set[Tuple[RoadMap.Lane, float]]: + def project_along(self, start_offset: float, distance: float) -> Set[Tuple[RoadMap.Lane, float]]: """Starting at start_offset along the lane, project locations (lane, offset tuples) reachable within distance, not including lane changes.""" result = set() @@ -442,9 +432,7 @@ def center_pose_at_point(self, point: Point) -> Pose: orientation = fast_quaternion_from_angle(vec_to_radians(desired_vector[:2])) return Pose(position=position, orientation=orientation) - def curvature_radius_at_offset( - self, offset: float, lookahead: int = 5 - ) -> float: + def curvature_radius_at_offset(self, offset: float, lookahead: int = 5) -> float: """lookahead (in meters) is the size of the window to use to compute the curvature, which must be at least 1 to make sense. This may return math.inf if the lane is straight.""" @@ -462,9 +450,7 @@ def curvature_radius_at_offset( heading_rad = vec_to_radians(vec[:2]) if prev_heading_rad is not None: # XXX: things like S curves can cancel out here - heading_deltas += min_angles_difference_signed( - heading_rad, prev_heading_rad - ) + heading_deltas += min_angles_difference_signed(heading_rad, prev_heading_rad) prev_heading_rad = heading_rad return i / heading_deltas if heading_deltas else math.inf @@ -676,25 +662,19 @@ def distance_between(self, start: RoutePoint, end: RoutePoint) -> float: """Distance along route between two points.""" raise NotImplementedError() - def project_along( - self, start: RoutePoint, distance: float - ) -> Set[Tuple[RoadMap.Lane, float]]: + def project_along(self, start: RoutePoint, distance: float) -> Set[Tuple[RoadMap.Lane, float]]: """Starting at point on the route, returns a set of possible locations (lane and offset pairs) further along the route that are distance away, not including lane changes.""" raise NotImplementedError() - def distance_from( - self, cur_lane: RouteLane, route_road: Optional[RoadMap.Road] = None - ) -> Optional[float]: + def distance_from(self, cur_lane: RouteLane, route_road: Optional[RoadMap.Road] = None) -> Optional[float]: """Returns the distance along the route from the beginning of the current lane to the beginning of the next occurrence of route_road, or if route_road is None, then to the end of the route.""" raise NotImplementedError() - def next_junction( - self, cur_lane: RouteLane, offset: float - ) -> Optional[Tuple[RoadMap.Lane, float]]: + def next_junction(self, cur_lane: RouteLane, offset: float) -> Optional[Tuple[RoadMap.Lane, float]]: """Returns a lane within the next junction along the route from beginning of the current lane to the returned lane it connects with in the junction, and the distance to it from this offset, or (None, inf) if there aren't any.""" @@ -756,9 +736,7 @@ def relative_heading(self, h: Heading) -> Heading: Returns: float: Relative heading in [-pi, pi]. """ - assert isinstance( - h, Heading - ), "Heading h ({}) must be an instance of smarts.core.coordinates.Heading".format( + assert isinstance(h, Heading), "Heading h ({}) must be an instance of smarts.core.coordinates.Heading".format( type(h) ) return self.heading.relative_to(h) @@ -821,9 +799,7 @@ def _normal_at_offset(self, offset: float) -> np.ndarray: @lru_cache(maxsize=1024) def to_lane_coord(self, world_point: Point) -> RefLinePoint: lc = RefLinePoint(s=self.offset_along_lane(world_point)) - offcenter_vector = ( - world_point.as_np_array - self.from_lane_coord(lc).as_np_array - ) + offcenter_vector = world_point.as_np_array - self.from_lane_coord(lc).as_np_array t_sign = np.sign(np.dot(offcenter_vector, self._normal_at_offset(lc.s))) return lc._replace(t=np.linalg.norm(offcenter_vector) * t_sign) @@ -867,10 +843,8 @@ def from_lane_coord(self, lane_pt: RefLinePoint) -> Point: """For a reference-line point in/along this segment, converts it to a world point.""" offset = lane_pt.s - self.offset return Point( - self.x - + (offset * self.dx - lane_pt.t * self.dy) / self.dist_to_next, - self.y - + (offset * self.dy + lane_pt.t * self.dx) / self.dist_to_next, + self.x + (offset * self.dx - lane_pt.t * self.dy) / self.dist_to_next, + self.y + (offset * self.dy + lane_pt.t * self.dx) / self.dist_to_next, ) class _OffsetWrapper: @@ -907,9 +881,7 @@ def segment_for_offset( segi -= 1 return segs[segi] - def _cache_lane_info( - self, lane: RoadMapWithCaches.Lane - ) -> List[RoadMapWithCaches._SegmentCache.Segment]: + def _cache_lane_info(self, lane: RoadMapWithCaches.Lane) -> List[RoadMapWithCaches._SegmentCache.Segment]: segs = self._lane_cache.get(lane.lane_id) if segs is not None: return segs From 493deb3eff55e60a6e1ae5f886aae209feec522b Mon Sep 17 00:00:00 2001 From: Saul Field Date: Tue, 14 Feb 2023 13:55:05 -0500 Subject: [PATCH 04/28] Lane functions --- smarts/core/argoverse_map.py | 177 +++++++++++++++++++++++------------ 1 file changed, 117 insertions(+), 60 deletions(-) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index d2bf45456a..fe4ab2d4de 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -8,12 +8,12 @@ import numpy as np -from smarts.core.coordinates import BoundingBox, Point, Pose -from smarts.core.road_map import RoadMap +from smarts.core.coordinates import BoundingBox, Point, Pose, RefLinePoint +from smarts.core.road_map import RoadMap, Waypoint from smarts.core.utils.glb import make_map_glb from smarts.sstudio.types import MapSpec from av2.map.map_api import ArgoverseStaticMap -from av2.map.lane_segment import LaneMarkType +from av2.map.lane_segment import LaneMarkType, LaneSegment import av2.geometry.polyline_utils as polyline_utils import av2.rendering.vector as vector_plotting_utils from shapely.geometry import Polygon @@ -67,10 +67,15 @@ def _load_map_data(self): cur_seg = lane_seg while True: left = cur_seg.left_lane_marking.mark_type - if cur_seg.left_neighbor_id is not None and left == LaneMarkType.DASHED_WHITE: + if ( + cur_seg.left_neighbor_id is not None + and left == LaneMarkType.DASHED_WHITE + ): # There is a valid lane to the left, so add it and continue neighbours.append(cur_seg.left_neighbor_id) - cur_seg = self._avm.vector_lane_segments[cur_seg.left_neighbor_id] + cur_seg = self._avm.vector_lane_segments[ + cur_seg.left_neighbor_id + ] else: break # This is the leftmost lane in the road, so stop @@ -81,7 +86,7 @@ def _load_map_data(self): road_id += f"-{seg_id}" lane_id = f"lane-{seg_id}" seg = self._avm.vector_lane_segments[seg_id] - lane = ArgoverseMap.Lane(lane_id, seg.polygon_boundary[:, :2], index) + lane = ArgoverseMap.Lane(self, lane_id, seg, index) assert lane_id not in self._lanes self._lanes[lane_id] = lane processed_ids.add(seg_id) @@ -101,7 +106,7 @@ def _load_map_data(self): road_id = f"road-{lane_seg.id}" lane_id = f"lane-{lane_seg.id}" - lane = ArgoverseMap.Lane(lane_id, lane_seg.polygon_boundary[:, :2], 0) + lane = ArgoverseMap.Lane(self, lane_id, lane_seg, 0) road = ArgoverseMap.Road(road_id, [lane]) lane._road = road @@ -110,6 +115,19 @@ def _load_map_data(self): self._roads[road_id] = road self._lanes[lane_id] = lane + # Patch in incoming/outgoing lanes now that all lanes have been created + for lane in self._lanes.values(): + lane._incoming_lanes = [ + self.lane_by_id(f"lane-{seg_id}") + for seg_id in lane.lane_seg.predecessors + if seg_id in all_ids + ] + lane._outgoing_lanes = [ + self.lane_by_id(f"lane-{seg_id}") + for seg_id in lane.lane_seg.successors + if seg_id in all_ids + ] + end = time.time() elapsed = round((end - start) * 1000.0, 3) self._log.info(f"Loading Argoverse map took: {elapsed} ms") @@ -132,10 +150,9 @@ def bounding_box(self) -> Optional[BoundingBox]: ) def is_same_map(self, map_spec) -> bool: - """Check if the MapSpec Object source points to the same RoadMap instance as the current""" - raise NotImplementedError() + return map_spec.source == self._map_spec.source - def _compute_lane_polygons(self): + def to_glb(self, glb_dir): polygons = [] for lane_id, lane in self._lanes.items(): metadata = { @@ -144,21 +161,18 @@ def _compute_lane_polygons(self): # "lane_index": lane.index, TODO } polygons.append((lane.shape(), metadata)) - return polygons - def to_glb(self, glb_dir): - polygons = self._compute_lane_polygons() # lane_dividers, edge_dividers = self._compute_traffic_dividers() - map_glb = make_map_glb(polygons, self.bounding_box, [], []) - map_glb.write_glb(Path(glb_dir) / "map.glb") - # road_lines_glb = self._make_road_line_glb(edge_dividers) # road_lines_glb.write_glb(Path(glb_dir) / "road_lines.glb") # lane_lines_glb = self._make_road_line_glb(lane_dividers) # lane_lines_glb.write_glb(Path(glb_dir) / "lane_lines.glb") + map_glb = make_map_glb(polygons, self.bounding_box, [], []) + map_glb.write_glb(Path(glb_dir) / "map.glb") + class Surface(RoadMap.Surface): def __init__(self, surface_id: str): self._surface_id = surface_id @@ -175,12 +189,21 @@ def surface_by_id(self, surface_id: str) -> RoadMap.Surface: return self._surfaces.get(surface_id) class Lane(RoadMap.Lane, Surface): - def __init__(self, lane_id: str, polygon, index: int): + def __init__( + self, map: "ArgoverseMap", lane_id: str, lane_seg: LaneSegment, index: int + ): super().__init__(lane_id) + self._map = map self._lane_id = lane_id - self._polygon = polygon + self.lane_seg = lane_seg + self._polygon = lane_seg.polygon_boundary[:, :2] + self._centerline = self._map._avm.get_lane_segment_centerline(lane_seg.id)[ + :, :2 + ] self._index = index self._road = None + self._incoming_lanes = None + self._outgoing_lanes = None def __hash__(self) -> int: return hash(self.lane_id) @@ -197,73 +220,107 @@ def road(self) -> RoadMap.Road: def speed_limit(self) -> Optional[float]: return None - @property + @cached_property def length(self) -> float: - """The length of this lane.""" - raise NotImplementedError() + length = 0 + for p1, p2 in zip(self._centerline, self._centerline[1:]): + length += np.linalg.norm(p2 - p1) + return length @property def in_junction(self) -> bool: - """If this lane is a part of a junction (usually an intersection.)""" - raise NotImplementedError() + raise self.lane_seg.is_intersection @property def index(self) -> int: - """when not in_junction, 0 is outer / right-most (relative to lane heading) lane on road. - otherwise, index scheme is implementation-dependent, but must be deterministic.""" - # TAI: UK roads - raise NotImplementedError() + return self._index - @property + @lru_cache(maxsize=4) + def shape( + self, buffer_width: float = 0.0, default_width: Optional[float] = None + ) -> Polygon: + return Polygon(self._polygon) + + @cached_property def lanes_in_same_direction(self) -> List[RoadMap.Lane]: - """returns all other lanes on this road where traffic goes - in the same direction. it is currently assumed these will be - adjacent to one another. In junctions, diverging lanes - should not be included.""" - raise NotImplementedError() + return [lane for lane in self.road.lanes if lane.lane_id != self.lane_id] - @property + @cached_property def lane_to_left(self) -> Tuple[RoadMap.Lane, bool]: - """Note: left is defined as 90 degrees clockwise relative to the lane heading. - (I.e., positive `t` in the RefLine coordinate system.) - Second result is True if lane is in the same direction as this one - In junctions, diverging lanes should not be included.""" - raise NotImplementedError() - - @property + result = None + for other in self.lanes_in_same_direction: + if other.index > self.index and ( + not result or other.index < result.index + ): + result = other + return result, True + + @cached_property def lane_to_right(self) -> Tuple[RoadMap.Lane, bool]: - """Note: right is defined as 90 degrees counter-clockwise relative to the lane heading. - (I.e., negative `t` in the RefLine coordinate system.) - Second result is True if lane is in the same direction as this one. - In junctions, diverging lanes should not be included.""" - raise NotImplementedError() + result = None + for other in self.lanes_in_same_direction: + if other.index < self.index and ( + not result or other.index > result.index + ): + result = other + return result, True @property def incoming_lanes(self) -> List[RoadMap.Lane]: - """Lanes leading into this lane.""" - raise NotImplementedError() + return self._incoming_lanes @property def outgoing_lanes(self) -> List[RoadMap.Lane]: - """Lanes leading out of this lane.""" - raise NotImplementedError() + return self._outgoing_lanes + @lru_cache(maxsize=16) def oncoming_lanes_at_offset(self, offset: float) -> List[RoadMap.Lane]: - """Returns a list of nearby lanes at offset that are (roughly) - parallel to this one but go in the opposite direction.""" - raise NotImplementedError() + result = [] + radius = 1.1 * self.width_at_offset(offset)[0] + pt = self.from_lane_coord(RefLinePoint(offset)) + nearby_lanes = self._map.nearest_lanes(pt, radius=radius) + if not nearby_lanes: + return result + my_vect = self.vector_at_offset(offset) + my_norm = np.linalg.norm(my_vect) + if my_norm == 0: + return result + threshold = -0.995562 # cos(175*pi/180) + for lane, _ in nearby_lanes: + if lane == self: + continue + lane_refline_pt = lane.to_lane_coord(pt) + lv = lane.vector_at_offset(lane_refline_pt.s) + lv_norm = np.linalg.norm(lv) + if lv_norm == 0: + continue + lane_angle = np.dot(my_vect, lv) / (my_norm * lv_norm) + if lane_angle < threshold: + result.append(lane) + return result @property def foes(self) -> List[RoadMap.Lane]: - """All lanes that in some ways intersect with (cross) this one, - including those that have the same outgoing lane as this one, - and so might require right-of-way rules. This should only - ever happen in junctions.""" raise NotImplementedError() - @lru_cache(maxsize=4) - def shape(self, buffer_width: float = 0.0, default_width: Optional[float] = None) -> Polygon: - return Polygon(self._polygon) + def waypoint_paths_for_pose( + self, pose: Pose, lookahead: int, route: RoadMap.Route = None + ) -> List[List[Waypoint]]: + raise NotImplementedError() + + def waypoint_paths_at_offset( + self, offset: float, lookahead: int = 30, route: RoadMap.Route = None + ) -> List[List[Waypoint]]: + raise NotImplementedError() + + def offset_along_lane(self, world_point: Point) -> float: + raise NotImplementedError() + + def width_at_offset(self, offset: float) -> Tuple[float, float]: + raise NotImplementedError() + + def from_lane_coord(self, lane_point: RefLinePoint) -> Point: + raise NotImplementedError() def lane_by_id(self, lane_id: str) -> RoadMap.Lane: lane = self._lanes.get(lane_id) From 6d9fa35be0cd9c7487781d7f1d03e20b2ae8d854 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Tue, 14 Feb 2023 16:35:39 -0500 Subject: [PATCH 05/28] Compute lane foes --- smarts/core/argoverse_map.py | 88 ++++++++++++++++++++++++++++++++++-- smarts/core/waymo_map.py | 6 +-- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index fe4ab2d4de..6394935d47 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -4,13 +4,15 @@ from pathlib import Path from cached_property import cached_property import time -from typing import Dict, List, Optional, Sequence, Tuple +from typing import Dict, List, Optional, Sequence, Set, Tuple import numpy as np +import rtree from smarts.core.coordinates import BoundingBox, Point, Pose, RefLinePoint from smarts.core.road_map import RoadMap, Waypoint from smarts.core.utils.glb import make_map_glb +from smarts.core.utils.math import line_intersect_vectorized from smarts.sstudio.types import MapSpec from av2.map.map_api import ArgoverseStaticMap from av2.map.lane_segment import LaneMarkType, LaneSegment @@ -55,6 +57,74 @@ def from_spec(cls, map_spec: MapSpec): assert avm.log_id == scenario_id, "Loaded map ID does not match expected ID" return cls(map_spec, avm) + def _compute_lane_intersections(self): + intersections: Dict[str, Set[str]] = dict() + + lane_ids_todo = [lane_id for lane_id in self._lanes.keys()] + + # Build rtree + lane_rtree = rtree.index.Index() + lane_rtree.interleaved = True + bboxes = dict() + for idx, lane_id in enumerate(lane_ids_todo): + lane_pts = self._lanes[lane_id]._centerline + bbox = ( + np.amin(lane_pts[:, 0]), + np.amin(lane_pts[:, 1]), + np.amax(lane_pts[:, 0]), + np.amax(lane_pts[:, 1]), + ) + bboxes[lane_id] = bbox + lane_rtree.add(idx, bbox) + + for lane_id in lane_ids_todo: + lane = self._lanes[lane_id] + lane_intersections = intersections.setdefault(lane_id, set()) + + # Filter out any lanes that don't intersect this lane's bbox + indicies = lane_rtree.intersection(bboxes[lane_id]) + + # Filter out any other lanes we don't want to check against + lanes_to_test = [] + for idx in indicies: + cand_id = lane_ids_todo[idx] + if cand_id == lane_id: + continue + # Skip intersections we've already computed + if cand_id in lane_intersections: + continue + # ... and sub-lanes of the same original lane + cand_lane = self._lanes[cand_id] + # Don't check intersection with incoming/outgoing lanes + if cand_lane in lane.incoming_lanes or cand_lane in lane.outgoing_lanes: + continue + # ... or lanes in same road (TAI?) + if lane.road == cand_lane.road: + continue + lanes_to_test.append(cand_id) + if not lanes_to_test: + continue + + # Main loop -- check each segment of the lane polyline against the + # polyline of each candidate lane (--> algorithm is O(l^2) + line1 = lane._centerline + for cand_id in lanes_to_test: + line2 = np.array(self._lanes[cand_id]._centerline) + C = np.roll(line2, 0, axis=0)[:-1] + D = np.roll(line2, -1, axis=0)[:-1] + for i in range(len(line1) - 1): + a = line1[i] + b = line1[i + 1] + if line_intersect_vectorized(a, b, C, D): + lane_intersections.add(cand_id) + intersections.setdefault(cand_id, set()).add(lane_id) + break + + for lane_id, intersect_ids in intersections.items(): + self._lanes[lane_id]._intersections = [ + self.lane_by_id(id) for id in intersect_ids + ] + def _load_map_data(self): start = time.time() @@ -128,6 +198,8 @@ def _load_map_data(self): if seg_id in all_ids ] + self._compute_lane_intersections() + end = time.time() elapsed = round((end - start) * 1000.0, 3) self._log.info(f"Loading Argoverse map took: {elapsed} ms") @@ -204,6 +276,7 @@ def __init__( self._road = None self._incoming_lanes = None self._outgoing_lanes = None + self._intersections = None def __hash__(self) -> int: return hash(self.lane_id) @@ -229,7 +302,7 @@ def length(self) -> float: @property def in_junction(self) -> bool: - raise self.lane_seg.is_intersection + return self.lane_seg.is_intersection @property def index(self) -> int: @@ -299,9 +372,16 @@ def oncoming_lanes_at_offset(self, offset: float) -> List[RoadMap.Lane]: result.append(lane) return result - @property + @cached_property def foes(self) -> List[RoadMap.Lane]: - raise NotImplementedError() + foes = set(self._intersections) + foes |= { + incoming + for outgoing in self.outgoing_lanes + for incoming in outgoing.incoming_lanes + if incoming != self + } + return list(foes) def waypoint_paths_for_pose( self, pose: Pose, lookahead: int, route: RoadMap.Route = None diff --git a/smarts/core/waymo_map.py b/smarts/core/waymo_map.py index 568f3c4737..82263ec938 100644 --- a/smarts/core/waymo_map.py +++ b/smarts/core/waymo_map.py @@ -295,7 +295,7 @@ def _compute_lane_intersections(self, composites: bool): if lane._feature_id == cand_lane._feature_id: continue # Don't check intersection with incoming/outgoing lanes - if cand_id in lane.incoming_lanes or cand_id in lane.outgoing_lanes: + if cand_lane in lane.incoming_lanes or cand_lane in lane.outgoing_lanes: continue # ... or lanes in same road (TAI?) if lane.road == cand_lane.road: @@ -1933,7 +1933,8 @@ def _equally_spaced_path( lp_spacing: float, ) -> List[Waypoint]: """given a list of LanePoints starting near point, return corresponding - Waypoints that may not be evenly spaced (due to lane change) but start at point.""" + Waypoints that may not be evenly spaced (due to lane change) but start at point. + """ continuous_variables = [ "positions_x", @@ -1949,7 +1950,6 @@ def _equally_spaced_path( parameter: [] for parameter in (continuous_variables + discrete_variables) } for idx, lanepoint in enumerate(path): - if lanepoint.is_inferred and 0 < idx < len(path) - 1: continue From 42acfb45dedede1c9a42ab6645efee76e34571e3 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Fri, 17 Feb 2023 13:22:24 -0500 Subject: [PATCH 06/28] Nearest lanes, routes --- smarts/core/argoverse_map.py | 165 +++++++++++++++++++++++-------- smarts/core/sumo_road_network.py | 8 +- smarts/core/utils/glb.py | 27 +++-- 3 files changed, 152 insertions(+), 48 deletions(-) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index 6394935d47..36a27456b3 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -2,6 +2,7 @@ import logging import math from pathlib import Path +import random from cached_property import cached_property import time from typing import Dict, List, Optional, Sequence, Set, Tuple @@ -10,7 +11,8 @@ import rtree from smarts.core.coordinates import BoundingBox, Point, Pose, RefLinePoint -from smarts.core.road_map import RoadMap, Waypoint +from smarts.core.road_map import RoadMap, RoadMapWithCaches, Waypoint +from smarts.core.route_cache import RouteWithCache from smarts.core.utils.glb import make_map_glb from smarts.core.utils.math import line_intersect_vectorized from smarts.sstudio.types import MapSpec @@ -19,12 +21,16 @@ import av2.geometry.polyline_utils as polyline_utils import av2.rendering.vector as vector_plotting_utils from shapely.geometry import Polygon +from shapely.geometry import Point as SPoint -class ArgoverseMap(RoadMap): +class ArgoverseMap(RoadMapWithCaches): """A road map for an Argoverse 2 scenario.""" + DEFAULT_LANE_SPEED = 16.67 # m/s + def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): + super().__init__() self._log = logging.getLogger(self.__class__.__name__) self._log.setLevel(logging.INFO) self._avm = avm @@ -34,6 +40,7 @@ def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): self._lanes: Dict[str, ArgoverseMap.Lane] = dict() self._roads: Dict[str, ArgoverseMap.Road] = dict() self._features = dict() + self._lane_rtree = None self._load_map_data() # self._waypoints_cache = SumoRoadNetwork._WaypointsCache() # self._lanepoints = None @@ -245,8 +252,8 @@ def to_glb(self, glb_dir): map_glb = make_map_glb(polygons, self.bounding_box, [], []) map_glb.write_glb(Path(glb_dir) / "map.glb") - class Surface(RoadMap.Surface): - def __init__(self, surface_id: str): + class Surface(RoadMapWithCaches.Surface): + def __init__(self, surface_id: str, road_map): self._surface_id = surface_id @property @@ -260,11 +267,11 @@ def is_drivable(self) -> bool: def surface_by_id(self, surface_id: str) -> RoadMap.Surface: return self._surfaces.get(surface_id) - class Lane(RoadMap.Lane, Surface): + class Lane(RoadMapWithCaches.Lane, Surface): def __init__( self, map: "ArgoverseMap", lane_id: str, lane_seg: LaneSegment, index: int ): - super().__init__(lane_id) + super().__init__(lane_id, map) self._map = map self._lane_id = lane_id self.lane_seg = lane_seg @@ -291,7 +298,7 @@ def road(self) -> RoadMap.Road: @property def speed_limit(self) -> Optional[float]: - return None + return ArgoverseMap.DEFAULT_LANE_SPEED @cached_property def length(self) -> float: @@ -300,6 +307,10 @@ def length(self) -> float: length += np.linalg.norm(p2 - p1) return length + @cached_property + def center_polyline(self) -> List[Point]: + return [Point(p[0], p[1]) for p in self._centerline] + @property def in_junction(self) -> bool: return self.lane_seg.is_intersection @@ -383,33 +394,58 @@ def foes(self) -> List[RoadMap.Lane]: } return list(foes) - def waypoint_paths_for_pose( - self, pose: Pose, lookahead: int, route: RoadMap.Route = None - ) -> List[List[Waypoint]]: - raise NotImplementedError() - - def waypoint_paths_at_offset( - self, offset: float, lookahead: int = 30, route: RoadMap.Route = None - ) -> List[List[Waypoint]]: - raise NotImplementedError() - - def offset_along_lane(self, world_point: Point) -> float: - raise NotImplementedError() - - def width_at_offset(self, offset: float) -> Tuple[float, float]: - raise NotImplementedError() - - def from_lane_coord(self, lane_point: RefLinePoint) -> Point: - raise NotImplementedError() - def lane_by_id(self, lane_id: str) -> RoadMap.Lane: lane = self._lanes.get(lane_id) assert lane, f"ArgoverseMap got request for unknown lane_id: '{lane_id}'" return lane - class Road(RoadMap.Road, Surface): + def _build_lane_r_tree(self): + result = rtree.index.Index() + result.interleaved = True + for idx, lane in enumerate(self._lanes.values()): + xs = lane._polygon[:, 0] + ys = lane._polygon[:, 1] + bounding_box = ( + np.amin(xs), + np.amin(ys), + np.amax(xs), + np.amax(ys), + ) + result.add(idx, bounding_box) + return result + + def _get_neighboring_lanes( + self, x: float, y: float, r: float = 0.1 + ) -> List[Tuple[RoadMapWithCaches.Lane, float]]: + neighboring_lanes = [] + if self._lane_rtree is None: + self._lane_rtree = self._build_lane_r_tree() + + spt = SPoint(x, y) + lanes = list(self._lanes.values()) + for i in self._lane_rtree.intersection((x - r, y - r, x + r, y + r)): + lane = lanes[i] + d = lane.shape().distance(spt) + if d < r: + neighboring_lanes.append((lane, d)) + return neighboring_lanes + + @lru_cache(maxsize=1024) + def nearest_lanes( + self, + point: Point, + radius: Optional[float] = None, + include_junctions: bool = False, + ) -> List[Tuple[RoadMapWithCaches.Lane, float]]: + if radius is None: + radius = 5 + candidate_lanes = self._get_neighboring_lanes(point[0], point[1], r=radius) + candidate_lanes.sort(key=lambda lane_dist_tup: lane_dist_tup[1]) + return candidate_lanes + + class Road(RoadMapWithCaches.Road, Surface): def __init__(self, road_id: str, lanes: List[RoadMap.Lane]): - super().__init__(road_id) + super().__init__(road_id, None) self._road_id = road_id self._lanes = lanes @@ -445,23 +481,29 @@ def is_composite(self) -> bool: @property def is_junction(self) -> bool: - """Note that a junction can be an intersection ('+') or a 'T', 'Y', 'L', etc.""" - raise NotImplementedError() + return False - @property + @cached_property def length(self) -> float: - """The length of this road.""" - raise NotImplementedError() + # Neighbouring lanes in Argoverse can be different lengths. Since this is + # just used for routes, we take the average lane length in this road. + return sum([lane.length for lane in self.lanes]) / len(self.lanes) @property def incoming_roads(self) -> List[RoadMap.Road]: - """All roads that lead into this road.""" - raise NotImplementedError() + return list( + {in_lane.road for lane in self.lanes for in_lane in lane.incoming_lanes} + ) @property def outgoing_roads(self) -> List[RoadMap.Road]: - """All roads that lead out of this road.""" - raise NotImplementedError() + return list( + { + out_lane.road + for lane in self.lanes + for out_lane in lane.outgoing_lanes + } + ) def oncoming_roads_at_point(self, point: Point) -> List[RoadMap.Road]: """Returns a list of nearby roads to point that are (roughly) @@ -479,10 +521,55 @@ def lanes(self) -> List[RoadMap.Lane]: return self._lanes def lane_at_index(self, index: int) -> RoadMap.Lane: - """Gets the lane with the given index.""" - raise NotImplementedError() + return self.lanes[index] def road_by_id(self, road_id: str) -> RoadMap.Road: road = self._roads.get(road_id) assert road, f"ArgoverseMap got request for unknown road_id: '{road_id}'" return road + + class Route(RouteWithCache): + def __init__(self, road_map): + super().__init__(road_map) + self._roads = [] + self._length = 0 + + @property + def roads(self) -> List[RoadMap.Road]: + return self._roads + + @property + def road_length(self) -> float: + return self._length + + def add_road(self, road: RoadMap.Road): + """Add a road to this route.""" + self._length += road.length + self._roads.append(road) + + @cached_property + def geometry(self) -> Sequence[Sequence[Tuple[float, float]]]: + return [list(road.shape(0.0).exterior.coords) for road in self.roads] + + def random_route( + self, + max_route_len: int = 10, + starting_road: Optional[RoadMap.Road] = None, + only_drivable: bool = True, + ) -> RoadMap.Route: + assert not starting_road or not only_drivable or starting_road.is_drivable + route = ArgoverseMap.Route(self) + next_roads = [starting_road] if starting_road else list(self._roads.values()) + if only_drivable: + next_roads = [r for r in next_roads if r.is_drivable] + while next_roads and len(route.roads) < max_route_len: + cur_road = random.choice(next_roads) + route.add_road(cur_road) + next_roads = list(cur_road.outgoing_roads) + return route + + def empty_route(self) -> RoadMap.Route: + return ArgoverseMap.Route(self) + + def route_from_road_ids(self, road_ids: Sequence[str]) -> RoadMap.Route: + return ArgoverseMap.Route.from_road_ids(self, road_ids) diff --git a/smarts/core/sumo_road_network.py b/smarts/core/sumo_road_network.py index da3420e089..76225f2972 100644 --- a/smarts/core/sumo_road_network.py +++ b/smarts/core/sumo_road_network.py @@ -41,7 +41,8 @@ from .lanepoints import LanePoints, LinkedLanePoint from .road_map import RoadMap, Waypoint from .route_cache import RouteWithCache -from .utils.geometry import buffered_shape, generate_meshes_from_polygons +from .utils.geometry import buffered_shape +from .utils.glb import make_map_glb from .utils.math import inplace_unwrap, radians_to_vec, vec_2d from smarts.core.utils.sumo import sumolib # isort:skip @@ -284,7 +285,7 @@ def scale_factor(self) -> float: def to_glb(self, glb_dir): lane_dividers, edge_dividers = self._compute_traffic_dividers() polys = self._compute_road_polygons() - map_glb = self._make_glb_from_polys(polys, lane_dividers, edge_dividers) + map_glb = make_map_glb(polys, self.bounding_box, [], []) map_glb.write_glb(Path(glb_dir) / "map.glb") road_lines_glb = self._make_road_line_glb(edge_dividers) @@ -1473,7 +1474,8 @@ def _equally_spaced_path( lp_spacing: float, ) -> List[Waypoint]: """given a list of LanePoints starting near point, that may not be evenly spaced, - returns the same number of Waypoints that are evenly spaced and start at point.""" + returns the same number of Waypoints that are evenly spaced and start at point. + """ continuous_variables = [ "positions_x", diff --git a/smarts/core/utils/glb.py b/smarts/core/utils/glb.py index 7b2d51ebc5..37c1ddaaa1 100644 --- a/smarts/core/utils/glb.py +++ b/smarts/core/utils/glb.py @@ -42,7 +42,9 @@ def write_glb(self, output_path: str): f.write(self._bytes) -def _generate_meshes_from_polygons(polygons: List[Tuple[Polygon, Dict[str, Any]]]) -> List[trimesh.Trimesh]: +def _generate_meshes_from_polygons( + polygons: List[Tuple[Polygon, Dict[str, Any]]] +) -> List[trimesh.Trimesh]: """Creates a mesh out of a list of polygons.""" meshes = [] @@ -64,7 +66,9 @@ def _generate_meshes_from_polygons(polygons: List[Tuple[Polygon, Dict[str, Any]] current_point_index += 1 triangles = triangulate_polygon(poly) for triangle in triangles: - face = np.array([point_dict.get((x, y, 0), -1) for x, y in triangle.exterior.coords]) + face = np.array( + [point_dict.get((x, y, 0), -1) for x, y in triangle.exterior.coords] + ) # Add face if not invalid if -1 not in face: faces.append(face) @@ -76,12 +80,19 @@ def _generate_meshes_from_polygons(polygons: List[Tuple[Polygon, Dict[str, Any]] # Trimesh doesn't support a coordinate-system="z-up" configuration, so we # have to apply the transformation manually. - mesh.apply_transform(trimesh.transformations.rotation_matrix(math.pi / 2, [-1, 0, 0])) + mesh.apply_transform( + trimesh.transformations.rotation_matrix(math.pi / 2, [-1, 0, 0]) + ) meshes.append(mesh) return meshes -def make_map_glb(polygons, bbox: BoundingBox, lane_dividers, edge_dividers) -> GLBData: +def make_map_glb( + polygons: List[Tuple[Polygon, Dict[str, Any]]], + bbox: BoundingBox, + lane_dividers, + edge_dividers, +) -> GLBData: scene = trimesh.Scene() # Attach additional information for rendering as metadata in the map glb @@ -98,7 +109,9 @@ def make_map_glb(polygons, bbox: BoundingBox, lane_dividers, edge_dividers) -> G meshes = _generate_meshes_from_polygons(polygons) for mesh in meshes: - mesh.visual = trimesh.visual.TextureVisuals(material=trimesh.visual.material.PBRMaterial()) + mesh.visual = trimesh.visual.TextureVisuals( + material=trimesh.visual.material.PBRMaterial() + ) road_id = mesh.metadata["road_id"] lane_id = mesh.metadata.get("lane_id") @@ -114,6 +127,8 @@ def make_road_line_glb(lines: List[List[Tuple[float, float]]]) -> GLBData: for line_pts in lines: vertices = [(*pt, 0.1) for pt in line_pts] point_cloud = trimesh.PointCloud(vertices=vertices) - point_cloud.apply_transform(trimesh.transformations.rotation_matrix(math.pi / 2, [-1, 0, 0])) + point_cloud.apply_transform( + trimesh.transformations.rotation_matrix(math.pi / 2, [-1, 0, 0]) + ) scene.add_geometry(point_cloud) return GLBData(gltf.export_glb(scene)) From fd392f2f43ce64913d23a050377dd65a2c38077f Mon Sep 17 00:00:00 2001 From: Saul Field Date: Fri, 17 Feb 2023 16:20:26 -0500 Subject: [PATCH 07/28] Width at offset --- smarts/core/argoverse_map.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index 36a27456b3..5d7a1bbb67 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -18,8 +18,7 @@ from smarts.sstudio.types import MapSpec from av2.map.map_api import ArgoverseStaticMap from av2.map.lane_segment import LaneMarkType, LaneSegment -import av2.geometry.polyline_utils as polyline_utils -import av2.rendering.vector as vector_plotting_utils +from av2.geometry.interpolate import interp_arc from shapely.geometry import Polygon from shapely.geometry import Point as SPoint @@ -275,16 +274,26 @@ def __init__( self._map = map self._lane_id = lane_id self.lane_seg = lane_seg - self._polygon = lane_seg.polygon_boundary[:, :2] - self._centerline = self._map._avm.get_lane_segment_centerline(lane_seg.id)[ - :, :2 - ] self._index = index self._road = None self._incoming_lanes = None self._outgoing_lanes = None self._intersections = None + self._polygon = lane_seg.polygon_boundary[:, :2] + self._centerline = self._map._avm.get_lane_segment_centerline(lane_seg.id)[ + :, :2 + ] + + # Compute equally-spaced points for lane boundaries by interpolating + n = len(self._centerline) + self.left_pts = interp_arc( + n, points=self.lane_seg.left_lane_boundary.xyz[:, :2] + ) + self.right_pts = interp_arc( + n, points=self.lane_seg.right_lane_boundary.xyz[:, :2] + ) + def __hash__(self) -> int: return hash(self.lane_id) @@ -300,6 +309,19 @@ def road(self) -> RoadMap.Road: def speed_limit(self) -> Optional[float]: return ArgoverseMap.DEFAULT_LANE_SPEED + @lru_cache(maxsize=1024) + def width_at_offset(self, lane_point_s: float) -> Tuple[float, float]: + world_point = self.from_lane_coord( + RefLinePoint(lane_point_s, 0) + ).as_np_array[:2] + deltas = self._centerline - world_point + dists = np.linalg.norm(deltas, axis=1) + closest_index = np.argmin(dists) + p1 = self.left_pts[closest_index] + p2 = self.right_pts[closest_index] + width = np.linalg.norm(p2 - p1) + return width, 1.0 + @cached_property def length(self) -> float: length = 0 From 40748d5ea8b0d7495b32b4247e61e8a35928fbc4 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Wed, 22 Feb 2023 13:58:59 -0500 Subject: [PATCH 08/28] Route generation and more geometry routines --- smarts/core/argoverse_map.py | 128 ++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index 5d7a1bbb67..70543662d9 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -1,4 +1,5 @@ from functools import lru_cache +import heapq import logging import math from pathlib import Path @@ -73,6 +74,7 @@ def _compute_lane_intersections(self): lane_rtree.interleaved = True bboxes = dict() for idx, lane_id in enumerate(lane_ids_todo): + # Using the centerline here is much faster than using the lane polygon lane_pts = self._lanes[lane_id]._centerline bbox = ( np.amin(lane_pts[:, 0]), @@ -285,6 +287,13 @@ def __init__( :, :2 ] + xs = self._polygon[:, 0] + ys = self._polygon[:, 1] + self._bbox = BoundingBox( + min_pt=Point(x=np.amin(xs), y=np.amin(ys)), + max_pt=Point(x=np.amax(xs), y=np.amax(ys)), + ) + # Compute equally-spaced points for lane boundaries by interpolating n = len(self._centerline) self.left_pts = interp_arc( @@ -416,6 +425,16 @@ def foes(self) -> List[RoadMap.Lane]: } return list(foes) + @lru_cache(maxsize=8) + def contains_point(self, point: Point) -> bool: + assert type(point) == Point + if ( + self._bbox.min_pt.x <= point[0] <= self._bbox.max_pt.x + and self._bbox.min_pt.y <= point[1] <= self._bbox.max_pt.y + ): + return self.shape().contains(point.as_shapely) + return False + def lane_by_id(self, lane_id: str) -> RoadMap.Lane: lane = self._lanes.get(lane_id) assert lane, f"ArgoverseMap got request for unknown lane_id: '{lane_id}'" @@ -465,12 +484,32 @@ def nearest_lanes( candidate_lanes.sort(key=lambda lane_dist_tup: lane_dist_tup[1]) return candidate_lanes + @lru_cache(maxsize=16) + def road_with_point(self, point: Point) -> RoadMap.Road: + radius = 5 + for nl, dist in self.nearest_lanes(point, radius): + if nl.contains_point(point): + return nl.road + return None + class Road(RoadMapWithCaches.Road, Surface): def __init__(self, road_id: str, lanes: List[RoadMap.Lane]): super().__init__(road_id, None) self._road_id = road_id self._lanes = lanes + x_mins, y_mins, x_maxs, y_maxs = [], [], [], [] + for lane in self._lanes: + x_mins.append(lane._bbox.min_pt.x) + y_mins.append(lane._bbox.min_pt.y) + x_maxs.append(lane._bbox.max_pt.x) + y_maxs.append(lane._bbox.max_pt.y) + + self._bbox = BoundingBox( + min_pt=Point(x=min(x_mins), y=min(y_mins)), + max_pt=Point(x=max(x_maxs), y=max(y_maxs)), + ) + def __hash__(self) -> int: return hash(self.road_id) @@ -545,6 +584,17 @@ def lanes(self) -> List[RoadMap.Lane]: def lane_at_index(self, index: int) -> RoadMap.Lane: return self.lanes[index] + @lru_cache(maxsize=8) + def contains_point(self, point: Point) -> bool: + if ( + self._bbox.min_pt.x <= point[0] <= self._bbox.max_pt.x + and self._bbox.min_pt.y <= point[1] <= self._bbox.max_pt.y + ): + for lane in self._lanes: + if lane.contains_point(point): + return True + return False + def road_by_id(self, road_id: str) -> RoadMap.Road: road = self._roads.get(road_id) assert road, f"ArgoverseMap got request for unknown road_id: '{road_id}'" @@ -564,7 +614,7 @@ def roads(self) -> List[RoadMap.Road]: def road_length(self) -> float: return self._length - def add_road(self, road: RoadMap.Road): + def _add_road(self, road: RoadMap.Road): """Add a road to this route.""" self._length += road.length self._roads.append(road) @@ -573,6 +623,80 @@ def add_road(self, road: RoadMap.Road): def geometry(self) -> Sequence[Sequence[Tuple[float, float]]]: return [list(road.shape(0.0).exterior.coords) for road in self.roads] + @staticmethod + def _shortest_route(start: RoadMap.Road, end: RoadMap.Road) -> List[RoadMap.Road]: + queue = [(start.length, start.road_id, start)] + came_from = dict() + came_from[start] = None + cost_so_far = dict() + cost_so_far[start] = start.length + current = None + + # Dijkstra’s Algorithm + while queue: + (_, _, current) = heapq.heappop(queue) + current: RoadMap.Road + if current == end: + break + for out_road in current.outgoing_roads: + new_cost = cost_so_far[current] + out_road.length + if out_road not in cost_so_far or new_cost < cost_so_far[out_road]: + cost_so_far[out_road] = new_cost + came_from[out_road] = current + heapq.heappush(queue, (new_cost, out_road.road_id, out_road)) + + # This means we couldn't find a valid route since the queue is empty + if current != end: + return [] + + # Reconstruct path + current = end + path = [] + while current != start: + path.append(current) + current = came_from[current] + path.append(start) + path.reverse() + return path + + def generate_routes( + self, + start_road: RoadMap.Road, + end_road: RoadMap.Road, + via: Optional[Sequence[RoadMap.Road]] = None, + max_to_gen: int = 1, + ) -> List[RoadMap.Route]: + assert ( + max_to_gen == 1 + ), "multiple route generation not yet supported for Argoverse" + new_route = ArgoverseMap.Route(self) + result = [new_route] + + roads = [start_road] + if via: + roads += via + if end_road != start_road: + roads.append(end_road) + + route_roads = [] + for cur_road, next_road in zip(roads, roads[1:] + [None]): + if not next_road: + route_roads.append(cur_road) + break + sub_route = ArgoverseMap._shortest_route(cur_road, next_road) or [] + if len(sub_route) < 2: + self._log.warning( + f"Unable to find valid path between {(cur_road.road_id, next_road.road_id)}." + ) + return result + # The sub route includes the boundary roads (cur_road, next_road). + # We clip the latter to prevent duplicates + route_roads.extend(sub_route[:-1]) + + for road in route_roads: + new_route._add_road(road) + return result + def random_route( self, max_route_len: int = 10, @@ -586,7 +710,7 @@ def random_route( next_roads = [r for r in next_roads if r.is_drivable] while next_roads and len(route.roads) < max_route_len: cur_road = random.choice(next_roads) - route.add_road(cur_road) + route._add_road(cur_road) next_roads = list(cur_road.outgoing_roads) return route From 7783596f068599de0826afe6c6afc40b7cdcdeb5 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Wed, 22 Feb 2023 15:44:59 -0500 Subject: [PATCH 09/28] Waypoints --- scenarios/argoverse/scenario.py | 2 +- smarts/core/argoverse_map.py | 325 +++++++++++++++++++++++++++++++- smarts/core/lanepoints.py | 119 ++++++++++++ 3 files changed, 437 insertions(+), 9 deletions(-) diff --git a/scenarios/argoverse/scenario.py b/scenarios/argoverse/scenario.py index 3826776f12..49a6a25748 100644 --- a/scenarios/argoverse/scenario.py +++ b/scenarios/argoverse/scenario.py @@ -15,7 +15,7 @@ gen_scenario( t.Scenario( - map_spec=t.MapSpec(source=f"{dataset_path}"), + map_spec=t.MapSpec(source=f"{dataset_path}", lanepoint_spacing=1.0), # traffic_histories=traffic_histories, ), output_dir=Path(__file__).parent, diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index 70543662d9..1ca20f75ea 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -11,11 +11,17 @@ import numpy as np import rtree -from smarts.core.coordinates import BoundingBox, Point, Pose, RefLinePoint +from smarts.core.coordinates import BoundingBox, Heading, Point, Pose, RefLinePoint +from smarts.core.lanepoints import LanePoints, LinkedLanePoint from smarts.core.road_map import RoadMap, RoadMapWithCaches, Waypoint from smarts.core.route_cache import RouteWithCache from smarts.core.utils.glb import make_map_glb -from smarts.core.utils.math import line_intersect_vectorized +from smarts.core.utils.math import ( + inplace_unwrap, + line_intersect_vectorized, + radians_to_vec, + vec_2d, +) from smarts.sstudio.types import MapSpec from av2.map.map_api import ArgoverseStaticMap from av2.map.lane_segment import LaneMarkType, LaneSegment @@ -42,12 +48,12 @@ def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): self._features = dict() self._lane_rtree = None self._load_map_data() - # self._waypoints_cache = SumoRoadNetwork._WaypointsCache() - # self._lanepoints = None - # if map_spec.lanepoint_spacing is not None: - # assert map_spec.lanepoint_spacing > 0 - # # XXX: this should be last here since LanePoints() calls road_network methods immediately - # self._lanepoints = LanePoints.from_sumo(self, spacing=map_spec.lanepoint_spacing) + self._waypoints_cache = ArgoverseMap._WaypointsCache() + if map_spec.lanepoint_spacing is not None: + assert map_spec.lanepoint_spacing > 0 + self._lanepoints = LanePoints.from_argoverse( + self, spacing=map_spec.lanepoint_spacing + ) @classmethod def from_spec(cls, map_spec: MapSpec): @@ -435,6 +441,43 @@ def contains_point(self, point: Point) -> bool: return self.shape().contains(point.as_shapely) return False + def waypoint_paths_for_pose( + self, pose: Pose, lookahead: int, route: RoadMap.Route = None + ) -> List[List[Waypoint]]: + if not self.is_drivable: + return [] + road_ids = [road.road_id for road in route.roads] if route else None + return self._waypoint_paths_at(pose.point, lookahead, road_ids) + + def waypoint_paths_at_offset( + self, offset: float, lookahead: int = 30, route: RoadMap.Route = None + ) -> List[List[Waypoint]]: + if not self.is_drivable: + return [] + wp_start = self.from_lane_coord(RefLinePoint(offset)) + road_ids = [road.road_id for road in route.roads] if route else None + return self._waypoint_paths_at(wp_start, lookahead, road_ids) + + def _waypoint_paths_at( + self, + point: Point, + lookahead: int, + filter_road_ids: Optional[Sequence[str]] = None, + ) -> List[List[Waypoint]]: + if not self.is_drivable: + return [] + closest_linked_lp = ( + self._map._lanepoints.closest_linked_lanepoint_on_lane_to_point( + point, self._lane_id + ) + ) + return self._map._waypoints_starting_at_lanepoint( + closest_linked_lp, + lookahead, + tuple(filter_road_ids) if filter_road_ids else (), + point, + ) + def lane_by_id(self, lane_id: str) -> RoadMap.Lane: lane = self._lanes.get(lane_id) assert lane, f"ArgoverseMap got request for unknown lane_id: '{lane_id}'" @@ -719,3 +762,269 @@ def empty_route(self) -> RoadMap.Route: def route_from_road_ids(self, road_ids: Sequence[str]) -> RoadMap.Route: return ArgoverseMap.Route.from_road_ids(self, road_ids) + + class _WaypointsCache: + def __init__(self): + self.lookahead = 0 + self.point = Point(0, 0) + self.filter_road_ids = () + self._starts = {} + + # XXX: all vehicles share this cache now (as opposed to before + # when it was in Plan.py and each vehicle had its own cache). + # TODO: probably need to add vehicle_id to the key somehow (or just make it bigger) + def _match(self, lookahead, point, filter_road_ids) -> bool: + return ( + lookahead <= self.lookahead + and point[0] == self.point[0] + and point[1] == self.point[1] + and filter_road_ids == self.filter_road_ids + ) + + def update( + self, + lookahead: int, + point: Point, + filter_road_ids: tuple, + llp, + paths: List[List[Waypoint]], + ): + """Update the current cache if not already cached.""" + if not self._match(lookahead, point, filter_road_ids): + self.lookahead = lookahead + self.point = point + self.filter_road_ids = filter_road_ids + self._starts = {} + self._starts[llp.lp.lane.index] = paths + + def query( + self, + lookahead: int, + point: Point, + filter_road_ids: tuple, + llp, + ) -> Optional[List[List[Waypoint]]]: + """Attempt to find previously cached waypoints""" + if self._match(lookahead, point, filter_road_ids): + hit = self._starts.get(llp.lp.lane.index, None) + if hit: + # consider just returning all of them (not slicing)? + return [path[: (lookahead + 1)] for path in hit] + return None + + def waypoint_paths( + self, + pose: Pose, + lookahead: int, + within_radius: float = 5, + route: RoadMap.Route = None, + ) -> List[List[Waypoint]]: + road_ids = [] + if route and route.roads: + road_ids = [road.road_id for road in route.roads] + if road_ids: + return self._waypoint_paths_along_route(pose.point, lookahead, road_ids) + closest_lps = self._lanepoints.closest_lanepoints( + [pose], within_radius=within_radius + ) + closest_lane = closest_lps[0].lane + waypoint_paths = [] + for lane in closest_lane.road.lanes: + waypoint_paths += lane._waypoint_paths_at(pose.point, lookahead) + return sorted(waypoint_paths, key=lambda p: p[0].lane_index) + + def _waypoint_paths_along_route( + self, point: Point, lookahead: int, route: Sequence[str] + ) -> List[List[Waypoint]]: + """finds the closest lane to vehicle's position that is on its route, + then gets waypoint paths from all lanes in its road there.""" + assert len(route) > 0, f"Expected at least 1 road in the route, got: {route}" + closest_llp_on_each_route_road = [ + self._lanepoints.closest_linked_lanepoint_on_road(point, road) + for road in route + ] + closest_linked_lp = min( + closest_llp_on_each_route_road, + key=lambda l_lp: np.linalg.norm( + vec_2d(l_lp.lp.pose.position) - vec_2d(point) + ), + ) + closest_lane = closest_linked_lp.lp.lane + waypoint_paths = [] + for lane in closest_lane.road.lanes: + waypoint_paths += lane._waypoint_paths_at(point, lookahead, route) + + return sorted(waypoint_paths, key=len, reverse=True) + + @staticmethod + def _equally_spaced_path( + path: Sequence[LinkedLanePoint], + point: Point, + lp_spacing: float, + ) -> List[Waypoint]: + """given a list of LanePoints starting near point, return corresponding + Waypoints that may not be evenly spaced (due to lane change) but start at point. + """ + + continuous_variables = [ + "positions_x", + "positions_y", + "headings", + "lane_width", + "speed_limit", + "lane_offset", + ] + discrete_variables = ["lane_id", "lane_index"] + + ref_lanepoints_coordinates = { + parameter: [] for parameter in (continuous_variables + discrete_variables) + } + for idx, lanepoint in enumerate(path): + if lanepoint.is_inferred and 0 < idx < len(path) - 1: + continue + + ref_lanepoints_coordinates["positions_x"].append( + lanepoint.lp.pose.position[0] + ) + ref_lanepoints_coordinates["positions_y"].append( + lanepoint.lp.pose.position[1] + ) + ref_lanepoints_coordinates["headings"].append( + lanepoint.lp.pose.heading.as_bullet + ) + ref_lanepoints_coordinates["lane_id"].append(lanepoint.lp.lane.lane_id) + ref_lanepoints_coordinates["lane_index"].append(lanepoint.lp.lane.index) + + ref_lanepoints_coordinates["lane_width"].append(lanepoint.lp.lane_width) + + ref_lanepoints_coordinates["lane_offset"].append( + lanepoint.lp.lane.offset_along_lane(lanepoint.lp.pose.point) + ) + + ref_lanepoints_coordinates["speed_limit"].append( + lanepoint.lp.lane.speed_limit + ) + + ref_lanepoints_coordinates["headings"] = inplace_unwrap( + ref_lanepoints_coordinates["headings"] + ) + first_lp_heading = ref_lanepoints_coordinates["headings"][0] + lp_position = path[0].lp.pose.point.as_np_array[:2] + vehicle_pos = point.as_np_array[:2] + heading_vec = radians_to_vec(first_lp_heading) + projected_distant_lp_vehicle = np.inner( + (vehicle_pos - lp_position), heading_vec + ) + + ref_lanepoints_coordinates["positions_x"][0] = ( + lp_position[0] + projected_distant_lp_vehicle * heading_vec[0] + ) + ref_lanepoints_coordinates["positions_y"][0] = ( + lp_position[1] + projected_distant_lp_vehicle * heading_vec[1] + ) + + cumulative_path_dist = np.cumsum( + np.sqrt( + np.ediff1d(ref_lanepoints_coordinates["positions_x"], to_begin=0) ** 2 + + np.ediff1d(ref_lanepoints_coordinates["positions_y"], to_begin=0) ** 2 + ) + ) + + if len(cumulative_path_dist) <= lp_spacing: + lp = path[0].lp + + return [ + Waypoint( + pos=lp.pose.position[:2], + heading=lp.pose.heading, + lane_width=lp.lane.width_at_offset(0)[0], + speed_limit=lp.lane.speed_limit, + lane_id=lp.lane.lane_id, + lane_index=lp.lane.index, + lane_offset=lp.lane.offset_along_lane(lp.pose.point), + ) + ] + + evenly_spaced_cumulative_path_dist = np.linspace( + 0, cumulative_path_dist[-1], len(path) + ) + + evenly_spaced_coordinates = {} + for variable in continuous_variables: + evenly_spaced_coordinates[variable] = np.interp( + evenly_spaced_cumulative_path_dist, + cumulative_path_dist, + ref_lanepoints_coordinates[variable], + ) + + for variable in discrete_variables: + ref_coordinates = ref_lanepoints_coordinates[variable] + evenly_spaced_coordinates[variable] = [] + jdx = 0 + for idx in range(len(path)): + while ( + jdx + 1 < len(cumulative_path_dist) + and evenly_spaced_cumulative_path_dist[idx] + > cumulative_path_dist[jdx + 1] + ): + jdx += 1 + + evenly_spaced_coordinates[variable].append(ref_coordinates[jdx]) + evenly_spaced_coordinates[variable].append(ref_coordinates[-1]) + + waypoint_path = [] + for idx in range(len(path)): + waypoint_path.append( + Waypoint( + pos=np.array( + [ + evenly_spaced_coordinates["positions_x"][idx], + evenly_spaced_coordinates["positions_y"][idx], + ] + ), + heading=Heading(evenly_spaced_coordinates["headings"][idx]), + lane_width=evenly_spaced_coordinates["lane_width"][idx], + speed_limit=evenly_spaced_coordinates["speed_limit"][idx], + lane_id=evenly_spaced_coordinates["lane_id"][idx], + lane_index=evenly_spaced_coordinates["lane_index"][idx], + lane_offset=evenly_spaced_coordinates["lane_offset"][idx], + ) + ) + + return waypoint_path + + def _waypoints_starting_at_lanepoint( + self, + lanepoint: LinkedLanePoint, + lookahead: int, + filter_road_ids: tuple, + point: Point, + ) -> List[List[Waypoint]]: + """computes equally-spaced Waypoints for all lane paths starting at lanepoint + up to lookahead waypoints ahead, constrained to filter_road_ids if specified.""" + + # The following acts sort of like lru_cache(1), but it allows + # for lookahead to be <= to the cached value... + cache_paths = self._waypoints_cache.query( + lookahead, point, filter_road_ids, lanepoint + ) + if cache_paths: + return cache_paths + + lanepoint_paths = self._lanepoints.paths_starting_at_lanepoint( + lanepoint, lookahead, filter_road_ids + ) + result = [ + ArgoverseMap._equally_spaced_path( + path, + point, + self._map_spec.lanepoint_spacing, + ) + for path in lanepoint_paths + ] + + self._waypoints_cache.update( + lookahead, point, filter_road_ids, lanepoint, result + ) + + return result diff --git a/smarts/core/lanepoints.py b/smarts/core/lanepoints.py index 3793285d4d..0527b6894d 100644 --- a/smarts/core/lanepoints.py +++ b/smarts/core/lanepoints.py @@ -491,6 +491,125 @@ def _shape_lanepoints_along_lane( return cls(shape_lps, spacing) + @classmethod + def from_argoverse( + cls, + argoverse_map, + spacing, + ): + """Computes the lane shape (start/shape/end) lanepoints for all lanes in + the network, the result of this function can be used to interpolate + lanepoints along lanes to the desired granularity. + """ + from .argoverse_map import ArgoverseMap + + assert type(argoverse_map) == ArgoverseMap + + def _shape_lanepoints_along_lane( + lane: RoadMap.Lane, + lanepoint_by_lane_memo: dict, + ) -> Tuple[LinkedLanePoint, List[LinkedLanePoint]]: + lane_queue = queue.Queue() + lane_queue.put((lane, None)) + shape_lanepoints = [] + initial_lanepoint = None + while not lane_queue.empty(): + curr_lane, previous_lp = lane_queue.get() + first_lanepoint = lanepoint_by_lane_memo.get(curr_lane.lane_id) + if first_lanepoint: + if previous_lp: + previous_lp.nexts.append(first_lanepoint) + continue + + lane_shape = curr_lane._centerline + + assert ( + len(lane_shape) >= 2 + ), f"{repr(lane_shape)} for lane_id={curr_lane.lane_id}" + + vd = lane_shape[1] - lane_shape[0] + heading = Heading(vec_to_radians(vd[:2])) + orientation = fast_quaternion_from_angle(heading) + + lane_width, _ = curr_lane.width_at_offset(0) + first_lanepoint = LinkedLanePoint( + lp=LanePoint( + lane=curr_lane, + pose=Pose(position=lane_shape[0], orientation=orientation), + lane_width=lane_width, + ), + nexts=[], + is_inferred=False, + ) + + if previous_lp is not None: + previous_lp.nexts.append(first_lanepoint) + + if initial_lanepoint is None: + initial_lanepoint = first_lanepoint + + lanepoint_by_lane_memo[curr_lane.lane_id] = first_lanepoint + shape_lanepoints.append(first_lanepoint) + curr_lanepoint = first_lanepoint + + for p1, p2 in zip(lane_shape[1:], lane_shape[2:]): + vd = p2 - p1 + heading_ = Heading(vec_to_radians(vd[:2])) + orientation_ = fast_quaternion_from_angle(heading_) + lane_width, _ = curr_lane.width_at_offset(0) + linked_lanepoint = LinkedLanePoint( + lp=LanePoint( + lane=curr_lane, + pose=Pose(position=p1, orientation=orientation_), + lane_width=lane_width, + ), + nexts=[], + is_inferred=False, + ) + + shape_lanepoints.append(linked_lanepoint) + curr_lanepoint.nexts.append(linked_lanepoint) + curr_lanepoint = linked_lanepoint + + # Add a lanepoint for the last point of the current lane + curr_lanepoint_lane = curr_lanepoint.lp.lane + lane_width, _ = curr_lanepoint_lane.width_at_offset(0) + last_linked_lanepoint = LinkedLanePoint( + lp=LanePoint( + lane=curr_lanepoint.lp.lane, + pose=Pose( + position=lane_shape[-1], + orientation=curr_lanepoint.lp.pose.orientation, + ), + lane_width=lane_width, + ), + nexts=[], + is_inferred=False, + ) + + shape_lanepoints.append(last_linked_lanepoint) + curr_lanepoint.nexts.append(last_linked_lanepoint) + curr_lanepoint = last_linked_lanepoint + + for out_lane in curr_lane.outgoing_lanes: + if out_lane and out_lane.is_drivable: + lane_queue.put((out_lane, curr_lanepoint)) + return initial_lanepoint, shape_lanepoints + + roads = argoverse_map._roads + lanepoint_by_lane_memo = {} + shape_lps = [] + + for road in roads.values(): + for lane in road.lanes: + if lane.is_drivable: + _, new_lps = _shape_lanepoints_along_lane( + lane, lanepoint_by_lane_memo + ) + shape_lps += new_lps + + return cls(shape_lps, spacing) + @staticmethod def _build_kd_tree(linked_lps: Sequence[LinkedLanePoint]) -> KDTree: return KDTree( From 7962251e5847b927a645dcfc39467501df33ad29 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Thu, 23 Feb 2023 17:11:09 -0500 Subject: [PATCH 10/28] Road/lane dividers --- smarts/core/argoverse_map.py | 71 +++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index 1ca20f75ea..aa3d19baaf 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -15,7 +15,7 @@ from smarts.core.lanepoints import LanePoints, LinkedLanePoint from smarts.core.road_map import RoadMap, RoadMapWithCaches, Waypoint from smarts.core.route_cache import RouteWithCache -from smarts.core.utils.glb import make_map_glb +from smarts.core.utils.glb import make_map_glb, make_road_line_glb from smarts.core.utils.math import ( inplace_unwrap, line_intersect_vectorized, @@ -35,6 +35,29 @@ class ArgoverseMap(RoadMapWithCaches): DEFAULT_LANE_SPEED = 16.67 # m/s + LANE_MARKINGS = frozenset( + { + LaneMarkType.DASH_SOLID_WHITE, + LaneMarkType.DASHED_WHITE, + LaneMarkType.DOUBLE_SOLID_WHITE, + LaneMarkType.DOUBLE_DASH_WHITE, + LaneMarkType.SOLID_WHITE, + LaneMarkType.SOLID_DASH_WHITE, + } + ) + + ROAD_MARKINGS = frozenset( + { + LaneMarkType.DASH_SOLID_YELLOW, + LaneMarkType.DASHED_YELLOW, + LaneMarkType.DOUBLE_SOLID_YELLOW, + LaneMarkType.DOUBLE_DASH_YELLOW, + LaneMarkType.SOLID_YELLOW, + LaneMarkType.SOLID_DASH_YELLOW, + LaneMarkType.SOLID_BLUE, + } + ) + def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): super().__init__() self._log = logging.getLogger(self.__class__.__name__) @@ -238,26 +261,56 @@ def bounding_box(self) -> Optional[BoundingBox]: def is_same_map(self, map_spec) -> bool: return map_spec.source == self._map_spec.source + def _compute_traffic_dividers(self) -> Tuple[List, List]: + lane_dividers = [] # divider between lanes with same traffic direction + road_dividers = [] # divider between roads with opposite traffic direction + processed_ids = [] + + for lane_seg in self._avm.get_scenario_lane_segments(): + if lane_seg.id in processed_ids: + continue + if lane_seg.right_neighbor_id is None: + cur_seg = lane_seg + while True: + if cur_seg.left_neighbor_id is None or cur_seg.id in processed_ids: + break # This is the leftmost lane in the road, so stop + else: + left_mark = cur_seg.left_lane_marking.mark_type + lane = self.lane_by_id(f"lane-{cur_seg.id}") + left_boundary = [(p[0], p[1]) for p in lane.left_pts] + if left_mark in ArgoverseMap.LANE_MARKINGS: + lane_dividers.append(left_boundary) + elif left_mark in ArgoverseMap.ROAD_MARKINGS: + road_dividers.append(left_boundary) + processed_ids.append(cur_seg.id) + cur_seg = self._avm.vector_lane_segments[ + cur_seg.left_neighbor_id + ] + + return lane_dividers, road_dividers + def to_glb(self, glb_dir): polygons = [] for lane_id, lane in self._lanes.items(): metadata = { "road_id": lane.road.road_id, "lane_id": lane_id, - # "lane_index": lane.index, TODO + "lane_index": lane.index, } polygons.append((lane.shape(), metadata)) - # lane_dividers, edge_dividers = self._compute_traffic_dividers() + lane_dividers, edge_dividers = self._compute_traffic_dividers() - # road_lines_glb = self._make_road_line_glb(edge_dividers) - # road_lines_glb.write_glb(Path(glb_dir) / "road_lines.glb") + map_glb = make_map_glb( + polygons, self.bounding_box, lane_dividers, edge_dividers + ) + map_glb.write_glb(Path(glb_dir) / "map.glb") - # lane_lines_glb = self._make_road_line_glb(lane_dividers) - # lane_lines_glb.write_glb(Path(glb_dir) / "lane_lines.glb") + road_lines_glb = make_road_line_glb(edge_dividers) + road_lines_glb.write_glb(Path(glb_dir) / "road_lines.glb") - map_glb = make_map_glb(polygons, self.bounding_box, [], []) - map_glb.write_glb(Path(glb_dir) / "map.glb") + lane_lines_glb = make_road_line_glb(lane_dividers) + lane_lines_glb.write_glb(Path(glb_dir) / "lane_lines.glb") class Surface(RoadMapWithCaches.Surface): def __init__(self, surface_id: str, road_map): From fa01f39e0c93e38d06b217102e61b15b19b2a4cd Mon Sep 17 00:00:00 2001 From: Saul Field Date: Fri, 24 Feb 2023 15:03:37 -0500 Subject: [PATCH 11/28] Traffic histories --- scenarios/argoverse/scenario.py | 26 ++++--- smarts/sstudio/genhistories.py | 125 +++++++++++++++++++++++++++++--- 2 files changed, 131 insertions(+), 20 deletions(-) diff --git a/scenarios/argoverse/scenario.py b/scenarios/argoverse/scenario.py index 49a6a25748..f5ac3ad6ce 100644 --- a/scenarios/argoverse/scenario.py +++ b/scenarios/argoverse/scenario.py @@ -3,20 +3,26 @@ from smarts.sstudio import gen_scenario from smarts.sstudio import types as t -dataset_path = None +# scenario_path is a directory with the following structure: +# /path/to/dataset/{scenario_id} +# ├── log_map_archive_{scenario_id}.json +# └── scenario_{scenario_id}.parquet -# traffic_histories = [ -# t.TrafficHistoryDataset( -# name=f"Argoverse", -# source_type="Argoverse", -# input_path=dataset_path, -# ) -# ] +scenario_id = None # e.g. "0000b6ab-e100-4f6b-aee8-b520b57c0530" +scenario_path = None # e.g. Path("/home/user/argoverse/train/") / scenario_id + +traffic_histories = [ + t.TrafficHistoryDataset( + name=f"Argoverse_{scenario_id}", + source_type="Argoverse", + input_path=scenario_path, + ) +] gen_scenario( t.Scenario( - map_spec=t.MapSpec(source=f"{dataset_path}", lanepoint_spacing=1.0), - # traffic_histories=traffic_histories, + map_spec=t.MapSpec(source=f"{scenario_path}", lanepoint_spacing=1.0), + traffic_histories=traffic_histories, ), output_dir=Path(__file__).parent, ) diff --git a/smarts/sstudio/genhistories.py b/smarts/sstudio/genhistories.py index 7706a758fb..e3cbfc4c43 100644 --- a/smarts/sstudio/genhistories.py +++ b/smarts/sstudio/genhistories.py @@ -24,6 +24,7 @@ import logging import math import os +from pathlib import Path import sqlite3 import sys from collections import deque @@ -43,16 +44,6 @@ from smarts.sstudio import types from smarts.waymo.waymo_utils import WaymoDatasetError -try: - # pytype: disable=import-error - from waymo_open_dataset.protos import scenario_pb2 - from waymo_open_dataset.protos.map_pb2 import TrafficSignalLaneState - - # pytype: enable=import-error -except ImportError: - print( - "You may not have installed the [waymo] dependencies required to use the waymo replay simulation. Install them first using the command `pip install -e .[waymo]` at the source directory." - ) METERS_PER_FOOT = 0.3048 DEFAULT_LANE_WIDTH = 3.7 # a typical US highway lane is 12ft ~= 3.7m wide @@ -796,10 +787,21 @@ def column_val_in_row(self, row, col_name: str) -> Any: class Waymo(_TrajectoryDataset): """A tool for conversion of a Waymo dataset for use within SMARTS.""" + try: + import waymo_open_dataset # pytype: disable=import-error + except ImportError: + print( + "You may not have installed the [waymo] dependencies required to use the waymo replay simulation. Install them first using the command `pip install -e .[waymo]` at the source directory." + ) + def __init__(self, dataset_spec: Dict[str, Any], output: str): super().__init__(dataset_spec, output) def _get_scenario(self): + from waymo_open_dataset.protos import ( + scenario_pb2, + ) # pytype: disable=import-error + if "scenario_id" not in self._dataset_spec: errmsg = "Dataset spec requires scenario_id to be set" self._log.error(errmsg) @@ -939,6 +941,10 @@ def lerp(a: float, b: float, t: float) -> float: yield rows[j] def _encode_tl_state(self, waymo_state) -> SignalLightState: + from waymo_open_dataset.protos.map_pb2 import ( + TrafficSignalLaneState, + ) # pytype: disable=import-error + if waymo_state == TrafficSignalLaneState.LANE_STATE_STOP: return SignalLightState.STOP if waymo_state == TrafficSignalLaneState.LANE_STATE_CAUTION: @@ -989,6 +995,103 @@ def column_val_in_row(self, row, col_name: str) -> Any: return row[col_name] +class Argoverse(_TrajectoryDataset): + """A tool for conversion of a Waymo dataset for use within SMARTS.""" + + def __init__(self, dataset_spec: Dict[str, Any], output: str): + super().__init__(dataset_spec, output) + + @property + def rows(self) -> Generator[Dict, None, None]: + try: + # pytype: disable=import-error + from av2.datasets.motion_forecasting.scenario_serialization import ( + load_argoverse_scenario_parquet, + ) + from av2.datasets.motion_forecasting.data_schema import ( + ObjectType as AvObjectType, + ) + + # pytype: enable=import-error + except ImportError: + print( + "You may not have installed the [argoverse] dependencies required to use the Argoverse 2 replay simulation. Install them first using the command `pip install -e .[argoverse]` at the source directory." + ) + + ALLOWED_TYPES = frozenset( + { + AvObjectType.VEHICLE, + AvObjectType.PEDESTRIAN, + AvObjectType.MOTORCYCLIST, + AvObjectType.CYCLIST, + AvObjectType.BUS, + } + ) + + def _lookup_agent_type(agent_type: AvObjectType) -> int: + # 1=motorcycle, 2=auto, 3=truck, 4=pedestrian/bicycle + if agent_type == AvObjectType.MOTORCYCLIST: + return 1 + elif agent_type == AvObjectType.VEHICLE: + return 2 + elif agent_type == AvObjectType.BUS: + return 3 + elif agent_type in [AvObjectType.PEDESTRIAN, AvObjectType.CYCLIST]: + return 4 # cyclist + else: + return 0 # other + + input_dir = Path(self._dataset_spec["input_path"]) + scenario_id = input_dir.stem + parquet_file = input_dir / f"scenario_{scenario_id}.parquet" + scenario = load_argoverse_scenario_parquet(parquet_file) + + # Normalize to start at 0, and convert to milliseconds + timestamps = (scenario.timestamps_ns - scenario.timestamps_ns[0]) * 1e-6 + + # The Ego vehicle has a string ID, so we need to give it a unique int ID + all_ids = [int(t.track_id) for t in scenario.tracks if t.track_id != "AV"] + ego_id = max(all_ids) + 1 + + for track in scenario.tracks: + # Only use dynamic objects + if track.object_type not in ALLOWED_TYPES: + continue + + if track.track_id == "AV": + is_ego = 1 + vehicle_id = ego_id + else: + is_ego = 0 + vehicle_id = int(track.track_id) + vehicle_type = _lookup_agent_type(track.object_type) + + for obj_state in track.object_states: + row = dict() + row["vehicle_id"] = vehicle_id + row["type"] = vehicle_type + row["sim_time"] = timestamps[obj_state.timestep] + row["position_x"] = obj_state.position[0] + row["position_y"] = obj_state.position[1] + row["heading_rad"] = constrain_angle( + (obj_state.heading - math.pi / 2) % (2 * math.pi) + ) + row["speed"] = np.linalg.norm(np.array(obj_state.velocity)) + row["lane_id"] = 0 + row["is_ego_vehicle"] = is_ego + + # Dimensions are not present in the Argoverse data. Setting these to 0 + # means default values for each vehicle type will be used. + # See TrafficHistory.decode_vehicle_type(). + row["length"] = 0 + row["height"] = 0 + row["width"] = 0 + yield row + + def column_val_in_row(self, row, col_name: str) -> Any: + return row[col_name] + + def import_dataset( dataset_spec: types.TrafficHistoryDataset, output_path: str, @@ -1012,6 +1115,8 @@ def import_dataset( dataset = Interaction(dataset_dict, output) elif source == "Waymo": dataset = Waymo(dataset_dict, output) + elif source == "Argoverse": + dataset = Argoverse(dataset_dict, output) else: raise ValueError( f"unsupported TrafficHistoryDataset type: {dataset_spec.source_type}" From 8486f169e495df2fc958b1c04d044361457da603 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Tue, 28 Feb 2023 19:00:53 -0500 Subject: [PATCH 12/28] GLB refactor --- smarts/core/opendrive_road_network.py | 101 ++++---------------------- smarts/core/sumo_road_network.py | 81 ++------------------- smarts/core/waymo_map.py | 73 ++----------------- 3 files changed, 26 insertions(+), 229 deletions(-) diff --git a/smarts/core/opendrive_road_network.py b/smarts/core/opendrive_road_network.py index 9824ccae92..89bd4f2ea1 100644 --- a/smarts/core/opendrive_road_network.py +++ b/smarts/core/opendrive_road_network.py @@ -32,10 +32,10 @@ import numpy as np import rtree -import trimesh -import trimesh.scene from cached_property import cached_property +from smarts.core.utils.glb import make_map_glb, make_road_line_glb + # pytype: disable=import-error @@ -75,11 +75,9 @@ from shapely.geometry import Point as SPoint from shapely.geometry import Polygon -from trimesh.exchange import gltf from smarts.core.road_map import RoadMap, RoadMapWithCaches, Waypoint from smarts.core.route_cache import RouteWithCache -from smarts.core.utils.geometry import generate_meshes_from_polygons from smarts.core.utils.key_wrapper import KeyWrapper from smarts.core.utils.math import ( CubicPolynomial, @@ -96,35 +94,6 @@ from .lanepoints import LanePoints, LinkedLanePoint -def _convert_camera(camera): - result = { - "name": camera.name, - "type": "perspective", - "perspective": { - "aspectRatio": camera.fov[0] / camera.fov[1], - "yfov": np.radians(camera.fov[1]), - "znear": float(camera.z_near), - # HACK: The trimesh gltf export doesn't include a zfar which Panda3D GLB - # loader expects. Here we override to make loading possible. - "zfar": float(camera.z_near + 100), - }, - } - return result - - -gltf._convert_camera = _convert_camera - - -class _GLBData: - def __init__(self, bytes_): - self._bytes = bytes_ - - def write_glb(self, output_path: str): - """Generate a geometry file.""" - with open(output_path, "wb") as f: - f.write(self._bytes) - - @dataclass class LaneBoundary: """Describes a lane boundary.""" @@ -763,32 +732,8 @@ def bounding_box(self) -> BoundingBox: ) def to_glb(self, glb_dir): - lane_dividers, edge_dividers = self._compute_traffic_dividers() - map_glb = self._make_glb_from_polys(lane_dividers, edge_dividers) - map_glb.write_glb(Path(glb_dir) / "map.glb") - - road_lines_glb = self._make_road_line_glb(edge_dividers) - road_lines_glb.write_glb(Path(glb_dir) / "road_lines.glb") - - lane_lines_glb = self._make_road_line_glb(lane_dividers) - lane_lines_glb.write_glb(Path(glb_dir) / "lane_lines.glb") - - def _make_road_line_glb(self, lines: List[List[Tuple[float, float]]]): - scene = trimesh.Scene() - for line_pts in lines: - vertices = [(*pt, 0.1) for pt in line_pts] - point_cloud = trimesh.PointCloud(vertices=vertices) - point_cloud.apply_transform( - trimesh.transformations.rotation_matrix(math.pi / 2, [-1, 0, 0]) - ) - scene.add_geometry(point_cloud) - return _GLBData(gltf.export_glb(scene)) - - def _make_glb_from_polys(self, lane_dividers, edge_dividers): - scene = trimesh.Scene() polygons = [] - for lane_id in self._lanes: - lane = self._lanes[lane_id] + for lane_id, lane in self._lanes.items(): metadata = { "road_id": lane.road.road_id, "lane_id": lane_id, @@ -796,36 +741,18 @@ def _make_glb_from_polys(self, lane_dividers, edge_dividers): } polygons.append((lane.shape(), metadata)) - meshes = generate_meshes_from_polygons(polygons) - - # Attach additional information for rendering as metadata in the map glb - # <2D-BOUNDING_BOX>: four floats separated by ',' (,,,), - # which describe x-minimum, y-minimum, x-maximum, and y-maximum - metadata = { - "bounding_box": ( - self.bounding_box.min_pt.x, - self.bounding_box.min_pt.y, - self.bounding_box.max_pt.x, - self.bounding_box.max_pt.y, - ) - } + lane_dividers, edge_dividers = self._compute_traffic_dividers() - # lane markings information - metadata["lane_dividers"] = lane_dividers - metadata["edge_dividers"] = edge_dividers + map_glb = make_map_glb( + polygons, self.bounding_box, lane_dividers, edge_dividers + ) + map_glb.write_glb(Path(glb_dir) / "map.glb") - for mesh in meshes: - mesh.visual = trimesh.visual.TextureVisuals( - material=trimesh.visual.material.PBRMaterial() - ) + road_lines_glb = make_road_line_glb(edge_dividers) + road_lines_glb.write_glb(Path(glb_dir) / "road_lines.glb") - road_id = mesh.metadata["road_id"] - lane_id = mesh.metadata.get("lane_id") - name = f"{road_id}" - if lane_id is not None: - name += f"-{lane_id}" - scene.add_geometry(mesh, name, extras=mesh.metadata) - return _GLBData(gltf.export_glb(scene, extras=metadata, include_normals=True)) + lane_lines_glb = make_road_line_glb(lane_dividers) + lane_lines_glb.write_glb(Path(glb_dir) / "lane_lines.glb") def _compute_traffic_dividers(self): lane_dividers = [] # divider between lanes with same traffic direction @@ -1766,7 +1693,8 @@ def _equally_spaced_path( width_threshold=None, ) -> List[Waypoint]: """given a list of LanePoints starting near point, return corresponding - Waypoints that may not be evenly spaced (due to lane change) but start at point.""" + Waypoints that may not be evenly spaced (due to lane change) but start at point. + """ continuous_variables = [ "positions_x", @@ -1785,7 +1713,6 @@ def _equally_spaced_path( skip_lanepoints = False index_skipped = [] for idx, lanepoint in enumerate(path): - if lanepoint.is_inferred and 0 < idx < len(path) - 1: continue diff --git a/smarts/core/sumo_road_network.py b/smarts/core/sumo_road_network.py index 76225f2972..a9265d5723 100644 --- a/smarts/core/sumo_road_network.py +++ b/smarts/core/sumo_road_network.py @@ -18,7 +18,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import logging -import math import os import random from functools import lru_cache @@ -27,13 +26,10 @@ from typing import Any, Dict, List, Optional, Sequence, Set, Tuple import numpy as np -import trimesh -import trimesh.scene from cached_property import cached_property from shapely.geometry import Point as shPoint from shapely.geometry import Polygon from shapely.ops import nearest_points, snap -from trimesh.exchange import gltf from smarts.sstudio.types import MapSpec @@ -42,42 +38,13 @@ from .road_map import RoadMap, Waypoint from .route_cache import RouteWithCache from .utils.geometry import buffered_shape -from .utils.glb import make_map_glb +from .utils.glb import make_map_glb, make_road_line_glb from .utils.math import inplace_unwrap, radians_to_vec, vec_2d from smarts.core.utils.sumo import sumolib # isort:skip from sumolib.net.edge import Edge # isort:skip -def _convert_camera(camera): - result = { - "name": camera.name, - "type": "perspective", - "perspective": { - "aspectRatio": camera.fov[0] / camera.fov[1], - "yfov": np.radians(camera.fov[1]), - "znear": float(camera.z_near), - # HACK: The trimesh gltf export doesn't include a zfar which Panda3D GLB - # loader expects. Here we override to make loading possible. - "zfar": float(camera.z_near + 100), - }, - } - return result - - -gltf._convert_camera = _convert_camera - - -class _GLBData: - def __init__(self, bytes_): - self._bytes = bytes_ - - def write_glb(self, output_path): - """Generate a `.glb` geometry file.""" - with open(output_path, "wb") as f: - f.write(self._bytes) - - class SumoRoadNetwork(RoadMap): """A road network for a SUMO source.""" @@ -283,15 +250,15 @@ def scale_factor(self) -> float: return self._default_lane_width / SumoRoadNetwork.DEFAULT_LANE_WIDTH def to_glb(self, glb_dir): - lane_dividers, edge_dividers = self._compute_traffic_dividers() polys = self._compute_road_polygons() - map_glb = make_map_glb(polys, self.bounding_box, [], []) + lane_dividers, edge_dividers = self._compute_traffic_dividers() + map_glb = make_map_glb(polys, self.bounding_box, lane_dividers, edge_dividers) map_glb.write_glb(Path(glb_dir) / "map.glb") - road_lines_glb = self._make_road_line_glb(edge_dividers) + road_lines_glb = make_road_line_glb(edge_dividers) road_lines_glb.write_glb(Path(glb_dir) / "road_lines.glb") - lane_lines_glb = self._make_road_line_glb(lane_dividers) + lane_lines_glb = make_road_line_glb(lane_dividers) lane_lines_glb.write_glb(Path(glb_dir) / "lane_lines.glb") class Surface(RoadMap.Surface): @@ -1280,44 +1247,6 @@ def _snap_external_holes(self, lane_to_poly, snap_threshold=2): if new_coords: lane_to_poly[lane_id] = (Polygon(new_coords), metadata) - def _make_road_line_glb(self, lines: List[List[Tuple[float, float]]]): - scene = trimesh.Scene() - for line_pts in lines: - vertices = [(*pt, 0.1) for pt in line_pts] - point_cloud = trimesh.PointCloud(vertices=vertices) - point_cloud.apply_transform( - trimesh.transformations.rotation_matrix(math.pi / 2, [-1, 0, 0]) - ) - scene.add_geometry(point_cloud) - return _GLBData(gltf.export_glb(scene)) - - def _make_glb_from_polys(self, polygons, lane_dividers, edge_dividers): - scene = trimesh.Scene() - meshes = generate_meshes_from_polygons(polygons) - # Attach additional information for rendering as metadata in the map glb - metadata = {} - - # <2D-BOUNDING_BOX>: four floats separated by ',' (,,,), - # which describe x-minimum, y-minimum, x-maximum, and y-maximum - metadata["bounding_box"] = self._graph.getBoundary() - - # lane markings information - metadata["lane_dividers"] = lane_dividers - metadata["edge_dividers"] = edge_dividers - - for mesh in meshes: - mesh.visual = trimesh.visual.TextureVisuals( - material=trimesh.visual.material.PBRMaterial() - ) - - road_id = mesh.metadata["road_id"] - lane_id = mesh.metadata.get("lane_id") - name = f"{road_id}" - if lane_id is not None: - name += f"-{lane_id}" - scene.add_geometry(mesh, name, extras=mesh.metadata) - return _GLBData(gltf.export_glb(scene, extras=metadata, include_normals=True)) - def _compute_traffic_dividers(self, threshold=1): lane_dividers = [] # divider between lanes with same traffic direction edge_dividers = [] # divider between lanes with opposite traffic direction diff --git a/smarts/core/waymo_map.py b/smarts/core/waymo_map.py index 82263ec938..87bd98d19b 100644 --- a/smarts/core/waymo_map.py +++ b/smarts/core/waymo_map.py @@ -32,11 +32,10 @@ import numpy as np import rtree -import trimesh from cached_property import cached_property from shapely.geometry import Point as SPoint from shapely.geometry import Polygon -from trimesh.exchange import gltf +from smarts.core.utils.glb import make_map_glb, make_road_line_glb from waymo_open_dataset.protos import scenario_pb2 from waymo_open_dataset.protos.map_pb2 import ( Crosswalk, @@ -54,7 +53,7 @@ from .road_map import RoadMap, RoadMapWithCaches, Waypoint from .route_cache import RouteWithCache from .utils.file import read_tfrecord_file -from .utils.geometry import buffered_shape, generate_meshes_from_polygons +from .utils.geometry import buffered_shape from .utils.math import ( inplace_unwrap, line_intersect_vectorized, @@ -64,35 +63,6 @@ ) -def _convert_camera(camera): - result = { - "name": camera.name, - "type": "perspective", - "perspective": { - "aspectRatio": camera.fov[0] / camera.fov[1], - "yfov": np.radians(camera.fov[1]), - "znear": float(camera.z_near), - # HACK: The trimesh gltf export doesn't include a zfar which Panda3D GLB - # loader expects. Here we override to make loading possible. - "zfar": float(camera.z_near + 100), - }, - } - return result - - -gltf._convert_camera = _convert_camera - - -class _GLBData: - def __init__(self, bytes_): - self._bytes = bytes_ - - def write_glb(self, output_path: str): - """Generate a geometry file.""" - with open(output_path, "wb") as f: - f.write(self._bytes) - - class WaymoMap(RoadMapWithCaches): """A map associated with a Waymo dataset""" @@ -992,14 +962,8 @@ def scale_factor(self) -> float: def to_glb(self, glb_dir): """Build a glb file for camera rendering and envision.""" - glb = self._make_glb_from_polys() - glb.write_glb(Path(glb_dir) / "map.glb") - - def _make_glb_from_polys(self): - scene = trimesh.Scene() polygons = [] - for lane_id in self._lanes: - lane = self._lanes[lane_id] + for lane_id, lane in self._lanes.items(): metadata = { "road_id": lane.road.road_id, "lane_id": lane_id, @@ -1007,36 +971,13 @@ def _make_glb_from_polys(self): } polygons.append((lane.shape(), metadata)) - meshes = generate_meshes_from_polygons(polygons) - - # Attach additional information for rendering as metadata in the map glb - # <2D-BOUNDING_BOX>: four floats separated by ',' (,,,), - # which describe x-minimum, y-minimum, x-maximum, and y-maximum - metadata = { - "bounding_box": ( - self.bounding_box.min_pt.x, - self.bounding_box.min_pt.y, - self.bounding_box.max_pt.x, - self.bounding_box.max_pt.y, - ) - } - - # lane markings information lane_dividers = self._compute_traffic_dividers() - metadata["lane_dividers"] = lane_dividers - for mesh in meshes: - mesh.visual = trimesh.visual.TextureVisuals( - material=trimesh.visual.material.PBRMaterial() - ) + map_glb = make_map_glb(polygons, self.bounding_box, lane_dividers, []) + map_glb.write_glb(Path(glb_dir) / "map.glb") - road_id = mesh.metadata["road_id"] - lane_id = mesh.metadata.get("lane_id") - name = f"{road_id}" - if lane_id is not None: - name += f"-{lane_id}" - scene.add_geometry(mesh, name, extras=mesh.metadata) - return _GLBData(gltf.export_glb(scene, extras=metadata, include_normals=True)) + lane_lines_glb = make_road_line_glb(lane_dividers) + lane_lines_glb.write_glb(Path(glb_dir) / "lane_lines.glb") def _compute_traffic_dividers(self): lane_dividers = [] # divider between lanes with same traffic direction From f01af469ac122f13d81e9e9338b165f125052ca9 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Wed, 1 Mar 2023 10:46:35 -0500 Subject: [PATCH 13/28] Copyright header, docstrings, type fixes --- smarts/core/argoverse_map.py | 52 +++++++++++++++++++++++++++++------- smarts/core/utils/glb.py | 32 ++++++++++++++++++---- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index aa3d19baaf..8ac1923643 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -1,15 +1,36 @@ -from functools import lru_cache +# Copyright (C) 2022. Huawei Technologies Co., Ltd. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + import heapq import logging -import math -from pathlib import Path import random -from cached_property import cached_property import time +from functools import lru_cache +from pathlib import Path from typing import Dict, List, Optional, Sequence, Set, Tuple import numpy as np import rtree +from cached_property import cached_property +from shapely.geometry import Point as SPoint +from shapely.geometry import Polygon from smarts.core.coordinates import BoundingBox, Heading, Point, Pose, RefLinePoint from smarts.core.lanepoints import LanePoints, LinkedLanePoint @@ -23,11 +44,16 @@ vec_2d, ) from smarts.sstudio.types import MapSpec -from av2.map.map_api import ArgoverseStaticMap -from av2.map.lane_segment import LaneMarkType, LaneSegment -from av2.geometry.interpolate import interp_arc -from shapely.geometry import Polygon -from shapely.geometry import Point as SPoint + +try: + from av2.geometry.interpolate import interp_arc + from av2.map.lane_segment import LaneMarkType, LaneSegment + from av2.map.map_api import ArgoverseStaticMap +except: + raise ImportError( + "You may not have installed the [argoverse] dependencies required for using Argoverse 2 maps with SMARTS. Install it first using the command `pip install -e .[argoverse]` at the source directory." + "" + ) class ArgoverseMap(RoadMapWithCaches): @@ -313,6 +339,8 @@ def to_glb(self, glb_dir): lane_lines_glb.write_glb(Path(glb_dir) / "lane_lines.glb") class Surface(RoadMapWithCaches.Surface): + """Surface representation for Argoverse maps.""" + def __init__(self, surface_id: str, road_map): self._surface_id = surface_id @@ -328,6 +356,8 @@ def surface_by_id(self, surface_id: str) -> RoadMap.Surface: return self._surfaces.get(surface_id) class Lane(RoadMapWithCaches.Lane, Surface): + """Lane representation for Argoverse maps.""" + def __init__( self, map: "ArgoverseMap", lane_id: str, lane_seg: LaneSegment, index: int ): @@ -589,6 +619,8 @@ def road_with_point(self, point: Point) -> RoadMap.Road: return None class Road(RoadMapWithCaches.Road, Surface): + """Road representation for Argoverse maps.""" + def __init__(self, road_id: str, lanes: List[RoadMap.Lane]): super().__init__(road_id, None) self._road_id = road_id @@ -697,6 +729,8 @@ def road_by_id(self, road_id: str) -> RoadMap.Road: return road class Route(RouteWithCache): + """Describes a route between Argoverse roads.""" + def __init__(self, road_map): super().__init__(road_map) self._roads = [] diff --git a/smarts/core/utils/glb.py b/smarts/core/utils/glb.py index 37c1ddaaa1..f1c53a0c27 100644 --- a/smarts/core/utils/glb.py +++ b/smarts/core/utils/glb.py @@ -1,13 +1,33 @@ -import numpy as np -from trimesh.exchange import gltf +# Copyright (C) 2022. Huawei Technologies Co., Ltd. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + import math -from typing import Any, Dict, List, Tuple +from pathlib import Path +from typing import Any, Dict, List, Tuple, Union import numpy as np import trimesh from shapely.geometry import Polygon -from smarts.core.coordinates import BoundingBox +from trimesh.exchange import gltf +from smarts.core.coordinates import BoundingBox from smarts.core.utils.geometry import triangulate_polygon @@ -36,7 +56,7 @@ class GLBData: def __init__(self, bytes_): self._bytes = bytes_ - def write_glb(self, output_path: str): + def write_glb(self, output_path: Union[str, Path]): """Generate a geometry file.""" with open(output_path, "wb") as f: f.write(self._bytes) @@ -93,6 +113,7 @@ def make_map_glb( lane_dividers, edge_dividers, ) -> GLBData: + """Create a GLB file from a list of road polygons.""" scene = trimesh.Scene() # Attach additional information for rendering as metadata in the map glb @@ -123,6 +144,7 @@ def make_map_glb( def make_road_line_glb(lines: List[List[Tuple[float, float]]]) -> GLBData: + """Create a GLB file from a list of road/lane lines.""" scene = trimesh.Scene() for line_pts in lines: vertices = [(*pt, 0.1) for pt in line_pts] From e6a05b29154edc65ff7a3661e20d1663cdd4cadf Mon Sep 17 00:00:00 2001 From: Saul Field Date: Wed, 1 Mar 2023 10:48:46 -0500 Subject: [PATCH 14/28] Ignore argoverse_map.py in tests --- .github/workflows/ci-base-tests-linux.yml | 3 ++- .github/workflows/ci-base-tests-mac.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-base-tests-linux.yml b/.github/workflows/ci-base-tests-linux.yml index 266b9da555..23bbf53046 100644 --- a/.github/workflows/ci-base-tests-linux.yml +++ b/.github/workflows/ci-base-tests-linux.yml @@ -41,9 +41,10 @@ jobs: -n auto \ --ignore-glob="**/ros.py" \ --ignore-glob="**/waymo_map.py" \ + --ignore-glob="**/argoverse_map.py" \ ${{matrix.tests}} \ --ignore=./smarts/core/tests/test_smarts_memory_growth.py \ --ignore=./smarts/core/tests/test_env_frame_rate.py \ --ignore=./smarts/env/tests/test_benchmark.py \ --ignore=./examples/tests/test_learning.py \ - -k 'not test_long_determinism' \ No newline at end of file + -k 'not test_long_determinism' diff --git a/.github/workflows/ci-base-tests-mac.yml b/.github/workflows/ci-base-tests-mac.yml index 905647f700..cd2bc54498 100644 --- a/.github/workflows/ci-base-tests-mac.yml +++ b/.github/workflows/ci-base-tests-mac.yml @@ -58,6 +58,7 @@ jobs: --doctest-modules \ -n auto \ --ignore-glob="**/waymo_map.py" \ + --ignore-glob="**/argoverse_map.py" \ ${{matrix.tests}} \ --ignore=./smarts/core/tests/test_smarts_memory_growth.py \ --ignore=./smarts/env/tests/test_benchmark.py \ @@ -66,4 +67,4 @@ jobs: --ignore=./smarts/core/tests/test_renderers.py \ --ignore=./smarts/core/tests/test_smarts.py \ --ignore=./smarts/core/tests/test_env_frame_rate.py \ - --ignore=./smarts/core/tests/test_observations.py \ No newline at end of file + --ignore=./smarts/core/tests/test_observations.py From 5df361b12cafef6b44c9c839b503783d33cc0ef9 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Wed, 1 Mar 2023 10:52:58 -0500 Subject: [PATCH 15/28] Format --- smarts/core/road_map.py | 67 ++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/smarts/core/road_map.py b/smarts/core/road_map.py index d306c53e30..6f2d168a26 100644 --- a/smarts/core/road_map.py +++ b/smarts/core/road_map.py @@ -100,7 +100,9 @@ def feature_by_id(self, feature_id: str) -> RoadMap.Feature: """Find a feature in this road map that has the given identifier.""" raise NotImplementedError() - def dynamic_features_near(self, point: Point, radius: float) -> List[Tuple[RoadMap.Feature, float]]: + def dynamic_features_near( + self, point: Point, radius: float + ) -> List[Tuple[RoadMap.Feature, float]]: """Find features within radius meters of the given point.""" result = [] for feat in self.dynamic_features: @@ -109,7 +111,9 @@ def dynamic_features_near(self, point: Point, radius: float) -> List[Tuple[RoadM result.append((feat, dist)) return result - def nearest_surfaces(self, point: Point, radius: Optional[float] = None) -> List[Tuple[RoadMap.Surface, float]]: + def nearest_surfaces( + self, point: Point, radius: Optional[float] = None + ) -> List[Tuple[RoadMap.Surface, float]]: """Find surfaces (lanes, roads, etc.) on this road map that are near the given point.""" raise NotImplementedError() @@ -120,7 +124,9 @@ def nearest_lanes( Returns a list of tuples of lane and distance, sorted by distance.""" raise NotImplementedError() - def nearest_lane(self, point: Point, radius: Optional[float] = None, include_junctions=True) -> RoadMap.Lane: + def nearest_lane( + self, point: Point, radius: Optional[float] = None, include_junctions=True + ) -> RoadMap.Lane: """Find the nearest lane on this road map to the given point.""" nearest_lanes = self.nearest_lanes(point, radius, include_junctions) return nearest_lanes[0][0] if nearest_lanes else None @@ -221,11 +227,14 @@ def features_near(self, pose: Pose, radius: float) -> List[RoadMap.Feature]: """The features on this surface near the given pose.""" raise NotImplementedError() - def shape(self, buffer_width: float = 0.0, default_width: Optional[float] = None) -> Polygon: + def shape( + self, buffer_width: float = 0.0, default_width: Optional[float] = None + ) -> Polygon: """Returns a convex polygon representing this surface, buffered by buffered_width (which must be non-negative), where buffer_width is a buffer around the perimeter of the polygon. In some situations, it may be desirable to also specify a `default_width`, in which case the returned polygon should have a convex shape where the - distance across it is no less than buffered_width + default_width at any point.""" + distance across it is no less than buffered_width + default_width at any point. + """ raise NotImplementedError() def contains_point(self, point: Point) -> bool: @@ -287,7 +296,8 @@ def in_junction(self) -> bool: @property def index(self) -> int: """when not in_junction, 0 is outer / right-most (relative to lane heading) lane on road. - otherwise, index scheme is implementation-dependent, but must be deterministic.""" + otherwise, index scheme is implementation-dependent, but must be deterministic. + """ # TAI: UK roads raise NotImplementedError() @@ -365,7 +375,9 @@ def width_at_offset(self, offset: float) -> Tuple[float, float]: a width estimate with no confidence.""" raise NotImplementedError() - def project_along(self, start_offset: float, distance: float) -> Set[Tuple[RoadMap.Lane, float]]: + def project_along( + self, start_offset: float, distance: float + ) -> Set[Tuple[RoadMap.Lane, float]]: """Starting at start_offset along the lane, project locations (lane, offset tuples) reachable within distance, not including lane changes.""" result = set() @@ -432,7 +444,9 @@ def center_pose_at_point(self, point: Point) -> Pose: orientation = fast_quaternion_from_angle(vec_to_radians(desired_vector[:2])) return Pose(position=position, orientation=orientation) - def curvature_radius_at_offset(self, offset: float, lookahead: int = 5) -> float: + def curvature_radius_at_offset( + self, offset: float, lookahead: int = 5 + ) -> float: """lookahead (in meters) is the size of the window to use to compute the curvature, which must be at least 1 to make sense. This may return math.inf if the lane is straight.""" @@ -450,7 +464,9 @@ def curvature_radius_at_offset(self, offset: float, lookahead: int = 5) -> float heading_rad = vec_to_radians(vec[:2]) if prev_heading_rad is not None: # XXX: things like S curves can cancel out here - heading_deltas += min_angles_difference_signed(heading_rad, prev_heading_rad) + heading_deltas += min_angles_difference_signed( + heading_rad, prev_heading_rad + ) prev_heading_rad = heading_rad return i / heading_deltas if heading_deltas else math.inf @@ -662,22 +678,29 @@ def distance_between(self, start: RoutePoint, end: RoutePoint) -> float: """Distance along route between two points.""" raise NotImplementedError() - def project_along(self, start: RoutePoint, distance: float) -> Set[Tuple[RoadMap.Lane, float]]: + def project_along( + self, start: RoutePoint, distance: float + ) -> Set[Tuple[RoadMap.Lane, float]]: """Starting at point on the route, returns a set of possible locations (lane and offset pairs) further along the route that are distance away, not including lane changes.""" raise NotImplementedError() - def distance_from(self, cur_lane: RouteLane, route_road: Optional[RoadMap.Road] = None) -> Optional[float]: + def distance_from( + self, cur_lane: RouteLane, route_road: Optional[RoadMap.Road] = None + ) -> Optional[float]: """Returns the distance along the route from the beginning of the current lane to the beginning of the next occurrence of route_road, or if route_road is None, then to the end of the route.""" raise NotImplementedError() - def next_junction(self, cur_lane: RouteLane, offset: float) -> Optional[Tuple[RoadMap.Lane, float]]: + def next_junction( + self, cur_lane: RouteLane, offset: float + ) -> Optional[Tuple[RoadMap.Lane, float]]: """Returns a lane within the next junction along the route from beginning of the current lane to the returned lane it connects with in the junction, - and the distance to it from this offset, or (None, inf) if there aren't any.""" + and the distance to it from this offset, or (None, inf) if there aren't any. + """ raise NotImplementedError() @@ -736,7 +759,9 @@ def relative_heading(self, h: Heading) -> Heading: Returns: float: Relative heading in [-pi, pi]. """ - assert isinstance(h, Heading), "Heading h ({}) must be an instance of smarts.core.coordinates.Heading".format( + assert isinstance( + h, Heading + ), "Heading h ({}) must be an instance of smarts.core.coordinates.Heading".format( type(h) ) return self.heading.relative_to(h) @@ -799,7 +824,9 @@ def _normal_at_offset(self, offset: float) -> np.ndarray: @lru_cache(maxsize=1024) def to_lane_coord(self, world_point: Point) -> RefLinePoint: lc = RefLinePoint(s=self.offset_along_lane(world_point)) - offcenter_vector = world_point.as_np_array - self.from_lane_coord(lc).as_np_array + offcenter_vector = ( + world_point.as_np_array - self.from_lane_coord(lc).as_np_array + ) t_sign = np.sign(np.dot(offcenter_vector, self._normal_at_offset(lc.s))) return lc._replace(t=np.linalg.norm(offcenter_vector) * t_sign) @@ -843,8 +870,10 @@ def from_lane_coord(self, lane_pt: RefLinePoint) -> Point: """For a reference-line point in/along this segment, converts it to a world point.""" offset = lane_pt.s - self.offset return Point( - self.x + (offset * self.dx - lane_pt.t * self.dy) / self.dist_to_next, - self.y + (offset * self.dy + lane_pt.t * self.dx) / self.dist_to_next, + self.x + + (offset * self.dx - lane_pt.t * self.dy) / self.dist_to_next, + self.y + + (offset * self.dy + lane_pt.t * self.dx) / self.dist_to_next, ) class _OffsetWrapper: @@ -881,7 +910,9 @@ def segment_for_offset( segi -= 1 return segs[segi] - def _cache_lane_info(self, lane: RoadMapWithCaches.Lane) -> List[RoadMapWithCaches._SegmentCache.Segment]: + def _cache_lane_info( + self, lane: RoadMapWithCaches.Lane + ) -> List[RoadMapWithCaches._SegmentCache.Segment]: segs = self._lane_cache.get(lane.lane_id) if segs is not None: return segs From 6cbc2a75d03cf55646a6cfa580469f150d26605b Mon Sep 17 00:00:00 2001 From: Saul Field Date: Wed, 1 Mar 2023 11:18:57 -0500 Subject: [PATCH 16/28] Update Makefile --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index d2bea0c57e..a09cd6b21a 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,8 @@ test: build-all-scenarios -n `expr \( \`nproc\` \/ 2 \& \`nproc\` \> 3 \) \| 2` \ --nb-exec-timeout 65536 \ ./examples/tests ./smarts/env ./envision ./smarts/core ./smarts/sstudio ./tests \ + --ignore=./smarts/core/waymo_map.py \ + --ignore=./smarts/core/argoverse_map.py \ --ignore=./smarts/core/tests/test_smarts_memory_growth.py \ --ignore=./smarts/core/tests/test_env_frame_rate.py \ --ignore=./smarts/env/tests/test_benchmark.py \ From 874f21e57a9bbfab26d2e78d996c22b86a06e232 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Wed, 1 Mar 2023 17:18:12 -0500 Subject: [PATCH 17/28] Fix docstring --- smarts/sstudio/genhistories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarts/sstudio/genhistories.py b/smarts/sstudio/genhistories.py index e3cbfc4c43..94ef571431 100644 --- a/smarts/sstudio/genhistories.py +++ b/smarts/sstudio/genhistories.py @@ -996,7 +996,7 @@ def column_val_in_row(self, row, col_name: str) -> Any: class Argoverse(_TrajectoryDataset): - """A tool for conversion of a Waymo dataset for use within SMARTS.""" + """A tool for conversion of an Argoverse 2 dataset for use within SMARTS.""" def __init__(self, dataset_spec: Dict[str, Any], output: str): super().__init__(dataset_spec, output) From e2357556e4724211f39fda77487390543ff2cbe5 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Thu, 2 Mar 2023 16:14:33 -0500 Subject: [PATCH 18/28] Minor updates --- scenarios/argoverse/scenario.py | 2 +- smarts/core/argoverse_map.py | 1 - smarts/core/utils/geometry.py | 6 +++++- smarts/sstudio/genhistories.py | 12 ++++++------ 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/scenarios/argoverse/scenario.py b/scenarios/argoverse/scenario.py index f5ac3ad6ce..4ce77e8ed0 100644 --- a/scenarios/argoverse/scenario.py +++ b/scenarios/argoverse/scenario.py @@ -13,7 +13,7 @@ traffic_histories = [ t.TrafficHistoryDataset( - name=f"Argoverse_{scenario_id}", + name=f"argoverse_{scenario_id}", source_type="Argoverse", input_path=scenario_path, ) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index 8ac1923643..65673fe32f 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -87,7 +87,6 @@ class ArgoverseMap(RoadMapWithCaches): def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): super().__init__() self._log = logging.getLogger(self.__class__.__name__) - self._log.setLevel(logging.INFO) self._avm = avm self._argoverse_scenario_id = avm.log_id self._map_spec = map_spec diff --git a/smarts/core/utils/geometry.py b/smarts/core/utils/geometry.py index 96fd573450..78cced13b1 100644 --- a/smarts/core/utils/geometry.py +++ b/smarts/core/utils/geometry.py @@ -43,4 +43,8 @@ def buffered_shape(shape, width: float = 1.0) -> Polygon: def triangulate_polygon(polygon: Polygon): """Attempts to convert a polygon into triangles.""" # XXX: shapely.ops.triangulate current creates a convex fill of triangles. - return [tri_face for tri_face in triangulate(polygon) if tri_face.centroid.within(polygon)] + return [ + tri_face + for tri_face in triangulate(polygon) + if tri_face.centroid.within(polygon) + ] diff --git a/smarts/sstudio/genhistories.py b/smarts/sstudio/genhistories.py index 94ef571431..16aa728058 100644 --- a/smarts/sstudio/genhistories.py +++ b/smarts/sstudio/genhistories.py @@ -1029,15 +1029,15 @@ def rows(self) -> Generator[Dict, None, None]: ) def _lookup_agent_type(agent_type: AvObjectType) -> int: - # 1=motorcycle, 2=auto, 3=truck, 4=pedestrian/bicycle + # See decode_vehicle_type in traffic_history.py if agent_type == AvObjectType.MOTORCYCLIST: - return 1 + return 1 # motorcycle elif agent_type == AvObjectType.VEHICLE: - return 2 + return 2 # passenger elif agent_type == AvObjectType.BUS: - return 3 + return 3 # truck elif agent_type in [AvObjectType.PEDESTRIAN, AvObjectType.CYCLIST]: - return 4 # cyclist + return 4 # pedestrian/bicycle else: return 0 # other @@ -1049,7 +1049,7 @@ def _lookup_agent_type(agent_type: AvObjectType) -> int: # Normalize to start at 0, and convert to milliseconds timestamps = (scenario.timestamps_ns - scenario.timestamps_ns[0]) * 1e-6 - # The Ego vehicle has a string ID, so we need to give it a unique int ID + # The ego vehicle has a string ID, so we need to give it a unique int ID all_ids = [int(t.track_id) for t in scenario.tracks if t.track_id != "AV"] ego_id = max(all_ids) + 1 From 939344b0c3fa1853eb93a91c357e30421bba68e7 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Thu, 2 Mar 2023 16:22:19 -0500 Subject: [PATCH 19/28] Fill in missing methods --- smarts/core/argoverse_map.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index 65673fe32f..e3411c02c8 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -644,16 +644,6 @@ def __hash__(self) -> int: def road_id(self) -> str: return self._road_id - @property - def type(self) -> int: - """The type of this road.""" - raise NotImplementedError() - - @property - def type_as_str(self) -> str: - """The type of this road.""" - raise NotImplementedError() - @property def composite_road(self) -> RoadMap.Road: """Return an abstract Road composed of one or more RoadMap.Road segments @@ -667,8 +657,11 @@ def is_composite(self) -> bool: and composed out of subordinate Road objects.""" return False - @property + @cached_property def is_junction(self) -> bool: + for lane in self.lanes: + if lane.foes or len(lane.incoming_lanes) > 1: + return True return False @cached_property @@ -693,16 +686,21 @@ def outgoing_roads(self) -> List[RoadMap.Road]: } ) + @lru_cache(maxsize=16) def oncoming_roads_at_point(self, point: Point) -> List[RoadMap.Road]: - """Returns a list of nearby roads to point that are (roughly) - parallel to this one but have lanes that go in the opposite direction.""" - raise NotImplementedError() + result = [] + for lane in self.lanes: + offset = lane.to_lane_coord(point).s + result += [ + ol.road + for ol in lane.oncoming_lanes_at_offset(offset) + if ol.road != self + ] + return result @property def parallel_roads(self) -> List[RoadMap.Road]: - """Returns roads that start and end at the same - point as this one.""" - raise NotImplementedError() + return [] @property def lanes(self) -> List[RoadMap.Lane]: From 6a6b30a8945aebae23de27877ce23182db143920 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Thu, 2 Mar 2023 19:19:28 -0500 Subject: [PATCH 20/28] Handle edge cases --- smarts/core/argoverse_map.py | 30 ++++++++++++++++++++++-------- smarts/sstudio/genhistories.py | 29 ++++++++++++++++------------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index e3411c02c8..70c56babc0 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -87,6 +87,7 @@ class ArgoverseMap(RoadMapWithCaches): def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): super().__init__() self._log = logging.getLogger(self.__class__.__name__) + self._log.setLevel(logging.INFO) self._avm = avm self._argoverse_scenario_id = avm.log_id self._map_spec = map_spec @@ -198,16 +199,22 @@ def _load_map_data(self): neighbours: List[int] = [] cur_seg = lane_seg while True: - left = cur_seg.left_lane_marking.mark_type + left_mark = cur_seg.left_lane_marking.mark_type + left_id = cur_seg.left_neighbor_id if ( - cur_seg.left_neighbor_id is not None - and left == LaneMarkType.DASHED_WHITE + left_id is not None + and left_mark == LaneMarkType.DASHED_WHITE + and left_id in self._avm.vector_lane_segments ): # There is a valid lane to the left, so add it and continue - neighbours.append(cur_seg.left_neighbor_id) - cur_seg = self._avm.vector_lane_segments[ - cur_seg.left_neighbor_id - ] + left_seg = self._avm.vector_lane_segments[left_id] + + # Edge case: sometimes there can be a cycle (2 lanes can have each other as their left neighbour) + if left_seg.left_neighbor_id == cur_seg.id: + break + + cur_seg = left_seg + neighbours.append(left_id) else: break # This is the leftmost lane in the road, so stop @@ -297,7 +304,14 @@ def _compute_traffic_dividers(self) -> Tuple[List, List]: if lane_seg.right_neighbor_id is None: cur_seg = lane_seg while True: - if cur_seg.left_neighbor_id is None or cur_seg.id in processed_ids: + if ( + cur_seg.left_neighbor_id is None + or cur_seg.id in processed_ids + or ( + cur_seg.left_neighbor_id + not in self._avm.vector_lane_segments + ) + ): break # This is the leftmost lane in the road, so stop else: left_mark = cur_seg.left_lane_marking.mark_type diff --git a/smarts/sstudio/genhistories.py b/smarts/sstudio/genhistories.py index 16aa728058..29a196dd37 100644 --- a/smarts/sstudio/genhistories.py +++ b/smarts/sstudio/genhistories.py @@ -787,20 +787,18 @@ def column_val_in_row(self, row, col_name: str) -> Any: class Waymo(_TrajectoryDataset): """A tool for conversion of a Waymo dataset for use within SMARTS.""" - try: - import waymo_open_dataset # pytype: disable=import-error - except ImportError: - print( - "You may not have installed the [waymo] dependencies required to use the waymo replay simulation. Install them first using the command `pip install -e .[waymo]` at the source directory." - ) - def __init__(self, dataset_spec: Dict[str, Any], output: str): super().__init__(dataset_spec, output) def _get_scenario(self): - from waymo_open_dataset.protos import ( - scenario_pb2, - ) # pytype: disable=import-error + try: + from waymo_open_dataset.protos import ( + scenario_pb2, + ) # pytype: disable=import-error + except ImportError: + print( + "You may not have installed the [waymo] dependencies required to use the waymo replay simulation. Install them first using the command `pip install -e .[waymo]` at the source directory." + ) if "scenario_id" not in self._dataset_spec: errmsg = "Dataset spec requires scenario_id to be set" @@ -941,9 +939,14 @@ def lerp(a: float, b: float, t: float) -> float: yield rows[j] def _encode_tl_state(self, waymo_state) -> SignalLightState: - from waymo_open_dataset.protos.map_pb2 import ( - TrafficSignalLaneState, - ) # pytype: disable=import-error + try: + from waymo_open_dataset.protos.map_pb2 import ( + TrafficSignalLaneState, + ) # pytype: disable=import-error + except ImportError: + print( + "You may not have installed the [waymo] dependencies required to use the waymo replay simulation. Install them first using the command `pip install -e .[waymo]` at the source directory." + ) if waymo_state == TrafficSignalLaneState.LANE_STATE_STOP: return SignalLightState.STOP From 2c4675a3e670642a28eb318744f2495efdcaf288 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Fri, 3 Mar 2023 15:02:30 -0500 Subject: [PATCH 21/28] Add changelog entry --- CHANGELOG.md | 1 + smarts/core/argoverse_map.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c13e0d2f8..27570dfb41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Copy and pasting the git commit messages is __NOT__ enough. ## [Unreleased] ### Added - Added objective, scenario description, and trained agent performance, to the Driving Smarts 2022 benchmark documentation. +- Added support for the [Argoverse 2 Motion Forecasting Dataset](https://www.argoverse.org/av2.html#forecasting-link) (see `scenarios/argoverse`). ### Changed - Unique id suffix is removed from vehicle name while building agent vehicle in `VehicleIndex.build_agent_vehicle()` function. ### Deprecated diff --git a/smarts/core/argoverse_map.py b/smarts/core/argoverse_map.py index 70c56babc0..0e570ec884 100644 --- a/smarts/core/argoverse_map.py +++ b/smarts/core/argoverse_map.py @@ -87,7 +87,6 @@ class ArgoverseMap(RoadMapWithCaches): def __init__(self, map_spec: MapSpec, avm: ArgoverseStaticMap): super().__init__() self._log = logging.getLogger(self.__class__.__name__) - self._log.setLevel(logging.INFO) self._avm = avm self._argoverse_scenario_id = avm.log_id self._map_spec = map_spec From f6386ff35e3f2a875bc1422f354cd585470deb1e Mon Sep 17 00:00:00 2001 From: Saul Field Date: Mon, 6 Mar 2023 16:38:57 -0500 Subject: [PATCH 22/28] Add argoverse install test --- .github/workflows/ci-base-tests-linux.yml | 2 +- .github/workflows/ci-base-tests-mac.yml | 2 +- Makefile | 1 + smarts/core/tests/test_argoverse.py | 49 +++++++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 smarts/core/tests/test_argoverse.py diff --git a/.github/workflows/ci-base-tests-linux.yml b/.github/workflows/ci-base-tests-linux.yml index 23bbf53046..3721f28b83 100644 --- a/.github/workflows/ci-base-tests-linux.yml +++ b/.github/workflows/ci-base-tests-linux.yml @@ -29,7 +29,7 @@ jobs: . ${{env.venv_dir}}/bin/activate pip install --upgrade pip pip install --upgrade wheel - pip install -e .[camera_obs,opendrive,rllib,test,test_notebook,torch,train,gym] + pip install -e .[camera_obs,opendrive,rllib,test,test_notebook,torch,train,gym,argoverse] - name: Run smoke tests run: | . ${{env.venv_dir}}/bin/activate diff --git a/.github/workflows/ci-base-tests-mac.yml b/.github/workflows/ci-base-tests-mac.yml index 5251f51947..682e3dd436 100644 --- a/.github/workflows/ci-base-tests-mac.yml +++ b/.github/workflows/ci-base-tests-mac.yml @@ -47,7 +47,7 @@ jobs: pip install --upgrade pip pip install --upgrade wheel pip install -r utils/setup/mac_requirements.txt - pip install -e .[camera_obs,opendrive,rllib,test,test_notebook,torch,train] + pip install -e .[camera_obs,opendrive,rllib,test,test_notebook,torch,train,argoverse] - name: Run smoke tests run: | . ${{env.venv_dir}}/bin/activate diff --git a/Makefile b/Makefile index a09cd6b21a..d6c13a5167 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ test: build-all-scenarios ./examples/tests ./smarts/env ./envision ./smarts/core ./smarts/sstudio ./tests \ --ignore=./smarts/core/waymo_map.py \ --ignore=./smarts/core/argoverse_map.py \ + --ignore=./smarts/core/tests/test_argoverse.py \ --ignore=./smarts/core/tests/test_smarts_memory_growth.py \ --ignore=./smarts/core/tests/test_env_frame_rate.py \ --ignore=./smarts/env/tests/test_benchmark.py \ diff --git a/smarts/core/tests/test_argoverse.py b/smarts/core/tests/test_argoverse.py new file mode 100644 index 0000000000..3c59da96d6 --- /dev/null +++ b/smarts/core/tests/test_argoverse.py @@ -0,0 +1,49 @@ +# MIT License +# +# Copyright (C) 2022. Huawei Technologies Co., Ltd. All rights reserved. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import subprocess +import sys + + +def test_argoverse_package_compatibility(): + """Test that the av2 package is installed and has no conflicts with the rest of the SMARTS dependencies.""" + + try: + import av2 + except ModuleNotFoundError: + print( + "The argoverse dependencies must be installed using `pip install -e .[argoverse]`" + ) + raise + + try: + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "check", + ] + ) + except subprocess.CalledProcessError: + print("There is an incompatibility with the installed packages") + raise From c1c18dd93f478edfe3625c2ca0f7bc3cbaace509 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Mon, 6 Mar 2023 16:39:29 -0500 Subject: [PATCH 23/28] Update smarts/core/utils/glb.py Co-authored-by: Tucker Alban --- smarts/core/utils/glb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarts/core/utils/glb.py b/smarts/core/utils/glb.py index f1c53a0c27..af9917cf99 100644 --- a/smarts/core/utils/glb.py +++ b/smarts/core/utils/glb.py @@ -136,7 +136,7 @@ def make_map_glb( road_id = mesh.metadata["road_id"] lane_id = mesh.metadata.get("lane_id") - name = f"{road_id}" + name = str(road_id) if lane_id is not None: name += f"-{lane_id}" scene.add_geometry(mesh, name, extras=mesh.metadata) From f97faf3eb0526e5420022eaaf12824214e884cc8 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Mon, 6 Mar 2023 16:40:50 -0500 Subject: [PATCH 24/28] Minor change from PR review --- smarts/sstudio/genhistories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smarts/sstudio/genhistories.py b/smarts/sstudio/genhistories.py index 29a196dd37..e3aa064d58 100644 --- a/smarts/sstudio/genhistories.py +++ b/smarts/sstudio/genhistories.py @@ -1039,7 +1039,7 @@ def _lookup_agent_type(agent_type: AvObjectType) -> int: return 2 # passenger elif agent_type == AvObjectType.BUS: return 3 # truck - elif agent_type in [AvObjectType.PEDESTRIAN, AvObjectType.CYCLIST]: + elif agent_type in {AvObjectType.PEDESTRIAN, AvObjectType.CYCLIST}: return 4 # pedestrian/bicycle else: return 0 # other From 8e11f9d7d34cde9989c71ab6b1252dc1e73e39b1 Mon Sep 17 00:00:00 2001 From: Saul Field Date: Mon, 13 Mar 2023 14:12:01 -0400 Subject: [PATCH 25/28] Move changelog entry and fix type errors --- CHANGELOG.md | 2 +- smarts/core/opendrive_road_network.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 893a004192..1ae3576060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Copy and pasting the git commit messages is __NOT__ enough. ## [Unreleased] ### Added - Agent manager now has `add_and_emit_social_agent` to generate a new social agent that is immediately in control of a vehicle. +- Added support for the [Argoverse 2 Motion Forecasting Dataset](https://www.argoverse.org/av2.html#forecasting-link) (see `scenarios/argoverse`) ### Changed - Changed the minimum supported Python version from 3.7 to 3.8 ### Deprecated @@ -25,7 +26,6 @@ Copy and pasting the git commit messages is __NOT__ enough. ## [1.0.7] # 2023-03-04 ### Added - Added objective, scenario description, and trained agent performance, to the Driving Smarts 2022 benchmark documentation. -- Added support for the [Argoverse 2 Motion Forecasting Dataset](https://www.argoverse.org/av2.html#forecasting-link) (see `scenarios/argoverse`). ### Changed - Unique id suffix is removed from vehicle name while building agent vehicle in `VehicleIndex.build_agent_vehicle()` function. ### Deprecated diff --git a/smarts/core/opendrive_road_network.py b/smarts/core/opendrive_road_network.py index 89bd4f2ea1..c9c9a0b8b7 100644 --- a/smarts/core/opendrive_road_network.py +++ b/smarts/core/opendrive_road_network.py @@ -1142,7 +1142,9 @@ def center_at_point(self, point: Point) -> Point: return super().center_at_point(point) @lru_cache(8) - def _edges_at_point(self, point: Point) -> Tuple[Point, Point]: + def _edges_at_point( + self, point: Point + ) -> Tuple[Optional[Point], Optional[Point]]: """Get the boundary points perpendicular to the center of the lane closest to the given world coordinate. Args: @@ -1155,15 +1157,20 @@ def _edges_at_point(self, point: Point) -> Tuple[Point, Point]: reference_line_vertices_len = int((len(self._lane_polygon) - 1) / 2) # left_edge - left_edge_shape = self._lane_polygon[:reference_line_vertices_len] - left_offset = offset_along_shape(point[:2], left_edge_shape) + left_edge_shape = [ + Point(x, y) for x, y in self._lane_polygon[:reference_line_vertices_len] + ] + left_offset = offset_along_shape(point, left_edge_shape) left_edge = position_at_shape_offset(left_edge_shape, left_offset) # right_edge - right_edge_shape = self._lane_polygon[ - reference_line_vertices_len : len(self._lane_polygon) - 1 + right_edge_shape = [ + Point(x, y) + for x, y in self._lane_polygon[ + reference_line_vertices_len : len(self._lane_polygon) - 1 + ] ] - right_offset = offset_along_shape(point[:2], right_edge_shape) + right_offset = offset_along_shape(point, right_edge_shape) right_edge = position_at_shape_offset(right_edge_shape, right_offset) return left_edge, right_edge @@ -1883,7 +1890,7 @@ def _waypoints_starting_at_lanepoint( lanepoint: LinkedLanePoint, lookahead: int, filter_road_ids: tuple, - point: Tuple[float, float, float], + point: Point, ) -> List[List[Waypoint]]: """computes equally-spaced Waypoints for all lane paths starting at lanepoint up to lookahead waypoints ahead, constrained to filter_road_ids if specified.""" From 724a0efda511baa0fac499753e18b5c3df214c3d Mon Sep 17 00:00:00 2001 From: Saul Field Date: Mon, 13 Mar 2023 14:13:00 -0400 Subject: [PATCH 26/28] Move changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 700ff3b05b..983fa89900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Copy and pasting the git commit messages is __NOT__ enough. ## [Unreleased] ### Added +- Added support for the [Argoverse 2 Motion Forecasting Dataset](https://www.argoverse.org/av2.html#forecasting-link) (see `scenarios/argoverse`) ### Changed ### Deprecated ### Fixed @@ -19,7 +20,6 @@ Copy and pasting the git commit messages is __NOT__ enough. ## [1.0.8] # 2023-03-10 ### Added - Agent manager now has `add_and_emit_social_agent` to generate a new social agent that is immediately in control of a vehicle. -- Added support for the [Argoverse 2 Motion Forecasting Dataset](https://www.argoverse.org/av2.html#forecasting-link) (see `scenarios/argoverse`) ### Changed - Changed the minimum supported Python version from 3.7 to 3.8 ### Deprecated From 6f1f703803c13eca24f7a8be4a3138af26a6160a Mon Sep 17 00:00:00 2001 From: Saul Field Date: Wed, 15 Mar 2023 12:56:17 -0400 Subject: [PATCH 27/28] Update docs with Argoverse instructions and example --- docs/_static/argoverse-replay.gif | Bin 0 -> 185405 bytes docs/ecosystem/argoverse.rst | 26 ++++++++++++++++++++++++++ docs/index.rst | 1 + 3 files changed, 27 insertions(+) create mode 100644 docs/_static/argoverse-replay.gif create mode 100644 docs/ecosystem/argoverse.rst diff --git a/docs/_static/argoverse-replay.gif b/docs/_static/argoverse-replay.gif new file mode 100644 index 0000000000000000000000000000000000000000..ab08f55ae9d90ddf934b71d779370963cdede9d1 GIT binary patch literal 185405 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 e;E~z-11XT~VCXbr?(2gO%{m8UDQPPxa{V`Y`_ for more info and download instructions. + +An example SMARTS scenario is located `here `_. After downloading the dataset, modify the ``scenario_id`` and ``scenario_path`` variables to point to the desired Argoverse scenario: + +.. code-block:: python + + # scenario_path is a directory with the following structure: + # /path/to/dataset/{scenario_id} + # ├── log_map_archive_{scenario_id}.json + # └── scenario_{scenario_id}.parquet + + scenario_id = "0000b6ab-e100-4f6b-aee8-b520b57c0530" + scenario_path = Path("/home/user/argoverse/train/") / scenario_id + +You can then run any of the examples with this scenario: + +.. code-block:: sh + + $ scl run --envision examples/egoless.py scenarios/argoverse + +.. image:: /_static/argoverse-replay.gif diff --git a/docs/index.rst b/docs/index.rst index 5563aab550..0bbbe10da2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -79,6 +79,7 @@ If you use SMARTS in your research, please cite the `paper Date: Wed, 15 Mar 2023 13:15:48 -0400 Subject: [PATCH 28/28] Update docs --- docs/ecosystem/argoverse.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/ecosystem/argoverse.rst b/docs/ecosystem/argoverse.rst index b68e6b6e2c..ba776c21bd 100644 --- a/docs/ecosystem/argoverse.rst +++ b/docs/ecosystem/argoverse.rst @@ -17,6 +17,12 @@ An example SMARTS scenario is located `here

-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