Metadata-Version: 2.4
Name: carla-driver-interface
Version: 2.3.1
Summary: Both ends of alpasim's egodriver gRPC contract: serve a driving policy, or drive one without CARLA
Author: Masaya Kataoka
Author-email: Masaya Kataoka <ms.kataoka@gmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Dist: grpcio>=1.60
Requires-Dist: protobuf>=4.25.0,<8
Requires-Dist: numpy>=1.21
Requires-Dist: scipy>=1.8
Requires-Dist: pillow>=10.0
Requires-Python: >=3.10, <3.15
Description-Content-Type: text/markdown

# carla-driver-interface

Both ends of [NVlabs/alpasim](https://github.com/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](https://github.com/autowarefoundation/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](#running-a-policy-in-alpasim)).

## Install

```console
$ 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`](https://github.com/hakuturu583/carla_driver_interface/blob/main/proto/README.md))
with field numbers and service names unchanged.

## Writing a policy

```python
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

```console
$ 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:

```python
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](#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.**

```console
$ 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-arg`s 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.**

```console
$ 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:

```console
$ 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](https://semver.org) 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

```console
$ 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`](https://github.com/hakuturu583/carla_driver_interface/blob/main/proto/README.md)). `--check`, also run by the test suite, verifies
they are current.

## Releasing

Releases are cut by merging, and go to
[PyPI](https://pypi.org/project/carla-driver-interface/) from
[`.github/workflows/release.yml`](https://github.com/hakuturu583/carla_driver_interface/blob/main/.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](#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`](https://github.com/hakuturu583/carla_driver_interface/blob/main/proto/LICENSE.alpasim)).
