Skip to main content

carla-driver-interface

Both ends of NVlabs/alpasim's egodriver.EgodriverService gRPC contract, light enough for a driving policy to depend on:

  • policy side: implement BaseDriver.drive() and serve it with run_server() / serving() (or carla-driver-interface serve --policy route_follower, or any package.module:Class through load_policy()); session bookkeeping, frame retention, ego history and the rig/local conversion are handled for you;
  • a CARLA-free runtime for tests: testing.FakeLoop drives any policy server over real gRPC on a straight road, rendering each declared pinhole camera (carla-driver-interface demo --driver localhost:50051).

The CARLA runtime lives in autoware_carla_scenario: with ego.entity=carla_driver it drives the same policy server inside a scenario. An upstream alpasim runtime drives it too — nothing about the alpasim wire format is changed, and carla-driver-interface alpasim packages a policy as alpasim's driver service (Running a policy in alpasim).

Install

$ pip install carla-driver-interface

Python 3.10–3.14. Dependencies: grpcio, protobuf (4.25+), numpy, scipy, Pillow. No CARLA, no alpasim distribution: the wire contract is vendored (proto/README.md) with field numbers and service names unchanged.

Writing a policy

from carla_driver_interface.driver import BaseDriver, DriveContext, DriveResult
from carla_driver_interface.geometry import Pose, Trajectory
from carla_driver_interface.server import run_server


class MyPolicy(BaseDriver):
    name = "my_policy"

    def drive(self, ctx: DriveContext) -> DriveResult:
        frame = ctx.session.latest_frame("camera_front")  # .as_array() -> RGB
        plan = Trajectory.empty()
        for i in range(1, 41):  # 4 s ahead, in the rig frame (x forward, y left)
            plan.append(
                ctx.time_now_us + i * 100_000, Pose.from_xyz_yaw(0.5 * i, 0.0, 0.0, 0.0)
            )
        return DriveResult(trajectory_in_rig=plan)


run_server(MyPolicy(), port=50051)

Every sensor reaches a policy the same way: as a frame in session.frame_history (CameraFrame, LidarFrame), announced through on_frame, each with as_array(). Beyond alpasim's cameras, a policy run by autoware_carla_scenario gets LiDAR sweeps (rig frame) that way and, with map_dir set to its copy of the runtime's driver.map_dir, the world's map as files (ctx.map) and every traffic light resolved into that map's own elements (ctx.stop_lines(), carla_driver_interface.hdmap). That holds for a Lanelet2 map the runtime was given rather than converted (driver.lanelet2_path) too: its lights are found on the map, as the first traffic-light element on or ahead of each stop point. CARLA ground truth arrives in ctx.renderer_data (None when an alpasim runtime drives the policy).

A policy can also work the ego's turn indicators: DriveResult(..., turn_indicator=TurnIndicator.ENABLE_LEFT) (carla_driver_interface.driver.TurnIndicator). The values are Autoware's TurnIndicatorsCommand ones (DISABLE = 1, ENABLE_LEFT = 2, ENABLE_RIGHT = 3), so a classifier over Autoware's recorded signal answers TurnIndicator(class_index + 1). The egodriver contract carries only the plan, so the server also serves carla_driver.v0.CarlaVehicleService on the same port: after each drive a CARLA runtime fetches the policy's VehicleCommand with get_vehicle_command. autoware_carla_scenario lights the ego's indicators with it and holds them until a later step sets them again. The default, NO_COMMAND, changes nothing -- what a policy that never sets it, or predates it, sends. An upstream alpasim runtime has nowhere to show them.

A camera's mount is session.cameras[logical_id].rig_to_camera: the pose of its optical frame in the rig (x right, y down, z along the optical axis), as alpasim's renderers declare it, whichever runtime drives the policy. geometry.optical_to_body gives the camera body's pose (x along the optical axis, y left, z up).

Testing a policy without CARLA

$ carla-driver-interface serve --policy route_follower --port 50051 &
$ carla-driver-interface demo --driver localhost:50051 --steps 200

or, in a test, FakeLoop("localhost:50051", [FakeCamera()]) from carla_driver_interface.testing (a context manager; .run(steps) returns a LoopResult). The ego follows the returned plan exactly, so what is tested is the policy and the wire, not a controller.

Serving on a Unix socket, or in the runtime's own process

A server binds a Unix domain socket when its host names one: serve --host unix:///run/policy.sock (build_server(policy, host="unix:///run/policy.sock"); the port is unused). A client dials the same address.

