Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions e2e_challenge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and wait for approval.
1. Build a Docker image that serves the AlpaSim driver gRPC API. Start with the [starter kit](starter_kit/README.md) for a minimal example or the [VAVAM sample submission](sample_submission_vavam/README.md) for a richer model-backed example, then customize and test locally.
1. Push the image to your team's ECR repository and use the challenge CLI to submit the image URI for evaluation.
1. Wait for evaluation and check status or leaderboard results.
1. For questions or concerns, please submit [github issues](https://github.com/NVlabs/alpasim/issues)

## Competition Resources

Expand Down
22 changes: 17 additions & 5 deletions plugins/mtgs/server/engine/mtgs.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,6 @@ def local2global_translation_xy(self) -> Optional[np.ndarray]:


class MTGSAssetManager:

def __init__(self, asset_folder_path: Path, device: torch.device):
self.asset_folder_path = asset_folder_path
self.current_asset_id = None
Expand Down Expand Up @@ -594,10 +593,23 @@ def load_asset(self):
)

road_height_map_path = self.asset_dir / "road_height_map"
self.road_height_map = dict(
map=np.load(road_height_map_path / "road_height_map.npy"),
sim2=Sim2.from_json(road_height_map_path / "sim2.json"),
)
road_height_map_file = road_height_map_path / "road_height_map.npy"
road_height_sim2_file = road_height_map_path / "sim2.json"
if road_height_map_file.exists() and road_height_sim2_file.exists():
self.road_height_map = dict(
map=np.load(road_height_map_file),
sim2=Sim2.from_json(road_height_sim2_file),
)
else:
# Older MTGS challenge bundles contain only the background checkpoint
# and video metadata. The current renderer does not consume
# ``road_height_map`` after loading, so preserve compatibility with
# those bundles while keeping the richer artifact format supported.
self.road_height_map = None
logger.warning(
"Asset %s has no road_height_map; continuing without it",
self.current_asset_id,
)

video_scene_dict_path = self.asset_dir / "video_scene_dict.pkl"
self.video_scene_dict = None
Expand Down
53 changes: 45 additions & 8 deletions plugins/mtgs/server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""

import argparse
import functools
import logging
from concurrent import futures
from pathlib import Path
Expand Down Expand Up @@ -45,6 +46,29 @@
}


def _asset_folder_map_from_extra_params(extra_params) -> dict[str, str]:
"""Return a validated scene-to-MTGS-asset mapping from dataset config."""
raw_mapping = extra_params.get("asset_folder_map")
if raw_mapping is None:
return {}
if not hasattr(raw_mapping, "items"):
raise ValueError(
"scene_provider.trajdata.dataset.extra_params.asset_folder_map "
"must be a mapping of scene IDs to asset folder IDs."
)

mapping: dict[str, str] = {}
for scene_id, asset_folder in raw_mapping.items():
if not isinstance(scene_id, str) or not scene_id:
raise ValueError("asset_folder_map scene IDs must be non-empty strings.")
if not isinstance(asset_folder, str) or not asset_folder:
raise ValueError(
f"asset_folder_map[{scene_id!r}] must be a non-empty string."
)
mapping[scene_id] = asset_folder
return mapping


