├── src ├── driver │ ├── README.md │ ├── src │ │ └── alpasim_driver │ │ │ ├── __init__.py │ │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_rectification_scaling.py │ │ │ └── __main__.py │ ├── .release.json │ └── pyproject.toml ├── grpc │ ├── alpasim_grpc │ │ ├── py.typed │ │ ├── v0 │ │ │ ├── __init__.py │ │ │ ├── runtime.proto │ │ │ ├── common.proto │ │ │ ├── physics.proto │ │ │ ├── traffic.proto │ │ │ └── controller.proto │ │ └── __init__.py │ ├── MANIFEST.in │ ├── scripts │ │ ├── __init__.py │ │ └── compile_protos.py │ ├── .gitignore │ ├── .release.json │ ├── pyproject.toml │ ├── README.md │ └── tutorial.md ├── utils │ ├── alpasim_utils │ │ ├── py.typed │ │ ├── print_asl │ │ │ ├── __init__.py │ │ │ └── __main__.py │ │ ├── asl_to_frames │ │ │ └── __init__.py │ │ ├── __init__.py │ │ └── logs.py │ ├── tests │ │ ├── data │ │ │ ├── no_map_artifact │ │ │ │ ├── artifact_no_map.usdz │ │ │ │ └── README.md │ │ │ └── xodr_artifact │ │ │ │ ├── README.md │ │ │ │ └── 026d6a39-bd8f-4175-bc61-fe50ed0403a3.usdz │ │ ├── asl_to_frames │ │ │ └── test_asl_to_frames.py │ │ ├── test_artifact.py │ │ └── test_logs.py │ ├── pyproject.toml │ └── README.md ├── wizard │ ├── alpasim_wizard │ │ ├── py.typed │ │ ├── __init__.py │ │ ├── s3 │ │ │ ├── __init__.py │ │ │ ├── sceneset.py │ │ │ └── usdz_metadata_to_suite_yaml.py │ │ ├── deployment │ │ │ ├── __init__.py │ │ │ └── dispatcher.py │ │ ├── __main__.py │ │ ├── check_config.py │ │ └── compatibility.py │ ├── .gitignore │ ├── configs │ │ ├── physics │ │ │ ├── disabled.yaml │ │ │ └── implemented_in_sensorsim.yaml │ │ ├── cicd │ │ │ ├── docker_oss.yaml │ │ │ ├── docker_internal.yaml │ │ │ └── slurm_oss.yaml │ │ ├── sim │ │ │ └── force_gt.yaml │ │ ├── cameras │ │ │ ├── 1cam.yaml │ │ │ ├── wide_only_cam.yaml │ │ │ ├── 2cam.yaml │ │ │ ├── 2cam_4k.yaml │ │ │ ├── 2cam_2168.yaml │ │ │ └── 2cam_1080.yaml │ │ ├── vehicle │ │ │ └── custom.yaml │ │ ├── deploy │ │ │ ├── local.yaml │ │ │ ├── local_oss.yaml │ │ │ ├── docker_build_only.yaml │ │ │ ├── local_oss_2gpus.yaml │ │ │ ├── ord.yaml │ │ │ └── ord_oss.yaml │ │ ├── exp │ │ │ ├── vavam_4hz.yaml │ │ │ └── vavam_4hz_eco.yaml │ │ ├── driver │ │ │ ├── alpamayo.yaml │ │ │ ├── vavam_runtime_configs.yaml │ │ │ └── vavam.yaml │ │ ├── topology │ │ │ └── 8-gpus-no-replicas.yaml │ │ ├── stable_manifest │ │ │ └── oss.yaml │ │ ├── experiment │ │ │ └── scenes │ │ │ │ └── local_suites_example.yaml │ │ └── custom_code │ │ │ └── driver.yaml │ ├── tests │ │ ├── conftest.py │ │ ├── test_compatibility_matrix.py │ │ ├── test_loadable_config.py │ │ └── strip_usdz.py │ └── pyproject.toml ├── runtime │ ├── alpasim_runtime │ │ ├── worker │ │ │ └── __init__.py │ │ ├── telemetry │ │ │ └── __init__.py │ │ ├── __init__.py │ │ ├── replay_services │ │ │ ├── __init__.py │ │ │ ├── physics_replay_service.py │ │ │ ├── controller_replay_service.py │ │ │ ├── traffic_replay_service.py │ │ │ ├── README.md │ │ │ ├── driver_replay_service.py │ │ │ └── sensorsim_replay_service.py │ │ ├── logs.py │ │ ├── endpoints.py │ │ ├── delay_buffer.py │ │ ├── autoresume.py │ │ └── scene_cache_monitor.py │ ├── tests │ │ ├── __init__.py │ │ ├── data │ │ │ ├── integration │ │ │ │ ├── 0.asl │ │ │ │ ├── 6ea1c7a3-98b7-4adc-b774-4d9526371a0b.usdz │ │ │ │ ├── generated-network-config.yaml │ │ │ │ └── generated-user-config-0.yaml │ │ │ ├── route_generator_sanity │ │ │ │ └── sanity_off_route.usdz │ │ │ ├── mock │ │ │ │ ├── network-config.yaml │ │ │ │ └── user-config.yaml │ │ │ ├── valid_network_config.yaml │ │ │ └── valid_user_config.yaml │ │ ├── test_smoke.py │ │ ├── fixtures.py │ │ ├── test_loop.py │ │ ├── README.md │ │ ├── test_delay_buffer.py │ │ ├── test_autoresume.py │ │ ├── test_logs.py │ │ ├── test_with_mocks.py │ │ ├── test_noise_models.py │ │ ├── test_config.py │ │ ├── services │ │ │ └── test_controller_api.py │ │ └── test_scene_cache_monitor.py │ ├── .gitignore │ ├── notebooks │ │ └── README.md │ ├── .release.json │ ├── README.md │ └── pyproject.toml ├── eval │ ├── .gitignore │ ├── tests │ │ └── README.md │ ├── src │ │ └── eval │ │ │ ├── __init__.py │ │ │ ├── aggregation │ │ │ ├── __init__.py │ │ │ └── utils.py │ │ │ ├── filtering │ │ │ ├── __init__.py │ │ │ ├── schema.py │ │ │ └── utils.py │ │ │ ├── scorers │ │ │ ├── __init__.py │ │ │ ├── safety.py │ │ │ ├── image.py │ │ │ └── base.py │ │ │ └── kratos_utils.py │ ├── .release.json │ ├── pyproject.toml │ └── README.md ├── physics │ ├── .dockerignore │ ├── tests │ │ └── data │ │ │ ├── README.md │ │ │ └── mesh_ground.ply │ ├── .gitignore │ ├── .gitattributes │ ├── alpasim_physics │ │ └── __init__.py │ ├── README.md │ ├── .release.json │ └── pyproject.toml ├── tools │ ├── map_utils │ │ ├── __init__.py │ │ └── test_plot_map.py │ ├── tests │ │ └── data │ │ │ └── last.usdz │ ├── __init__.py │ ├── scripts │ │ └── launch-vscode-auto-port.sh │ ├── pyproject.toml │ └── run-on-ord │ │ └── resume_template.sh └── controller │ ├── tests │ └── test_server.py │ ├── .release.json │ ├── pyproject.toml │ ├── alpasim_controller │ └── system_manager.py │ └── tools │ └── plot_controller.py ├── .python-version ├── .flake8 ├── docs ├── assets │ └── images │ │ ├── thumbnail.gif │ │ └── alpasim-architecture.png ├── ONBOARDING.md ├── DATA_PIPELINE.md └── DESIGN.md ├── data ├── nre-artifacts │ └── ego-hoods │ │ ├── hyperion_8 │ │ └── camera_front_wide_120fov.png │ │ └── hyperion_8_1 │ │ └── camera_front_wide_120fov.png ├── .gitignore └── README.md ├── CHANGELOG.md ├── .github ├── PULL_REQUEST_TEMPLATE │ ├── bugfix.md │ └── feature.md └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pyproject.toml ├── .gitattributes ├── .dockerignore ├── .gitignore ├── .release.json ├── mypy.ini ├── Dockerfile ├── .pre-commit-config.yaml ├── SECURITY.md ├── MAINTAINERS.md ├── setup_local_env.sh └── AGENTS.md /src/driver/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /src/grpc/alpasim_grpc/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/alpasim_utils/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/wizard/alpasim_wizard/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/driver/src/alpasim_driver/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/worker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = E203, W503 3 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/telemetry/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/grpc/MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include ./alpasim_grpc *.proto -------------------------------------------------------------------------------- /src/wizard/.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | outputs/ 4 | -------------------------------------------------------------------------------- /src/eval/.gitignore: -------------------------------------------------------------------------------- 1 | out.txt 2 | out.ipynb 3 | out.log 4 | metrics_results.txt 5 | -------------------------------------------------------------------------------- /src/driver/src/alpasim_driver/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit tests for alpasim_driver.""" 2 | -------------------------------------------------------------------------------- /src/eval/tests/README.md: -------------------------------------------------------------------------------- 1 | Tests can be run with `uv run pytest .` from `src/eval`. 2 | -------------------------------------------------------------------------------- /src/physics/.dockerignore: -------------------------------------------------------------------------------- 1 | tools/ci 2 | build.env 3 | dist/ 4 | .dockerignore 5 | .gitlab-ci.yml 6 | .git 7 | -------------------------------------------------------------------------------- /docs/assets/images/thumbnail.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/alpasim/HEAD/docs/assets/images/thumbnail.gif -------------------------------------------------------------------------------- /src/eval/src/eval/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | -------------------------------------------------------------------------------- /src/runtime/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | -------------------------------------------------------------------------------- /src/wizard/alpasim_wizard/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | -------------------------------------------------------------------------------- /src/eval/src/eval/aggregation/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | -------------------------------------------------------------------------------- /src/eval/src/eval/filtering/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | -------------------------------------------------------------------------------- /src/grpc/alpasim_grpc/v0/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | -------------------------------------------------------------------------------- /src/wizard/alpasim_wizard/s3/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | -------------------------------------------------------------------------------- /src/utils/alpasim_utils/print_asl/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | -------------------------------------------------------------------------------- /docs/assets/images/alpasim-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/alpasim/HEAD/docs/assets/images/alpasim-architecture.png -------------------------------------------------------------------------------- /src/utils/alpasim_utils/asl_to_frames/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | -------------------------------------------------------------------------------- /src/grpc/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | # Scripts package for alpasim_grpc 5 | -------------------------------------------------------------------------------- /src/physics/tests/data/README.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | `mesh_ground.ply` is a ground mesh that comes from: `dataset-clipgt-6c8946a6-87bb-47e4-af61-87697939ce29_runid-k890vhf5` 3 | -------------------------------------------------------------------------------- /src/physics/.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.ini 3 | *__pycache__ 4 | *.nsys-rep 5 | build 6 | !/tools/**/build 7 | build.env 8 | src/_version.py 9 | venv/ 10 | *.egg-info 11 | -------------------------------------------------------------------------------- /src/tools/map_utils/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """Map visualization and analysis utilities.""" 5 | -------------------------------------------------------------------------------- /src/tools/tests/data/last.usdz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9d2bf6f760a952dcac3397d7d79998bec5d5a1e0169c590da9de122d8fceeddf 3 | size 83177899 4 | -------------------------------------------------------------------------------- /src/physics/tests/data/mesh_ground.ply: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:88aa4fd08b4482b3869b5ddf8a6669555e935c5ebc3c322970f938fbf3f26bf2 3 | size 64153879 4 | -------------------------------------------------------------------------------- /src/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """Alpasim tools collection.""" 5 | 6 | __version__ = "0.1.0" 7 | -------------------------------------------------------------------------------- /data/nre-artifacts/ego-hoods/hyperion_8/camera_front_wide_120fov.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/alpasim/HEAD/data/nre-artifacts/ego-hoods/hyperion_8/camera_front_wide_120fov.png -------------------------------------------------------------------------------- /src/runtime/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | __pycache__ 3 | .mypy_cache 4 | .vscode 5 | .pytest_cache 6 | build 7 | !/tools/**/build 8 | *.egg-info 9 | dist/ 10 | .history/ 11 | build.env 12 | -------------------------------------------------------------------------------- /src/runtime/tests/data/integration/0.asl: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:cc361b97892489f1884f685a75a7c80b62740e6085c938c04020742409084115 3 | size 13226341 4 | -------------------------------------------------------------------------------- /data/nre-artifacts/ego-hoods/hyperion_8_1/camera_front_wide_120fov.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVlabs/alpasim/HEAD/data/nre-artifacts/ego-hoods/hyperion_8_1/camera_front_wide_120fov.png -------------------------------------------------------------------------------- /src/utils/tests/data/no_map_artifact/artifact_no_map.usdz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:564d6f608b8f3b474a9436a96985c4da384285a654834331aaeea0c20cde0686 3 | size 3021855 4 | -------------------------------------------------------------------------------- /src/driver/src/alpasim_driver/__main__.py: -------------------------------------------------------------------------------- 1 | """Module entrypoint to run the VAM driver via `python -m alpasim_driver`.""" 2 | 3 | from .main import main 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /src/utils/alpasim_utils/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """Utility functions for Alpamayo Sim.""" 5 | 6 | __version__ = "0.52.0" 7 | -------------------------------------------------------------------------------- /src/utils/tests/data/xodr_artifact/README.md: -------------------------------------------------------------------------------- 1 | Note: `026d6a39-bd8f-4175-bc61-fe50ed0403a3.usdz` comes from the hugging face 2 | physical AI dataset, but is stripped down to contain items needed for maps. 3 | -------------------------------------------------------------------------------- /src/wizard/configs/physics/disabled.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # 3 | runtime: 4 | endpoints: 5 | physics: 6 | skip: true 7 | default_scenario_parameters: 8 | physics_update_mode: NONE 9 | -------------------------------------------------------------------------------- /src/runtime/tests/data/route_generator_sanity/sanity_off_route.usdz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ef0b764427384633fe22fa80e892feec5ecb53ad601e2afca0354217b4b7f63c 3 | size 397641 4 | -------------------------------------------------------------------------------- /src/grpc/.gitignore: -------------------------------------------------------------------------------- 1 | .history/ 2 | # prevent the protobuf compiled files from being added to the repo 3 | *_pb2.py 4 | *_pb2_grpc.py 5 | *.pyi 6 | 7 | build/ 8 | dist/ 9 | *.egg-info/ 10 | *.whl 11 | *__pycache__ -------------------------------------------------------------------------------- /src/runtime/tests/data/integration/6ea1c7a3-98b7-4adc-b774-4d9526371a0b.usdz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2b50d521226ae7a632fce03ea5ef0651b5c619a485386a99669e65f9afee4b95 3 | size 8712344 4 | -------------------------------------------------------------------------------- /src/utils/tests/data/xodr_artifact/026d6a39-bd8f-4175-bc61-fe50ed0403a3.usdz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ebb7a90c6643af4248745079f370e52edb15b012330d6162f80a7ff4111d6693 3 | size 14371507 4 | -------------------------------------------------------------------------------- /src/runtime/tests/data/mock/network-config.yaml: -------------------------------------------------------------------------------- 1 | controller: 2 | addresses : [] 3 | driver: 4 | addresses : [] 5 | physics: 6 | addresses : [] 7 | sensorsim: 8 | addresses : [] 9 | trafficsim: 10 | addresses : [] 11 | -------------------------------------------------------------------------------- /src/utils/tests/data/no_map_artifact/README.md: -------------------------------------------------------------------------------- 1 | Note: `artifact_no_map.usdz` comes from `ffb7340e-ba11-4d8e-83c8-288f3f2f1924.usdz` 2 | and has many of the subfiles related to rendering (the checkpoint, meshes, etc.) 3 | removed to save size. 4 | -------------------------------------------------------------------------------- /src/wizard/configs/physics/implemented_in_sensorsim.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # 3 | services: 4 | physics: 5 | image: "*sensorsim*" # Special handling in: configuration.py 6 | command: ["noop"] # Special handling in: deployment/docker_compose.py 7 | -------------------------------------------------------------------------------- /src/wizard/configs/cicd/docker_oss.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # CI integration test configuration, used for docker-compose CI tests 3 | # This config uses the oss stable manifest images 4 | 5 | defaults: 6 | - common 7 | - /stable_manifest/oss 8 | - _self_ 9 | -------------------------------------------------------------------------------- /src/wizard/configs/sim/force_gt.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | defaults: 4 | - /physics: disabled 5 | - /controller: perfect_control 6 | - _self_ 7 | 8 | runtime: 9 | default_scenario_parameters: 10 | force_gt_duration_us: 20_000_000 # 20s 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | This document lists major updates which change UX and require adaptation. 3 | It should be sorted by date (more recent on top) and link to MRs which introduce the changes. 4 | 5 | ## Example Quick Summary (date) - PR Link 6 | * Description of the change. 7 | -------------------------------------------------------------------------------- /src/runtime/tests/test_smoke.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | # \/ this is all it takes to validate that imports work 5 | import alpasim_grpc.v0 # noqa: F401 6 | from alpasim_runtime.simulate import __main__ # noqa: F401 7 | -------------------------------------------------------------------------------- /src/runtime/tests/data/valid_network_config.yaml: -------------------------------------------------------------------------------- 1 | sensorsim: 2 | addresses: 3 | - "nre:6000" 4 | 5 | driver: 6 | addresses: 7 | - "avm:6500" 8 | 9 | physics: 10 | addresses: 11 | - "physics:6100" 12 | 13 | trafficsim: 14 | addresses: 15 | - "trafficsim:6200" 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bugfix.md: -------------------------------------------------------------------------------- 1 | # Bug Fix 2 | **Issue:** Closes #... 3 | 4 | **Root Cause:** 5 | Describe what caused the bug. 6 | 7 | **Fix Summary:** 8 | Explain what was changed to resolve it. 9 | 10 | **Tests:** 11 | - [ ] Added regression test 12 | - [ ] Verified against simulation log 13 | -------------------------------------------------------------------------------- /src/runtime/notebooks/README.md: -------------------------------------------------------------------------------- 1 | # Running the notebooks in uv 2 | 3 | To run the notebooks in uv, use the following command: 4 | ```bash 5 | uv run --extra=nb python -m ipykernel install --user --name alpasim --display-name "Python (alpasim)" 6 | uv run --extra=nb jupyter notebook notebooks/.ipynb 7 | ``` 8 | -------------------------------------------------------------------------------- /src/wizard/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import sys 5 | from pathlib import Path 6 | 7 | # Add the parent directory to the Python path 8 | parent_dir = Path(__file__).parent.parent.absolute() 9 | sys.path.insert(0, str(parent_dir)) 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | # New Feature 2 | **Description:** 3 | Explain the new feature or capability. 4 | 5 | **Issue:** Closes #... 6 | 7 | **Motivation:** 8 | Why is this feature useful or needed? 9 | 10 | **Implementation Details:** 11 | Any key design decisions, parameters, or algorithm notes. 12 | -------------------------------------------------------------------------------- /src/wizard/configs/cameras/1cam.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | runtime: 4 | default_scenario_parameters: 5 | cameras: 6 | - 7 | height: 320 8 | width: 512 9 | logical_id: camera_front_wide_120fov 10 | 11 | frame_interval_us: 100_000 12 | shutter_duration_us: 30_000 13 | first_frame_offset_us: -30_000 14 | -------------------------------------------------------------------------------- /src/runtime/tests/data/integration/generated-network-config.yaml: -------------------------------------------------------------------------------- 1 | controller: 2 | addresses: 3 | - controller-0:6004 4 | driver: 5 | addresses: 6 | - driver-0:6000 7 | physics: 8 | addresses: 9 | - physics-0:6002 10 | sensorsim: 11 | addresses: 12 | - sensorsim-0:6001 13 | trafficsim: 14 | addresses: 15 | - trafficsim-0:6003 16 | -------------------------------------------------------------------------------- /src/wizard/alpasim_wizard/deployment/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """Deployment strategies module for AlpasimWizard.""" 5 | 6 | from .docker_compose import DockerComposeDeployment 7 | from .slurm import SlurmDeployment 8 | 9 | __all__ = [ 10 | "DockerComposeDeployment", 11 | "SlurmDeployment", 12 | ] 13 | -------------------------------------------------------------------------------- /src/runtime/tests/fixtures.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import pytest 5 | from alpasim_utils.artifact import Artifact 6 | 7 | 8 | @pytest.fixture(scope="session") 9 | def sample_artifact(): 10 | usdz_file = "tests/data/mock/026d6a39-bd8f-4175-bc61-fe50ed0403a3.usdz" 11 | artifact = Artifact(source=usdz_file) 12 | return artifact 13 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | alpackages/*.tar 2 | cosmos-models/ 3 | nre-artifacts/artifact-* 4 | rollouts/default-savedir/* 5 | trafficsim/rollouts/ 6 | ncore-artifacts 7 | nre-artifacts 8 | docker-compose/**/*.txt 9 | docker-compose/**/*.out 10 | 11 | *.sqsh 12 | *.out 13 | *.parquet 14 | 15 | config/generated-network-config.yaml 16 | config/generated-user-config-*.yaml 17 | docker-compose/generated-docker-compose.yaml 18 | 19 | kpi/* 20 | -------------------------------------------------------------------------------- /src/controller/tests/test_server.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from alpasim_controller.server import construct_version 5 | 6 | 7 | def test_construct_version(): 8 | """Ensure version is at least filled.""" 9 | version = construct_version() 10 | assert version.version_id is not None 11 | assert version.grpc_api_version is not None 12 | assert version.git_hash is not None 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "alpasim_workspace" 3 | version = "0.8.0" 4 | description = "Workspace for Alpamayo Sim development" 5 | readme = "README.md" 6 | requires-python = ">=3.11,<3.13" 7 | dependencies = ["nuplan-devkit>=1.1.0"] 8 | 9 | [tool.uv.workspace] 10 | members = [ 11 | "src/controller", 12 | "src/eval", 13 | "src/grpc", 14 | "src/runtime", 15 | "src/utils", 16 | "src/physics", 17 | "src/tools", 18 | "src/driver", 19 | ] 20 | -------------------------------------------------------------------------------- /src/physics/.gitattributes: -------------------------------------------------------------------------------- 1 | *.mtl filter=lfs diff=lfs merge=lfs -text 2 | *.obj filter=lfs diff=lfs merge=lfs -text 3 | *.urdf filter=lfs diff=lfs merge=lfs -text 4 | *.usd filter=lfs diff=lfs merge=lfs -text 5 | *.ply filter=lfs diff=lfs merge=lfs -text 6 | *.blend filter=lfs diff=lfs merge=lfs -text 7 | *.itar filter=lfs diff=lfs merge=lfs -text 8 | *.usdz filter=lfs diff=lfs merge=lfs -text 9 | tests/data/mesh_ground.ply filter=lfs diff=lfs merge=lfs -text 10 | -------------------------------------------------------------------------------- /src/physics/alpasim_physics/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from alpasim_grpc import API_VERSION_MESSAGE 5 | from alpasim_grpc.v0.common_pb2 import VersionId 6 | 7 | VERSION_MESSAGE = VersionId( 8 | version_id="0.2.0", # TODO: hook up to scm 9 | git_hash="", # TODO: hook up to scm 10 | grpc_api_version=API_VERSION_MESSAGE, 11 | ) 12 | 13 | __all__ = ("__version__", "VERSION_MESSAGE") 14 | -------------------------------------------------------------------------------- /src/wizard/configs/cameras/wide_only_cam.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | runtime: 4 | default_scenario_parameters: 5 | cameras: 6 | - 7 | height: 320 8 | width: 512 9 | logical_id: camera_front_wide_120fov 10 | 11 | # If you change this see note in base_config.yaml about `driver.inference.Cframes_subsample` 12 | frame_interval_us: 100_000 13 | shutter_duration_us: 30_000 14 | first_frame_offset_us: -30_000 15 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # Alpasim Data 2 | This directory contains subdirectories which we will need to mount in the various docker containers to run alpasim. 3 | 4 | 1. `nre-artifacts` 5 | * Contains the `.usdz` files which are scene reconstructions for neural rendering with NRE 6 | * Also used by the runtime to obtain ground truth vehicle trajectories and physics for access to scene meshes 7 | 2. `trafficsim` (To be released at a later date) 8 | * Contains scene road maps and configuration for the traffic simulator 9 | -------------------------------------------------------------------------------- /src/physics/README.md: -------------------------------------------------------------------------------- 1 | # AlpaSim Physics 2 | 3 | This project contains the code for the Physics micro-service of the AlpaSim project, which uses a 4 | mesh of the environment to constrain the motion of simulated agents to the ground surface. It does 5 | not handle collisions or vehicle dynamics. 6 | 7 | ## Environment Setup 8 | 9 | `uv` is used to manage the development environment. 10 | 11 | ## Running the Sim Service 12 | 13 | Run `uv run physics_server ` to start a server for the micro-service. Use `--help` to see 14 | available options. 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/ddb/tests/test-case.asl filter=lfs diff=lfs merge=lfs -text 2 | src/ddb/tests/test-case.rclog filter=lfs diff=lfs merge=lfs -text 3 | *.asl filter=lfs diff=lfs merge=lfs -text 4 | **/*.asl filter=lfs diff=lfs merge=lfs -text 5 | **/*.rclog filter=lfs diff=lfs merge=lfs -text 6 | **/*.usdz filter=lfs diff=lfs merge=lfs -text 7 | src/runtime/tests/data/integration/0.asl filter=lfs diff=lfs merge=lfs -text 8 | src/tools/tests/data/last.usdz filter=lfs diff=lfs merge=lfs -text 9 | images/alpasim-architecture.png filter=lfs diff=lfs merge=lfs -text 10 | -------------------------------------------------------------------------------- /src/wizard/configs/cameras/2cam.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | runtime: 4 | default_scenario_parameters: 5 | cameras: 6 | - 7 | height: 320 8 | width: 512 9 | logical_id: camera_front_wide_120fov 10 | 11 | frame_interval_us: 33_000 12 | shutter_duration_us: 15_000 13 | first_frame_offset_us: -15_000 14 | - 15 | height: 320 16 | width: 512 17 | logical_id: camera_front_tele_30fov 18 | 19 | frame_interval_us: 33_000 20 | shutter_duration_us: 15_000 21 | first_frame_offset_us: -15_000 22 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Use allowlist approach - ignore everything by default 2 | * 3 | 4 | # Only include what's needed for the runtime 5 | !src/runtime/** 6 | !src/grpc/** 7 | !src/utils/** 8 | !src/ddb/** 9 | !src/eval/** 10 | !src/avmf/** 11 | !src/physics/** 12 | !src/controller/** 13 | !pyproject.toml 14 | !src/driver/** 15 | !src/wizard/configs/** 16 | 17 | # Exclude specific runtime directories/files we don't need 18 | src/runtime/build.env 19 | src/runtime/test_output 20 | **/.venv 21 | **/*.egg-info 22 | **/__pycache__ 23 | **/.pytest_cache 24 | **/dist 25 | **/build 26 | **/.mypy_cache 27 | -------------------------------------------------------------------------------- /src/tools/map_utils/test_plot_map.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from pathlib import Path 5 | 6 | from map_utils.plot_map import main 7 | 8 | 9 | def test_sanity(): 10 | # Resolve test data relative to the tools package root to work in or out of CI 11 | tools_root = Path(__file__).resolve().parents[1] 12 | artifact_name = tools_root / "tests/data/last.usdz" 13 | preview_route = True 14 | no_block = True 15 | # run, no assertion except for no exceptions 16 | main(str(artifact_name), preview_route, no_block) 17 | -------------------------------------------------------------------------------- /src/wizard/configs/cicd/docker_internal.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # CI integration test configuration, used for docker-compose CI tests 3 | # This config uses the internal stable manifest images 4 | # Inherits run_method: "NONE" from docker_oss to allow CICD to run docker-compose manually 5 | 6 | defaults: 7 | - common 8 | - /stable_manifest/internal 9 | - _self_ 10 | 11 | wizard: 12 | run_method: "NONE" # CICD runs docker-compose manually after generation 13 | 14 | defines: 15 | # The repo-relative resolver makes the path relative to the git repo root. 16 | filesystem: ${repo-relative:"data"} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | .pytest_cache/ 3 | .vscode/ 4 | **/test_output/** 5 | build.env 6 | runtime.build.env 7 | ddb.build.env 8 | .netrc 9 | netrc.secret 10 | __pycache__/ 11 | .mypy_cache/ 12 | .wizard*/ 13 | 14 | # AI stuff 15 | .claude 16 | .serena 17 | .zed 18 | CLAUDE.md 19 | 20 | **/*.egg-info 21 | **/build/ 22 | **/dist/ 23 | 24 | tools/run-on-ord/runs 25 | tools/run-on-ord/outputs 26 | src/tools/run-on-ord/runs 27 | src/tools/run-on-ord/outputs 28 | data/vavam-driver/* 29 | 30 | # uv.lock is only used in local development. On the server, we use uv tool run, 31 | # which does not require uv.lock. 32 | **/uv.lock 33 | -------------------------------------------------------------------------------- /src/grpc/alpasim_grpc/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import importlib.metadata 5 | 6 | from alpasim_grpc.v0.common_pb2 import VersionId 7 | 8 | version_str = importlib.metadata.version("alpasim_grpc") 9 | 10 | if version_str is None: 11 | raise RuntimeError("Could not find the version of the alpasim_grpc package") 12 | 13 | __version__ = tuple(int(v) for v in version_str.split(".")) # (0, 0, 0) 14 | 15 | API_VERSION_MESSAGE = VersionId.APIVersion( 16 | major=__version__[0], 17 | minor=__version__[1], 18 | patch=__version__[2], 19 | ) 20 | -------------------------------------------------------------------------------- /src/runtime/tests/test_loop.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import pytest 5 | from alpasim_runtime.config import VehicleConfig 6 | from alpasim_runtime.loop import get_ds_rig_to_aabb_center_transform 7 | 8 | 9 | def test_get_ds_rig_to_aabb_center_transform(): 10 | vehicle_config = VehicleConfig( 11 | aabb_x_m=6.0, aabb_y_m=2.0, aabb_z_m=1.0, aabb_x_offset_m=-2.5 12 | ) 13 | qvec = get_ds_rig_to_aabb_center_transform(vehicle_config) 14 | assert qvec.quat.tolist() == pytest.approx([0.0, 0.0, 0.0, 1.0]) # no rotation 15 | assert qvec.vec3.tolist() == [0.5, 0.0, 0.5] 16 | -------------------------------------------------------------------------------- /src/wizard/configs/vehicle/custom.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | # Explicitly sets the vehicle dimensions for the simulator. 4 | # Even if you want to modify just a single dimension, with the default being `vehicle: null`, 5 | # you need to specify all other dimensions as well. This config file simplifies that by allowing 6 | # `+vehicle=custom runtime.default_scenario_parameters.vehicle.aabb_x_m=`. 7 | runtime: 8 | default_scenario_parameters: 9 | vehicle: 10 | aabb_x_m: 5.393 11 | aabb_y_m: 2.109 12 | aabb_z_m: 1.503 13 | aabb_x_offset_m: -1.3 14 | aabb_y_offset_m: 0.0 15 | aabb_z_offset_m: 0.0 16 | -------------------------------------------------------------------------------- /src/eval/.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/release-it@18/schema/release-it.json", 3 | "git": { 4 | 5 | "requireUpstream": true, 6 | "requireCleanWorkingDir": false, 7 | "commitMessage": "Release pipeline, Alpasim EVAL image, v${version}", 8 | "tagName": "alpasim-eval-v${version}", 9 | "tagAnnotation": "[Automated release tag]" 10 | }, 11 | "plugins": { 12 | "@release-it/bumper": { 13 | "in": { 14 | "file": "pyproject.toml", 15 | "path": "project.version" 16 | }, 17 | "out": { 18 | "file": "pyproject.toml", 19 | "path": "project.version" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/grpc/.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/release-it@18/schema/release-it.json", 3 | "git": { 4 | 5 | "requireUpstream": true, 6 | "requireCleanWorkingDir": false, 7 | "commitMessage": "Release pipeline, Alpasim gRPC package, v${version}", 8 | "tagName": "alpasim-grpc-v${version}", 9 | "tagAnnotation": "[Automated release tag]" 10 | }, 11 | "plugins": { 12 | "@release-it/bumper": { 13 | "in": { 14 | "file": "pyproject.toml", 15 | "path": "project.version" 16 | }, 17 | "out": { 18 | "file": "pyproject.toml", 19 | "path": "project.version" 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/driver/.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/release-it@18/schema/release-it.json", 3 | "git": { 4 | "requireUpstream": true, 5 | "requireCleanWorkingDir": false, 6 | "commitMessage": "Release pipeline, Alpasim VAM Driver image, v${version}", 7 | "tagName": "alpasim-vam-driver-v${version}", 8 | "tagAnnotation": "[Automated release tag]" 9 | }, 10 | "plugins": { 11 | "@release-it/bumper": { 12 | "in": { 13 | "file": "pyproject.toml", 14 | "path": "project.version" 15 | }, 16 | "out": { 17 | "file": "pyproject.toml", 18 | "path": "project.version" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/runtime/.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/release-it@18/schema/release-it.json", 3 | "git": { 4 | 5 | "requireUpstream": true, 6 | "requireCleanWorkingDir": false, 7 | "commitMessage": "Release pipeline, Alpasim Runtime image, v${version}", 8 | "tagName": "alpasim-rumtime-v${version}", 9 | "tagAnnotation": "[Automated release tag]" 10 | }, 11 | "plugins": { 12 | "@release-it/bumper": { 13 | "in": { 14 | "file": "pyproject.toml", 15 | "path": "project.version" 16 | }, 17 | "out": { 18 | "file": "pyproject.toml", 19 | "path": "project.version" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/release-it@18/schema/release-it.json", 3 | "git": { 4 | 5 | "requireUpstream": true, 6 | "requireCleanWorkingDir": false, 7 | "commitMessage": "Release pipeline, Alpasim base image, v${version}", 8 | "tagName": "alpasim-base-v${version}", 9 | "tagAnnotation": "[Automated release tag]" 10 | }, 11 | "plugins": { 12 | "@release-it/bumper": { 13 | "in": { 14 | "file": "pyproject.toml", 15 | "path": "project.version" 16 | }, 17 | "out": { 18 | "file": "pyproject.toml", 19 | "path": "project.version" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/eval/src/eval/filtering/schema.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from dataclasses import dataclass, field 5 | from typing import List 6 | 7 | 8 | @dataclass 9 | class MetricFilter: 10 | """Single metric filter configuration. Defines a _good_ trajectory.""" 11 | 12 | column: str 13 | operator: str # one of: >, <, >=, <=, ==, != 14 | value: float 15 | 16 | 17 | @dataclass 18 | class TrajectoryFilterConfig: 19 | """Configuration for filtering metrics.""" 20 | 21 | remove_trajectories_with_event: List[MetricFilter] = field(default_factory=list) 22 | remove_timesteps_after_event: List[MetricFilter] = field(default_factory=list) 23 | -------------------------------------------------------------------------------- /src/physics/.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/release-it@18/schema/release-it.json", 3 | "git": { 4 | 5 | "requireUpstream": true, 6 | "requireCleanWorkingDir": false, 7 | "commitMessage": "Release pipeline, Alpasim physics image, v${version}", 8 | "tagName": "alpasim-physics-v${version}", 9 | "tagAnnotation": "[Automated release tag]" 10 | }, 11 | "plugins": { 12 | "@release-it/bumper": { 13 | "in": { 14 | "file": "pyproject.toml", 15 | "path": "project.version" 16 | }, 17 | "out": { 18 | "file": "pyproject.toml", 19 | "path": "project.version" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/controller/.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/release-it@18/schema/release-it.json", 3 | "git": { 4 | 5 | "requireUpstream": true, 6 | "requireCleanWorkingDir": false, 7 | "commitMessage": "Release pipeline, Alpasim oss_controller image, v${version}", 8 | "tagName": "alpasim-oss_controller-v${version}", 9 | "tagAnnotation": "[Automated release tag]" 10 | }, 11 | "plugins": { 12 | "@release-it/bumper": { 13 | "in": { 14 | "file": "pyproject.toml", 15 | "path": "project.version" 16 | }, 17 | "out": { 18 | "file": "pyproject.toml", 19 | "path": "project.version" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/wizard/configs/deploy/local.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # Config for running on workstations with a single 40gb+ GPU, see TUTORIAL 3 | # for extra instructions. In short, use cmd line arg "+deploy=local" with wizard. 4 | 5 | defaults: 6 | - _self_ 7 | - /stable_manifest/oss 8 | 9 | defines: 10 | # The repo-relative resolver makes the path relative to the git repo root. 11 | filesystem: ${repo-relative:"data"} 12 | 13 | services: 14 | driver: 15 | replicas_per_container: 1 16 | gpus: [0] 17 | 18 | sensorsim: 19 | replicas_per_container: 1 20 | gpus: [0] 21 | 22 | physics: 23 | replicas_per_container: 1 24 | gpus: [0] 25 | 26 | controller: 27 | replicas_per_container: 1 28 | gpus: null 29 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | __version__ = (0, 3, 0) 5 | 6 | import os 7 | import pathlib 8 | 9 | from alpasim_grpc import API_VERSION_MESSAGE 10 | from alpasim_grpc.v0.common_pb2 import VersionId 11 | 12 | source_directory = pathlib.Path(__file__).parent.resolve() 13 | git_hash = os.environ.get("GIT_HASH", "Unknown") 14 | if os.environ.get("GIT_DIRTY", "false") == "true": 15 | git_hash = git_hash + "+dirty" 16 | 17 | VERSION_MESSAGE = VersionId( 18 | version_id=".".join(str(v) for v in __version__), 19 | git_hash=git_hash, 20 | grpc_api_version=API_VERSION_MESSAGE, 21 | ) 22 | 23 | __all__ = ("__version__", "VERSION_MESSAGE") 24 | -------------------------------------------------------------------------------- /src/driver/pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [build-system] 3 | requires = ["uv_build>=0.8.20,<0.9.0"] 4 | build-backend = "uv_build" 5 | 6 | [project] 7 | name = "alpasim_driver" 8 | version = "0.4.0" 9 | description = "Adapters for open-source drivers in Alpasim" 10 | readme = "README.md" 11 | requires-python = ">=3.11,<3.13" 12 | dependencies = [ 13 | "vam", 14 | "einops", 15 | "numpy", 16 | "opencv-python", 17 | "torch", 18 | "pillow", 19 | "rich", 20 | "click", 21 | "grpcio", 22 | "hydra-core", 23 | "omegaconf", 24 | "alpasim-grpc" 25 | ] 26 | 27 | [project.scripts] 28 | vam_driver = "alpasim_driver.main:main" 29 | 30 | [tool.uv.sources] 31 | vam = { git = "https://github.com/valeoai/VideoActionModel.git" } 32 | alpasim-grpc = { workspace = true } 33 | -------------------------------------------------------------------------------- /src/runtime/tests/data/valid_user_config.yaml: -------------------------------------------------------------------------------- 1 | endpoints: 2 | sensorsim: 3 | n_concurrent_rollouts: 2 4 | driver: 5 | n_concurrent_rollouts: 2 6 | physics: 7 | n_concurrent_rollouts: 2 8 | skip: false 9 | trafficsim: 10 | n_concurrent_rollouts: 2 11 | skip: false 12 | do_shutdown: true 13 | 14 | scenarios: 15 | - 16 | scene_id: "clipgt-f94a6ae5-019e-4467-840f-5376b5255828" 17 | n_sim_steps: 120 18 | n_rollouts: 1 19 | force_gt_duration_us: 1700000 20 | physics_update_mode: "ALL_ACTORS" 21 | 22 | cameras: 23 | - logical_id: camera_front_wide_120fov 24 | height: 320 25 | width: 512 26 | frame_interval_us: 100000 27 | shutter_duration_us: 30000 28 | first_frame_offset_us: -30000 29 | -------------------------------------------------------------------------------- /src/wizard/alpasim_wizard/__main__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import os 5 | 6 | from .schema import AlpasimConfig 7 | from .setup_omegaconf import main_wrapper, update_scene_config, validate_config 8 | from .wizard import AlpasimWizard 9 | 10 | 11 | def run_wizard(cfg: AlpasimConfig) -> None: 12 | cfg.wizard.log_dir = os.path.abspath(cfg.wizard.log_dir) 13 | # First validate the configuration 14 | validate_config(cfg) 15 | # Then update scene config if needed 16 | update_scene_config(cfg) 17 | # Finally create and run the wizard 18 | AlpasimWizard.create(cfg).cast() 19 | 20 | 21 | def main() -> None: 22 | main_wrapper(run_wizard) 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /src/wizard/alpasim_wizard/check_config.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import logging 5 | 6 | from .schema import AlpasimConfig 7 | from .setup_omegaconf import main_wrapper 8 | 9 | logger = logging.getLogger("alpasim_wizard") 10 | logger.setLevel(logging.INFO) 11 | 12 | 13 | def check_config(cfg: AlpasimConfig) -> None: 14 | """ 15 | Sanity-checks the config file. Can be used on the login node. 16 | """ 17 | if cfg.services.sensorsim is None: 18 | # TODO: could we run in these conditions? 19 | raise ValueError("Missing 'sensorsim' config in 'services' section.") 20 | 21 | 22 | def main() -> None: 23 | main_wrapper(check_config) 24 | 25 | 26 | if __name__ == "__main__": 27 | main() 28 | -------------------------------------------------------------------------------- /src/wizard/configs/exp/vavam_4hz.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # Experiment config overrides to run vavam at 4Hz (double the default 2Hz frequency) 3 | 4 | runtime: 5 | default_scenario_parameters: 6 | egopose_interval_us: 250_000 # 4Hz (halved from 500_000) 7 | control_timestep_us: 250_000 # 4Hz (halved from 500_000) 8 | time_start_offset_us: 250_000 # Keep in sync with above 9 | 10 | cameras: 11 | - height: 1080 12 | width: 1920 13 | logical_id: camera_front_wide_120fov 14 | frame_interval_us: 250_000 # 4Hz (halved from 500_000) 15 | shutter_duration_us: 30_000 16 | first_frame_offset_us: -30_000 17 | 18 | driver: 19 | inference: 20 | subsample_factor: 2 # Subsample every 2nd frame 21 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/replay_services/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """ 5 | ASL replay services for integration testing. 6 | """ 7 | 8 | from .base_replay_servicer import BaseReplayServicer 9 | from .controller_replay_service import ControllerReplayService 10 | from .driver_replay_service import DriverReplayService 11 | from .physics_replay_service import PhysicsReplayService 12 | from .sensorsim_replay_service import SensorsimReplayService 13 | from .traffic_replay_service import TrafficReplayService 14 | 15 | __all__ = [ 16 | "BaseReplayServicer", 17 | "DriverReplayService", 18 | "PhysicsReplayService", 19 | "TrafficReplayService", 20 | "ControllerReplayService", 21 | "SensorsimReplayService", 22 | ] 23 | -------------------------------------------------------------------------------- /src/controller/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ "setuptools>=61.0" ] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "alpasim_controller" 7 | version = "0.13.0" 8 | description = "A simple, open source controller + vehicle model" 9 | requires-python = ">=3.11,<3.12" 10 | dependencies = [ 11 | "numpy", 12 | "do_mpc", 13 | "pytest", 14 | "matplotlib", 15 | "alpasim_grpc", 16 | "alpasim_utils", 17 | ] 18 | 19 | authors = [ 20 | {name = "Michael Watson", email = "mwatson@nvidia.com"}, 21 | ] 22 | 23 | [tool.setuptools.packages.find] 24 | include = [ "alpasim_controller", "tests" ] 25 | 26 | [tool.pytest.ini_options] 27 | asyncio_mode = "auto" 28 | 29 | [tool.uv.sources] 30 | alpasim_grpc = {workspace = true} 31 | alpasim_utils = {workspace = true} 32 | -------------------------------------------------------------------------------- /src/tools/scripts/launch-vscode-auto-port.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-License-Identifier: Apache-2.0 3 | # Copyright (c) 2025 NVIDIA Corporation 4 | 5 | apt-get update 6 | apt-get install -y net-tools 7 | 8 | find_free_port() { 9 | local port=$1 10 | local max_port=$2 11 | while [ $port -le $max_port ]; do 12 | if netstat -an | grep -q ":$port "; then 13 | ((port++)) 14 | else 15 | echo $port 16 | return 0 17 | fi 18 | done 19 | 20 | echo "No free port found in the specified range" 21 | return 1 22 | } 23 | 24 | ROOT=$PWD 25 | PORT=$(find_free_port 7000 8000) 26 | 27 | pushd /mnt/vscode 28 | echo bash code_server_simple.sh alpasim${SLURM_JOB_ID} ${PORT} 29 | bash code_server_simple.sh alpasim${SLURM_JOB_ID} ${PORT} 30 | popd 31 | -------------------------------------------------------------------------------- /src/wizard/configs/cameras/2cam_4k.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | runtime: 4 | default_scenario_parameters: 5 | cameras: 6 | - 7 | height: 2160 8 | width: 3456 9 | logical_id: camera_front_wide_120fov 10 | 11 | # If you change this see note in base_config.yaml about `driver.inference.Cframes_subsample` 12 | frame_interval_us: 100_000 13 | shutter_duration_us: 30_000 14 | first_frame_offset_us: -30_000 15 | - 16 | height: 2160 17 | width: 3456 18 | logical_id: camera_front_tele_30fov 19 | 20 | # If you change this see note in base_config.yaml about `driver.inference.Cframes_subsample` 21 | frame_interval_us: 100_000 22 | shutter_duration_us: 30_000 23 | first_frame_offset_us: -30_000 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: 28 | - Nvidia driver/CUDA version (e.g. from `nvidia-smi`): 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /src/wizard/configs/deploy/local_oss.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # Config for running on workstations with a single 40gb+ GPU, see TUTORIAL 3 | # for extra instructions. In short, use cmd line arg "+deploy=local_oss" with wizard. 4 | 5 | defaults: 6 | - /stable_manifest/oss 7 | - _self_ 8 | 9 | defines: 10 | # The repo-relative resolver makes the path relative to the git repo root. 11 | filesystem: ${repo-relative:"data"} 12 | 13 | services: 14 | driver: 15 | replicas_per_container: 1 16 | gpus: [0] 17 | 18 | sensorsim: 19 | replicas_per_container: 1 20 | gpus: [0] 21 | 22 | physics: 23 | replicas_per_container: 1 24 | gpus: [0] 25 | 26 | trafficsim: 27 | replicas_per_container: 1 28 | gpus: [0] 29 | 30 | controller: 31 | replicas_per_container: 1 32 | gpus: null 33 | -------------------------------------------------------------------------------- /src/utils/tests/asl_to_frames/test_asl_to_frames.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from alpasim_utils.asl_to_frames.__main__ import determine_save_dir 5 | 6 | 7 | def test_determine_save_dir(): 8 | log_path = "mnt/rollouts/cliggt-hash/0.asl" 9 | 10 | # nominal path, log_save_dir unspecified 11 | save_dir = determine_save_dir(log_path, None) 12 | expected_save_dir = "mnt/rollouts/cliggt-hash/0_asl_frames" 13 | assert save_dir == expected_save_dir, f"{save_dir=} {expected_save_dir=}" 14 | 15 | # path required by kpi, log_save_dir specified 16 | save_dir = determine_save_dir(log_path, "mnt/outputs") 17 | expected_save_dir = "mnt/outputs/rollouts/cliggt-hash/0" 18 | assert save_dir == expected_save_dir, f"{save_dir=} {expected_save_dir=}" 19 | -------------------------------------------------------------------------------- /src/utils/tests/test_artifact.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from alpasim_utils.artifact import Artifact 5 | 6 | 7 | def test_artifact_no_map(): 8 | usdz_file = "tests/data/no_map_artifact/artifact_no_map.usdz" 9 | artifact = Artifact(source=usdz_file) 10 | 11 | # expect that the map is None (no exceptions) 12 | assert artifact.map is None 13 | 14 | 15 | def test_xodr_artifact(): 16 | usdz_file = "tests/data/xodr_artifact/026d6a39-bd8f-4175-bc61-fe50ed0403a3.usdz" 17 | artifact = Artifact(source=usdz_file) 18 | 19 | # expect that the map is not None (no exceptions) 20 | assert artifact.map is not None 21 | assert ( 22 | artifact.map.map_id 23 | == "alpasim_usdz:clipgt-026d6a39-bd8f-4175-bc61-fe50ed0403a3" 24 | ) 25 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.11 3 | 4 | warn_return_any = False 5 | warn_unused_configs = True 6 | disallow_untyped_defs = True 7 | follow_imports = normal 8 | check_untyped_defs = True 9 | 10 | [mypy-scipy.*] 11 | ignore_missing_imports = True 12 | 13 | [mypy-maglev.*] 14 | ignore_missing_imports = True 15 | 16 | [mypy-filelock] 17 | ignore_missing_imports = true 18 | 19 | [mypy-boto3] 20 | ignore_missing_imports = true 21 | 22 | [mypy-botocore.client] 23 | ignore_missing_imports = true 24 | 25 | [mypy-alpasim_grpc.utils.database_utils.util] 26 | ignore_missing_imports = true 27 | 28 | [mypy-setuptools] 29 | ignore_missing_imports = True 30 | 31 | [mypy-kratos.*] 32 | ignore_missing_imports = True 33 | 34 | [mypy-mergedeep.*] 35 | ignore_missing_imports = True 36 | 37 | [mypy-trajdata.*] 38 | ignore_missing_imports = True 39 | -------------------------------------------------------------------------------- /src/wizard/configs/exp/vavam_4hz_eco.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # Experiment config overrides to run vavam at 4Hz (double the default 2Hz frequency) 3 | 4 | runtime: 5 | default_scenario_parameters: 6 | egopose_interval_us: 250_000 # 4Hz (halved from 500_000) 7 | control_timestep_us: 250_000 # 4Hz (halved from 500_000) 8 | time_start_offset_us: 250_000 # Keep in sync with above 9 | 10 | cameras: 11 | - height: 1080 12 | width: 1920 13 | logical_id: camera_front_wide_120fov 14 | frame_interval_us: 250_000 # 4Hz (halved from 500_000) 15 | shutter_duration_us: 30_000 16 | first_frame_offset_us: -30_000 17 | 18 | driver: 19 | inference: 20 | subsample_factor: 2 # Subsample every 2nd frame 21 | trajectory_optimizer: 22 | enabled: true 23 | -------------------------------------------------------------------------------- /src/grpc/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ "setuptools>=61.0", "grpcio-tools", "protobuf>=4.0.0" ] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "alpasim_grpc" 7 | version = "0.53.0" 8 | description = "gRPC API definitions for Alpamayo Sim" 9 | requires-python = ">=3.11,<=3.12" 10 | dependencies = [ 11 | "grpcio", 12 | "grpcio-tools", 13 | "dataclasses-json>=0.6.7", 14 | "protobuf>=4.0.0,<5.0.0", 15 | "numpy", 16 | "pytest", 17 | "pytest-asyncio", 18 | ] 19 | 20 | [[project.authors]] 21 | name = "Michal Tyszkiewicz" 22 | email = "mtyszkiewicz@nvidia.com" 23 | 24 | [project.scripts] 25 | compile-protos = "scripts.compile_protos:compile_protos" 26 | clean-protos = "scripts.compile_protos:clean_proto_files" 27 | 28 | [tool.setuptools.packages.find] 29 | where = [ "." ] 30 | include = [ "alpasim_grpc*", "scripts*" ] 31 | -------------------------------------------------------------------------------- /src/wizard/alpasim_wizard/s3/sceneset.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from __future__ import annotations 5 | 6 | import logging 7 | from dataclasses import dataclass 8 | 9 | import pandas as pd 10 | 11 | logger = logging.getLogger("alpasim_wizard") 12 | 13 | 14 | @dataclass 15 | class SceneIdAndUuid: 16 | scene_id: str 17 | uuid: str 18 | 19 | @staticmethod 20 | def list_from_df(df: pd.DataFrame) -> list[SceneIdAndUuid]: 21 | if "scene_id" not in df.columns or "uuid" not in df.columns: 22 | raise ValueError( 23 | f"DataFrame must have columns 'scene_id' and 'uuid'. Got {df.columns}." 24 | ) 25 | 26 | return [ 27 | SceneIdAndUuid(scene_id, uuid) 28 | for scene_id, uuid in zip(df["scene_id"], df["uuid"]) 29 | ] 30 | -------------------------------------------------------------------------------- /src/wizard/configs/driver/alpamayo.yaml: -------------------------------------------------------------------------------- 1 | # Should be used in defaults list, e.g. 2 | # - /driver: alpamayo 3 | 4 | write_debug_images: false 5 | # Trajectory recovery heuristic parameters; set `enabled` true to activate 6 | # Note: remember to set `send_recording_ground_truth` to true as well. 7 | trajectory_recovery: 8 | enabled: false 9 | num_points_comparison: 20 10 | num_points_recovery: 40 11 | max_distance_threshold: 3.0 12 | inference: 13 | # Overwrite the minimum required historical timesteps that was temporarily increased 14 | # as a WAR for failing replay tests. 15 | min_num_historical_waypoints: \${inference.num_historical_waypoints} 16 | # Overwrite the rig file to match default alpasim rig config. 17 | # TODO: Remove this once Alpasim is updated with the new rig config. 18 | viz_cfg: 19 | rig_calibration_file: "3415_rig.json" 20 | selection_strategy: CLOSEST_3D 21 | -------------------------------------------------------------------------------- /src/wizard/configs/cameras/2cam_2168.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | runtime: 4 | default_scenario_parameters: 5 | cameras: 6 | - 7 | # This is the original camera resolution, NRE will not do any resizing. 8 | height: 2168 9 | width: 3848 10 | logical_id: camera_front_wide_120fov 11 | 12 | # If you change this see note in base_config.yaml about `driver.inference.Cframes_subsample` 13 | frame_interval_us: 100_000 14 | shutter_duration_us: 30_000 15 | first_frame_offset_us: -30_000 16 | - 17 | height: 2168 18 | width: 3848 19 | logical_id: camera_front_tele_30fov 20 | 21 | # If you change this see note in base_config.yaml about `driver.inference.Cframes_subsample` 22 | frame_interval_us: 100_000 23 | shutter_duration_us: 30_000 24 | first_frame_offset_us: -30_000 25 | -------------------------------------------------------------------------------- /src/runtime/tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Tests should be written in the `tests` directory. The test files should be named `test_.py` where `` is 4 | the name of the module being tested. Tests should be written for all public functions and classes in the module, aside 5 | from trivial data classes. 6 | 7 | ## Running Tests 8 | 9 | A prerequisite for running the tests is to have a suitable environment and the `pytest` package installed. Given that 10 | this project is expected to be run inside a docker container, this is used as a basis for the proposed configuration. 11 | 12 | 13 | Note that the following instructions assume you are running from the src/runtime directory. 14 | 15 | With `uv`: 16 | 17 | ```bash 18 | cd src/runtime 19 | uv sync --all-extras # This installs dev dependencies including pytest 20 | ``` 21 | 22 | And then, to run: 23 | ```bash 24 | uv run pytest 25 | ``` 26 | -------------------------------------------------------------------------------- /src/physics/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ "setuptools>=64" ] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "alpasim-physics" 7 | version = "1.8.0" 8 | description = "Physics micro-service of the Alpamayo Sim" 9 | requires-python = ">=3.11,<=3.12" 10 | dependencies = [ 11 | "alpasim_grpc", 12 | "alpasim_utils", 13 | "numpy", 14 | "warp-lang", 15 | "point_cloud_utils", 16 | "pyyaml", 17 | "dataclasses_json", 18 | "pytest" 19 | ] 20 | 21 | [[project.authors]] 22 | name = "Riccardo de Lutio" 23 | email = "rdelutio@nvidia.com" 24 | 25 | [project.optional-dependencies] 26 | vis = [ "polyscope" ] 27 | local = [ "gradio" ] 28 | 29 | [project.scripts] 30 | physics_server = "alpasim_physics.server:main" 31 | 32 | [tool.uv.sources] 33 | alpasim_grpc = {workspace = true} 34 | alpasim_utils = {workspace = true} 35 | 36 | [tool.setuptools] 37 | packages = [ "alpasim_physics" ] 38 | -------------------------------------------------------------------------------- /src/wizard/configs/cameras/2cam_1080.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | runtime: 4 | default_scenario_parameters: 5 | cameras: 6 | - 7 | # We want 1080x1920 but NRE will resize to match original camera aspect ratio. 8 | # So we set 1080x1900 here, which will be resized to 1080x1916. 9 | height: 1080 10 | width: 1900 11 | logical_id: camera_front_wide_120fov 12 | 13 | # If you change this see note in base_config.yaml about `driver.inference.Cframes_subsample` 14 | frame_interval_us: 100_000 15 | shutter_duration_us: 30_000 16 | first_frame_offset_us: -30_000 17 | - 18 | height: 1080 19 | width: 1900 20 | logical_id: camera_front_tele_30fov 21 | 22 | # If you change this see note in base_config.yaml about `driver.inference.Cframes_subsample` 23 | frame_interval_us: 100_000 24 | shutter_duration_us: 30_000 25 | first_frame_offset_us: -30_000 26 | -------------------------------------------------------------------------------- /src/wizard/configs/topology/8-gpus-no-replicas.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | # Meant for Alpasim development. 4 | # A config for 8 GPU nodes which unlike the ORD default only places 1 endpoint per GPU. 5 | # This is lower perf than the default but easier to analyze for performance. 6 | 7 | services: 8 | sensorsim: 9 | environments: 10 | - OMP_NUM_THREADS=1 11 | 12 | gpus: [3, 4, 5, 6, 7] 13 | replicas_per_container: 1 14 | 15 | driver: 16 | replicas_per_container: 1 17 | gpus: [0] 18 | 19 | physics: 20 | replicas_per_container: 1 21 | gpus: [1] 22 | 23 | trafficsim: 24 | replicas_per_container: 1 25 | gpus: [2] 26 | 27 | runtime: 28 | endpoints: 29 | sensorsim: 30 | n_concurrent_rollouts: 2 31 | 32 | driver: 33 | n_concurrent_rollouts: 10 34 | 35 | physics: 36 | n_concurrent_rollouts: 10 37 | skip: false 38 | 39 | trafficsim: 40 | n_concurrent_rollouts: 10 41 | skip: false 42 | -------------------------------------------------------------------------------- /src/utils/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "alpasim_utils" 7 | version = "0.52.0" 8 | description = "Utility functions for Alpamayo Sim" 9 | requires-python = ">=3.11,<=3.12" 10 | dependencies = [ 11 | "alpasim_grpc", 12 | "dataclasses-json>=0.6.7", 13 | "numpy", 14 | "scipy", 15 | "aiofiles", 16 | "csaps", 17 | "imageio[ffmpeg]", 18 | "pandas", 19 | "pytest", 20 | "pytest-asyncio", 21 | "trajdata-alpasim @ git+https://github.com/NVlabs/trajdata.git@alpasim" 22 | ] 23 | 24 | [project.scripts] 25 | asl-to-frames = "alpasim_utils.asl_to_frames.__main__:main" 26 | print-asl = "alpasim_utils.print_asl.__main__:main" 27 | 28 | [[project.authors]] 29 | name = "Michal Tyszkiewicz" 30 | email = "mtyszkiewicz@nvidia.com" 31 | 32 | [tool.uv.sources] 33 | alpasim_grpc = {workspace = true} 34 | 35 | [tool.setuptools.packages.find] 36 | where = ["."] 37 | include = ["alpasim_utils*"] 38 | -------------------------------------------------------------------------------- /src/eval/src/eval/scorers/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from eval.schema import EvalConfig 5 | from eval.scorers.base import ScorerGroup 6 | from eval.scorers.collision import CollisionScorer 7 | from eval.scorers.ground_truth import GroundTruthScorer 8 | from eval.scorers.image import ImageScorer 9 | from eval.scorers.minADE import MinADEScorer 10 | from eval.scorers.offroad import OffRoadScorer 11 | from eval.scorers.plan_deviation import PlanDeviationScorer 12 | from eval.scorers.safety import SafetyScorer 13 | 14 | SCORERS = [ 15 | CollisionScorer, 16 | OffRoadScorer, 17 | GroundTruthScorer, 18 | MinADEScorer, 19 | PlanDeviationScorer, 20 | ImageScorer, 21 | SafetyScorer, 22 | ] 23 | 24 | 25 | def create_scorer_group(cfg: EvalConfig) -> ScorerGroup: 26 | """Initialize all scorers.""" 27 | scorers = [] 28 | for scorer in SCORERS: 29 | scorers.append(scorer(cfg)) 30 | return ScorerGroup(scorers) 31 | -------------------------------------------------------------------------------- /src/eval/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ "setuptools>=61.0" ] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "alpasim_eval" 7 | version = "1.29.0" 8 | description = "Add your description here" 9 | readme = "README.md" 10 | requires-python = ">=3.11,<3.13" 11 | dependencies = [ 12 | "pandas", 13 | "polars[all]", 14 | "seaborn", 15 | "seaborn-polars", 16 | "matplotlib", 17 | "papermill", 18 | "ipykernel", 19 | "alpasim_grpc", 20 | "alpasim_utils", 21 | "omegaconf", 22 | "shapely", 23 | "trajdata-alpasim @ git+https://github.com/NVlabs/trajdata.git@alpasim", 24 | "rich[jupyter]", 25 | "pandas", 26 | "pytest", 27 | "csaps", 28 | "ipywidgets", 29 | "opencv-python", 30 | "Pillow", 31 | "numpy", 32 | "yappi", 33 | "dataclasses-json" 34 | ] 35 | 36 | [project.scripts] 37 | alpasim-eval = "eval.main:main" 38 | alpasim-aggregation = "eval.aggregation.main:main" 39 | 40 | [tool.uv.sources] 41 | alpasim_grpc = {workspace = true} 42 | alpasim_utils = {workspace = true} 43 | -------------------------------------------------------------------------------- /src/wizard/configs/stable_manifest/oss.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | defaults: 4 | - /driver: vavam # Driver config 5 | - _self_ 6 | # Set of 'stable' image (images which have been tested to work together) 7 | # and relevant dependencies which describe the conditions those images 8 | # have been validated. 9 | scenes: 10 | scene_ids: 11 | - clipgt-05bb8212-63e1-40a8-b4fc-3142c0e94646 # taken from huggingface 12 | local: 13 | directory: "all-usdzs" # relative to nre-artifacts directory 14 | services: 15 | sensorsim: 16 | image: docker.io/carlasimulator/nvidia-nurec-grpc:0.2.0 17 | external_image: true 18 | driver: 19 | image: alpasim-base:0.1.1 20 | physics: 21 | image: alpasim-base:0.1.1 22 | runtime: 23 | image: alpasim-base:0.1.1 24 | controller: 25 | image: alpasim-base:0.1.1 26 | eval: 27 | image: alpasim-base:0.1.1 28 | trafficsim: 29 | image: alpasim-base:0.1.1 # temporary stub 30 | runtime: 31 | default_scenario_parameters: 32 | # no latency 33 | planner_delay_us: 0 34 | -------------------------------------------------------------------------------- /src/runtime/tests/test_delay_buffer.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import pytest 5 | from alpasim_runtime.delay_buffer import DelayBuffer 6 | 7 | 8 | def test_delay_buffer(): 9 | delay_buffer = DelayBuffer(1) 10 | 11 | OBJ1 = "obj1" 12 | OBJ2 = "obj2" 13 | OBJ3 = "obj3" 14 | delay_buffer.add(OBJ1, 1) 15 | assert delay_buffer.at(0) == OBJ1 # Note: primed using the first object 16 | assert delay_buffer.at(1) == OBJ1 17 | 18 | delay_buffer.add(OBJ2, 2) 19 | delay_buffer.add(OBJ3, 3) 20 | assert delay_buffer.at(2) == OBJ1 21 | assert delay_buffer.at(3) == OBJ2 22 | assert delay_buffer.at(4) == OBJ3 23 | 24 | 25 | def test_delay_buffer_raise_on_out_of_order(): 26 | delay_buffer = DelayBuffer(1) 27 | 28 | OBJ1 = "obj1" 29 | OBJ2 = "obj2" 30 | delay_buffer.add(OBJ1, 1) 31 | with pytest.raises(ValueError): 32 | delay_buffer.add(OBJ2, 0) 33 | assert delay_buffer.at(1) == OBJ1 34 | assert delay_buffer.at(2) == OBJ1 35 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/replay_services/physics_replay_service.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """ 5 | Physics replay service implementation. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import logging 11 | from typing import Any 12 | 13 | from alpasim_grpc.v0 import physics_pb2_grpc 14 | from alpasim_runtime.replay_services.asl_reader import ASLReader 15 | 16 | import grpc 17 | 18 | from .base_replay_servicer import BaseReplayServicer 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class PhysicsReplayService(BaseReplayServicer, physics_pb2_grpc.PhysicsServiceServicer): 24 | """Replay service for the physics service""" 25 | 26 | def __init__(self, asl_reader: ASLReader): 27 | super().__init__(asl_reader, "physics") 28 | 29 | def ground_intersection(self, request: Any, context: grpc.ServicerContext) -> Any: 30 | """Return recorded constrained poses""" 31 | return self.validate_request("ground_intersection", request, context) 32 | -------------------------------------------------------------------------------- /src/wizard/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "alpasim_wizard" 7 | version = "0.0.1" 8 | requires-python = ">=3.11.0" 9 | classifiers = [ 10 | "Programming Language :: Python :: 3", 11 | "Operating System :: OS Independent", 12 | ] 13 | 14 | dependencies = [ 15 | "hydra-core", 16 | "pyYAML", 17 | "GitPython", 18 | "pandas", 19 | "filelock", 20 | "tqdm", 21 | "typing-extensions", 22 | "pandas", 23 | "pytest", 24 | "pytest-asyncio", 25 | "pandas-stubs", 26 | "types-PyYAML", 27 | # Pin cryptography to avoid GLIBC 2.33 requirement on compute nodes 28 | "cryptography>=41.0.3,<42.0.0", 29 | ] 30 | 31 | [tool.pytest.ini_options] 32 | asyncio_default_fixture_loop_scope = "function" 33 | 34 | [tool.setuptools] 35 | py-modules = ["alpasim_wizard"] 36 | 37 | # Executable for `uv tool run` 38 | [project.scripts] 39 | alpasim_wizard = "alpasim_wizard.__main__:main" 40 | alpasim_check_config = "alpasim_wizard.check_config:main" 41 | -------------------------------------------------------------------------------- /src/wizard/configs/experiment/scenes/local_suites_example.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # Example config showing how to use local scene suites 3 | # 4 | # Usage: python -m alpasim_wizard +experiment/scenes=local_suites_example 5 | # Or override suite: python -m alpasim_wizard +experiment/scenes=local_suites_example scenes.test_suite_id=validation_set 6 | 7 | scenes: 8 | source: local 9 | test_suite_id: test_set # Which suite to use from local.suites 10 | 11 | local: 12 | directory: "all-usdzs" 13 | 14 | # Define your suites here - all in one place! 15 | suites: 16 | test_set: 17 | - clipgt-2137d456-2516-48a5-ace1-8d9a32cb8c72 18 | - clipgt-19071dab-38d8-47a2-a6d2-0f4a9c73e73a 19 | 20 | validation_set: 21 | - clipgt-3151fedc-3f3b-4f4e-814b-46780904da95 22 | - clipgt-c14c031a-8c17-4d08-aa4d-23c020a6871e 23 | 24 | full_set: 25 | - clipgt-2137d456-2516-48a5-ace1-8d9a32cb8c72 26 | - clipgt-19071dab-38d8-47a2-a6d2-0f4a9c73e73a 27 | - clipgt-3151fedc-3f3b-4f4e-814b-46780904da95 28 | - clipgt-c14c031a-8c17-4d08-aa4d-23c020a6871e 29 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/replay_services/controller_replay_service.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """ 5 | Controller (VDC) replay service implementation. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import logging 11 | from typing import Any 12 | 13 | from alpasim_grpc.v0 import controller_pb2_grpc 14 | from alpasim_runtime.replay_services.asl_reader import ASLReader 15 | 16 | import grpc 17 | 18 | from .base_replay_servicer import BaseReplayServicer 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class ControllerReplayService( 24 | BaseReplayServicer, controller_pb2_grpc.VDCServiceServicer 25 | ): 26 | """Replay service for the controller/VDC service""" 27 | 28 | def __init__(self, asl_reader: ASLReader): 29 | super().__init__(asl_reader, "controller") 30 | 31 | def run_controller_and_vehicle( 32 | self, request: Any, context: grpc.ServicerContext 33 | ) -> Any: 34 | """Return recorded propagated poses""" 35 | return self.validate_request("run_controller_and_vehicle", request, context) 36 | -------------------------------------------------------------------------------- /src/tools/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "alpasim-tools" 7 | version = "0.1.0" 8 | description = "Collection of tools for Alpasim development and analysis" 9 | requires-python = ">=3.11" 10 | license = {text = "Proprietary"} 11 | 12 | # Base dependencies that most tools need 13 | dependencies = [ 14 | "numpy", 15 | "Pillow", 16 | "alpasim_runtime", 17 | "matplotlib", 18 | "alpasim_grpc", 19 | "alpasim_utils", 20 | "alpasim_eval", 21 | "opencv-python", 22 | "polars", 23 | "tqdm", 24 | "rich", 25 | "omegaconf", 26 | "protobuf", 27 | "grpcio", 28 | "scipy", 29 | "pytest", 30 | "black", 31 | "ruff", 32 | "mypy", 33 | "pyqt5", 34 | ] 35 | 36 | # Local development sources 37 | [tool.uv.sources] 38 | alpasim_runtime = {workspace = true} 39 | alpasim_grpc = {workspace = true} 40 | alpasim_utils = {workspace = true} 41 | alpasim_eval = {workspace = true} 42 | 43 | [tool.hatch.build.targets.wheel] 44 | packages = [ 45 | "map_utils", 46 | "data_generation", 47 | "mock-driver", 48 | ] 49 | -------------------------------------------------------------------------------- /src/grpc/scripts/compile_protos.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: Apache-2.0 3 | # Copyright (c) 2025 NVIDIA Corporation 4 | 5 | """Compile proto files to Python modules.""" 6 | import os 7 | 8 | from grpc_tools import command 9 | 10 | 11 | def clean_proto_files() -> None: 12 | """Delete all generated proto files (*.py except __init__.py and *.pyi).""" 13 | # Clean files in alpasim_grpc/v0 directory 14 | for root, unused_dirs, files in os.walk("alpasim_grpc/v0"): 15 | for file in files: 16 | if (file.endswith(".py") and file != "__init__.py") or file.endswith( 17 | ".pyi" 18 | ): 19 | file_path = os.path.join(root, file) 20 | print(f"Deleting {file_path}") 21 | os.remove(file_path) 22 | 23 | 24 | def compile_protos() -> None: 25 | # First clean old proto files 26 | print("Cleaning old proto files...") 27 | clean_proto_files() 28 | 29 | # Use the same grpc_tools.command API for exact compatibility 30 | command.build_package_protos(".", strict_mode=True) 31 | print("Proto compilation completed successfully!") 32 | -------------------------------------------------------------------------------- /src/wizard/configs/cicd/slurm_oss.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # SLURM smoke test configuration, used for SLURM CI tests 3 | # This config uses the oss stable manifest images 4 | # 5 | # Note: This file contains the awkward mounting of log_dir/src_copy, which is required 6 | # to work around the inability to mount /tmp/builds/... when performing sbatch calls. 7 | # It assumes that run-test-slurm.sh has copied the source to log_dir/src_copy. 8 | 9 | defaults: 10 | - common 11 | - /stable_manifest/oss 12 | - _self_ 13 | 14 | defines: 15 | filesystem: ${oc.env:ALPASIM_LUSTRE_SHARED_CACHE} 16 | 17 | wizard: 18 | run_method: "SLURM" 19 | slurm_gpu_partition: ${oc.env:CI_SLURM_PARTITION} 20 | slurm_cpu_partition: "cpu" 21 | 22 | services: 23 | eval: 24 | volumes: 25 | - "${wizard.log_dir}:/mnt/log_dir" 26 | - "${scenes.database.scene_cache}:/mnt/nre-data" 27 | - "${wizard.log_dir}/src_copy:/mnt/src" 28 | post_eval_aggregation: 29 | volumes: 30 | - "${wizard.log_dir}:/mnt/log_dir" 31 | - "${or:${wizard.array_job_dir},${wizard.log_dir}}:/mnt/array_job_dir" 32 | - "${wizard.log_dir}/src_copy:/mnt/src" 33 | -------------------------------------------------------------------------------- /src/grpc/alpasim_grpc/v0/runtime.proto: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright (c) 2025 NVIDIA Corporation 3 | 4 | syntax = "proto3"; 5 | 6 | import "alpasim_grpc/v0/common.proto"; 7 | 8 | service RuntimeService { 9 | rpc simulate (SimulationRequest) returns (SimulationReturn); 10 | } 11 | 12 | message RolloutSpec { 13 | string scenario_id = 1; 14 | fixed64 random_seed = 2; 15 | } 16 | 17 | message SimulationRequest { 18 | message DriverAddress { 19 | string ip = 1; 20 | uint32 port = 2; 21 | } 22 | 23 | repeated DriverAddress available_drivers = 1; 24 | repeated RolloutSpec rollout_specs = 2; // note a scenario may be requested multiple times 25 | } 26 | 27 | message SimulationReturn { 28 | message RolloutReturn { 29 | RolloutSpec rollout_spec = 1; 30 | string results_json = 2; 31 | } 32 | 33 | common.VersionId runtime_version = 1; 34 | common.VersionId nre_version = 2; 35 | common.VersionId physics_version = 3; 36 | common.VersionId driver_version = 4; 37 | common.VersionId traffic_version = 5; 38 | 39 | repeated RolloutReturn rollout_returns = 6; 40 | } 41 | -------------------------------------------------------------------------------- /src/runtime/tests/test_autoresume.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import os 5 | 6 | from alpasim_runtime.autoresume import ( 7 | find_num_complete_rollouts, 8 | mark_rollout_complete, 9 | remove_incomplete_rollouts, 10 | ) 11 | 12 | 13 | def test_autoresume_mark_complete_and_remove_incomplete(tmp_path): 14 | batch_uuid_1 = "uuid_1" 15 | batch_uuid_2 = "uuid_2" 16 | os.mkdir(tmp_path / "scene_id") 17 | SCENE_ID = "scene_id" 18 | scene_dir = tmp_path / SCENE_ID 19 | os.mkdir(scene_dir / batch_uuid_1) 20 | os.mkdir(scene_dir / batch_uuid_2) 21 | 22 | num_complete = find_num_complete_rollouts(tmp_path, SCENE_ID) 23 | assert num_complete == 0 24 | 25 | mark_rollout_complete(scene_dir, batch_uuid_1) 26 | 27 | num_complete = find_num_complete_rollouts(tmp_path, SCENE_ID) 28 | assert num_complete == 1 29 | 30 | num_dirs_before_remove = len(os.listdir(scene_dir)) 31 | assert num_dirs_before_remove == 2 32 | 33 | remove_incomplete_rollouts(tmp_path, SCENE_ID) 34 | num_dirs_after_remove = len(os.listdir(scene_dir)) 35 | assert num_dirs_after_remove == 1 36 | -------------------------------------------------------------------------------- /src/wizard/configs/deploy/docker_build_only.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | defaults: 4 | - /stable_manifest/oss # Add oss for images that need to be built 5 | - _self_ # Override the GPUs to null 6 | 7 | defines: 8 | # The repo-relative resolver makes the path relative to the git repo root. 9 | filesystem: ${repo-relative:"data"} 10 | 11 | 12 | services: 13 | driver: 14 | replicas_per_container: 1 15 | gpus: null 16 | 17 | sensorsim: 18 | replicas_per_container: 1 19 | gpus: null 20 | 21 | physics: 22 | replicas_per_container: 1 23 | gpus: null 24 | 25 | trafficsim: 26 | replicas_per_container: 1 27 | gpus: null 28 | 29 | controller: 30 | replicas_per_container: 1 31 | gpus: null 32 | 33 | ddb: 34 | replicas_per_container: 1 35 | gpus: null 36 | 37 | eval: 38 | replicas_per_container: 1 39 | gpus: null 40 | 41 | 42 | wizard: 43 | run_method: "NONE" 44 | dry_run: true 45 | validate_mount_points: false 46 | log_dir: "./generated_configs" 47 | # Include ddb and eval in the services list so they get added to docker-compose.yaml 48 | run_eval_services: ["ddb", "eval"] 49 | 50 | scenes: 51 | scene_ids: [] 52 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/logs.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import asyncio 5 | import logging 6 | from dataclasses import dataclass, field 7 | from typing import Self 8 | 9 | from alpasim_grpc.v0.logging_pb2 import LogEntry 10 | from alpasim_utils.logs import LogWriter 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | @dataclass(kw_only=True) 16 | class LogWriterManager: 17 | """Composes multiple LogWriter instances.""" 18 | 19 | log_writers: list[LogWriter] = field(default_factory=list) 20 | 21 | async def log_message(self, message: LogEntry) -> None: 22 | log_tasks = [log_writer.log_message(message) for log_writer in self.log_writers] 23 | await asyncio.gather(*log_tasks) 24 | 25 | async def __aenter__(self) -> Self: 26 | aenter_tasks = [log_writer.__aenter__() for log_writer in self.log_writers] 27 | await asyncio.gather(*aenter_tasks) 28 | return self 29 | 30 | async def __aexit__(self, *args, **kwargs) -> None: 31 | aexit_tasks = [ 32 | log_writer.__aexit__(*args, **kwargs) for log_writer in self.log_writers 33 | ] 34 | await asyncio.gather(*aexit_tasks) 35 | -------------------------------------------------------------------------------- /src/runtime/tests/test_logs.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from tempfile import TemporaryDirectory 5 | 6 | from alpasim_grpc.v0.logging_pb2 import LogEntry, RolloutMetadata 7 | from alpasim_runtime.logs import LogWriterManager 8 | from alpasim_utils.logs import LogWriter, async_read_pb_log 9 | 10 | 11 | async def test_log_is_written() -> None: 12 | """Checks that writing an example message to LogWriter creates an .asl file which can be decoded successfully""" 13 | written_message = LogEntry( 14 | rollout_metadata=RolloutMetadata( 15 | rollout_index=3, 16 | # leave the other fields to defaults 17 | ) 18 | ) 19 | 20 | with TemporaryDirectory() as temp_dir: 21 | asl_path = f"{temp_dir}/log.asl" 22 | 23 | asl_log_writer = LogWriter(file_path=asl_path) 24 | 25 | log_writer_manager = LogWriterManager(log_writers=[asl_log_writer]) 26 | async with log_writer_manager: 27 | await log_writer_manager.log_message(message=written_message) 28 | 29 | n_messages = 0 30 | async for read_message in async_read_pb_log(asl_path, raise_on_malformed=True): 31 | n_messages += 1 32 | 33 | assert n_messages == 1, "read more messages than were written" 34 | assert read_message == written_message 35 | -------------------------------------------------------------------------------- /src/eval/src/eval/kratos_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from glob import glob 4 | from pathlib import Path 5 | from typing import Any 6 | 7 | import yaml 8 | 9 | logger = logging.getLogger("kratos_utils") 10 | 11 | 12 | def _load_yaml(file_path: str | Path) -> dict[str, Any]: 13 | path = Path(file_path) if isinstance(file_path, str) else file_path 14 | if not path.exists(): 15 | logger.warning("File not found at %s", path) 16 | return {} 17 | with open(path, "r", encoding="utf-8") as f: 18 | return yaml.safe_load(f) 19 | 20 | 21 | def get_metadata(config_dir: Path) -> dict[str, Any]: 22 | # Read run metadata from same directory as config 23 | run_metadata_path = config_dir / "run_metadata.yaml" 24 | run_metadata = _load_yaml(run_metadata_path) 25 | logger.debug("Loaded run metadata: %s", run_metadata) 26 | yamls_to_upload = glob(f"{config_dir}/*.yaml", recursive=True) 27 | # Convert string paths from glob to Path objects consistently 28 | yaml_paths = [ 29 | Path(path) for path in yamls_to_upload if Path(path) != run_metadata_path 30 | ] 31 | logger.debug("Yamls to upload: %s", yaml_paths) 32 | yaml_dict = {path.name: _load_yaml(path) for path in yaml_paths} 33 | # Serialize the dictionary to JSON string 34 | run_metadata["yamls"] = json.dumps(yaml_dict) 35 | return run_metadata 36 | -------------------------------------------------------------------------------- /src/wizard/configs/driver/vavam_runtime_configs.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | runtime: 4 | default_scenario_parameters: 5 | force_gt_duration_us: 3_000_000 6 | n_sim_steps: 100 7 | n_rollouts: 1 8 | egopose_interval_us: 500_000 9 | control_timestep_us: 500_000 10 | time_start_offset_us: 500_000 # Needs to be in sync 11 | ego_mask_rig_config_id: null 12 | 13 | cameras: 14 | - height: 1080 15 | width: 1920 16 | logical_id: camera_front_wide_120fov 17 | frame_interval_us: 500_000 # also needs to be in sync 18 | shutter_duration_us: 30_000 19 | first_frame_offset_us: -30_000 20 | 21 | extra_cameras: 22 | # # Original nuscenes pinhole reference for comparison: 23 | - logical_id: camera_front_wide_120fov 24 | rig_to_camera: 25 | translation_m: [1.65897811, -0.01443456, 1.51539499] 26 | rotation_xyzw: [-0.49929397355810856, 0.5039939168301356, -0.4972939976976715, 0.49939397235113037] 27 | # intrinsics: 28 | # model: opencv_pinhole 29 | # opencv_pinhole: 30 | # focal_length: [1545.0, 1545.0] 31 | # principal_point: [960.0, 560.0] 32 | # radial: [-0.356123, 0.172545, -0.05231, 0.0, 0.0, 0.0] 33 | # tangential: [-0.00213, 0.000464] 34 | # thin_prism: [0.0, 0.0, 0.0, 0.0] 35 | # resolution_hw: [1080, 1920] 36 | # shutter_type: ROLLING_TOP_TO_BOTTOM 37 | -------------------------------------------------------------------------------- /src/runtime/README.md: -------------------------------------------------------------------------------- 1 | # Alpasim runtime 2 | 3 | Refer to [CONTRIBUTING.md](../../CONTRIBUTING.md#coordinate-systems) for the coordinate frame conventions used by the runtime services. 4 | 5 | ## Configuration 6 | ### Zero delay mode 7 | The simulator has multiple "clocks" ticking under the hood, triggering events for each camera, egopose information (GPS) and policy decision-making. In general a policy decision may be requested "out of sync" with the input information, requiring the policy to extrapolate to the current state (pose) in order to make decisions for the future. This is challenging and may be desirable to turn off to simplify debugging. The example config below explains how to do that. The flag `scenarios[i].assert_zero_decision_delay` enables an assertion to warn the user if they have misconfigured the remaining parameters. 8 | 9 | ```yaml 10 | scenarios: 11 | - 12 | # ... 13 | egopose_interval_us: 99_000 # a multiple of camera's `frame_interval_us` 14 | control_timestep_us: 99_000 # a multiple of camera's `frame_interval_us` 15 | 16 | time_start_offset_us: 297_000 # a multiple of `control_timestep_us` 17 | 18 | assert_zero_decision_delay: true # adds an assertion to error out if something is misconfigured 19 | 20 | cameras: 21 | - 22 | # ... 23 | frame_interval_us: 33_000 24 | shutter_duration_us: 15_000 25 | first_frame_offset_us: -15_000 # negative `shutter_duration_us` 26 | ``` 27 | -------------------------------------------------------------------------------- /src/tools/run-on-ord/resume_template.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # SPDX-License-Identifier: Apache-2.0 3 | # Copyright (c) 2025 NVIDIA Corporation 4 | 5 | # Usage: Just execute this script. 6 | 7 | # Find the current directory where this script resides (ARRAY_JOB_DIR) 8 | SCRIPT_PATH="$(readlink -f "$0")" 9 | ARRAY_JOB_DIR="$(dirname "$SCRIPT_PATH")" 10 | echo "Resuming job from directory: $ARRAY_JOB_DIR" 11 | TIMESTAMP=$(date +%Y_%m_%d__%H_%M_%S) 12 | 13 | OLD_AGGREGATE_DIR="$ARRAY_JOB_DIR/aggregate_old_${TIMESTAMP}" 14 | # 15 | # Rename aggregate folder if it exists 16 | if [ -d "$ARRAY_JOB_DIR/aggregate" ]; then 17 | echo "Renaming existing aggregate folder to ${OLD_AGGREGATE_DIR}" 18 | mv "$ARRAY_JOB_DIR/aggregate" "$OLD_AGGREGATE_DIR" 19 | else 20 | # Create OLD_AGGREGATE_DIR if it doesn't exist 21 | mkdir -p "$OLD_AGGREGATE_DIR" 22 | fi 23 | 24 | 25 | # Perform cleanup work before resuming 26 | echo "Performing cleanup work..." 27 | # Move any temporary files or lock files to OLD_AGGREGATE_DIR if they exist 28 | if [ -f "$ARRAY_JOB_DIR/post_eval_aggregation.json" ]; then 29 | mv "$ARRAY_JOB_DIR/post_eval_aggregation.json" "$OLD_AGGREGATE_DIR/" 2>/dev/null 30 | fi 31 | if [ -f "$ARRAY_JOB_DIR/post_eval_aggregation.lock" ]; then 32 | mv "$ARRAY_JOB_DIR/post_eval_aggregation.lock" "$OLD_AGGREGATE_DIR/" 2>/dev/null 33 | fi 34 | 35 | 36 | echo "Cleanup complete, ready to resume job" 37 | 38 | # The actual resume command will be appended by submit.sh 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Example command (run from repo root): 2 | # docker build --secret id=netrc,src=$HOME/.netrc -t alpasim_base:latest -f Dockerfile . 3 | 4 | FROM nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04 5 | 6 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 7 | 8 | RUN apt-get update && apt-get install -y \ 9 | git \ 10 | ffmpeg \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | COPY . /repo 14 | 15 | # Configure uv 16 | ENV UV_LINK_MODE=copy 17 | 18 | # Compile protos 19 | WORKDIR /repo/src/grpc 20 | RUN --mount=type=secret,id=netrc,target=/root/.netrc \ 21 | --mount=type=cache,target=/root/.cache/uv \ 22 | NETRC=/root/.netrc uv sync 23 | RUN uv run compile-protos --no-sync 24 | 25 | WORKDIR /repo 26 | 27 | RUN --mount=type=secret,id=netrc,target=/root/.netrc \ 28 | --mount=type=cache,target=/root/.cache/uv \ 29 | NETRC=/root/.netrc uv sync --all-packages 30 | 31 | # Note: maglev.av has name collisions with PyAV (both use `import av`). 32 | # Patch torchvision to trigger its "av not available" fallback path. 33 | RUN for f in .venv/lib/python*/site-packages/torchvision/io/video.py \ 34 | .venv/lib/python*/site-packages/torchvision/io/video_reader.py; do \ 35 | [ -f "$f" ] && sed -i 's/import av$/raise ImportError("maglev.av collision")/' "$f"; \ 36 | done || true 37 | 38 | ENV UV_CACHE_DIR=/tmp/uv-cache 39 | ENV UV_NO_SYNC=1 40 | 41 | 42 | #ENTRYPOINT ["uv", "run", "python", "-m", "alpasim_controller.server"] 43 | -------------------------------------------------------------------------------- /src/eval/src/eval/filtering/utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from pathlib import Path 5 | from typing import Sequence 6 | 7 | import polars as pl 8 | from omegaconf import OmegaConf 9 | 10 | from eval.aggregation.modifiers import ( 11 | MetricAggregationModifiers, 12 | RemoveTimestepsAfterEvent, 13 | RemoveTrajectoryWithEvent, 14 | ) 15 | from eval.filtering.schema import TrajectoryFilterConfig 16 | 17 | OPERATOR_MAP = { 18 | ">": lambda x, y: x > y, 19 | "<": lambda x, y: x < y, 20 | ">=": lambda x, y: x >= y, 21 | "<=": lambda x, y: x <= y, 22 | "==": lambda x, y: x == y, 23 | "!=": lambda x, y: x != y, 24 | } 25 | 26 | 27 | def read_filter_config(filter_path: Path | str) -> TrajectoryFilterConfig: 28 | 29 | filter_config = OmegaConf.merge( 30 | OmegaConf.structured(TrajectoryFilterConfig), OmegaConf.load(filter_path) 31 | ) 32 | return filter_config 33 | 34 | 35 | def get_modifiers_from_filter_config( 36 | filter_config: TrajectoryFilterConfig, 37 | ) -> Sequence[MetricAggregationModifiers]: 38 | return [ 39 | RemoveTrajectoryWithEvent( 40 | OPERATOR_MAP[filter.operator](pl.col(filter.column), filter.value) 41 | ) 42 | for filter in filter_config.remove_trajectories_with_event 43 | ] + [ 44 | RemoveTimestepsAfterEvent( 45 | OPERATOR_MAP[filter.operator](pl.col(filter.column), filter.value) 46 | ) 47 | for filter in filter_config.remove_timesteps_after_event 48 | ] 49 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3 3 | 4 | exclude: ^(.gitlab-ci.yml|cicd/gitlab/|src/grpc|tools/migration_assistant.py) # TODO(alpasim): enable in future 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v5.0.0 8 | hooks: 9 | # list of supported hooks: https://pre-commit.com/hooks.html 10 | - id: trailing-whitespace 11 | - id: end-of-file-fixer 12 | - id: check-yaml 13 | - id: debug-statements 14 | - id: detect-private-key 15 | - id: check-executables-have-shebangs 16 | - id: check-toml 17 | - id: check-case-conflict 18 | - id: check-added-large-files 19 | args: ["--maxkb=1024"] 20 | - repo: https://github.com/pycqa/isort 21 | rev: 5.13.2 22 | hooks: 23 | - id: isort 24 | name: isort (python) 25 | args: ["--profile", "black", "--filter-files"] 26 | - repo: https://github.com/psf/black-pre-commit-mirror 27 | rev: 24.10.0 28 | hooks: 29 | - id: black 30 | - repo: https://github.com/PyCQA/flake8 31 | rev: 7.1.1 32 | hooks: 33 | - id: flake8 34 | args: ["--max-line-length=120"] # TODO(alpasim): decide if we want to enforce line length 35 | - repo: https://github.com/pre-commit/mirrors-mypy 36 | rev: "f56614d" 37 | hooks: 38 | - id: mypy 39 | exclude: ^(src/wizard/tests) 40 | files: ^src/wizard/.*\.py$ 41 | args: 42 | - --config-file=mypy.ini 43 | additional_dependencies: ["types-PyYAML", "hydra-core", "GitPython", "pandas","pandas-stubs", "types-aiofiles"] 44 | -------------------------------------------------------------------------------- /src/eval/src/eval/scorers/safety.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from eval.data import AggregationType, MetricReturn, SimulationResult 5 | from eval.scorers.base import Scorer 6 | 7 | 8 | class SafetyScorer(Scorer): 9 | """Scorer for safety metrics (currently from the optional safety monitor output). 10 | 11 | Adds the following metrics: 12 | * safety_monitor_triggered: Indication of the safety monitor triggering 13 | """ 14 | 15 | def calculate(self, simulation_result: SimulationResult) -> list[MetricReturn]: 16 | """Calculate metrics for entire trajectory.""" 17 | 18 | triggers = [] 19 | 20 | for ts_idx, ts in enumerate(simulation_result.timestamps_us): 21 | driver_response_at_time = ( 22 | simulation_result.driver_responses.get_driver_response_for_time( 23 | ts, "now" 24 | ) 25 | ) 26 | if (driver_response_at_time is None) or ( 27 | driver_response_at_time.safety_monitor_safe is None 28 | ): 29 | triggers.append(False) 30 | else: 31 | triggers.append(not (driver_response_at_time.safety_monitor_safe)) 32 | 33 | return [ 34 | MetricReturn( 35 | name="safety_monitor_triggered", 36 | values=triggers, 37 | valid=[True] * len(triggers), 38 | timestamps_us=list(simulation_result.timestamps_us), 39 | time_aggregation=AggregationType.MAX, 40 | ) 41 | ] 42 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/replay_services/traffic_replay_service.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """ 5 | Traffic replay service implementation. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import logging 11 | 12 | from alpasim_grpc.v0 import traffic_pb2, traffic_pb2_grpc 13 | from alpasim_runtime.replay_services.asl_reader import ASLReader 14 | 15 | import grpc 16 | 17 | from .base_replay_servicer import BaseReplayServicer 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class TrafficReplayService(BaseReplayServicer, traffic_pb2_grpc.TrafficServiceServicer): 23 | """Replay service for the traffic service""" 24 | 25 | def __init__(self, asl_reader: ASLReader): 26 | super().__init__(asl_reader, "trafficsim") 27 | 28 | def get_metadata( 29 | self, request: traffic_pb2.MetadataRequest, context: grpc.ServicerContext 30 | ) -> traffic_pb2.MetadataResponse: 31 | """Return metadata from ASL""" 32 | metadata = traffic_pb2.TrafficModuleMetadata() 33 | version_id = self.get_version(request, context) 34 | metadata.version_id.CopyFrom(version_id) 35 | metadata.minimum_history_length_us = 1000000 # 1 second default 36 | 37 | map_id = self.asl_reader.get_map_id() 38 | metadata.supported_map_ids.append(map_id) 39 | return metadata 40 | 41 | def simulate( 42 | self, request: traffic_pb2.SimulateRequest, context: grpc.ServicerContext 43 | ) -> traffic_pb2.SimulateResponse: 44 | """Return recorded traffic poses""" 45 | return self.validate_request("simulate", request, context) 46 | -------------------------------------------------------------------------------- /src/eval/src/eval/scorers/image.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import numpy as np 5 | 6 | from eval.data import AggregationType, MetricReturn, SimulationResult 7 | from eval.schema import EvalConfig, ImageScorerConfig 8 | from eval.scorers.base import Scorer 9 | 10 | BLACK_THRESHOLD = 10 11 | 12 | 13 | class ImageScorer(Scorer): 14 | """Scorer for image quality metrics. 15 | 16 | Adds the following metrics: 17 | * img_is_black: Whether the image is black. 18 | * More to come... 19 | """ 20 | 21 | def __init__(self, cfg: EvalConfig): 22 | super().__init__(cfg) 23 | scorer_config: ImageScorerConfig = cfg.scorers.image 24 | self.camera_logical_id = scorer_config.camera_logical_id 25 | 26 | def calculate(self, simulation_result: SimulationResult) -> list[MetricReturn]: 27 | camera = simulation_result.cameras.camera_by_logical_id[self.camera_logical_id] 28 | 29 | all_black_values = [] 30 | timestamps_us = [] 31 | 32 | for time in simulation_result.timestamps_us: 33 | img = camera.image_at_time(time) 34 | if img is None: 35 | continue 36 | all_black_values.append(float(np.all(np.array(img) <= BLACK_THRESHOLD))) 37 | timestamps_us.append(time) 38 | 39 | return [ 40 | MetricReturn( 41 | name="img_is_black", 42 | values=all_black_values, 43 | valid=[True] * len(all_black_values), 44 | timestamps_us=timestamps_us, 45 | time_aggregation=AggregationType.MAX, 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /src/wizard/tests/test_compatibility_matrix.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import pytest 5 | from alpasim_wizard.compatibility import CompatibilityMatrix, _hydra_key_to_nre_version 6 | 7 | 8 | def test_hydra_key_to_nre_version(): 9 | assert _hydra_key_to_nre_version("0_2_335-deadbeef") == "0.2.335-deadbeef" 10 | assert _hydra_key_to_nre_version("1_0_0-abcdef") == "1.0.0-abcdef" 11 | assert ( 12 | _hydra_key_to_nre_version("stripped-0_2_335-deadbeef") 13 | == "stripped-0.2.335-deadbeef" 14 | ) 15 | 16 | with pytest.raises(KeyError): 17 | # too many underscores 18 | _hydra_key_to_nre_version("0_2_335_deadbeef") 19 | 20 | with pytest.raises(KeyError): 21 | # too few underscores 22 | _hydra_key_to_nre_version("2_335-deadbeef") 23 | 24 | 25 | def test_compatibility_matrix_lookup(): 26 | config = { 27 | "0_2_335-deadbeef": {"0_2_223-cafef00d": True, "0_1_0-abcdef": True}, 28 | "0_2_223-cafef00d": {"0_1_0-abcdef": True}, 29 | } 30 | matrix = CompatibilityMatrix.from_config(config) 31 | 32 | assert matrix.lookup("0.2.335-deadbeef") == set( 33 | ["0.2.335-deadbeef", "0.2.223-cafef00d", "0.1.0-abcdef"] 34 | ) 35 | assert matrix.lookup("0.2.223-cafef00d") == set( 36 | ["0.2.223-cafef00d", "0.1.0-abcdef"] 37 | ) 38 | assert matrix.lookup("0.1.0-abcdef") == set(["0.1.0-abcdef"]) 39 | 40 | 41 | def test_compatibility_matrix_self_compatibility_error(): 42 | config = { 43 | "0_2_335-deadbeef": {"0_2_335-deadbeef": True}, 44 | } 45 | 46 | with pytest.raises(ValueError): 47 | CompatibilityMatrix.from_config(config) 48 | -------------------------------------------------------------------------------- /src/grpc/README.md: -------------------------------------------------------------------------------- 1 | # Alpasim core library 2 | This folder contains gRPC definitions for interoperation between alpasim components and certain common utility functions. 3 | 4 | Refer to [CONTRIBUTING.md](../../CONTRIBUTING.md#coordinate-systems) for the coordinate frame conventions shared across the runtime and gRPC APIs. 5 | 6 | ## gRPC APIs 7 | The APIs defined in this repository are used by the alpasim components, 8 | including in additional repositories: 9 | 1. [Neural rendering engine](https://www.nvidia.com/en-us/glossary/3d-reconstruction/) 10 | 2. Traffic model (coming soon) 11 | 3. [Physics model](/src/physics) 12 | 4. [Driver](/src/driver) 13 | 14 | 15 | ### Building and installing 16 | 17 | Protobuf definitions compiled into python are not packaged and must be compiled 18 | first with running 19 | ```python 20 | uv run compile-protos 21 | ``` 22 | from this folder. This does not happen automatically when installing the 23 | `gRPC` package! 24 | This command also re-compiles them after you changed the definitions. 25 | 26 | You can also clean them with 27 | ```python 28 | uv run clean-protos 29 | ``` 30 | 31 | 32 | ### Usage 33 | Primarily this repository contains protobufs specifying the microservice APIs. 34 | After installing usage is as follows: 35 | 36 | ```python 37 | import grpc 38 | 39 | from alpasim_grpc.v0.sensorsim_pb2 import RenderRequest, RenderReturn 40 | from alpasim_grpc.v0.sensorsim_pb2_grpc import SensorsimServiceStub 41 | 42 | with grpc.insecure_channel('host:port') as channel: 43 | service = SensorsimServiceStub(channel) 44 | render_request = RenderRequest( 45 | scene_id="scene_id", 46 | # ... 47 | ) 48 | response: RenderReturn = service.render(render_request) 49 | ``` 50 | -------------------------------------------------------------------------------- /src/wizard/tests/test_loadable_config.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import tempfile 5 | 6 | from alpasim_wizard.utils import _process_config_values_for_saving 7 | from omegaconf import OmegaConf 8 | 9 | 10 | class TestLoadableConfig: 11 | """Test config processing for save/load round-trip with escaped interpolations.""" 12 | 13 | def test_save_and_load_round_trip_preserves_literal_strings(self): 14 | """Test that saving and loading configs preserves literal strings with ${}.""" 15 | # Create config with strings that look like interpolations but should be literal 16 | cfg = { 17 | "literal1": "This is a literal: ${not_an_interpolation}", 18 | "literal2": "This is a literal: \\${not_an_interpolation}", 19 | "literal3": "This is a literal: \\\\${not_an_interpolation}", 20 | "literal4": "This is a literal: \\\\\\${not_an_interpolation}", 21 | } 22 | 23 | # Process config for saving (escape $ characters) 24 | cfg_escaped = OmegaConf.create(_process_config_values_for_saving(cfg)) 25 | 26 | # Save to temporary file 27 | with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml") as tmp_file: 28 | save_path = tmp_file.name 29 | OmegaConf.save(cfg_escaped, save_path) 30 | loaded_cfg = OmegaConf.load(save_path) 31 | 32 | # Verify the loaded values are exactly what we expect 33 | assert loaded_cfg.literal1 == cfg["literal1"] 34 | assert loaded_cfg.literal2 == cfg["literal2"] 35 | assert loaded_cfg.literal3 == cfg["literal3"] 36 | assert loaded_cfg.literal4 == cfg["literal4"] 37 | -------------------------------------------------------------------------------- /src/runtime/tests/data/integration/generated-user-config-0.yaml: -------------------------------------------------------------------------------- 1 | enable_autoresume: false 2 | endpoints: 3 | controller: 4 | n_concurrent_rollouts: 14 5 | skip: false 6 | do_shutdown: true 7 | driver: 8 | n_concurrent_rollouts: 14 9 | physics: 10 | n_concurrent_rollouts: 14 11 | skip: false 12 | sensorsim: 13 | n_concurrent_rollouts: 2 14 | trafficsim: 15 | n_concurrent_rollouts: 14 16 | skip: false 17 | scenarios: 18 | - assert_zero_decision_delay: true 19 | cameras: 20 | - logical_id: camera_front_wide_120fov 21 | height: 320 22 | width: 512 23 | frame_interval_us: 100000 24 | shutter_duration_us: 30000 25 | first_frame_offset_us: -30000 26 | - logical_id: camera_front_tele_30fov 27 | height: 320 28 | width: 512 29 | frame_interval_us: 100000 30 | shutter_duration_us: 30000 31 | first_frame_offset_us: -30000 32 | control_timestep_us: 100000 33 | egomotion_noise: 34 | cov_orientation_x: 0.0 35 | cov_orientation_y: 0.0 36 | cov_orientation_z: 0.0007 37 | cov_x: 0.05 38 | cov_y: 0.05 39 | cov_z: 0.0 40 | enabled: false 41 | time_constant_orientation: 5.0 42 | time_constant_position: 3.0 43 | egopose_interval_us: 100000 44 | force_gt_duration_us: 1700000 45 | min_traffic_duration_us: 3000000 46 | n_rollouts: 1 47 | n_sim_steps: 60 48 | physics_update_mode: EGO_ONLY 49 | planner_delay_us: 0 50 | route_generator_type: MAP 51 | scene_id: clipgt-c14c031a-8c17-4d08-aa4d-23c020a6871e 52 | send_av_messages: false 53 | send_recording_ground_truth: false 54 | time_start_offset_us: 300000 55 | vehicle: null 56 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | NVIDIA is dedicated to the security and trust of our software products and services, including all source code repositories managed through our organization. 4 | 5 | If you need to report a security issue, please use the appropriate contact points outlined below. **Please do not report security vulnerabilities through GitHub.** 6 | 7 | ## Reporting Potential Security Vulnerability in an NVIDIA Product 8 | 9 | To report a potential security vulnerability in any NVIDIA product: 10 | - Web: [Security Vulnerability Submission Form](https://www.nvidia.com/object/submit-security-vulnerability.html) 11 | - E-Mail: psirt@nvidia.com 12 | - We encourage you to use the following PGP key for secure email communication: [NVIDIA public PGP Key for communication](https://www.nvidia.com/en-us/security/pgp-key) 13 | - Please include the following information: 14 | - Product/Driver name and version/branch that contains the vulnerability 15 | - Type of vulnerability (code execution, denial of service, buffer overflow, etc.) 16 | - Instructions to reproduce the vulnerability 17 | - Proof-of-concept or exploit code 18 | - Potential impact of the vulnerability, including how an attacker could exploit the vulnerability 19 | 20 | While NVIDIA currently does not have a bug bounty program, we do offer acknowledgement when an externally reported security issue is addressed under our coordinated vulnerability disclosure policy. Please visit our [Product Security Incident Response Team (PSIRT)](https://www.nvidia.com/en-us/security/psirt-policies/) policies page for more information. 21 | 22 | ## NVIDIA Product Security 23 | 24 | For all security-related concerns, please visit NVIDIA's Product Security portal at https://www.nvidia.com/en-us/security 25 | -------------------------------------------------------------------------------- /src/controller/alpasim_controller/system_manager.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import logging 5 | 6 | from alpasim_controller.system import System 7 | from alpasim_grpc.v0 import common_pb2, controller_pb2 8 | 9 | 10 | class SystemManager: 11 | """ 12 | The SystemManager class manages multiple vehicle dynamics and control systems. 13 | """ 14 | 15 | def __init__(self, log_dir): 16 | self._log_dir = log_dir 17 | self._systems = {} # session uuid to System mapping 18 | 19 | def close_session(self, request: controller_pb2.VDCSessionCloseRequest): 20 | if request.session_uuid in self._systems: 21 | logging.info(f"Closing session: {request.session_uuid}") 22 | del self._systems[request.session_uuid] 23 | return common_pb2.Empty() 24 | raise KeyError(f"Session {request.session_uuid} does not exist") 25 | 26 | def _create_system(self, session_uuid: str, state: common_pb2.StateAtTime): 27 | logging.info(f"Creating system for session_uuid: {session_uuid}") 28 | self._systems[session_uuid] = System( 29 | f"{self._log_dir}/alpasim_controller_{session_uuid}.csv", state 30 | ) 31 | 32 | def run_controller_and_vehicle_model( 33 | self, request: controller_pb2.RunControllerAndVehicleModelRequest 34 | ): 35 | logging.debug( 36 | f"run_controller_and_vehicle called for session_uuid: {request.session_uuid}" 37 | ) 38 | if request.session_uuid not in self._systems: 39 | self._create_system(request.session_uuid, request.state) 40 | system = self._systems[request.session_uuid] 41 | return system.run_controller_and_vehicle_model(request) 42 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/replay_services/README.md: -------------------------------------------------------------------------------- 1 | # Replay Services 2 | 3 | Services for replaying recorded simulation data in Alpasim. 4 | Primary use case at the moment is the `test_runtime_integration_replay.py` test 5 | which checks that the runtime sends the same requests as in the logs. 6 | This is helpful to make sure nothing changed after refactoring. 7 | **Note that this test is manual as we don't want to burden developers to update 8 | the recorded ASL logs every time they make an intentional change to runtime** 9 | 10 | ## Components 11 | 12 | - **ASLReplay**: Replays ASL (Alpasim Scene Language) files containing actor trajectories and scene data 13 | - **ReplayServiceBase**: Base class for implementing custom replay services 14 | 15 | ## Generating replay files 16 | 17 | To generate the needed files to be replayed, run, from src/wizard: 18 | 19 | ``` 20 | RUN_DIR=/home/migl/workspace/alpasim/.wizard \ 21 | uv run python -m alpasim_wizard \ 22 | wizard.log_dir=${RUN_DIR} \ 23 | +deploy=local \ 24 | runtime.endpoints.trafficsim.skip=False \ 25 | scenes.scene_ids="[clipgt-c14c031a-8c17-4d08-aa4d-23c020a6871e]" \ 26 | runtime.default_scenario_parameters.n_sim_steps=60 27 | ``` 28 | 29 | And copy the asl log, network-config and user-config files to 30 | src/runtime/tests/data/integration. 31 | 32 | ## Changing the scene 33 | 34 | If you change the scene_id, make sure to also add the updated usdz file to the 35 | folder - it's recommended to modify the contents of the usdz file and remove 36 | all the large rendering artifacts we don't need. 37 | You can zip it back up with (from within the unziped usdz folder) 38 | ``` 39 | zip -r -0 6ea1c7a3-98b7-4adc-b774-4d9526371a0b.usdz ./* 40 | ``` 41 | 42 | To upload it to git, use `git lfs track "path/to/file.(usdz|asl)"` 43 | -------------------------------------------------------------------------------- /docs/ONBOARDING.md: -------------------------------------------------------------------------------- 1 | # Onboarding 2 | Alpasim depends on access to the following: 3 | 4 | * Hugging Face access 5 | * Used for downloading simulation artifacts 6 | * Data is [here](https://huggingface.co/datasets/nvidia/PhysicalAI-Autonomous-Vehicles-NuRec/tree/main/sample_set/25.07_release ) 7 | * See info on data [here](https://huggingface.co/datasets/nvidia/PhysicalAI-Autonomous-Vehicles-NuRec/blob/main/README.md#dataset-format) 8 | for more information on the contents of artifacts used to define scenes 9 | * You will need to create a free Hugging Face account if you do not already have one and create 10 | an access token with read access. See [access tokens](https://huggingface.co/settings/tokens). 11 | * A version of `uv` installed (see [here](https://docs.astral.sh/uv/getting-started/installation/)) 12 | * Example installation command for Ubuntu: `curl -LsSf https://astral.sh/uv/install.sh | sh`. 13 | * Docker installed (see [setup instructions](https://docs.docker.com/engine/install/ubuntu/)) 14 | * Docker compose installed (see [setup instructions](https://docs.docker.com/compose/install/linux/)) 15 | * The wizard needs `docker`, `docker-compose-plugin`, and `docker-buildx-plugin` 16 | * Docker needs to be able to run without `sudo`. If you see a permission error when running 17 | `docker` commands, add yourself to the docker group: `sudo usermod -aG docker $USER` 18 | * CUDA 12.6 or greater installed (see [here](https://developer.nvidia.com/cuda-downloads) for instructions) 19 | * Install the NVIDIA Container Toolkit (see [here](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)) 20 | 21 | Once you have access to the above, please follow instructions in the [tutorial](/docs/TUTORIAL.md) to 22 | get started running Alpasim. 23 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/endpoints.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """Central registry of gRPC service stubs and endpoint helpers.""" 5 | 6 | from typing import Type 7 | 8 | from alpasim_grpc.v0.controller_pb2_grpc import VDCServiceStub 9 | from alpasim_grpc.v0.egodriver_pb2_grpc import EgodriverServiceStub 10 | from alpasim_grpc.v0.physics_pb2_grpc import PhysicsServiceStub 11 | from alpasim_grpc.v0.sensorsim_pb2_grpc import SensorsimServiceStub 12 | from alpasim_grpc.v0.traffic_pb2_grpc import TrafficServiceStub 13 | from alpasim_runtime.config import NetworkSimulatorConfig 14 | 15 | # Central mapping of service names to their gRPC stub classes. 16 | # The keys match the attribute names in NetworkSimulatorConfig. 17 | SERVICE_STUBS: dict[str, Type] = { 18 | "driver": EgodriverServiceStub, 19 | "sensorsim": SensorsimServiceStub, 20 | "physics": PhysicsServiceStub, 21 | "trafficsim": TrafficServiceStub, 22 | "controller": VDCServiceStub, 23 | } 24 | 25 | 26 | def get_service_endpoints( 27 | network_config: NetworkSimulatorConfig, 28 | services: list[str] | None = None, 29 | ) -> dict[str, tuple[Type, list[str]]]: 30 | """ 31 | Get service stubs paired with their addresses from the network config. 32 | 33 | Args: 34 | network_config: The network configuration containing service addresses. 35 | services: Optional list of service names to include. If None, includes all. 36 | 37 | Returns: 38 | Dict mapping service name -> (stub_class, addresses list). 39 | """ 40 | if services is None: 41 | services = list(SERVICE_STUBS.keys()) 42 | 43 | return { 44 | name: (SERVICE_STUBS[name], getattr(network_config, name).addresses) 45 | for name in services 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/README.md: -------------------------------------------------------------------------------- 1 | # Alpasim Utils 2 | 3 | This module contains utility functions for Alpamayo Sim that are shared across multiple services. 4 | 5 | ## Components 6 | 7 | - **artifact.py**: Artifact management and loading utilities 8 | - **trajectory.py**/**qvec.py**: Trajectory data structures and operations (QVec, Trajectory, DynamicState) 9 | - **logs.py**: ASL log reading and writing utilities 10 | - **scenario.py**: Scenario data structures (AABB, TrafficObjects, Rig, VehicleConfig, CameraId) 11 | - **asl_to_frames/**: Command-line tool for extracting frames from ASL logs 12 | - **print_asl/**: Command-line tool for printing ASL log contents 13 | 14 | ## Installation 15 | 16 | This module is typically installed as a dependency by other Alpasim services. It requires `alpasim_grpc` for protobuf message definitions. 17 | 18 | ## Usage 19 | 20 | ```python 21 | from alpasim_utils.qvec import QVec 22 | from alpasim_utils.trajectory import Trajectory 23 | from alpasim_utils.artifact import Artifact 24 | from alpasim_utils.logs import async_read_pb_log 25 | ``` 26 | 27 | ## Utilities 28 | 29 | The package also includes some analysis utilities. 30 | 31 | ### ASL 32 | 33 | Within `alpasim_utils`, there are tools for reading/writing alpasim logs (`.asl`) and converting 34 | gRPC trajectory messages into python objects with useful methods. 35 | 36 | There are also two executables that take advantage of these utilities: 37 | * `uv run -m alpasim_utils.print_asl ` is useful for dumping the content of an `.asl` file 38 | in human-readable string format for debugging purposes 39 | * `uv run -m alpasim_utils.asl_to_frames` allows for dumping raw frames or `.mp4` videos which were 40 | provided to the egodriver in the course of the simulation 41 | 42 | In both cases use `--help` to learn about the command line arguments. 43 | -------------------------------------------------------------------------------- /src/runtime/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ "setuptools>=61.0" ] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "alpasim-runtime" 7 | version = "1.48.0" 8 | description = "Runtime of the Alpamayo Sim" 9 | requires-python = ">=3.11,<3.12" 10 | authors = [{name = "Michal Tyszkiewicz", email = "mtyszkiewicz@nvidia.com"}] 11 | dependencies = [ 12 | "aiofiles>=24.1.0", 13 | "alpasim-grpc", # Local grpc package 14 | "alpasim-utils", # Local utils package 15 | "matplotlib>=3.8.0", # Metrics plotting 16 | "mergedeep>=1.3.4", 17 | "numpy>=2.0.0", 18 | "omegaconf>=2.3.0", 19 | "polars>=1.0.0", # Metrics data processing 20 | "pyarrow>=15.0.0", # Required for polars to_pandas() conversion 21 | "prometheus-client>=0.21.0", 22 | "psutil>=5.9", 23 | "nvidia-ml-py>=11.5.0", # GPU monitoring 24 | "protobuf>=4.25.3", 25 | "pyyaml>=6.0.2", 26 | "rich>=13.0.0", 27 | "scipy>=1.15.0", 28 | "seaborn>=0.13.0", # Metrics plotting 29 | "csaps>=1.2.0", 30 | "trajdata-alpasim @ git+https://github.com/NVlabs/trajdata.git@alpasim", 31 | "pytest>=7.0.0", 32 | "pytest-asyncio>=0.20.0", 33 | "mypy>=1.0.0", 34 | "types-protobuf", 35 | "types-aiofiles", 36 | "grpcio>=1.70.0", 37 | "pytest-grpc>=0.8.0", 38 | "pyyaml>=6.0.2", 39 | ] 40 | 41 | [project.optional-dependencies] 42 | nb = [ 43 | "jupyter", 44 | "ipykernel", 45 | ] 46 | 47 | [tool.uv.sources] 48 | alpasim-grpc = {workspace = true} 49 | alpasim-utils = {workspace = true} 50 | 51 | [tool.setuptools.packages.find] 52 | include = [ "alpasim_runtime", "tests" ] 53 | 54 | [tool.pytest.ini_options] 55 | asyncio_mode = "auto" 56 | markers = [ 57 | "manual: marks tests as manual execution only", 58 | ] 59 | # To run manual tests, run: `pytest -m 'manual'` 60 | addopts = "-m 'not manual'" 61 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers Guide 2 | 3 | This document provides an introduction to project management and an overview of processes for the Alpasim project. 4 | 5 | ## Overview 6 | 7 | ### Assumptions 8 | 9 | Alpasim is meant to be an open-source, best-effort research project. As such, the processes for 10 | maintaining the project are designed to allow for rapid development/iteration while maintaining 11 | a reasonable level of quality. 12 | 13 | ### Constraints 14 | 15 | The project is maintained by a small team with limited time and resources. Therefore, 16 | the processes are designed to minimize overhead and maximize efficiency. 17 | 18 | ## Software Development Process 19 | 20 | ### Branching Strategy 21 | 22 | - **`main`** - Primary branch, requires fast-forward merges only 23 | - **Feature branches** - Created from `main` for new work 24 | - **Branch naming** - Use descriptive names (e.g., `dev//`) 25 | 26 | 27 | ### Rebasing 28 | 29 | This repository requires **fast-forward merges only** to maintain a linear commit history. If your branch falls behind `main`, you must rebase: 30 | 31 | **Note:** Force pushes are expected and necessary when rebasing due to the linear history requirement. 32 | 33 | ### Merge Request Process 34 | 35 | Merge/Pull requests are required for all changes to the codebase. Templates are provided to ensure 36 | consistency and completeness. 37 | 38 | ## Release Process 39 | 40 | Versioning is automated by the CICD pipeline to ensure that versions remain uniquely identified. 41 | There is no plan to develop process for qualification/certification of releases at this time. 42 | Updates will be rolled out continuously via the `main` branch. 43 | 44 | ## Roles and Responsibilities 45 | 46 | **Program Manager/PIC**: Max Igl 47 | 48 | **Primary Researcher**: Max Igl 49 | 50 | **Owner**: Max Igl 51 | 52 | **PLC Security PIC**: Michael Watson 53 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/delay_buffer.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from collections import deque 5 | from typing import Any 6 | 7 | 8 | class DelayBuffer: 9 | """ 10 | This class provides the capability to perform time-based delays of arbitrary 11 | data types through the specification of a delay duration in microseconds and 12 | the addition/retrieval of objects to the delay model at specific timestamps. 13 | """ 14 | 15 | def __init__(self, delay_us: int): 16 | """ 17 | Initialize the delay model with a specified delay in microseconds. 18 | 19 | :param delay_us: Delay duration in microseconds. 20 | """ 21 | self.delay_us = delay_us 22 | self.queue: deque = deque() 23 | 24 | def add(self, obj: Any, timestamp_us: int) -> None: 25 | """ 26 | Add an object to the delay model at a specific timestamp. 27 | 28 | :param obj: The object to be delayed. 29 | :param timestamp_us: The timestamp at which the object is added. 30 | """ 31 | if self.queue and (timestamp_us <= self.queue[-1][0]): 32 | raise ValueError("Timestamps must be in strictly ascending order") 33 | self.queue.append((timestamp_us, obj)) 34 | 35 | def at(self, timestamp_us: int) -> Any: 36 | """ 37 | Retrieve the object that has completed its delay at the given timestamp. 38 | 39 | :param timestamp_us: The current timestamp to check for delayed objects. 40 | :return: The oldest object that has met the delay requirement, None if no object is ready. 41 | """ 42 | while ( 43 | len(self.queue) > 1 and (timestamp_us - self.queue[1][0]) >= self.delay_us 44 | ): 45 | self.queue.popleft() 46 | if self.queue: 47 | return self.queue[0][1] 48 | return None 49 | -------------------------------------------------------------------------------- /src/wizard/alpasim_wizard/s3/usdz_metadata_to_suite_yaml.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """ 5 | This script takes a usdz_metadata.csv file produced by the populate_db script 6 | and creates a suite_yaml file that can be input to the create_suite script. 7 | The output yaml file has the following format: 8 | 9 | ```yaml 10 | suites: 11 | - name: 12 | scenes: 13 | - 14 | - 15 | ``` 16 | """ 17 | 18 | import argparse 19 | import logging 20 | 21 | import pandas as pd 22 | import yaml 23 | 24 | logging.basicConfig(level=logging.INFO) 25 | logger = logging.getLogger("alpasim_wizard") 26 | 27 | 28 | def main() -> None: 29 | parser = argparse.ArgumentParser(description=__doc__) 30 | parser.add_argument("--usdz-metadata-csv", type=str, required=True) 31 | parser.add_argument("--suite-name", type=str, required=True) 32 | args = parser.parse_args() 33 | 34 | metadata_df = pd.read_csv(args.usdz_metadata_csv) 35 | config = { 36 | "suites": [ 37 | { 38 | "name": args.suite_name, 39 | "scenes": metadata_df["scene_id"].tolist(), 40 | } 41 | ] 42 | } 43 | 44 | suite_yaml_path = f"suite_{args.suite_name}.yaml" 45 | with open(suite_yaml_path, "w") as f: 46 | yaml.dump(config, f) 47 | 48 | # Print the yaml file 49 | with open(suite_yaml_path, "r") as f: 50 | logger.info(f"{f.read()}") 51 | 52 | logger.info(f"Suite yaml file created at {suite_yaml_path}") 53 | logger.info(f"Total number of scenes: {len(metadata_df)}") 54 | 55 | # Unique artifact versions 56 | unique_artifact_versions = metadata_df["nre_version_string"].unique() 57 | logger.info(f"Unique artifact versions: {unique_artifact_versions}") 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /src/runtime/tests/test_with_mocks.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import asyncio 5 | 6 | import pytest 7 | from alpasim_grpc.v0 import sensorsim_pb2 8 | from alpasim_runtime.simulate.__main__ import create_arg_parser, run_simulation 9 | 10 | 11 | @pytest.mark.asyncio 12 | async def test_mocks(monkeypatch: pytest.MonkeyPatch, tmp_path): 13 | async def fake_get_available_cameras(self, scene_id: str): 14 | del scene_id # skip-specific scenes ignored in mock mode 15 | cameras = [] 16 | for logical_id in ( 17 | "camera_front_wide_120fov", 18 | "camera_front_tele_30fov", 19 | ): 20 | cameras.append( 21 | sensorsim_pb2.AvailableCamerasReturn.AvailableCamera( 22 | logical_id=logical_id, 23 | intrinsics=sensorsim_pb2.CameraSpec( 24 | logical_id=logical_id, 25 | shutter_type=sensorsim_pb2.ShutterType.GLOBAL, 26 | ), 27 | ) 28 | ) 29 | return cameras 30 | 31 | monkeypatch.setattr( 32 | "alpasim_runtime.services.sensorsim_service.SensorsimService.get_available_cameras", 33 | fake_get_available_cameras, 34 | ) 35 | 36 | # Create required run_metadata.yaml for get_run_name() 37 | run_metadata = tmp_path / "run_metadata.yaml" 38 | run_metadata.write_text("run_name: test_mocks\n") 39 | 40 | parser = create_arg_parser() 41 | parsed_args = parser.parse_args( 42 | [ 43 | "--user-config=tests/data/mock/user-config.yaml", 44 | "--network-config=tests/data/mock/network-config.yaml", 45 | "--usdz-glob=tests/data/**/*.usdz", 46 | f"--log-dir={tmp_path}", 47 | ] 48 | ) 49 | success = await asyncio.wait_for(run_simulation(parsed_args), timeout=35) 50 | assert success 51 | -------------------------------------------------------------------------------- /src/wizard/tests/strip_usdz.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """ 5 | Script for stripping away all content of a USDZ file besides the metadata. This is useful for preparing lightweight 6 | artifacts for testing. 7 | """ 8 | 9 | import argparse 10 | import glob 11 | import io 12 | import zipfile 13 | 14 | import yaml 15 | 16 | 17 | def strip_usdz(usdz_bytes: bytes) -> bytes: 18 | # Read the original ZIP file from bytes 19 | with zipfile.ZipFile(io.BytesIO(usdz_bytes), "r") as zip_ref: 20 | metadata = zip_ref.read("metadata.yaml") 21 | 22 | # Parse the metadata 23 | metadata_dict = yaml.safe_load(metadata) 24 | metadata_dict["version_string"] = f"stripped-{metadata_dict['version_string']}" 25 | metadata_dict["uuid"] = f"stripped-{metadata_dict['uuid']}" 26 | 27 | # Write the modified metadata back to bytes 28 | metadata_str = yaml.dump(metadata_dict) 29 | metadata_bytes = metadata_str.encode("utf-8") 30 | 31 | # Create a new ZIP file in memory 32 | output_zip_bytes = io.BytesIO() 33 | with zipfile.ZipFile(output_zip_bytes, "w", zipfile.ZIP_STORED) as zip_out: 34 | zip_out.writestr("metadata.yaml", metadata_bytes) 35 | 36 | return output_zip_bytes.getvalue() 37 | 38 | 39 | if __name__ == "__main__": 40 | parser = argparse.ArgumentParser(description=__doc__) 41 | parser.add_argument("usdz_glob", type=str) 42 | parser.add_argument("output_dir", type=str) 43 | args = parser.parse_args() 44 | 45 | usdz_files = glob.glob(args.usdz_glob) 46 | 47 | for file in usdz_files: 48 | with open(file, "rb") as usdz_file: 49 | stripped_bytes = strip_usdz(usdz_file.read()) 50 | 51 | output_path = f"{args.output_dir}/{file.split('/')[-1]}" 52 | print(f"Writing stripped USDZ to {output_path}") 53 | with open(output_path, "wb") as output_file: 54 | output_file.write(stripped_bytes) 55 | -------------------------------------------------------------------------------- /src/eval/src/eval/scorers/base.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import logging 5 | from abc import ABC, abstractmethod 6 | 7 | from eval.data import EvaluationResultContainer, MetricReturn, SimulationResult 8 | from eval.schema import EvalConfig 9 | 10 | logger = logging.getLogger("alpasim_eval") 11 | 12 | 13 | class Scorer(ABC): 14 | """Abstract base class for scoring metrics.""" 15 | 16 | def __init__(self, cfg: EvalConfig) -> None: 17 | self.cfg = cfg 18 | 19 | @abstractmethod 20 | def calculate(self, simulation_result: SimulationResult) -> list[MetricReturn]: 21 | """Calculate metrics for entire trajectory.""" 22 | pass 23 | 24 | def run( 25 | self, eval_result_container: EvaluationResultContainer 26 | ) -> EvaluationResultContainer: 27 | """Run the scorer.""" 28 | if eval_result_container.sim_result is None: 29 | raise ValueError("Simulation result is not set.") 30 | eval_result_container.add_metric_results( 31 | self.calculate(eval_result_container.sim_result) 32 | ) 33 | return eval_result_container 34 | 35 | 36 | class ScorerGroup(Scorer): 37 | """Group of scorers.""" 38 | 39 | def __init__(self, scorers: list[Scorer]) -> None: 40 | self.scorers = scorers 41 | 42 | def calculate(self, simulation_result: SimulationResult) -> list[MetricReturn]: 43 | """Calculate metrics for entire trajectory.""" 44 | results = [] 45 | for scorer in self.scorers: 46 | try: 47 | results.extend(scorer.calculate(simulation_result)) 48 | except Exception as e: 49 | # We're not raising an error here because we want to continue 50 | # scoring other metrics even if one metric fails. 51 | logger.error( 52 | "Error calculating metrics for %s: %s", scorer.__class__.__name__, e 53 | ) 54 | return results 55 | -------------------------------------------------------------------------------- /src/runtime/tests/test_noise_models.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import math 5 | 6 | import numpy as np 7 | import pytest 8 | from alpasim_runtime.config import EgomotionNoiseModelConfig 9 | from alpasim_runtime.noise_models import EgomotionNoiseModel 10 | 11 | 12 | def test_EgomotionNoiseModel(): 13 | cov_r = 0.1 14 | cov_o = 0.01 15 | tau_r = 1.0 16 | tau_o = 2.0 17 | DT = 0.1 18 | P_r_ss = cov_r * DT**2 / (2 * tau_r) 19 | P_o_ss = cov_o * DT**2 / (2 * tau_o) 20 | 21 | noise_model = EgomotionNoiseModel( 22 | np.eye(3) * cov_r, np.eye(3) * cov_o, tau_r, tau_o 23 | ) 24 | # sanity check that the noise is stable relative to theoretical steady state 25 | for i in range(100): 26 | noise = noise_model.update(DT) 27 | assert np.linalg.norm(noise.vec3) < 5.0 * math.sqrt(P_r_ss) 28 | assert np.linalg.norm(noise.quat[0:3]) < 5.0 * math.sqrt(P_o_ss) 29 | assert np.linalg.norm(noise.quat) == pytest.approx(1.0) 30 | 31 | 32 | def test_EgomotionNoiseModel_raise_on_invalid_construction(): 33 | with pytest.raises(ValueError): 34 | EgomotionNoiseModel(np.eye(2), np.eye(3), 1.0, 1.0) 35 | with pytest.raises(ValueError): 36 | EgomotionNoiseModel(np.eye(3), np.eye(2), 1.0, 1.0) 37 | with pytest.raises(ValueError): 38 | EgomotionNoiseModel(np.eye(3), np.eye(3), -1.0, 1.0) 39 | with pytest.raises(ValueError): 40 | EgomotionNoiseModel(np.eye(3), np.eye(3), 1.0, -1.0) 41 | 42 | 43 | def test_EgomotionNoiseModel_from_config(): 44 | config = EgomotionNoiseModelConfig() 45 | config.enabled = False 46 | assert EgomotionNoiseModel.from_config(config) is None 47 | 48 | # Create the noise model, spot checking internal values 49 | config.enabled = True 50 | egomotion_noise = EgomotionNoiseModel.from_config(config) 51 | assert egomotion_noise is not None 52 | assert egomotion_noise.time_constant_position == pytest.approx( 53 | config.time_constant_position 54 | ) 55 | -------------------------------------------------------------------------------- /src/wizard/configs/deploy/local_oss_2gpus.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # Config for running on workstations with two 40gb+ GPUs, see TUTORIAL 3 | # for extra instructions. In short, use cmd line arg "+deploy=local_2gpus" with wizard. 4 | 5 | defaults: 6 | - /stable_manifest/oss 7 | - _self_ 8 | 9 | defines: 10 | # The repo-relative resolver makes the path relative to the git repo root. 11 | filesystem: ${repo-relative:"data"} 12 | nre_cache_size: 4 # nr_concurrent_rollouts + 1 13 | physics_cache_size: 16 # should match or exceed concurrent unique scenes 14 | 15 | services: 16 | sensorsim: 17 | environments: 18 | - OMP_NUM_THREADS=1 19 | 20 | # leave some room for traffic sim if enabled in sub-configs 21 | # - PYTORCH_CUDA_ALLOC_CONF=garbage_collection_threshold:0.7 22 | replicas_per_container: 3 23 | gpus: [1] 24 | 25 | driver: 26 | replicas_per_container: 3 27 | gpus: [0] 28 | 29 | physics: 30 | replicas_per_container: 1 31 | gpus: [0, 1] 32 | 33 | trafficsim: 34 | replicas_per_container: 1 35 | gpus: [0, 1] 36 | 37 | controller: 38 | replicas_per_container: 6 39 | gpus: null 40 | 41 | runtime: 42 | nr_workers: 4 # 12 endpoints / 4 workers = 3 endpoints per worker 43 | endpoints: 44 | # scaling the number of rollouts assigned to each endpoint to add up 45 | # # instances x # rollouts = number of rollouts 46 | # SENS: 1 (nr_gpus) x 3 (replicas_per_container) x 4 (n_concurrent_rollouts) = 12 47 | # DRIV: 1 (nr_gpus) x 3 (replicas_per_container) x 4 (n_concurrent_rollouts) = 12 48 | # PHYS: 2 (nr_gpus) x 1 (replicas_per_container) x 6 (n_concurrent_rollouts) = 12 49 | # CONT: 1 (nr_gpus) x 6 (replicas_per_container) x 2 (n_concurrent_rollouts) = 12 50 | sensorsim: 51 | n_concurrent_rollouts: 4 52 | 53 | driver: 54 | n_concurrent_rollouts: 4 55 | skip: false 56 | 57 | physics: 58 | n_concurrent_rollouts: 6 59 | skip: false 60 | 61 | controller: 62 | n_concurrent_rollouts: 2 63 | skip: false 64 | 65 | trafficsim: 66 | skip: true 67 | -------------------------------------------------------------------------------- /src/runtime/tests/test_config.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import omegaconf 5 | import pytest 6 | import yaml 7 | from alpasim_runtime import config 8 | 9 | 10 | def test_typed_parse_config_valid(): 11 | # sanity check for user config 12 | user_cfg = config.typed_parse_config( 13 | "tests/data/valid_user_config.yaml", config.UserSimulatorConfig 14 | ) 15 | assert user_cfg.scenarios[0].force_gt_duration_us == 1700000 16 | 17 | # also check that the default values are honored for those not in the yaml 18 | default = config.ScenarioConfig() 19 | assert user_cfg.scenarios[0].control_timestep_us == default.control_timestep_us 20 | 21 | # sanity check for network config 22 | network_cfg = config.typed_parse_config( 23 | "tests/data/valid_network_config.yaml", config.NetworkSimulatorConfig 24 | ) 25 | assert network_cfg.sensorsim.addresses[0] == "nre:6000" 26 | assert network_cfg.trafficsim.addresses[0] == "trafficsim:6200" 27 | 28 | 29 | def test_typed_parse_config_invalid_config_type(): 30 | # attempt to create a user config from a network config file 31 | with pytest.raises(omegaconf.errors.ConfigKeyError): 32 | config.typed_parse_config( 33 | "tests/data/valid_network_config.yaml", config.UserSimulatorConfig 34 | ) 35 | 36 | 37 | def test_typed_parse_config_file_not_found(): 38 | # attempt to create a user config from a non-existent file 39 | with pytest.raises(FileNotFoundError): 40 | config.typed_parse_config("non_existent_file.yaml", config.UserSimulatorConfig) 41 | 42 | 43 | def test_typed_parse_config_invalid_yaml(tmp_path): 44 | not_yaml = tmp_path / "not_yaml.txt" 45 | not_yaml.write_text("&&&this is not a yaml file\n") 46 | 47 | with pytest.raises(yaml.YAMLError): 48 | config.typed_parse_config(not_yaml, config.UserSimulatorConfig) 49 | 50 | 51 | # TODO(mwatson, mtyszkiewicz): What should happen when the config is empty? Currently, 52 | # no error is raised, and we return an empty config object. Is this the desired behavior? 53 | -------------------------------------------------------------------------------- /src/wizard/configs/deploy/ord.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # Config for running on ORD, points to artifacts and caches managed by alpasim team 3 | # Use using the cmd line arg "+deploy=ord" when calling the wizard or sbatch submit.sh 4 | 5 | defaults: 6 | - /stable_manifest/internal 7 | - _self_ 8 | 9 | defines: 10 | filesystem: "/lustre/fs12/portfolios/av/projects/av_alpamayo_sim/.cache" 11 | nre_cache_size: 8 12 | physics_cache_size: 16 # should match or exceed concurrent unique scenes 13 | 14 | wizard: 15 | run_method: "SLURM" 16 | 17 | services: 18 | sensorsim: 19 | environments: 20 | - OMP_NUM_THREADS=1 21 | 22 | # leave some room for traffic sim if enabled in sub-configs 23 | - PYTORCH_CUDA_ALLOC_CONF=garbage_collection_threshold:0.7 24 | 25 | replicas_per_container: 1 26 | gpus: [0, 1, 2, 3] 27 | 28 | 29 | driver: 30 | environments: 31 | - PYTORCH_CUDA_ALLOC_CONF=garbage_collection_threshold:0.5 # 40gb should be ample 32 | 33 | gpus: [4, 5, 6, 7] 34 | replicas_per_container: 1 35 | 36 | volumes: 37 | # Base config 38 | - "${defines.alpackages}:/mnt/alpackages" 39 | - "${wizard.log_dir}:/mnt/output" 40 | 41 | # New mount for lustre 42 | - "/lustre:/lustre" 43 | 44 | physics: 45 | replicas_per_container: 1 46 | gpus: [2, 3, 4, 5, 6, 7] 47 | 48 | controller: 49 | replicas_per_container: 2 50 | gpus: null 51 | 52 | trafficsim: 53 | replicas_per_container: 1 54 | gpus: [0, 1] 55 | 56 | runtime: 57 | endpoints: 58 | # scaling the number of rollouts assigned to each endpoint to add up 59 | # # instances x # rollouts = number of rollouts 60 | # 4 x 3 = 12 61 | # 4 x 3 = 12 62 | # 6 x 2 = 12 63 | # 2 x 6 = 12 64 | sensorsim: 65 | n_concurrent_rollouts: 3 66 | 67 | driver: 68 | n_concurrent_rollouts: 3 69 | 70 | physics: 71 | n_concurrent_rollouts: 2 72 | skip: false 73 | 74 | controller: 75 | n_concurrent_rollouts: 6 76 | skip: false 77 | 78 | trafficsim: 79 | skip: true 80 | -------------------------------------------------------------------------------- /src/wizard/configs/deploy/ord_oss.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | # Config for running the OSS stack on ORD resources. 3 | # Use the cmd line arg "+deploy=ord_oss" when calling the wizard or sbatch submit.sh 4 | 5 | defaults: 6 | - /stable_manifest/oss 7 | - _self_ 8 | 9 | defines: 10 | filesystem: "/lustre/fs12/portfolios/av/projects/av_alpamayo_sim/.cache" 11 | nre_cache_size: 5 # Because below we have 2 sensorsim instances on each GPU 12 | physics_cache_size: 6 # should match or exceed concurrent unique scenes 13 | 14 | eval: 15 | num_processes: 32 16 | 17 | wizard: 18 | run_method: "SLURM" 19 | 20 | services: 21 | sensorsim: 22 | environments: 23 | - OMP_NUM_THREADS=1 24 | 25 | # leave some room for traffic sim if enabled in sub-configs 26 | - PYTORCH_CUDA_ALLOC_CONF=garbage_collection_threshold:0.7 27 | 28 | replicas_per_container: 4 29 | gpus: [0, 1, 2, 3] 30 | 31 | 32 | driver: 33 | replicas_per_container: 8 34 | gpus: [4, 5, 6, 7] 35 | 36 | physics: 37 | replicas_per_container: 4 38 | gpus: [4, 5, 6, 7] 39 | 40 | controller: 41 | replicas_per_container: 16 42 | gpus: null 43 | 44 | trafficsim: 45 | replicas_per_container: 1 46 | gpus: [0, 1] 47 | 48 | runtime: 49 | # 64 endpoints / 8 workers = 8 endpoints per worker 50 | nr_workers: 8 51 | endpoints: 52 | # scaling the number of rollouts assigned to each endpoint to add up 53 | # # instances x # rollouts = number of rollouts 54 | # SENS: 4 (nr_gpus) x 4 (replicas_per_container) x 4 (n_concurrent_rollouts) = 64 55 | # DRIV: 4 (nr_gpus) x 8 (replicas_per_container) x 2 (n_concurrent_rollouts) = 64 56 | # PHYS: 4 (nr_gpus) x 4 (replicas_per_container) x 4 (n_concurrent_rollouts) = 64 57 | # CONT: 1 (nr_gpus) x 16 (replicas_per_container) x 4 (n_concurrent_rollouts) = 64 58 | sensorsim: 59 | n_concurrent_rollouts: 4 60 | 61 | driver: 62 | n_concurrent_rollouts: 2 63 | skip: false 64 | 65 | physics: 66 | n_concurrent_rollouts: 4 67 | skip: false 68 | 69 | controller: 70 | n_concurrent_rollouts: 4 71 | skip: false 72 | 73 | trafficsim: 74 | skip: true 75 | -------------------------------------------------------------------------------- /src/grpc/alpasim_grpc/v0/common.proto: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright (c) 2025 NVIDIA Corporation 3 | 4 | // This file is manually kept in sync with protos in NRE (nre.grpc.protos). 5 | // In the future we should develop an abstraction layer to avoid directly baking NRE protos into the codebase. 6 | 7 | syntax = "proto3"; 8 | 9 | package common; 10 | 11 | //////////////////////////////////////////////////////////////////////////////// 12 | // Common types 13 | //////////////////////////////////////////////////////////////////////////////// 14 | 15 | message Empty {} 16 | 17 | message Quat { 18 | float w = 1; 19 | float x = 2; 20 | float y = 3; 21 | float z = 4; 22 | } 23 | 24 | message Vec3 { 25 | float x = 1; 26 | float y = 2; 27 | float z = 3; 28 | } 29 | 30 | message Pose { 31 | // convention: translation (`vec`) is applied 1st, then rotation `quat` 32 | Vec3 vec = 1; 33 | Quat quat = 2; 34 | } 35 | 36 | message DynamicState { 37 | Vec3 angular_velocity = 1; 38 | Vec3 linear_velocity = 2; 39 | Vec3 linear_acceleration = 3; 40 | Vec3 angular_acceleration = 4; 41 | } 42 | 43 | message AABB { // axis aligned bounding box 44 | float size_x = 1; 45 | float size_y = 2; 46 | float size_z = 3; 47 | } 48 | 49 | message PoseAtTime { 50 | Pose pose = 1; 51 | fixed64 timestamp_us = 2; 52 | } 53 | 54 | message StateAtTime { 55 | fixed64 timestamp_us = 1; 56 | Pose pose = 2; 57 | DynamicState state = 3; 58 | } 59 | 60 | message Trajectory { 61 | repeated PoseAtTime poses = 1; 62 | } 63 | 64 | message VersionId { 65 | message APIVersion { 66 | uint32 major = 1; 67 | uint32 minor = 2; 68 | uint32 patch = 3; 69 | } 70 | 71 | string version_id = 1; 72 | string git_hash = 2; 73 | 74 | // grpc_api_version should be set to alpasim_grpc.API_VERSION_MESSAGE 75 | APIVersion grpc_api_version = 3; 76 | 77 | // any additional data such as hardware config? 78 | } 79 | 80 | message SessionRequestStatus {} // empty; reserved name in case we want to extend it in the future 81 | 82 | message AvailableScenesReturn { 83 | repeated string scene_ids = 1; 84 | } 85 | 86 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/replay_services/driver_replay_service.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """ 5 | Driver (Egodriver) replay service implementation. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import logging 11 | from typing import Any 12 | 13 | from alpasim_grpc.v0 import egodriver_pb2_grpc 14 | from alpasim_runtime.replay_services.asl_reader import ASLReader 15 | 16 | import grpc 17 | 18 | from .base_replay_servicer import BaseReplayServicer 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class DriverReplayService( 24 | BaseReplayServicer, egodriver_pb2_grpc.EgodriverServiceServicer 25 | ): 26 | """Replay service for the driver/policy service""" 27 | 28 | def __init__(self, asl_reader: ASLReader): 29 | super().__init__(asl_reader, "driver") 30 | 31 | def drive(self, request: Any, context: grpc.ServicerContext) -> Any: 32 | """Return recorded trajectory""" 33 | return self.validate_request("drive", request, context) 34 | 35 | def submit_image_observation( 36 | self, request: Any, context: grpc.ServicerContext 37 | ) -> Any: 38 | """Validate image submission""" 39 | return self.validate_request("submit_image_observation", request, context) 40 | 41 | def submit_egomotion_observation( 42 | self, request: Any, context: grpc.ServicerContext 43 | ) -> Any: 44 | """Validate egomotion data""" 45 | return self.validate_request("submit_egomotion_observation", request, context) 46 | 47 | def submit_route(self, request: Any, context: grpc.ServicerContext) -> Any: 48 | """Validate route request""" 49 | return self.validate_request("submit_route", request, context) 50 | 51 | def submit_recording_ground_truth( 52 | self, request: Any, context: grpc.ServicerContext 53 | ) -> Any: 54 | """Validate ground truth""" 55 | return self.validate_request("submit_recording_ground_truth", request, context) 56 | 57 | def submit_av_message(self, request: Any, context: grpc.ServicerContext) -> Any: 58 | """Validate AV message (if supported)""" 59 | # AV messages might not be in all ASL files, so allow missing 60 | return self.validate_request("submit_av_message", request, context) 61 | -------------------------------------------------------------------------------- /src/utils/alpasim_utils/print_asl/__main__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import argparse 5 | import asyncio 6 | from typing import Optional 7 | 8 | from alpasim_grpc.v0.logging_pb2 import LogEntry 9 | from alpasim_utils.logs import async_read_pb_log 10 | 11 | 12 | async def print_asl( 13 | file_path: str, 14 | start: int, 15 | end: Optional[int], 16 | message_types: set[str], 17 | just_types: bool, 18 | ) -> None: 19 | message_i = 0 20 | async for log_entry in async_read_pb_log(file_path): 21 | if message_i == end: 22 | break 23 | 24 | if message_i < start: 25 | continue 26 | 27 | if log_entry.WhichOneof("log_entry") in message_types: 28 | if just_types: 29 | print(log_entry.WhichOneof("log_entry")) 30 | else: 31 | print(log_entry) 32 | 33 | message_i += 1 34 | 35 | 36 | log_entry_fields: tuple[str] = tuple(field.name for field in LogEntry.DESCRIPTOR.fields) 37 | 38 | if __name__ == "__main__": 39 | parser = argparse.ArgumentParser() 40 | parser.add_argument( 41 | "asl_file", 42 | type=str, 43 | help="Path of the .asl file to print", 44 | ) 45 | parser.add_argument( 46 | "--start", 47 | type=int, 48 | default=0, 49 | help="Index of the first message to print (default: 0)", 50 | ) 51 | parser.add_argument( 52 | "--end", 53 | type=int, 54 | default=None, 55 | help="Index of the last message to print (default: print all)", 56 | ) 57 | parser.add_argument( 58 | "--message-types", 59 | nargs="+", 60 | choices=log_entry_fields, 61 | default=log_entry_fields, 62 | metavar="MSG_TYPE", 63 | help=f"Message types to print, by default all of {', '.join(log_entry_fields)}.", 64 | ) 65 | parser.add_argument( 66 | "--just-types", 67 | action="store_true", 68 | help="Only print the type of the message (and not the content)", 69 | ) 70 | args = parser.parse_args() 71 | 72 | asyncio.run( 73 | print_asl( 74 | file_path=args.asl_file, 75 | start=args.start, 76 | end=args.end, 77 | message_types=frozenset(args.message_types), 78 | just_types=args.just_types, 79 | ) 80 | ) 81 | -------------------------------------------------------------------------------- /src/grpc/alpasim_grpc/v0/physics.proto: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright (c) 2025 NVIDIA Corporation 3 | 4 | syntax = "proto3"; 5 | 6 | package physics; 7 | 8 | import "alpasim_grpc/v0/common.proto"; 9 | 10 | service PhysicsService { 11 | rpc ground_intersection (PhysicsGroundIntersectionRequest) returns (PhysicsGroundIntersectionReturn); 12 | rpc get_version (common.Empty) returns (common.VersionId); 13 | rpc get_available_scenes (common.Empty) returns (common.AvailableScenesReturn); 14 | rpc shut_down (common.Empty) returns (common.Empty); 15 | } 16 | 17 | message PhysicsGroundIntersectionRequest { 18 | message PosePair { 19 | // `now_pose` and `future_pose` are active transforms local->aabb for the 20 | // bounding-box center at the current and future timestamps. The physics 21 | // system adjusts `future_pose`; the runtime assumes `now_pose` is already 22 | // valid (typically copied from the previous call). See CONTRIBUTING.md, 23 | // "Coordinate Systems". 24 | common.Pose now_pose = 1; 25 | common.Pose future_pose = 2; 26 | } 27 | 28 | message EgoData { 29 | common.AABB aabb = 1; // TODO: replace with mesh ID? 30 | PosePair pose_pair = 2; 31 | } 32 | 33 | message OtherObject { 34 | // mostly vehicles but likely we don't want pedestrians to clip into the ground either 35 | common.AABB aabb = 1; 36 | PosePair pose_pair = 2; 37 | } 38 | 39 | // to identify the ground mesh 40 | string scene_id = 1; 41 | 42 | // start and end of all PosePairs in the message 43 | fixed64 now_us = 2; 44 | fixed64 future_us = 3; 45 | 46 | optional EgoData ego_data = 4; 47 | repeated OtherObject other_objects = 5; 48 | } 49 | 50 | message PhysicsGroundIntersectionReturn { 51 | enum Status { 52 | SUCCESSFUL_UPDATE = 0; 53 | UNKNOWN = 1; 54 | INSUFFICIENT_POINTS_FITPLANE = 2; 55 | HIGH_TRANSLATION = 3; 56 | HIGH_ROTATION = 4; 57 | 58 | } 59 | 60 | message ReturnPose { 61 | // Pose represents the active transform local->aabb for the center of 62 | // the AABB (see CONTRIBUTING.md, "Coordinate Systems"). 63 | common.Pose pose = 1; 64 | Status status = 2; 65 | } 66 | 67 | optional ReturnPose ego_pose = 1; 68 | repeated ReturnPose other_poses = 2; // return in same order as in request 69 | } 70 | -------------------------------------------------------------------------------- /src/controller/tools/plot_controller.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import argparse 5 | 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | 9 | 10 | def main(log_file): 11 | # load csv data 12 | data = np.loadtxt(log_file, delimiter=",", skiprows=1) 13 | 14 | plt.figure(figsize=(10, 6)) 15 | plt.subplot(3, 1, 1) 16 | plt.plot(data[:, 0], data[:, 1], label="X Position") 17 | plt.title("Position Over Time") 18 | plt.ylabel("X Position (m)") 19 | plt.subplot(3, 1, 2) 20 | plt.plot(data[:, 0], data[:, 2], label="Y Position") 21 | plt.ylabel("Y Position (m)") 22 | plt.subplot(3, 1, 3) 23 | plt.plot(data[:, 0], data[:, 3], label="Z Position") 24 | plt.ylabel("Z Position (m)") 25 | 26 | plt.figure(figsize=(10, 6)) 27 | plt.plot(data[:, 1], data[:, 2], label="Trajectory") 28 | plt.title("Trajectory in XY Plane") 29 | 30 | plt.figure(figsize=(10, 6)) 31 | plt.subplot(2, 1, 1) 32 | plt.plot(data[:, 0], data[:, 8], label="X Velocity") 33 | plt.title("Velocity Over Time") 34 | plt.ylabel("X Velocity (m/s)") 35 | 36 | plt.subplot(2, 1, 2) 37 | plt.plot(data[:, 0], data[:, 9], label="Y Velocity") 38 | plt.ylabel("Y Velocity (m/s)") 39 | 40 | plt.figure(figsize=(10, 6)) 41 | plt.subplot(2, 1, 1) 42 | plt.plot(data[:, 0], data[:, 11], "--", label="steering angle command") 43 | plt.plot(data[:, 0], data[:, 15], label="steering angle") 44 | plt.title("Steering Angle Over Time") 45 | 46 | plt.subplot(2, 1, 2) 47 | plt.plot(data[:, 0], data[:, 12], "--", label="accel command") 48 | plt.plot(data[:, 0], data[:, 16], label="accel") 49 | plt.title("Acceleration Over Time") 50 | 51 | plt.figure(figsize=(10, 6)) 52 | plt.subplot(3, 1, 1) 53 | plt.plot(data[:, 0], data[:, 17], label="X Position Error") 54 | plt.title("Position Error Over Time") 55 | plt.ylabel("X Position Error (m)") 56 | plt.subplot(3, 1, 2) 57 | plt.plot(data[:, 0], data[:, 18], label="Y Position Error") 58 | plt.ylabel("Y Position Error (m)") 59 | plt.subplot(3, 1, 3) 60 | plt.plot(data[:, 0], data[:, 19], label="Yaw Error") 61 | plt.ylabel("Yaw Error (rad)") 62 | 63 | plt.show(block=True) 64 | 65 | 66 | if __name__ == "__main__": 67 | parser = argparse.ArgumentParser(description="Plot Controller") 68 | parser.add_argument("--log", type=str, required=True, help="Path to data file") 69 | args = parser.parse_args() 70 | 71 | main(args.log) 72 | -------------------------------------------------------------------------------- /docs/DATA_PIPELINE.md: -------------------------------------------------------------------------------- 1 | # Alpasim data 2 | This document is meant to describe data handling in Alpasim to help test and data engineers build the test cases they want and troubleshoot issues. 3 | 4 | ## asl files 5 | The output of simulation in alpasim are `asl` files (it stands for AlpaSim Log). These are a 6 | size-delimited protobuf stream with a custom schema defined 7 | [here](/src/grpc/alpasim_grpc/v0/logging.proto). Each rollout will create its own `asl` file with 8 | three types of messages: 9 | * A metadata header (see `RolloutMetadata`) aiming to help with reproducibility and book keeping 10 | * Actor poses (see `ActorPoses`) messages which inform about the location of all actors (including 11 | `'EGO'`) in global coordinate space 12 | * Microservice requests and responses (see `*_request`/`*_return` messages) which enable 13 | reproducing behavior of a given service in replay mode without starting up the entire simulator 14 | 15 | > :green_book: `RolloutCameraImage` requests allow for assembling an `.mp4` video out of an `.asl` 16 | log. 17 | 18 | > :warning: The simulation header doesn't specify the `usdz` file uuid. 19 | 20 | ### Reading asl logs 21 | `alpasim-grpc` provides [async_read_pb_log](//src/utils/alpasim_utils/logs.py) for reading `asl` 22 | logs as a stream of messages. 23 | An example usage to print the first 20 messages in a log (since `async_read_pb_log` is an async 24 | function it needs to be executed from a jupyter notebook or submitted to an async runtime loop): 25 | 26 | ```python 27 | from alpasim_grpc.utils.logs import async_read_pb_log 28 | 29 | i = 0 30 | async for log_entry in async_read_pb_log(".asl"): 31 | print(log_entry) 32 | i += 1 33 | if i == 20: 34 | break 35 | ``` 36 | 37 | results in 38 | 39 | ``` 40 | rollout_metadata { 41 | session_metadata { 42 | session_uuid: "a5823758-a782-11ef-aa43-0242c0a89003" 43 | scene_id: "clipgt-3055a5c9-53e8-4e20-b41a-19c0f917b081" 44 | batch_size: 1 45 | n_sim_steps: 120 46 | start_timestamp_us: 1689697803493732 47 | control_timestep_us: 99000 48 | } 49 | actor_definitions { 50 | } 51 | force_gt_duration: 1700000 52 | version_ids { 53 | runtime_version { 54 | version_id: "0.3.0" 55 | git_hash: "83bf78502c43dabac683d68b3712cdca17f6a810+dirty" 56 | grpc_api_version { 57 | minor: 24 58 | } 59 | } 60 | egodriver_version { 61 | version_id: "0.0.0" 62 | git_hash: "mock" 63 | grpc_api_version { 64 | minor: 23 65 | ... 66 | image_bytes: "\377\330\377\340\000\020JFIF\000\001..." 67 | } 68 | } 69 | 70 | Output is truncated. 71 | ``` 72 | -------------------------------------------------------------------------------- /src/wizard/configs/custom_code/driver.yaml: -------------------------------------------------------------------------------- 1 | # @package _global_ 2 | 3 | defines: 4 | driver_code_path: ${wizard.log_dir}/driver_code 5 | 6 | # Inference config values of the model can be overridden here in the yaml or 7 | # from the command line, e.g.: 8 | # > sbatch submit.sh ++driver.inference.Cframes=4 9 | # Or in this file, e.g.: 10 | # > driver: 11 | # > inference: 12 | # > Cframes: 4 13 | # > # If we want to use interpolation, we need to escape it here. 14 | # > min_num_historical_waypoints: \${inference.num_historical_waypoints} 15 | 16 | services: 17 | # we'll override the Alpamayo endpoint definition specific in `base_config.yaml` to add an extra volume mount 18 | # and install the code it contains inside the container 19 | driver: 20 | volumes: 21 | # \/ same mounts for alpackages/output as in the ORD config + a new mount pointing to your home directory 22 | - "${defines.alpackages}:/mnt/alpackages" 23 | - "${wizard.log_dir}:/mnt/output" 24 | 25 | # \/ New mount for your code. 26 | # The image contains code under /alpamayo but instead of replacing it we mount to a new path /mnt/code. 27 | # It is assumed that driver_code_path contains a valid virtual environment for uv in .venv. 28 | - "${defines.driver_code_path}:/mnt/code" 29 | - "/lustre:/lustre" 30 | 31 | command: 32 | # Run alpsim_driver from the mounted directory. 33 | - "cd /mnt/code" 34 | - "&&" 35 | # NOTE while alpamayo uses `uv` it requires custom steps and permissions set up to build the environment 36 | # (see Dockerfile.inference). Therefore `uv sync` or `uv run` will likely fail. There are two options: 37 | # 1) Reuse the container's uv environment (recommended) 38 | # This assumes that the driver image has matching requirements with the mounted code. 39 | - "/opt/venv/bin/python projects/alpasim/alpasim_driver.py" 40 | # OR 41 | # 2) Use the user's custom uv environment in `.venv` without syncing. 42 | # This assumes that the mounted code has a valid uv environment in `.venv`. 43 | # Uncomment the following line and comment the previous one if you want to use this option. 44 | # - "uv run --no-sync projects/alpasim/alpasim_driver.py" 45 | - "--config-path=/mnt/code/projects/onboard/onboard_inference/configs" 46 | - "--config-name=default_alpasim" 47 | - "model_path=/mnt/alpackages/${defines.driver_model}" 48 | - "host=0.0.0.0" 49 | - "port={port}" 50 | - "output_dir=/mnt/output" 51 | # This converts the driver config to a string of cmd line args, 52 | # allow to override the driver config from the command. 53 | - "${cmd-line-args:${driver}}" 54 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/replay_services/sensorsim_replay_service.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """ 5 | Sensorsim replay service implementation with image correlation. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import logging 11 | 12 | from alpasim_grpc.v0 import sensorsim_pb2, sensorsim_pb2_grpc 13 | from alpasim_runtime.replay_services.asl_reader import ASLReader 14 | 15 | import grpc 16 | 17 | from .base_replay_servicer import BaseReplayServicer 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class SensorsimReplayService( 23 | BaseReplayServicer, sensorsim_pb2_grpc.SensorsimServiceServicer 24 | ): 25 | """Replay service for the sensorsim service""" 26 | 27 | def __init__(self, asl_reader: ASLReader): 28 | super().__init__(asl_reader, "sensorsim") 29 | 30 | def render_rgb( 31 | self, request: sensorsim_pb2.RGBRenderRequest, context: grpc.ServicerContext 32 | ) -> sensorsim_pb2.RGBRenderReturn: 33 | """Validate render request and return appropriate image""" 34 | # Validate the render request 35 | self.validate_request("render_rgb", request, context) 36 | 37 | # Extract camera ID from the request 38 | camera_id: str = request.camera_intrinsics.logical_id 39 | 40 | # Find corresponding image data from driver_camera_image entries 41 | # Pass the timestamp from the render request for accurate matching 42 | timestamp_us = getattr(request, "frame_start_us", 0) 43 | image_data = self.asl_reader.get_driver_image_for_camera( 44 | camera_id, timestamp_us 45 | ) 46 | 47 | if image_data is None: 48 | raise ValueError(f"No image data found for camera {camera_id}") 49 | 50 | # Create and return the response 51 | return sensorsim_pb2.RGBRenderReturn(image_bytes=image_data) 52 | 53 | def get_available_cameras( 54 | self, 55 | request: sensorsim_pb2.AvailableCamerasRequest, 56 | context: grpc.ServicerContext, 57 | ) -> sensorsim_pb2.AvailableCamerasReturn: 58 | """Return camera configuration from ASL log""" 59 | # Validate the request against the ASL log and get the recorded response 60 | return self.validate_request("get_available_cameras", request, context) 61 | 62 | def get_available_trajectories( 63 | self, 64 | request: sensorsim_pb2.AvailableTrajectoriesRequest, 65 | context: grpc.ServicerContext, 66 | ) -> sensorsim_pb2.AvailableTrajectoriesReturn: 67 | """Return available trajectories (if any)""" 68 | # Not typically used in standard simulations 69 | return sensorsim_pb2.AvailableTrajectoriesReturn() 70 | -------------------------------------------------------------------------------- /src/grpc/alpasim_grpc/v0/traffic.proto: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright (c) 2025 NVIDIA Corporation 3 | 4 | syntax = "proto3"; 5 | 6 | package traffic; 7 | 8 | import "alpasim_grpc/v0/common.proto"; 9 | 10 | service TrafficService { 11 | rpc start_session (TrafficSessionRequest) returns (common.SessionRequestStatus); 12 | rpc close_session (TrafficSessionCloseRequest) returns (common.Empty); 13 | rpc simulate (TrafficRequest) returns (TrafficReturn); 14 | rpc get_metadata (common.Empty) returns (TrafficModuleMetadata); 15 | rpc shut_down (common.Empty) returns (common.Empty); 16 | } 17 | 18 | message ObjectTrajectory { 19 | common.AABB aabb = 1; 20 | // For all objects (including ego), points describe the active transform 21 | // local->aabb for the bounding-box center (see CONTRIBUTING.md, "Coordinate 22 | // Systems"). 23 | common.Trajectory trajectory = 2; 24 | string object_id = 3; 25 | bool is_static = 4; 26 | } 27 | 28 | message ObjectTrajectoryUpdate { 29 | string object_id = 1; 30 | // Trajectory of the bounding-box center as active transforms 31 | // local->aabb (see CONTRIBUTING.md, "Coordinate Systems"). 32 | common.Trajectory trajectory = 2; 33 | } 34 | 35 | message TrafficSessionRequest { 36 | string session_uuid = 1; 37 | string map_id = 2; 38 | fixed64 random_seed = 3; 39 | repeated ObjectTrajectory logged_object_trajectories = 4; 40 | // Earliest time in microseconds when the traffic model takes over control. 41 | // For objects appearing later in the scene, they will be handed over after 42 | // `traffic_models.minimum_hisotry_length` steps of log-history, but not 43 | // before `handover_time_us`. 44 | optional fixed64 handover_time_us = 5; 45 | } 46 | 47 | message TrafficSessionCloseRequest { 48 | string session_uuid = 1; 49 | } 50 | 51 | message TrafficRequest { 52 | string session_uuid = 1; 53 | fixed64 time_query_us = 2; // time for which the next predictions are requested 54 | 55 | // Updates to trajectories not controlled by the traffic model, primarily 56 | // the ego-agent. Needs to include poses until at least `time_query_us`. 57 | repeated ObjectTrajectoryUpdate object_trajectory_updates = 3; 58 | } 59 | 60 | message TrafficReturn { 61 | // Returns the updated trajectories for all objects in the scene. 62 | // The last pose in the trajectory is the predicted pose at `time_query_us`. 63 | // Convention: Maintains the order of objects in `TrafficSessionRequest`. 64 | repeated ObjectTrajectoryUpdate object_trajectory_updates = 1; 65 | } 66 | 67 | message TrafficModuleMetadata { 68 | common.VersionId version_id = 1; 69 | fixed64 minimum_history_length_us = 2; 70 | repeated string supported_map_ids = 3; 71 | } 72 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/autoresume.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """ 5 | The autocomplete module provides a way to indicate which rollouts completed successfully and which 6 | did not. This is useful for resuming a batch of rollouts that were interrupted. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | import glob 12 | import logging 13 | import os 14 | import shutil 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | TRACKER_FILE_NAME = "_complete" 20 | 21 | 22 | def mark_rollout_complete(save_path_root: str, batch_uuid: str) -> None: 23 | """ 24 | Mark the rollout as complete by creating a file in the batch directory 25 | Args: 26 | save_path_root: asl scene directory (i.e. //) 27 | batch_uuid: the directory name of the batch 28 | """ 29 | marker_file = os.path.join( 30 | save_path_root, 31 | batch_uuid, 32 | TRACKER_FILE_NAME, 33 | ) 34 | # touch the file to mark the session as complete. allow existing file to be overwritten 35 | with open(marker_file, "w"): 36 | pass 37 | 38 | 39 | def find_num_complete_rollouts(log_dir_root: str, scene_id: str) -> int: 40 | """ 41 | Find the number of completed rollouts in the scene directory 42 | Args: 43 | log_dir_root: root directory of the logs (i.e. wizard_log_dir) 44 | scene_id: scene id 45 | Returns: 46 | number of completed rollouts as measured by the presence of the tracker file 47 | """ 48 | save_path_root = os.path.join(log_dir_root, scene_id) 49 | tracker_file_glob = os.path.join(save_path_root, "*", TRACKER_FILE_NAME) 50 | tracker_files = glob.glob(tracker_file_glob) 51 | num_finished_rollouts = len(tracker_files) 52 | logger.info( 53 | f"Autoresume {scene_id}: found {num_finished_rollouts} completed rollouts with " 54 | f"{tracker_file_glob=}" 55 | ) 56 | return num_finished_rollouts 57 | 58 | 59 | def remove_incomplete_rollouts(log_dir_root: str, scene_id: str) -> None: 60 | """ 61 | Remove incomplete rollouts (those without the tracker file) in the scene directory 62 | Args: 63 | log_dir_root: root directory of the logs (i.e. wizard_log_dir) 64 | scene_id: scene id 65 | """ 66 | save_path_root = os.path.join(log_dir_root, scene_id) 67 | if not os.path.isdir(save_path_root): # no existing results 68 | return 69 | for item in os.listdir(save_path_root): 70 | if os.path.isdir(os.path.join(save_path_root, item)) and not os.path.exists( 71 | os.path.join(save_path_root, item, TRACKER_FILE_NAME) 72 | ): 73 | logger.info( 74 | f"Removing incomplete results: {os.path.join(save_path_root, item)}" 75 | ) 76 | shutil.rmtree(os.path.join(save_path_root, item)) 77 | -------------------------------------------------------------------------------- /setup_local_env.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # SPDX-License-Identifier: Apache-2.0 3 | # Copyright (c) 2025 NVIDIA Corporation 4 | 5 | # Ensure the script is sourced, not executed 6 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then 7 | echo "❌ This script must be sourced, not executed. Use:" 8 | echo " source $0" 9 | exit 1 10 | fi 11 | 12 | # Get the repository root directory 13 | REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) 14 | 15 | # Probably not necessary, but just in case, we do an lfs pull 16 | echo "Ensuring Git LFS files are pulled..." 17 | git lfs pull 18 | if [[ $? -ne 0 ]]; then 19 | echo "❌ Git LFS pull failed. Exiting." 20 | return 1 21 | fi 22 | 23 | # Setup GRPC 24 | echo "Setting up GRPC..." 25 | pushd "${REPO_ROOT}/src/grpc" > /dev/null 26 | uv run compile-protos 27 | if [[ $? -ne 0 ]]; then 28 | echo "❌ Failed to compile protobufs. Exiting." 29 | popd > /dev/null 30 | return 1 31 | fi 32 | popd > /dev/null 33 | 34 | 35 | # Download vavam models if not already present 36 | VAVAM_DIR="${REPO_ROOT}/data/vavam-driver" 37 | if [[ ! -d "${VAVAM_DIR}" ]]; then 38 | echo "Downloading vavam assets..." 39 | ./data/download_vavam_assets.sh --model vavam-b 40 | if [[ $? -ne 0 ]]; then 41 | echo "❌ Failed to download VAVAM models. Exiting." 42 | rm -rf "${VAVAM_DIR}" 43 | return 1 44 | fi 45 | else 46 | echo "VAVAM models already present. Skipping download." 47 | fi 48 | 49 | # Ensure Hugging Face cli is installed and logged in 50 | # check for binary hf 51 | if ! command -v hf &> /dev/null; then 52 | echo "Hugging Face CLI not found. Installing with pip install -U huggingface_hub? (y/n)" 53 | read -r response 54 | if [[ "$response" == "y" ]]; then 55 | pip install -U huggingface_hub 56 | else 57 | echo "❌ Hugging Face CLI is required. Exiting." 58 | return 1 59 | fi 60 | fi 61 | 62 | echo "Logging into Hugging Face CLI..." 63 | hf auth login 64 | if [[ $? -ne 0 ]]; then 65 | echo "❌ Failed to log into Hugging Face CLI. If you have a Hugging Face token," 66 | echo " you may not have sufficient privileges on that token. Exiting." 67 | return 1 68 | fi 69 | 70 | # Download the sample model to cache 71 | hf download --repo-type=dataset \ 72 | --local-dir=data/nre-artifacts/all-usdzs \ 73 | nvidia/PhysicalAI-Autonomous-Vehicles-NuRec \ 74 | sample_set/25.07_release/Batch0001/05bb8212-63e1-40a8-b4fc-3142c0e94646/05bb8212-63e1-40a8-b4fc-3142c0e94646.usdz 75 | if [[ $? -ne 0 ]]; then 76 | echo "❌ Failed to download sample data from Hugging Face. If you have a Hugging Face token," 77 | echo " you may not have sufficient privileges on that token. Exiting." 78 | return 1 79 | fi 80 | 81 | 82 | # Install Wizard in development mode 83 | echo "Installing Wizard in development mode..." 84 | uv tool install -e "${REPO_ROOT}/src/wizard" 85 | 86 | echo "Setup complete" 87 | -------------------------------------------------------------------------------- /src/wizard/alpasim_wizard/compatibility.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import logging 5 | from dataclasses import dataclass 6 | 7 | from typing_extensions import Self 8 | 9 | logger = logging.getLogger("alpasim_wizard") 10 | 11 | 12 | def _hydra_key_to_nre_version(key: str) -> str: 13 | """ 14 | Hydra doesn't allow dots in keys, so we use underscores instead. 15 | E.g. `0_2_335-deadbeef` instead of `0.2.335-deadbeef`. 16 | """ 17 | # we expect two underscores, warn the user if the number is different 18 | if key.count("_") != 2 and key.count(".") != 2: 19 | message = f"Expected exactly two underscores or dots in {key=}." 20 | raise KeyError(message) 21 | 22 | return key.replace("_", ".") 23 | 24 | 25 | @dataclass(kw_only=True) 26 | class CompatibilityMatrix: 27 | """ 28 | Compatibility matrix for NRE versions and artifact versions. 29 | This class does three things: 30 | 1. Validates that the compatibility matrix is well-formed (no self-compatibility) 31 | 2. Converts hydra keys to NRE version strings 32 | 3. Looks up compatible artifact versions for a given NRE version 33 | """ 34 | 35 | # this is keyed by nre version, unlike USZDatabaseConfig.artifact_compatibility_matrix 36 | # which replaces dots with underscores 37 | _matrix: dict[str, dict[str, bool]] 38 | 39 | @classmethod 40 | def from_config( 41 | cls, 42 | artifact_compatibility_matrix: dict[str, dict[str, bool]], 43 | ) -> Self: 44 | # check for no self-compatibility 45 | for nre_version, compatibility in artifact_compatibility_matrix.items(): 46 | if nre_version in compatibility: 47 | raise ValueError( 48 | f"Configuration error: {nre_version=} lists itself in compatibility matrix ({compatibility=})." 49 | ) 50 | 51 | # apply hydra key transformation 52 | matrix = { 53 | _hydra_key_to_nre_version(nre_version): { 54 | _hydra_key_to_nre_version(artifact_version): is_compatible 55 | for artifact_version, is_compatible in compatibility.items() 56 | } 57 | for nre_version, compatibility in artifact_compatibility_matrix.items() 58 | } 59 | 60 | return cls(_matrix=matrix) 61 | 62 | def lookup(self, nre_version: str) -> set[str]: 63 | """Returns a set of compatible artifact versions for the given NRE version.""" 64 | if nre_version not in self._matrix: 65 | return set([nre_version]) 66 | 67 | compatibility = self._matrix[nre_version] 68 | return set( 69 | [nre_version] 70 | + [ 71 | version 72 | for version, is_compatible in compatibility.items() 73 | if is_compatible 74 | ] 75 | ) 76 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Repository Guidelines 2 | 3 | ## Project Structure & Module Organization 4 | - Core packages live in `src/`; each module (e.g., `runtime`, `wizard`, `grpc`, `utils`, `ddb`, `physics`, `avmf`, `tools`, `controller`, `driver`, `eval`) bundles its code with a colocated `tests` folder. 5 | - Shared docs in `docs/` (onboarding, design), assets & sample data in `data/`, CI wiring in `cicd/`. 6 | - Proto files compile under `src/grpc`; generated stubs feed other packages. 7 | - Scripts and helpers sit in `tools/` (includes `buildauth`, map utilities). Keep temp artifacts under `tmp/` or gitignored caches. 8 | 9 | ## Build, Test, and Development Commands 10 | - Create/update a local env: `./setup_local_env.sh` (uses `uv` to create `.venv`, install editable packages, compile gRPC stubs, and register `pre-commit` hooks). 11 | - Activate tooling with `source .venv/bin/activate`, then run module tasks using `uv run …`. 12 | - Run the fast test bundle: `uv run pytest` (respects default `-m 'not manual'` marker). Target a module with `uv run pytest src/runtime/tests`. 13 | - Static checks: `pre-commit run --all-files` covers `black`, `ruff`, import sorting, and basic lint. Type-check runtime-heavy code via `uv run mypy src/runtime`. 14 | 15 | ## Coding Style & Naming Conventions 16 | - Python 3.11+, 4-space indentation, limit files to UTF-8 ASCII unless data demands otherwise. 17 | - Auto-format with `black`; keep imports sorted by the hooks. Use `ruff` to satisfy lint warnings before pushing. 18 | - Follow PEP 8 naming plus domain hints: prefix vectors/poses with frames (`pose_local_to_rig`, `velocity_vehicle_in_local`) to avoid ambiguity in physics/AV math. 19 | - Document complex flows with concise comments; prefer dataclasses and type hints for public APIs. 20 | 21 | ## Testing Guidelines 22 | - Place tests next to their module under `src//tests` and name files `test_*.py`. 23 | - Default pytest config skips `@pytest.mark.manual` suites; mark long-running or cluster-dependent cases accordingly. 24 | - Use fixtures over hard-coded paths; when acting on sample assets, reference `data/` or create temporary files. 25 | - Extend async tests with `pytest-asyncio`; keep gRPC client stubs isolated to avoid network side-effects. 26 | 27 | ## Commit & Pull Request Guidelines 28 | - Keep commits focused and imperative (`runtime: guard invalid rig transforms`). Avoid `wip` in final history. 29 | - Rebase onto `main` before submitting; force-pushes are expected after rebases due to the linear history requirement. 30 | - Pipelines auto-bump versions for touched packages; allow the bot-generated commit to land and re-trigger CI if needed. 31 | - PRs should explain scenario impact, reference issue IDs, and attach logs/screens for wizard/runtime regressions. Confirm tests and `pre-commit` pass before requesting review. 32 | 33 | 34 | ## Other conventions 35 | - Conventions on the used coordinate frames can be found in `CONTRIBUTING.md` 36 | -------------------------------------------------------------------------------- /src/grpc/tutorial.md: -------------------------------------------------------------------------------- 1 | # GRPC tutorial 2 | This is meant as a tutorial for implementing gRPC (google remote procedure call) endpoints for the alpamayo simulator. 3 | 4 | ## Preliminaries 5 | gRPC combines 6 | 1. A language-agnostic binary serialization/deserialization format (`protobuf`) 7 | 2. A language-agnostic way to describe services (without implementation) 8 | 3. A bunch of packages for different languages which can generate code to serialize/deserialize the messages and provide implementation stubs (think `C` headers) for serving/consuming the APIs. 9 | 10 | The format for describing messages and services is `.proto`. The package for serving/consuming `gRPC` is called `grpcio` in pip and imported as `import grpc`. The package for autogenerating code is called `grpcio-tools` on pip and is imported as `grpc_tools` in Python. You do not need the latter in your code. 11 | 12 | ## The role of this project 13 | This module contains the `.proto` files defining all network interfaces in alpasim and code 14 | (`compile_protos.py`) which will use `grpcio-tools` to build these into Python "headers" (empty 15 | base classes). All you should need to do is `uv run compile-protos` and include this package as 16 | editable. 17 | 18 | ## Generated files 19 | For a given `.proto` file the compiler will create 3 files: `_pb2.py`, `_pb2_grpc.py`, and `_pb2.pyi`. 20 | 21 | The `*_pb2.py` files contain message (struct) definitions for the serialization format - think `dataclass` but completely unreadable. `*_pb2.pyi` provides type hints for those, making your IDE actually helpful. When building your service, you will receive these structs as inputs and return them as outputs. 22 | 23 | The `*_pb2_grpc.py` files contain the "headers" for the service itself and unfortunately comes without `.pyi` hints. This file will contain 3 objects of interest. On the example of a `runtime.proto` file defining the following service 24 | ```proto 25 | service RuntimeService { 26 | rpc simulate (SimulationRequest) returns (SimulationReturn); 27 | } 28 | ``` 29 | the generated `runtime_pb2_grpc.py` will contain 30 | 1. `class RuntimeServiceStub` 31 | 2. `class RuntimeServiceServicer` 32 | 3. `def add_RuntimeServiceServicer_to_server` 33 | 34 | Number 1. is for **clients**, 2. and 3. are for the **server**. 35 | 36 | ### Directory structure 37 | Unfortunately, the bare gRPC codegen doesn't produce valid Python packages - without `__init__.py` and relative imports you can't just output the generated files to an arbitrary location in your codebase and import them from there. This is the reason for this package, which has a well defined root, contains "hand-made" `__init__.py` and after installing allows imports like `from alpasim_grpc.v0.your_service_pb2_grpc import your_thing`. 38 | 39 | ## Implementing 40 | For more information on implementation, see the [gRPC official docs](https://grpc.io/docs/languages/python/basics/). 41 | -------------------------------------------------------------------------------- /src/grpc/alpasim_grpc/v0/controller.proto: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright (c) 2025 NVIDIA Corporation 3 | 4 | syntax = "proto3"; 5 | 6 | package controller; 7 | 8 | import "alpasim_grpc/v0/common.proto"; 9 | 10 | 11 | service VDCService { 12 | rpc get_version(common.Empty) returns (common.VersionId); 13 | rpc start_session (VDCSessionRequest) returns (common.SessionRequestStatus); 14 | rpc close_session (VDCSessionCloseRequest) returns (common.Empty); 15 | rpc run_controller_and_vehicle(RunControllerAndVehicleModelRequest) 16 | returns (RunControllerAndVehicleModelResponse) { 17 | } 18 | rpc shut_down(common.Empty) returns (common.Empty); 19 | } 20 | 21 | message VDCSessionRequest { 22 | string session_uuid = 1; 23 | 24 | message VehicleAndControllerParams { 25 | string rig_file = 1; // Rig file to use for the controller/vehicle model 26 | repeated string amend_files = 2; // Amend files to the configuration 27 | } 28 | VehicleAndControllerParams vehicle_and_controller_params = 2; 29 | } 30 | 31 | message VDCSessionCloseRequest { 32 | string session_uuid = 1; 33 | } 34 | 35 | message RunControllerAndVehicleModelRequest { 36 | // Unique identifier for the instance to be propagated 37 | string session_uuid = 1; 38 | 39 | // Vehicle state at the current step. 40 | // - `state.pose` is the active transform local->rig (see CONTRIBUTING.md, "Coordinate Systems") 41 | // - angular and linear rates are resolved in the rig frame 42 | common.StateAtTime state = 2; 43 | 44 | // Desired trajectory expressed in the rig frame at the timestamp of the first pose. 45 | // Each pose is relative to the rig origin. 46 | common.Trajectory planned_trajectory_in_rig = 3; 47 | 48 | // The requested timestamp at the end of the propagation. 49 | int64 future_time_us = 4; 50 | 51 | // For a configuration period at the beginning of the simulation, the system 52 | // is in a replay/priming phase. During this phase, special care should be taken 53 | // to match the dynamic state in the vehicle model to prevent unintended drift. 54 | bool coerce_dynamic_state = 5; 55 | } 56 | 57 | message RunControllerAndVehicleModelResponse { 58 | // Ground-truth active transform local->rig after propagation. 59 | common.PoseAtTime pose_local_to_rig = 1; 60 | 61 | // Estimated active transform local->rig after propagation. 62 | common.PoseAtTime pose_local_to_rig_estimated = 2; 63 | 64 | // Ground-truth dynamic state (velocity, acceleration) in rig frame after propagation. 65 | common.DynamicState dynamic_state = 3; 66 | 67 | // Estimated dynamic state in rig frame after propagation. 68 | common.DynamicState dynamic_state_estimated = 4; 69 | } 70 | -------------------------------------------------------------------------------- /src/runtime/tests/data/mock/user-config.yaml: -------------------------------------------------------------------------------- 1 | nr_workers: 1 2 | endpoints: 3 | controller: 4 | n_concurrent_rollouts: 14 5 | skip: true 6 | do_shutdown: true 7 | driver: 8 | n_concurrent_rollouts: 14 9 | skip: true 10 | physics: 11 | n_concurrent_rollouts: 14 12 | skip: true 13 | sensorsim: 14 | n_concurrent_rollouts: 2 15 | skip: true 16 | trafficsim: 17 | n_concurrent_rollouts: 14 18 | skip: true 19 | enable_autoresume: false 20 | # Because we're running sensorsim in skip mode, make sure cameras are defined. 21 | extra_cameras: 22 | - logical_id: camera_front_wide_120fov 23 | rig_to_camera: 24 | translation_m: [0.0, 0.0, 0.0] 25 | rotation_xyzw: [0.0, 0.0, 0.0, 1.0] 26 | intrinsics: 27 | model: opencv_pinhole 28 | opencv_pinhole: 29 | focal_length: [800.0, 800.0] 30 | principal_point: [400.0, 200.0] 31 | radial: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 32 | tangential: [0.0, 0.0] 33 | thin_prism: [0.0, 0.0, 0.0, 0.0] 34 | resolution_hw: [320, 512] 35 | shutter_type: ROLLING_TOP_TO_BOTTOM 36 | - logical_id: camera_front_tele_30fov 37 | rig_to_camera: 38 | translation_m: [0.0, 0.0, 0.0] 39 | rotation_xyzw: [0.0, 0.0, 0.0, 1.0] 40 | intrinsics: 41 | model: opencv_pinhole 42 | opencv_pinhole: 43 | focal_length: [800.0, 800.0] 44 | principal_point: [400.0, 200.0] 45 | radial: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] 46 | tangential: [0.0, 0.0] 47 | thin_prism: [0.0, 0.0, 0.0, 0.0] 48 | resolution_hw: [320, 512] 49 | shutter_type: ROLLING_TOP_TO_BOTTOM 50 | scenarios: 51 | - assert_zero_decision_delay: true 52 | cameras: 53 | - logical_id: camera_front_wide_120fov 54 | height: 320 55 | width: 512 56 | frame_interval_us: 100000 57 | shutter_duration_us: 30000 58 | first_frame_offset_us: -30000 59 | - logical_id: camera_front_tele_30fov 60 | height: 320 61 | width: 512 62 | frame_interval_us: 100000 63 | shutter_duration_us: 30000 64 | first_frame_offset_us: -30000 65 | control_timestep_us: 100000 66 | egomotion_noise: 67 | cov_orientation_x: 0.0 68 | cov_orientation_y: 0.0 69 | cov_orientation_z: 0.0007 70 | cov_x: 0.05 71 | cov_y: 0.05 72 | cov_z: 0.0 73 | enabled: true 74 | time_constant_orientation: 5.0 75 | time_constant_position: 3.0 76 | egopose_interval_us: 100000 77 | force_gt_duration_us: 99999999999999 # large for mock test to prevent need for real drive trajectories 78 | n_rollouts: 1 79 | n_sim_steps: 160 80 | physics_update_mode: NONE 81 | planner_delay_us: 500000 82 | ego_mask_rig_config_id: "hyperion_8_1" 83 | route_generator_type: MAP 84 | scene_id: clipgt-026d6a39-bd8f-4175-bc61-fe50ed0403a3 #clipgt-1ea7dc88-88ed-4c91-81fe-b6eb489cfa71 # Note: needs to match file from tests/data/mock/dataset-clipgt-1ea7dc88... 85 | time_start_offset_us: 300000 86 | vehicle: null 87 | -------------------------------------------------------------------------------- /src/runtime/tests/services/test_controller_api.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from unittest.mock import AsyncMock 5 | 6 | import numpy as np 7 | import pytest 8 | from alpasim_runtime.logs import LogWriter 9 | from alpasim_runtime.services.controller_service import ControllerService 10 | from alpasim_utils.qvec import QVec 11 | from alpasim_utils.trajectory import Trajectory 12 | 13 | 14 | @pytest.fixture 15 | def default_args(): 16 | qv = QVec(vec3=np.array([1.0, 0.0, 0.0]), quat=np.array([0.0, 0.0, 0.0, 1.0])) 17 | trajectory = Trajectory( 18 | timestamps_us=np.array([0], dtype=np.uint64), 19 | poses=QVec.stack([qv]), 20 | ) 21 | 22 | args = { 23 | "session_uuid": "session_uuid", 24 | "now_us": 1000000, 25 | "pose_local_to_rig": QVec( 26 | vec3=np.array([1.0, 0.0, 0.0]), quat=np.array([0.0, 0.0, 0.0, 1.0]) 27 | ), 28 | "rig_linear_velocity_in_rig": np.array([1.0, 0.0, 0.0]), 29 | "rig_angular_velocity_in_rig": np.array([0.0, 0.0, 0.0]), 30 | "rig_reference_trajectory_in_rig": trajectory, 31 | "future_us": 10000, 32 | "fallback_pose_local_to_rig_future": QVec( 33 | vec3=np.array([5.0, 0.0, 0.0]), quat=np.array([0.0, 0.0, 0.0, 1.0]) 34 | ), 35 | "force_gt": False, 36 | } 37 | return args 38 | 39 | 40 | def test_create_run_controller_and_vehicle_request(default_args): 41 | # Remove fallback_pose_local_to_rig_future and log_writer as they're not needed for request creation 42 | args_for_request = default_args.copy() 43 | del args_for_request["fallback_pose_local_to_rig_future"] 44 | 45 | request = ControllerService.create_run_controller_and_vehicle_request( 46 | **args_for_request 47 | ) 48 | # spot check 49 | assert request.session_uuid == default_args["session_uuid"] 50 | assert request.state.timestamp_us == default_args["now_us"] 51 | 52 | 53 | async def test_skip_controller_instance_run_controller_and_vehicle(default_args): 54 | """ 55 | Check that the ControllerService in skip mode returns the fallback pose 56 | """ 57 | # Create a mock log writer 58 | mock_log_writer = AsyncMock(spec=LogWriter) 59 | 60 | # Create controller service in skip mode 61 | controller = ControllerService(address="localhost:50051", skip=True) 62 | 63 | # Create a session with the controller 64 | async with controller.session( 65 | uuid=default_args["session_uuid"], log_writer=mock_log_writer 66 | ): 67 | # Remove session_uuid from args since it's now accessed via session_info 68 | args = default_args.copy() 69 | del args["session_uuid"] 70 | 71 | propagated_pose_pair = await controller.run_controller_and_vehicle(**args) 72 | pose = propagated_pose_pair.pose_local_to_rig 73 | pose_est = propagated_pose_pair.pose_local_to_rig_estimate 74 | expected_pose = default_args["fallback_pose_local_to_rig_future"] 75 | 76 | assert pose.vec3 == pytest.approx(expected_pose.vec3) 77 | assert pose.quat == pytest.approx(expected_pose.quat) 78 | 79 | assert pose_est.vec3 == pytest.approx(expected_pose.vec3) 80 | assert pose_est.quat == pytest.approx(expected_pose.quat) 81 | -------------------------------------------------------------------------------- /src/wizard/alpasim_wizard/deployment/dispatcher.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """Command dispatcher with logging capabilities for the wizard.""" 5 | 6 | import logging 7 | import subprocess 8 | from pathlib import Path 9 | from typing import Union 10 | 11 | logger = logging.getLogger("alpasim_wizard_dispatcher") 12 | 13 | 14 | class OsDispatchError(RuntimeError): 15 | """Error raised when command execution fails.""" 16 | 17 | pass 18 | 19 | 20 | def dispatch_command( 21 | cmd: str, 22 | log_dir: Union[Path, str], 23 | dry_run: bool = False, 24 | blocking: bool = True, 25 | ) -> str: 26 | """Execute a command with logging. 27 | 28 | Args: 29 | cmd: The command to execute 30 | log_dir: Directory for logging command output 31 | dry_run: If True, commands are logged but not executed 32 | blocking: If True, wait for completion and return output; if False, return immediately 33 | 34 | Returns: 35 | Command output if blocking=True, empty string otherwise 36 | """ 37 | log_dir = Path(log_dir) 38 | 39 | # Ensure log directories exist 40 | txt_logs_dir = log_dir / "txt-logs" 41 | txt_logs_dir.mkdir(parents=True, exist_ok=True) 42 | 43 | cmd_log_file = txt_logs_dir / "os_dispatch_log.txt" 44 | output_log_file = txt_logs_dir / "os_dispatch_output.txt" 45 | 46 | # Log the command 47 | with open(cmd_log_file, "a") as f: 48 | f.write(f"{cmd}\n") 49 | 50 | # Handle dry-run mode 51 | if dry_run: 52 | logger.info(f"[DRY-RUN] Would execute: {cmd}") 53 | return "" 54 | 55 | logger.info(f"Executing: {cmd}") 56 | 57 | # Execute the command with output logging 58 | with open(output_log_file, "a") as log_file: 59 | log_file.write(f"\n{'='*60}\n") 60 | log_file.write(f"Command: {cmd}\n") 61 | log_file.write(f"{'='*60}\n") 62 | # Run the command and capture output 63 | try: 64 | process = subprocess.Popen( 65 | cmd, 66 | shell=True, 67 | stdout=subprocess.PIPE, 68 | stderr=subprocess.STDOUT, 69 | text=True, 70 | bufsize=1, 71 | ) 72 | 73 | if blocking: 74 | output_lines = [] 75 | if process.stdout: 76 | for line in iter(process.stdout.readline, ""): 77 | log_file.write(line) 78 | logger.debug(line.rstrip()) 79 | output_lines.append(line) 80 | process.stdout.close() 81 | 82 | # Wait for completion and check return code 83 | return_code = process.wait() 84 | if return_code != 0: 85 | raise OsDispatchError( 86 | f"Command failed with return code {return_code}: {cmd}" 87 | ) 88 | 89 | return "".join(output_lines) 90 | else: 91 | # Non-blocking: start process and return immediately 92 | logger.info(f"Started non-blocking process: {cmd}") 93 | return "" 94 | 95 | except subprocess.SubprocessError as e: 96 | raise OsDispatchError(f"Failed to execute command '{cmd}': {e}") 97 | -------------------------------------------------------------------------------- /src/eval/src/eval/aggregation/utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import json 5 | import logging 6 | import os 7 | from pathlib import Path 8 | 9 | from filelock import FileLock 10 | 11 | logger = logging.getLogger("alpasim_eval.post_eval_aggregation") 12 | 13 | 14 | def incr_counter_and_check_aggregation_start(log_dir: str) -> bool: 15 | """ 16 | Increments counter and checks if post_eval_aggregation should be started in 17 | this job. 18 | 19 | Returns True if we're the last job in the array or if there is no array job. 20 | """ 21 | lock = FileLock(Path(log_dir) / "post_eval_aggregation.lock") 22 | task_count = int(os.environ.get("SLURM_ARRAY_TASK_COUNT", 0)) 23 | task_id = int(os.environ.get("SLURM_ARRAY_TASK_ID", 0)) 24 | if task_count == 0: 25 | logger.info("No array job, don't need to check counter.") 26 | return True 27 | with lock: 28 | # Set `prev_finished_jobs` by loading it from the file if it exists. 29 | # Otherwise set it to 0. 30 | if not (Path(log_dir) / "post_eval_aggregation.json").is_file(): 31 | logger.info("No post_eval_aggregation.json file, starting one.") 32 | prev_finished_jobs = 0 33 | prev_finished_job_ids = [] 34 | else: 35 | with open(Path(log_dir) / "post_eval_aggregation.json", "r") as f: 36 | data = json.load(f) 37 | prev_finished_jobs = data["finished_jobs"] 38 | prev_finished_job_ids = data["finished_job_ids"] 39 | logger.info( 40 | "Loaded post_eval_aggregation.json file, prev_finished_jobs: %d, prev_finished_job_ids: %s", 41 | prev_finished_jobs, 42 | prev_finished_job_ids, 43 | ) 44 | 45 | # Increment the counter in the file (create it if it doesn't exist). 46 | with open(Path(log_dir) / "post_eval_aggregation.json", "w") as f: 47 | json.dump( 48 | { 49 | "finished_jobs": prev_finished_jobs + 1, 50 | "finished_job_ids": prev_finished_job_ids + [task_id], 51 | }, 52 | f, 53 | ) 54 | logger.info( 55 | "Wrote post_eval_aggregation.json file, prev_finished_jobs + 1: %d, prev_finished_job_ids: %s", 56 | prev_finished_jobs + 1, 57 | prev_finished_job_ids, 58 | ) 59 | # Check if we're the last job and should start post_eval_aggregation 60 | if prev_finished_jobs < task_count - 1: 61 | logger.info( 62 | "Not the last job, skipping post_eval_aggregation. " 63 | "prev_finished_jobs: %d, task_count: %d, prev_finished_job_ids: %s", 64 | prev_finished_jobs, 65 | task_count, 66 | prev_finished_job_ids, 67 | ) 68 | return False 69 | elif prev_finished_jobs == task_count - 1: 70 | # We're the last job 71 | logger.info("Last job, starting post_eval_aggregation") 72 | return True 73 | elif prev_finished_jobs > task_count - 1: 74 | logger.warning( 75 | "More jobs finished than expected, probably running manually?" 76 | " If not, this might be a bug?" 77 | ) 78 | return True 79 | return False 80 | -------------------------------------------------------------------------------- /src/runtime/tests/test_scene_cache_monitor.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | from unittest.mock import Mock 5 | 6 | import pytest 7 | from alpasim_runtime.scene_cache_monitor import SceneCacheMonitor 8 | 9 | 10 | def test_scene_cache_monitor_init(): 11 | """Test that SceneCacheMonitor initializes with empty dictionaries.""" 12 | monitor = SceneCacheMonitor() 13 | assert monitor._address_to_scene_ids_in_use == {} 14 | assert monitor._max_unique_scene_count_per_address == {} 15 | 16 | 17 | def test_scene_cache_monitor_update_increment_nominal(): 18 | """Test incrementing usage for all nominal cases: new service/scene, 19 | existing service/new scene, existing service/existing scene.""" 20 | monitor = SceneCacheMonitor() 21 | service1 = Mock() 22 | service1.address = "service1:8000" 23 | 24 | service2 = Mock() 25 | service2.address = "service2:8000" 26 | 27 | # Case 1: New service and new scene_id 28 | monitor.increment(service1, "scene1") 29 | assert monitor._address_to_scene_ids_in_use == {service1.address: {"scene1": 1}} 30 | assert monitor._max_unique_scene_count_per_address == {service1.address: 1} 31 | 32 | # Case 2: Existing service with a new scene_id 33 | monitor.increment(service1, "scene2") 34 | assert monitor._address_to_scene_ids_in_use == { 35 | service1.address: {"scene1": 1, "scene2": 1} 36 | } 37 | assert monitor._max_unique_scene_count_per_address == {service1.address: 2} 38 | 39 | # Case 3: Existing service and existing scene_id (increment count) 40 | monitor.increment(service1, "scene1") 41 | assert monitor._address_to_scene_ids_in_use == { 42 | service1.address: {"scene1": 2, "scene2": 1} 43 | } 44 | assert monitor._max_unique_scene_count_per_address == {service1.address: 2} 45 | 46 | # Case 4: New service 47 | for i in range(3): 48 | monitor.increment(service2, "sceneA") 49 | assert monitor._address_to_scene_ids_in_use == { 50 | service1.address: {"scene1": 2, "scene2": 1}, 51 | service2.address: {"sceneA": 3}, 52 | } 53 | 54 | # Decrement back to original state, check that max count remains 55 | monitor.decrement(service1, "scene1") 56 | monitor.decrement(service1, "scene1") 57 | monitor.decrement(service1, "scene2") 58 | assert monitor._max_unique_scene_count_per_address == { 59 | service1.address: 2, 60 | service2.address: 1, 61 | } 62 | 63 | 64 | def test_scene_cache_monitor_update_decrement_non_existent_service_raises_exception(): 65 | """Test that decrementing for a non-existent service raises and exception.""" 66 | monitor = SceneCacheMonitor() 67 | service = Mock() 68 | service.address = "service1:8000" 69 | 70 | with pytest.raises(ValueError): 71 | monitor.decrement(service, "scene1") 72 | assert monitor._address_to_scene_ids_in_use == {} 73 | 74 | 75 | def test_scene_cache_monitor_update_decrement_non_existent_scene_returns_failure(): 76 | """Test that decrementing for a non-existent scene_id returns failure.""" 77 | monitor = SceneCacheMonitor() 78 | service = Mock() 79 | service.address = "service1:8000" 80 | 81 | monitor.increment(service, "scene1") 82 | with pytest.raises(ValueError): 83 | monitor.decrement(service, "scene2") 84 | assert monitor._address_to_scene_ids_in_use == {"service1:8000": {"scene1": 1}} 85 | -------------------------------------------------------------------------------- /src/eval/README.md: -------------------------------------------------------------------------------- 1 | ## Evaluation 2 | 3 | This module is a refactored version of the `KPI` service. It 4 | * Reads in ASL logs 5 | * Computes metrics (see [`src/eval/scorers/__init__.py`](src/eval/scorers/__init__.py) for list of 6 | implemented "Scorers") 7 | * And generates a video 8 | 9 | ### Configuration 10 | 11 | See [schema.py](src/eval/schema.py). 12 | 13 | ## Writing your own metric scorer 14 | 15 | A key motivation for this module was to make writing new scorers fast and easy. 16 | To do so, we: 17 | * Rely heavily on dataclasses for storing the information parsed from ASL. The 18 | information is organized hierarchically, with the root being `EvaluationResultContainer` in 19 | [`data.py`](src/eval/data.py). 20 | * We don't use indexing by index, but always by timestamp_us, to reduce 21 | off-by-one errors. 22 | * We rely on the `Trajectory` class from AlpaSim, which allows indexing into 23 | trajectories by timestamp. We expand this class to `RenderableTrajectory` in 24 | [`data.py`](src/eval/data.py) which also contains the bounding box and knows how 25 | to render itself onto a video frame. 26 | * Lastly, we also rely heavily on the `shapely` library, to abstract away complex 27 | geometric computations such as `distance`, `contains`, `project`, 28 | `intersects`, etc... The `RenderableTrajectory` class has helper methods to 29 | convert itself to shapely objects. 30 | * We also have a `ShapelyMap` class, which is primarily used for fast video 31 | rendering of maps. For computing map-based metrics, it's probably easiest to use the 32 | `trajdata.vec_map` directly, which is also stored in 33 | `EvaluationResultContainer.sim_result` and allows querying for current lanes, etc.. 34 | 35 | ### Running locally 36 | 37 | This part of the codebase is managed by `uv`. 38 | 39 | ``` 40 | curl -LsSf https://astral.sh/uv/install.sh | sh 41 | ``` 42 | 43 | Recommended workflow: 44 | 45 | 1. First run the wizard normally (after installing it with `uv tool install -e 46 | src/wizard`) and generating ASL files. 47 | ```bash 48 | alpasim_wizard wizard.log_dir= +deploy=local 49 | ``` 50 | 2. Execute this from `src/eval`: 51 | ```bash 52 | uv run alpasim-eval \ 53 | --asl_search_glob=/asl/clipgt-d8cbf4ca-b7ff-44bd-a5be-260f736a02fe/15f2c488-10ad-11f0-b123-0242c0a84004/\*\*/\*.asl \ 54 | --config_path=eval-config.yaml \ 55 | --output_dir=/eval \ 56 | --trajdata_cache_dir=/data/trafficsim/unified_data_cache \ 57 | --usdz_glob="/data/nre-artifacts/all-usdzs/**/*.usdz" 58 | ``` 59 | 60 | The environment is shared with that of the main project and is automatically managed by `uv`. 61 | 62 | ### Overview over the codebase, e.g. for writing 63 | 64 | Main components of the codebase: 65 | * [`data.py`](src/eval/data.py) contains most datastructures. Start exploring 66 | from `EvaluationResultContainer` 67 | * Parsing ASL logs is done in `load_simulation_results()` in [`main.py`](src/eval/main.py) 68 | * Scorers are implemented in the folder [`scorers`](src/eval/scorers/). If you 69 | add a new scorer, don't forget to add it to the list in 70 | [`scorers.__init__.py`](src/eval/scorers/__init__.py) 71 | * Scorers produce metrics per timestamp per rollout. These results are 72 | aggregated in 73 | [`eval_aggregation.py`](src/eval/aggregation/eval_aggregation.py). As long as 74 | you conform to the existing datastructure, you probably won't need to touch this. 75 | * Lastly, video generation is done in [`video.py`](src/eval/video.py) 76 | -------------------------------------------------------------------------------- /src/wizard/configs/driver/vavam.yaml: -------------------------------------------------------------------------------- 1 | # Should be used in defaults list, e.g. 2 | # - /driver: vavam 3 | # Type validation happens at driver runtime via OmegaConf.structured merge 4 | 5 | defaults: 6 | - vavam_runtime_configs # Camera and simulation configs 7 | - _self_ # YAML values override schema defaults 8 | 9 | # VAM Driver Configuration for Alpasim 10 | 11 | # Logging level (uses wizard's global setting) 12 | log_level: ${wizard.log_level} 13 | 14 | # Model configuration 15 | model: 16 | # checkpoint_path: "/mnt/vavam_driver/VAM_width_2048_pretrained_139k.pt" # Path to JIT compiled VQ tokenizer 17 | checkpoint_path: "/mnt/vavam_driver/VAM_width_1024_pretrained_139k.pt" # Path to JIT compiled VQ tokenizer 18 | # config_path: "${oc.env:VAM_CHECKPOINT_PATH,/mnt/alpackages/vam_driver.tar}" # Path to VAM checkpoint 19 | device: "cuda" # Device to run inference on (cuda/cpu) 20 | dtype: "float16" # Data type for inference (float16/float32) 21 | tokenizer_path: "/mnt/vavam_driver/VQ_ds16_16384_llamagen_encoder.jit" 22 | 23 | # Server configuration 24 | host: "0.0.0.0" 25 | port: ??? 26 | 27 | # Inference configuration 28 | inference: 29 | context_length: 1 # Number of temporal frames to use as context 30 | image_height: 900 # Expected image height from NeuroNCAP (VAM scales NeuroNCAP frames to 288) 31 | image_width: 1600 # Expected image width from NeuroNCAP (VAM scales NeuroNCAP frames to 512) 32 | use_cameras: ["camera_front_wide_120fov"] # Cameras to receive images for (VAM requires exactly one) 33 | max_batch_size: 32 # Maximum batch size for inference 34 | subsample_factor: 1 # Subsample every Nth frame (1 = no subsampling, 2 = every other) 35 | 36 | rectification: 37 | # Rectify camera_front_wide_120fov renders back to the NuScenes pinhole model. 38 | camera_front_wide_120fov: 39 | focal_length: [1545.0, 1545.0] 40 | # Note: principal_point uses [x, y] order (width, height coords), 41 | # while resolution_hw uses [height, width] order (as the _hw suffix 42 | # indicates). So the relative order is flipped! 43 | # We're setting the pp to 560 to mimic nuPlans rendering at 1120 and 44 | # cropping the bottom 40px. 45 | principal_point: [960.0, 560.0] 46 | resolution_hw: [1080, 1920] 47 | # radial: null 48 | # tangential: null 49 | # thin_prism: null 50 | radial: [-0.356123, 0.172545, -0.05231, 0.0, 0.0, 0.0] 51 | tangential: [-0.00213, 0.000464] 52 | thin_prism: [0.0, 0.0, 0.0, 0.0] 53 | 54 | # We first apply rectification from f-theta to pinhole, then optionally appy 55 | # pinhole distortion correction to the pinhole frame. 56 | # To make sure the rectified image is large enough to cover the pinhole 57 | # camera's (after distortion correction) field of view, we overscan the 58 | # rectified image. 59 | max_overscan_scale: 2.0 60 | safety_margin_px: 10 61 | 62 | # Route configuration 63 | route: 64 | default_command: 2 # Default command: 0=right, 1=left, 2=straight 65 | use_waypoint_commands: true # Whether to interpret waypoints as commands 66 | command_distance_threshold: 3.0 # Lateral displacement threshold in meters (from VAM) 67 | min_lookahead_distance: 20.0 # Minimum distance to look ahead for waypoints in meters 68 | 69 | # Output configuration 70 | output_dir: "/mnt/output/driver" 71 | 72 | # Trajectory configuration 73 | trajectory: 74 | prediction_horizon: 6 # Number of future points to predict (@ 2Hz) 75 | frequency_hz: 2 # Output frequency in Hz 76 | 77 | # Trajectory optimization (VaVam-Eco, but without GT information) 78 | trajectory_optimizer: 79 | enabled: false 80 | 81 | plot_debug_images: false 82 | -------------------------------------------------------------------------------- /src/runtime/alpasim_runtime/scene_cache_monitor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import defaultdict 3 | from typing import Optional 4 | 5 | from alpasim_runtime.services.service_base import ServiceBase 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class SceneCacheMonitor: 11 | def __init__(self, service_cache_size: Optional[int] = None) -> None: 12 | self._address_to_scene_ids_in_use: dict[str, dict[str, int]] = defaultdict( 13 | lambda: defaultdict(lambda: 0) 14 | ) 15 | self._max_unique_scene_count_per_address: dict[str, int] = defaultdict( 16 | lambda: 0 17 | ) 18 | self._service_cache_size = service_cache_size 19 | self._warned_about_cache_size = False 20 | 21 | def increment(self, service: ServiceBase, scene_id: str) -> None: 22 | """ 23 | Increment method for scene cache usage tracking. 24 | 25 | Args: 26 | service: The service instance 27 | scene_id: The scene ID to track 28 | """ 29 | self._address_to_scene_ids_in_use[service.address][scene_id] += 1 30 | self._update_usage() 31 | 32 | def decrement(self, service: ServiceBase, scene_id: str) -> None: 33 | """ 34 | Decrement method for scene cache usage tracking. 35 | 36 | Args: 37 | service: The service instance 38 | scene_id: The scene ID to track 39 | """ 40 | if service.address not in self._address_to_scene_ids_in_use: 41 | raise ValueError( 42 | f"Attempted to decrement scene ID usage for service at {service.address} " 43 | f"but no scene IDs are currently tracked for this service." 44 | ) 45 | elif scene_id not in self._address_to_scene_ids_in_use[service.address]: 46 | raise ValueError( 47 | f"Attempted to decrement scene ID usage for service at {service.address} " 48 | f"but scene ID {scene_id} is not currently tracked for this service." 49 | ) 50 | self._address_to_scene_ids_in_use[service.address][scene_id] -= 1 51 | if self._address_to_scene_ids_in_use[service.address][scene_id] <= 0: 52 | del self._address_to_scene_ids_in_use[service.address][scene_id] 53 | 54 | def _update_usage(self) -> None: 55 | for address, scene_id_usage in self._address_to_scene_ids_in_use.items(): 56 | unique_scene_count = len(scene_id_usage) 57 | self._max_unique_scene_count_per_address[address] = max( 58 | self._max_unique_scene_count_per_address[address], 59 | unique_scene_count, 60 | ) 61 | if ( 62 | not self._warned_about_cache_size 63 | and self._service_cache_size is not None 64 | ): 65 | if unique_scene_count >= self._service_cache_size: 66 | logger.warning( 67 | f"Service at {address} is using {unique_scene_count} unique scene IDs, " 68 | f"which meets/exceeds the configured cache size of {self._service_cache_size}. " 69 | f"Increasing the service cache size to avoid massive performance issues." 70 | ) 71 | self._warned_about_cache_size = True 72 | 73 | def __del__(self): 74 | for ( 75 | address, 76 | max_unique_scene_count, 77 | ) in self._max_unique_scene_count_per_address.items(): 78 | logger.info( 79 | f"Service at {address} had a maximum of {max_unique_scene_count} unique scene IDs in use concurrently." 80 | ) 81 | -------------------------------------------------------------------------------- /src/utils/tests/test_logs.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | import alpasim_utils.logs as logs 5 | import pytest 6 | import pytest_asyncio 7 | from alpasim_grpc.v0.logging_pb2 import ActorPoses, LogEntry, RolloutMetadata 8 | 9 | 10 | @pytest.fixture 11 | def valid_actor_poses() -> ActorPoses: 12 | actor_poses = ActorPoses() 13 | actor_poses.timestamp_us = 1234 14 | actor_pose_0 = actor_poses.actor_poses.add() 15 | actor_pose_0.actor_id = "EGO" 16 | actor_pose_0.actor_pose.vec.x = 1.0 17 | actor_pose_0.actor_pose.vec.y = 2.0 18 | actor_pose_0.actor_pose.vec.z = 3.0 19 | actor_pose_0.actor_pose.quat.w = 1.0 20 | 21 | return actor_poses 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_LogWriter_error_on_invalid_file_name(): 26 | with pytest.raises(ValueError): 27 | logs.LogWriter("") 28 | 29 | 30 | @pytest.mark.asyncio 31 | async def test_LogWriter_error_on_invalid_file_handle(tmp_path, valid_actor_poses): 32 | log_writer = logs.LogWriter(tmp_path / "test_log.asl") 33 | with pytest.raises(RuntimeError): 34 | await log_writer.log_message(LogEntry(actor_poses=valid_actor_poses)) 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_LogWriter_write(tmp_path, valid_actor_poses): 39 | log_writer = logs.LogWriter(tmp_path / "test_log.asl") 40 | async with log_writer: 41 | await log_writer.log_message(LogEntry(actor_poses=valid_actor_poses)) 42 | 43 | # verify that the data was written 44 | with open(tmp_path / "test_log.asl", "rb") as f: 45 | data = f.read() 46 | log_entry = LogEntry() 47 | # note that the first 4 bytes are the size of the message. This seems like 48 | # something that might be revisited later, e.g. to move to a magic number 49 | # separation, which would be more robust to dropouts/corrpution. 50 | log_entry.ParseFromString(data[4:]) 51 | assert log_entry.actor_poses.timestamp_us == valid_actor_poses.timestamp_us 52 | assert ( 53 | log_entry.actor_poses.actor_poses[0].actor_id 54 | == valid_actor_poses.actor_poses[0].actor_id 55 | ) 56 | 57 | 58 | @pytest_asyncio.fixture 59 | async def sample_log_file(tmp_path, valid_actor_poses): 60 | file_path = tmp_path / "sample_log.asl" 61 | log_writer = logs.LogWriter(file_path) 62 | async with log_writer: 63 | # write out the session metadata 64 | metadata = RolloutMetadata() 65 | metadata.session_metadata.scene_id = "test_scene" 66 | await log_writer.log_message(LogEntry(rollout_metadata=metadata)) 67 | 68 | # write out a handful of actor poses 69 | actor_poses = ActorPoses() 70 | actor_poses.CopyFrom(valid_actor_poses) 71 | for timestamp in range(0, 100000, 10000): 72 | actor_poses.timestamp_us = timestamp 73 | actor_poses.actor_poses[0].actor_pose.vec.x = float(timestamp) / 1e6 74 | await log_writer.log_message(LogEntry(actor_poses=actor_poses)) 75 | return file_path 76 | 77 | 78 | @pytest.mark.asyncio 79 | async def test_async_read_pb_stream(sample_log_file): 80 | # read the sample log file, and perform some sanity checks 81 | message_count = 0 82 | async for message in logs.async_read_pb_stream(sample_log_file, LogEntry): 83 | message_count += 1 84 | assert message.WhichOneof("log_entry") in ["actor_poses", "rollout_metadata"] 85 | assert message_count == 11 # 1 metadata + 10 actor poses 86 | 87 | 88 | @pytest.mark.asyncio 89 | async def test_async_read_pb_log(sample_log_file): 90 | # read the sample log file, and perform some sanity checks 91 | message_count = 0 92 | async for message in logs.async_read_pb_log(sample_log_file): 93 | message_count += 1 94 | assert message.WhichOneof("log_entry") in ["actor_poses", "rollout_metadata"] 95 | assert message_count == 11 # 1 metadata + 10 actor poses 96 | -------------------------------------------------------------------------------- /src/driver/src/alpasim_driver/tests/test_rectification_scaling.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from alpasim_driver.rectification import ( 3 | _FthetaCamera, 4 | _scale_ftheta_intrinsics_to_resolution, 5 | ) 6 | from alpasim_grpc.v0 import sensorsim_pb2 7 | 8 | 9 | def test_resolution_scaling_is_not_applied_twice() -> None: 10 | """Scaling intrinsics to a lower resolution should scale pixel radii once.""" 11 | 12 | native_resolution = (1000, 2000) # (H, W) 13 | target_resolution = (500, 1000) # exact 0.5 scale 14 | scale = target_resolution[1] / native_resolution[1] 15 | 16 | intrinsics = sensorsim_pb2.FthetaCameraParam() 17 | intrinsics.principal_point_x = native_resolution[1] / 2 18 | intrinsics.principal_point_y = native_resolution[0] / 2 19 | intrinsics.angle_to_pixeldist_poly.extend([0.0, 1000.0]) # linear radius 20 | 21 | # Ray leaning along +X, still inside the FOV. 22 | theta = 0.2 23 | ray_x = np.array([[np.sin(theta), 0.0, np.cos(theta)]], dtype=np.float64) 24 | 25 | # Baseline at native resolution. 26 | native_cam = _FthetaCamera(intrinsics, native_resolution) 27 | native_pixels, _ = native_cam.ray_to_pixel(ray_x) 28 | native_dx = native_pixels[0, 0] - intrinsics.principal_point_x 29 | 30 | scaled_intrinsics = _scale_ftheta_intrinsics_to_resolution( 31 | intrinsics, native_resolution, target_resolution 32 | ) 33 | scaled_cam = _FthetaCamera(scaled_intrinsics, target_resolution) 34 | scaled_pixels, _ = scaled_cam.ray_to_pixel(ray_x) 35 | scaled_dx = scaled_pixels[0, 0] - scaled_intrinsics.principal_point_x 36 | 37 | assert np.isclose(scaled_dx / native_dx, scale, atol=1e-6) 38 | 39 | # And the Y-axis behaves symmetrically. 40 | ray_y = np.array([[0.0, np.sin(theta), np.cos(theta)]], dtype=np.float64) 41 | native_pixels_y, _ = native_cam.ray_to_pixel(ray_y) 42 | native_dy = native_pixels_y[0, 1] - intrinsics.principal_point_y 43 | 44 | scaled_pixels_y, _ = scaled_cam.ray_to_pixel(ray_y) 45 | scaled_dy = scaled_pixels_y[0, 1] - scaled_intrinsics.principal_point_y 46 | 47 | assert np.isclose(scaled_dy / native_dy, scale, atol=1e-6) 48 | 49 | 50 | def test_anisotropic_scaling_respects_axes() -> None: 51 | """Anisotropic resizing should scale x/y offsets independently.""" 52 | 53 | native_resolution = (1000, 2000) # (H, W) 54 | target_resolution = (500, 2500) # scales: y=0.5, x=1.25 55 | scale_y = target_resolution[0] / native_resolution[0] 56 | scale_x = target_resolution[1] / native_resolution[1] 57 | 58 | intrinsics = sensorsim_pb2.FthetaCameraParam() 59 | intrinsics.principal_point_x = native_resolution[1] / 2 60 | intrinsics.principal_point_y = native_resolution[0] / 2 61 | intrinsics.angle_to_pixeldist_poly.extend([0.0, 1000.0]) # linear radius 62 | 63 | theta = 0.2 64 | ray_x = np.array([[np.sin(theta), 0.0, np.cos(theta)]], dtype=np.float64) 65 | ray_y = np.array([[0.0, np.sin(theta), np.cos(theta)]], dtype=np.float64) 66 | 67 | native_cam = _FthetaCamera(intrinsics, native_resolution) 68 | native_pixels_x, _ = native_cam.ray_to_pixel(ray_x) 69 | native_dx = native_pixels_x[0, 0] - intrinsics.principal_point_x 70 | native_pixels_y, _ = native_cam.ray_to_pixel(ray_y) 71 | native_dy = native_pixels_y[0, 1] - intrinsics.principal_point_y 72 | 73 | scaled_intrinsics = _scale_ftheta_intrinsics_to_resolution( 74 | intrinsics, native_resolution, target_resolution 75 | ) 76 | scaled_cam = _FthetaCamera(scaled_intrinsics, target_resolution) 77 | scaled_pixels_x, _ = scaled_cam.ray_to_pixel(ray_x) 78 | scaled_dx = scaled_pixels_x[0, 0] - scaled_intrinsics.principal_point_x 79 | scaled_pixels_y, _ = scaled_cam.ray_to_pixel(ray_y) 80 | scaled_dy = scaled_pixels_y[0, 1] - scaled_intrinsics.principal_point_y 81 | 82 | assert np.isclose(scaled_dx / native_dx, scale_x, atol=1e-6) 83 | assert np.isclose(scaled_dy / native_dy, scale_y, atol=1e-6) 84 | -------------------------------------------------------------------------------- /docs/DESIGN.md: -------------------------------------------------------------------------------- 1 | # Alpasim design 2 | Alpasim is an AV simulator focused on three principles: 3 | 1. Sensor fidelity 4 | 2. Horizontal scalability 5 | 3. Hackability for research 6 | 7 | Real-time and very precise physics are non-goals. 8 | 9 | For these reasons we implement Alpasim as a collection of microservices (enabling scalability) 10 | which are implemented in Python (accessible to researchers) and communicate via gRPC. 11 | 12 | Core services include the [Neural Rendering Engine (NRE)](https://www.nvidia.com/en-us/glossary/3d-reconstruction/) 13 | and neural traffic simulator (coming soon). Additionally, we have a 14 | [physics simulation module](/src/physics) (ground constraints for egovehicle and non-ego actors), 15 | a [controller/vehicle model](/src/controller), and a 16 | [runtime](https://gitlab-master.nvidia.com/alpamayo/alpasim-runtime) which drives the simulation 17 | loop by issuing calls to the respective services and produces logs. An [eval module](src/eval) 18 | runs outside of the main simulation loop and consumes the logs to compute metrics for autonomous 19 | driving. 20 | 21 | The simulator interfaces with [a driver](/src/driver) - the egovehicle policy network, which is the 22 | main target of the simulation and creates trajectories to completee the feedback loop. The services 23 | communicate with a gRPC protocol defined in the [gRPC API](/src/grpc/). 24 | 25 | ![Alpasim architecture diagram](/docs/assets/images/alpasim-architecture.png) 26 | 27 | ## Data flow of the simulation 28 | The diagram illustrates the logical flow of the simulation instance 29 | * **Wizard** sets up the simulation configuration and (often) launches the microservices 30 | * **Runtime** keeps track of the world state 31 | * The world state is fed as bounding boxes to **trafficsim**, which actuates non-ego actors 32 | (pedestrians, vehicles, etc) 33 | * The world state is also used by **NRE** to produce camera frames for the ego vehicle 34 | * Sensor readings are used by the **driver** to make decisions about actuating the ego vehicle 35 | * The actuation request (planned path) is fed to the **controller**, which models the vehicle 36 | controller and dynamics, providing (uncorrected) egomotion 37 | * Both the actuation of the ego vehicle and actors is passed to **physics** which 38 | applies constraints such that the vehicles stay on the ground 39 | * The resulting updated state is passed to the **runtime** which logs it and repeats the loop 40 | * The logs persist after simulation and can be used to compute metrics with the **eval** module 41 | 42 | The software implementation places the runtime at the 43 | center, as a node for all communications. The remaining services can be replicated according to 44 | their computational requirements (in general 45 | `ego policy > sensor sim > controller sim > traffic sim > physics sim`). This design 46 | facilitates synchronized logging and lets the runtime double as a load balancer between the replicas 47 | of remaining services but it also means that the runtime is about as IO intense as all other 48 | services _combined_. 49 | 50 | The runtime is a gRPC client and needs to be aware of the addresses of all other microservices; 51 | the microservices are server daemons and make no requests of their own. The containers can be run 52 | in any way the user wishes (on arbitrary multiple machines) as long as the runtime is aware of the 53 | addresses and the filesystem mounts contain the necessary files. This repository focuses on 54 | configuration for running them all jointly on a single machine via `docker compose` or with 55 | `slurm`. 56 | 57 | ## Links to source code 58 | The microservices/components can be found here: 59 | * [controller](/src/controller): a simple vehicle controller + model 60 | * [driver](/src/driver): a service that runs driving policies 61 | * [eval](/src/eval): an evaluation framework that processes data 62 | * [physics](/src/physics): ground-mesh interaction modeling 63 | * [runtime](/src/runtime): the simulation runtime 64 | * trafficsim (coming soon) 65 | -------------------------------------------------------------------------------- /src/utils/alpasim_utils/logs.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # Copyright (c) 2025 NVIDIA Corporation 3 | 4 | """ 5 | Implements methods for reading and writing logs as length-delimited protobuf 6 | files. 7 | See https://seb-nyberg.medium.com/length-delimited-protobuf-streams-a39ebc4a4565 8 | for explanation of the format and rationale. 9 | """ 10 | 11 | import logging 12 | import os 13 | import struct 14 | from dataclasses import dataclass 15 | from typing import AsyncGenerator, Optional, Self, Type, TypeVar 16 | 17 | import aiofiles 18 | import aiofiles.os 19 | from alpasim_grpc.v0.logging_pb2 import LogEntry 20 | from google.protobuf.message import Message 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | @dataclass 26 | class LogWriter: 27 | """ 28 | Class for writing protobuf logs. 29 | The current implementation is just a wrapper around an open file but in the future 30 | we may exceed the OS limit of 4096 open files and want to rework this class to keep 31 | logs in memory and only open files to write periodically. 32 | """ 33 | 34 | file_path: str 35 | file_handle: Optional[aiofiles.threadpool.binary.AsyncBufferedIOBase] = None 36 | 37 | def __init__(self, file_path: str) -> None: 38 | self.file_path = file_path 39 | if not self.file_path: 40 | raise ValueError("Log file path must be non-empty.") 41 | 42 | async def __aenter__(self) -> Self: 43 | file_dir, file_name = os.path.split(self.file_path) 44 | if file_dir: 45 | await aiofiles.os.makedirs(file_dir, exist_ok=True) 46 | self.file_handle = await aiofiles.open(self.file_path, "wb") 47 | return self 48 | 49 | async def __aexit__(self, *args, **kwargs) -> None: 50 | if self.file_handle is None: 51 | raise AssertionError( 52 | "__aexit__ called with self.file_handle == None (should be initialized in __aenter__)." 53 | ) 54 | await self.file_handle.close() 55 | self.file_handle = None 56 | 57 | async def log_message(self, message: LogEntry) -> None: 58 | if self.file_handle is None: 59 | raise RuntimeError( 60 | "Using LogWriter.log_message outside of `async with log_writer`." 61 | ) 62 | await write_pb_stream(self.file_handle, message) 63 | 64 | 65 | async def write_pb_stream( 66 | file: aiofiles.threadpool.binary.AsyncBufferedIOBase, message: Message 67 | ) -> None: 68 | message_binary = message.SerializeToString() 69 | size_prefix = struct.pack(">L", len(message_binary)) 70 | await file.write(size_prefix + message_binary) 71 | 72 | 73 | M = TypeVar("M", bound=Message) 74 | 75 | 76 | async def async_read_pb_stream( 77 | fname: str, message_type: Type[M], raise_on_malformed: bool = False 78 | ) -> AsyncGenerator[M, None]: 79 | async with aiofiles.open(fname, "rb") as file: 80 | while (size_prefix_chunk := await file.read(4)) != b"": # detect EOF 81 | (message_size,) = struct.unpack(">L", size_prefix_chunk) 82 | message_chunk = await file.read(message_size) 83 | if len(message_chunk) != message_size: 84 | error = f"Malformed file (expected {message_size} bytes, found {len(message_chunk)})" 85 | if raise_on_malformed: 86 | raise IOError(error) 87 | else: 88 | logger.warning(error) 89 | break 90 | else: 91 | message = message_type.FromString(message_chunk) 92 | yield message 93 | 94 | 95 | async def async_read_pb_log( 96 | fname: str, raise_on_malformed: bool = False 97 | ) -> AsyncGenerator[LogEntry, None]: 98 | async for log_entry in async_read_pb_stream( 99 | fname, message_type=LogEntry, raise_on_malformed=raise_on_malformed 100 | ): 101 | yield log_entry 102 | --------------------------------------------------------------------------------