A runtime that runs the policy in its own process can skip the socket altogether:

from carla_driver_interface.inprocess import in_process_channel
from carla_driver_interface.protocol import EgodriverServiceStub

# A grpc.Channel; mode="alpasim", as on a server.
channel = in_process_channel(MyPolicy())
# The generated stubs, unchanged.
stub = EgodriverServiceStub(channel)

InProcessChannel keeps the contract byte for byte: each request is serialized and parsed by the servicer's own deserializer, each response likewise, so both sides see the bytes the wire would carry. The servicer is the same EgodriverServicer, with a context that does what grpc's does (abort, set_code/set_details, an exception as UNKNOWN, invocation, initial and trailing metadata -- the contract revision is negotiated through them), and a failed call is a grpc.RpcError with the code and details a server's would carry. The test suite runs every wire test over TCP, a Unix socket and this channel and compares what the client sees, errors included. Only the socket's own behaviour is missing: a call runs on the calling thread, so a deadline is reported, as DEADLINE_EXCEEDED, when the call returns rather than cutting it short.

Running a policy in alpasim

alpasim runs its driver as a container its wizard starts, and its runtime drives it over the same egodriver.EgodriverService. Three commands take a policy there.

1. Serve in alpasim mode. serve --mode alpasim serves the policy as before, except for the two opaque bytes fields the contract leaves to each runtime, and what a client that declares nothing is taken to speak:

--mode carla (default) --mode alpasim
DriveRequest.renderer_data read as CarlaRendererData (ctx.renderer_data, ctx.map, LiDAR) never parsed: it is alpasim's renderer's own payload; ctx.renderer_data is None
DriveResponse.debug_info.unstructured_debug_info a CarlaDriveDebugInfo the pickled dict alpasim's evaluation unpickles (policy_name, inference_seconds, scalars), as alpasim's own driver answers
a client that declares no contract revision (Versioning) speaks revision 1: its cameras are translated speaks revision 2, alpasim's own convention

Everything else (sessions, frames, ego history, the rig/local conversion) is the same, and so is a policy: BaseDriver does not change. SIGTERM stops the server cleanly in either mode, so docker stop does too. Serve alpasim in alpasim mode: in carla mode its cameras would be taken as revision 1 and turned into the wrong frame (the server logs that it translated them).

2. Build an image.

$ carla-driver-interface alpasim build-image --tag my-policy:0.1 \
      --install ./my_policy --policy my_policy.driver:MyPolicy \
      --policy-arg cruise_speed_mps=8

installs carla-driver-interface (this version from PyPI, or --carla-driver-interface PATH for a checkout) and every --install requirement (a name, a URL, or a local project directory or wheel, copied into the build context) with one pip install on --base-image (default python:3.12-slim; any image with python, pip and bash). The policy and its --policy-args are baked in as the image's defaults (CARLA_DRIVER_INTERFACE_POLICY, CARLA_DRIVER_INTERFACE_POLICY_ARGS), its module is imported at build time (--no-check-policy to skip), and the image serves alpasim mode on port 50051 when run by itself. --apt adds Debian packages, --no-build writes the context and its Dockerfile without building, and --docker-arg passes arguments to docker build.

3. Hand it to alpasim's wizard.

$ carla-driver-interface alpasim wizard-config --image my-policy:0.1 --out ./alpasim-conf
$ cd alpasim && uv run alpasim_wizard deploy=local topology=1gpu \
      driver=carla_driver_interface 'hydra.searchpath=[file:///abs/path/to/alpasim-conf]' \
      wizard.log_dir=$PWD/run

writes the config group driver=carla_driver_interface (--name to change it): the image as alpasim's driver service (services.driver, an external image), started with serve --mode alpasim on the port the wizard assigns, logging to <log_dir>/driver/. --policy and --policy-arg override the image's defaults, --volume and --env add mounts (weights, say) and variables, and --camera ID[:WxH][@HZ] replaces alpasim's default cameras. --pinhole ID:HFOV[@X,Y,Z[,ROLL,PITCH,YAW]] has the renderer render camera ID through an undistorted pinhole model instead (runtime.extra_cameras), mounted in the rig frame (x forward, y left, z up, from the ground below the rear axle; m and degrees) and at its --camera resolution; the recorded vehicle's hood mask is then turned off. With NRE, ID must be one of the scene's cameras. Everything else -- renderer, physics, controller, scenes, evaluation -- is alpasim's, unchanged.