def parse_args(arg_list: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="MTGS Sensorsim Service Server",
Expand All @@ -59,7 +83,7 @@ def parse_args(arg_list: list[str] | None = None) -> argparse.Namespace:
"--cache-size",
type=int,
default=2,
help="LRU cache size for renderer instances",
help="LRU cache size for renderer instances and scene data sources",
)
parser.add_argument("--device", type=str, default="cuda", choices=["cuda", "cpu"])
parser.add_argument(
Expand All @@ -73,6 +97,7 @@ def parse_args(arg_list: list[str] | None = None) -> argparse.Namespace:

def create_get_scene_function(
user_config: UserSimulatorConfig,
cache_size: int = 2,
) -> tuple[Callable, Callable]:
if not TRAJDATA_AVAILABLE:
raise ImportError("trajdata is required for MTGS sensorsim server.")
Expand All @@ -97,6 +122,12 @@ def create_get_scene_function(
mapped_name = DATASET_NAME_MAPPING.get(dataset_config.name, dataset_config.name)
mtgs_asset_base_path = str(Path(asset_base_path_config) / mapped_name / "assets")
logger.info(f"MTGS asset path: {mtgs_asset_base_path}")
asset_folder_map = _asset_folder_map_from_extra_params(extra_params)
if asset_folder_map:
logger.info(
"Loaded %d explicit scene-to-asset-folder mappings",
len(asset_folder_map),
)

params = trajdata_provider_config_to_params(trajdata_config)
logger.info("Creating UnifiedDataset from config")
Expand All @@ -106,12 +137,8 @@ def create_get_scene_function(
scene_id_to_idx = scene_name_to_index_from_dataset(dataset)
logger.info(f"Built scene_id mapping for {len(scene_id_to_idx)} scenes")

scene_cache = {}

@functools.lru_cache(maxsize=cache_size)
def get_scene(scene_id: str) -> TrajdataDataSource:
if scene_id in scene_cache:
return scene_cache[scene_id]

scene_idx = scene_id_to_idx.get(scene_id)
if scene_idx is None:
raise KeyError(f"Scene {scene_id} not found in dataset")
Expand All @@ -127,9 +154,17 @@ def get_scene(scene_id: str) -> TrajdataDataSource:
vector_map_params=dataset.vector_map_params,
smooth_trajectories=user_config.smooth_trajectories,
asset_base_path=mtgs_asset_base_path,
asset_folder_resolver=(
(
lambda resolved_scene: asset_folder_map.get(
resolved_scene.name, resolved_scene.name
)
)
if asset_folder_map
else None
),
)

scene_cache[scene_id] = data_source
logger.info(f"Loaded scene {scene_id}, asset_path={data_source.asset_path}")
return data_source

Expand Down Expand Up @@ -167,7 +202,9 @@ def main(arg_list: list[str] | None = None) -> None:
return

try:
get_scene, get_available_scene_ids = create_get_scene_function(user_config)
get_scene, get_available_scene_ids = create_get_scene_function(
user_config, cache_size=args.cache_size
)
except Exception as e:
logger.error(f"Failed to create get_scene function: {e}")
return
Expand Down
102 changes: 98 additions & 4 deletions plugins/mtgs/tests/test_mtgs_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@ def test_mtgs_engine_importable():
pytest.skip(f"Server dependencies not installed: {e}")


def test_mtgs_asset_manager_allows_missing_road_height_map(tmp_path, monkeypatch):
import torch
from alpasim_mtgs.server.engine.mtgs import MTGSAssetManager

asset_id = "legacy-asset"
background_dir = tmp_path / asset_id / "background"
background_dir.mkdir(parents=True)
(background_dir / f"{asset_id}.ckpt").touch()
monkeypatch.setattr(torch, "load", lambda *args, **kwargs: {})

manager = MTGSAssetManager(tmp_path, torch.device("cpu"))
manager.reset(asset_id)

assert manager.road_height_map is None


def _mtgs_user_config(
*,
extra_params: dict | None = None,
Expand Down Expand Up @@ -176,26 +192,38 @@ def get_scene(self, idx):

def fake_trajdata_data_source(**kwargs):
captured["data_source_kwargs"] = kwargs
return SimpleNamespace(asset_path="/tmp/mtgs-assets/navtest/assets/scene-a")
resolver = kwargs.get("asset_folder_resolver")
asset_folder = (
resolver(kwargs["scene"]) if resolver is not None else kwargs["scene"].name
)
return SimpleNamespace(
asset_path=f"/tmp/mtgs-assets/navtest/assets/{asset_folder}"
)

monkeypatch.setattr(mtgs_main, "TRAJDATA_AVAILABLE", True)
monkeypatch.setattr(mtgs_main, "UnifiedDataset", FakeUnifiedDataset)
monkeypatch.setattr(mtgs_main, "TrajdataDataSource", fake_trajdata_data_source)

get_scene, get_available_scene_ids = mtgs_main.create_get_scene_function(
_mtgs_user_config(
extra_params={"asset_base_path": "/tmp/mtgs-assets"},
extra_params={
"asset_base_path": "/tmp/mtgs-assets",
"asset_folder_map": {"scene-a": "asset-b"},
},
vector_map_params={"incl_road_lanes": True, "incl_road_edges": True},
)
)

assert get_available_scene_ids() == ["scene-a"]
data_source = get_scene("scene-a")

assert data_source.asset_path == "/tmp/mtgs-assets/navtest/assets/scene-a"
assert data_source.asset_path == "/tmp/mtgs-assets/navtest/assets/asset-b"
assert captured["dataset_params"]["desired_data"] == ["nuplan_test"]
assert captured["dataset_params"]["dataset_kwargs"] == {
"nuplan_test": {"asset_base_path": "/tmp/mtgs-assets"}
"nuplan_test": {
"asset_base_path": "/tmp/mtgs-assets",
"asset_folder_map": {"scene-a": "asset-b"},
}
}
assert captured["cache_scene"].name == "scene-a"

Expand All @@ -208,3 +236,69 @@ def fake_trajdata_data_source(**kwargs):
"incl_road_edges": True,
}
assert kwargs["asset_base_path"] == "/tmp/mtgs-assets/navtest/assets"
assert kwargs["asset_folder_resolver"](kwargs["scene"]) == "asset-b"

get_unmapped_scene, _ = mtgs_main.create_get_scene_function(
_mtgs_user_config(extra_params={"asset_base_path": "/tmp/mtgs-assets"})
)
assert (
get_unmapped_scene("scene-a").asset_path
== "/tmp/mtgs-assets/navtest/assets/scene-a"
)


def test_mtgs_scene_loader_cache_is_bounded(monkeypatch):
from alpasim_mtgs.server import main as mtgs_main

created_scene_ids = []

class FakeScene:
env_name = "nuplan_test"

def __init__(self, name):
self.name = name

class FakeUnifiedDataset:
def __init__(self, **params):
self.vector_map_params = params.get("vector_map_params", {})
self._scenes = [FakeScene(f"scene-{idx}") for idx in range(3)]

@property
def scene_name_to_index(self):
return {scene.name: idx for idx, scene in enumerate(self._scenes)}

@property
def map_api(self):
return None

def get_scene_cache(self, scene):
return object()

def num_scenes(self):
return len(self._scenes)

def get_scene(self, idx):
return self._scenes[idx]

def fake_trajdata_data_source(**kwargs):
scene_id = kwargs["scene"].name
created_scene_ids.append(scene_id)
return SimpleNamespace(asset_path=f"/tmp/mtgs-assets/navtest/assets/{scene_id}")

monkeypatch.setattr(mtgs_main, "TRAJDATA_AVAILABLE", True)
monkeypatch.setattr(mtgs_main, "UnifiedDataset", FakeUnifiedDataset)
monkeypatch.setattr(mtgs_main, "TrajdataDataSource", fake_trajdata_data_source)

get_scene, _ = mtgs_main.create_get_scene_function(
_mtgs_user_config(extra_params={"asset_base_path": "/tmp/mtgs-assets"}),
cache_size=2,
)

first_scene = get_scene("scene-0")
assert get_scene("scene-0") is first_scene
get_scene("scene-1")
get_scene("scene-2")

assert get_scene.cache_info().currsize == 2
assert get_scene("scene-0") is not first_scene
assert created_scene_ids == ["scene-0", "scene-1", "scene-2", "scene-0"]
7 changes: 3 additions & 4 deletions src/controller/alpasim_controller/mpc_impl/linear_mpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ def _linearize_dynamics(self, x_op: np.ndarray) -> tuple[np.ndarray, np.ndarray]
v_cg_x = x_op[self.IVX]
v_cg_y = x_op[self.IVY]
yaw_rate = x_op[self.IYAW_RATE]
steering = x_op[self.ISTEERING]

# Use kinematic model at low speeds
use_kinematic = v_cg_x < params.kinematic_threshold_speed
Expand All @@ -232,7 +231,7 @@ def _linearize_dynamics(self, x_op: np.ndarray) -> tuple[np.ndarray, np.ndarray]
sin_yaw = math.sin(yaw)

if use_kinematic:
# Kinematic model linearization
# Kinematic model linearization.
A[self.IX, self.IYAW] = -v_cg_x * sin_yaw
A[self.IX, self.IVX] = cos_yaw

Expand All @@ -242,14 +241,14 @@ def _linearize_dynamics(self, x_op: np.ndarray) -> tuple[np.ndarray, np.ndarray]
A[self.IYAW, self.IYAW_RATE] = 1.0
A[self.IVX, self.IACCEL] = 1.0

# Freeze v_cg_x as a scheduling parameter. Adding its product derivatives
# without an affine residual would double the operating-point terms.
GAIN = 10.0
l_r = params.l_rig_to_cg
L = params.wheelbase
A[self.IVY, self.IVX] = GAIN * steering * l_r / L
A[self.IVY, self.IVY] = -GAIN
A[self.IVY, self.ISTEERING] = GAIN * v_cg_x * l_r / L

A[self.IYAW_RATE, self.IVX] = GAIN * steering / L
A[self.IYAW_RATE, self.ISTEERING] = GAIN * v_cg_x / L
A[self.IYAW_RATE, self.IYAW_RATE] = -GAIN

Expand Down
Loading
Loading