For example, VisionPilot (vision_pilot.driver.VisionPilotDriver) on its own camera, a 1920x1280, 50 degree pinhole at 10 Hz:

$ carla-driver-interface alpasim wizard-config --name vision_pilot \
      --image vision-pilot-alpasim:0.1 --out ./alpasim-conf \
      --volume /path/to/weights:/mnt/weights:ro \
      --camera camera_front_wide_120fov:1920x1280@10 \
      --pinhole camera_front_wide_120fov:50@2.969,-0.0243,2.116,-0.10,0.11,0.23

Successor to carla-driver-interface 0.1.0

This package replaces the previous carla-driver-interface 0.1.0 of this repository, which had two halves: a driver built on the alpasim-grpc distribution (Python 3.11–3.12 only), and a CARLA runtime (CarlaRuntime, CarlaWorldAdapter, FakeWorld, carla-driver-interface run). From 1.0.0:

  • the driver half is this package. It was developed as autoware-carla-egodriver, a workspace member of autoware_carla_scenario, and moves here under the old name so policies and scenario packages version independently;
  • the runtime half is autoware_carla_scenario's ego.entity=carla_driver, which owns the CARLA world, the ego and the traffic, and drives the policy over the same contract. testing.FakeLoop replaces FakeWorld for CARLA-free tests.

Migrating from 0.1.0

Policies port by changing imports; BaseDriver, DriveContext, DriveResult, SessionState, CameraFrame, Pose and Trajectory keep their names and fields.

0.1.0 1.0.0
carla_driver_interface.driver.base (BaseDriver, DriveContext, ...) carla_driver_interface.driver
carla_driver_interface.driver.server (run_server, serving, build_server) carla_driver_interface.server
carla_driver_interface.driver.service.CarlaEgodriverServicer carla_driver_interface.service.EgodriverServicer
carla_driver_interface.driver.policies carla_driver_interface.policies (plus load_policy)
carla_driver_interface.grpc_api (messages, stubs) carla_driver_interface.protocol
carla_driver_interface.grpc_api.extension (pack_renderer_data, unpack_debug_info, ...) carla_driver_interface.protocol
carla_driver_interface.grpc_api.API_VERSION_MESSAGE carla_driver_interface.protocol.ALPASIM_API_VERSION
alpasim_grpc.v0.{common,egodriver,sensorsim}_pb2 carla_driver_interface.protocol.{common,egodriver,sensorsim,carla_driver}_pb2
carla_driver_interface.geometry (Pose, Trajectory), .polyline unchanged
geometry.quat_mul, yaw_to_quat_xyzw, quat_to_yaw, ... Pose methods, or scipy.spatial.transform.Rotation
BaseDriver.on_image(session, frame) BaseDriver.on_frame(session, frame) (cameras and LiDAR)
carla_driver_interface.fakes.FakeWorld carla_driver_interface.testing.FakeLoop
carla_driver_interface.runtime (CarlaRuntime, ...), carla-driver-interface run autoware_carla_scenario, ego.entity=carla_driver
carla_driver_interface.compat, carla-driver-interface compat-report removed

alpasim_grpc.v0.runtime_pb2 (RolloutSpec, SimulationReturn, ...) is not vendored: it is the runtime's own contract, not the egodriver one. Messages not re-exported by name in protocol (CarlaDriveSessionInfo, CarlaCompatReport, ...) remain available from protocol.carla_driver_pb2.

Versioning

carla-driver-interface follows semantic versioning on its own, independently of autoware_carla_scenario or any policy. Its major version is tied to the contract revision (carla_driver_interface.contract): what the wire means beyond the messages alpasim's proto fixes.

Revision Releases AvailableCamera.rig_to_camera
1 1.x the camera body in the rig (x along the optical axis, y left, z up)
2 2.x the camera's optical frame in the rig (x right, y down, z along the optical axis), as alpasim declares it

Each side declares its revision in gRPC metadata (carla-driver-interface-contract): a server in its answer to get_version, a client with start_session. A side that declares none is taken to speak revision 1, except an alpasim runtime (--mode alpasim), which speaks alpasim's own convention, revision 2. A 2.x server translates whatever its client speaks, and a 2.x client (contract.negotiate, as FakeLoop does) declares its cameras in whatever its server speaks. A policy always sees revision 2. So releases pair across processes, where no package range can see both sides:

Client \ policy server 1.x 2.x
autoware_carla_scenario 4.x (carla-driver-interface 1.x) yes yes (translated by the server)
a carla-driver-interface 2.x client yes (translated by the client) yes
an alpasim runtime no: 1.x reads alpasim's cameras as camera bodies yes

Within one environment, depend on a range of one major (carla-driver-interface>=2.0,<3), not an exact pin.

  • patch: fixes that change neither the wire nor the Python API;
  • minor: additions — new fields or messages in the carla_driver extension (old peers ignore them), new Python API;
  • major: anything that breaks a peer or a policy — a new contract revision, a changed or removed field number, a renamed service, a removed Python API. A new revision comes with the translation from the previous one, so peers of adjacent majors still pair.

The vendored alpasim contract is never edited; following a newer alpasim revision is a minor release when it is wire-compatible and a major one when it is not.

Development

$ uv sync --group dev
$ uv run ruff check . && uv run ruff format --check .
$ uv run mypy src tests scripts
$ uv run pytest -q

The generated protobuf modules in src/carla_driver_interface/_proto are committed; regenerate them with uv run python scripts/compile_protos.py (on Python 3.10–3.12, see proto/README.md). --check, also run by the test suite, verifies they are current.

Releasing

Releases are cut by merging, and go to PyPI from .github/workflows/release.yml through Trusted Publishing — no API token is stored anywhere.

Every PR carries exactly one of the labels bump patch, bump minor or bump major (the Check Version Bump Label check fails otherwise), chosen by the versioning rules above. Do not edit version in pyproject.toml by hand. When the PR is merged to main, the release workflow:

  1. bumps version in pyproject.toml by the label, re-locks (uv lock) and pushes the commit chore: bump version to X.Y.Z to main;
  2. tags it vX.Y.Z and creates the GitHub Release;
  3. builds the sdist and wheel (uv build), checks them with twine check --strict, installs the wheel into a clean virtual environment on every supported Python (3.10–3.14) and runs carla-driver-interface --help;
  4. publishes to PyPI.

A merge without a bump label releases nothing.

Re-running a failed release. If a step after the bump failed (the tag, the Release, PyPI), do not re-run the push run — it would bump again. Run the workflow by hand instead (Actions → Release → Run workflow, on main) with the version it bumped to, e.g. 1.2.3. It builds from that version's tag, or else from its chore: bump version to 1.2.3 commit, never from a later main, and reuses an existing tag or Release; files already on PyPI are skipped.

The successor keeps the predecessor's version line: it starts from 0.1.0, and the PR that introduces it is merged with bump major, which releases 1.0.0 like any other release.

Pushing to main. The bump commit and the tag are pushed with GITHUB_TOKEN, which works while main has no branch protection rules. If main gets a ruleset whose bypass list does not include github-actions[bot], add a GH_PAT repository secret (a token that may push to main and create tags); the workflow uses it whenever it exists.

One-time PyPI setup. PyPI needs a (pending) trusted publisher for the project carla-driver-interface (the distribution name in pyproject.toml) with owner hakuturu583, repository carla_driver_interface, workflow release.yml and environment pypi (PyPI → Your account → Publishing), and this repository needs a pypi environment (Settings → Environments). Until then the publish job fails and the built distributions remain available as the run's pypi-dist artifact.

License

Apache-2.0. The vendored alpasim protos are Apache-2.0 as well (proto/LICENSE.alpasim).

Metadata

Release files for carla-driver-interface 2.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for carla-driver-interface 2.3.1
File Size Uploaded Provenance
carla_driver_interface-2.3.1.tar.gz 85.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for carla-driver-interface 2.3.1
File Interpreter ABI Platform
carla_driver_interface-2.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 177.7 kB

Release files / carla_driver_interface-2.3.1.tar.gz

Download URL carla_driver_interface-2.3.1.tar.gz
Size 85.0 kB
Tags Source
SHA-256 checksum
How to use checksums
305152bb24acaebb87bfddee1494605ed891c74c17cb5e5bc2b9529015604201
BLAKE2b-256 checksum
How to use checksums
3d16f16dc29a4343b1bf8b8816434a0acf8664380975397d2418350dfcc02080
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / carla_driver_interface-2.3.1-py3-none-any.whl

Download URL carla_driver_interface-2.3.1-py3-none-any.whl
Size 92.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
598441ae0416de3e42be749886ff38abaa4137dd6c4811fa0ae95d6a4313b217
BLAKE2b-256 checksum
How to use checksums
102013a862bcc8c4b23d92d878ffa772ab5d792602dbaa435330d733e8d82efe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for carla-driver-interface 2.3.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for carla_driver_interface-2.3.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page