diff --git a/.gitignore b/.gitignore index b7faf40..1e851a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,207 +1,59 @@ -# Byte-compiled / optimized / DLL files +# ========================= +# Python +# ========================= __pycache__/ -*.py[codz] +*.py[cod] *$py.class -# C extensions -*.so +# Virtual environments +.venv/ +venv/ +env/ +ENV/ # Distribution / packaging -.Python build/ -develop-eggs/ dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ *.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec +.eggs/ -# Installer logs -pip-log.txt -pip-delete-this-directory.txt +# ========================= +# PyCharm / JetBrains +# ========================= +.idea/ +*.iml + +# ========================= +# OS files +# ========================= +.DS_Store +Thumbs.db +desktop.ini + +# ========================= +# Logs +# ========================= +*.log -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ +# ========================= +# Coverage / test artifacts +# ========================= .coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py.cover -.hypothesis/ +htmlcov/ .pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock -#poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -#pdm.lock -#pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -#pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.envrc -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc -# Cursor -# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to -# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data -# refer to https://docs.cursor.com/context/ignore-files -.cursorignore -.cursorindexingignore +# ========================= +# CARLA / large outputs +# ========================= +recordings/ +*.rec +*.mp4 +*.png +*.jpg +*.npy +*.csv + +# If you later dump raw sensor frames +data/ +outputs/ -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ diff --git a/docs/metrics_catalog.md b/docs/metrics_catalog.md new file mode 100644 index 0000000..3aed34c --- /dev/null +++ b/docs/metrics_catalog.md @@ -0,0 +1,181 @@ +# CARLA Observability Toolkit — Metrics Catalog (v1) +Target: CARLA Python API 0.10.0 +Scope: Vehicle, World, Sensors, Events +Note: “Tick” refers to `world.tick()` / `world.on_tick(callback)` cadence. + +--- +## 0) Timebase & Cadence Notes + +- In asynchronous mode (`world.get_settings().synchronous_mode == False`), `world.get_snapshot().timestamp.delta_seconds` varies per tick. +- In synchronous mode, `fixed_delta_seconds` defines tick duration; `delta_seconds` should match it. +- Sensor streams provide `SensorData.frame` and `SensorData.timestamp` in seconds. + +## 1.1 Actor Identity & Metadata + +(Actor = carla.Actor; Vehicle inherits Actor) + +| Metric | Units | Type | Update | Source | +| --------------------------------- | ----: | --------------- | ---------- | --------------------------------------------- | +| actor.id | - | int | static | `vehicle.id` | +| actor.type_id | - | string | static | `vehicle.type_id` | +| actor.is_alive | bool | bool | per tick | `vehicle.is_alive` | +| actor.is_active | bool | bool | per tick | `vehicle.is_active` | +| actor.actor_state | - | enum | per tick | `vehicle.actor_state` | +| actor.attributes | - | dict | static | `vehicle.attributes` | +| actor.bounding_box.extent.(x,y,z) | m | vector3 (float) | static-ish | `vehicle.bounding_box.extent` *(half-extent)* | + +Bounding box extent represents half-dimensions of the actor in meters. + +### 1.2 Vehicle State + +(Actor = carla.Vehicle) + +| Metric | Units | Type | Update | Source | +| -------------------------------- | ----: | ----- | -------- | ----------------------------------------------- | +| vehicle.transform.location.x | m | float | per tick | `vehicle.get_transform().location.x` | +| vehicle.transform.location.y | m | float | per tick | `vehicle.get_transform().location.y` | +| vehicle.transform.location.z | m | float | per tick | `vehicle.get_transform().location.z` | +| vehicle.transform.rotation.pitch | deg | float | per tick | `vehicle.get_transform().rotation.pitch` | +| vehicle.transform.rotation.yaw | deg | float | per tick | `vehicle.get_transform().rotation.yaw` | +| vehicle.transform.rotation.roll | deg | float | per tick | `vehicle.get_transform().rotation.roll` | +| vehicle.velocity.x | m/s | float | per tick | `vehicle.get_velocity().x` | +| vehicle.velocity.y | m/s | float | per tick | `vehicle.get_velocity().y` | +| vehicle.velocity.z | m/s | float | per tick | `vehicle.get_velocity().z` | +| vehicle.speed | m/s | float | per tick | `vehicle.get_velocity()` *(computed magnitude)* | +| vehicle.acceleration.x | m/s² | float | per tick | `vehicle.get_acceleration().x` | +| vehicle.acceleration.y | m/s² | float | per tick | `vehicle.get_acceleration().y` | +| vehicle.acceleration.z | m/s² | float | per tick | `vehicle.get_acceleration().z` | +| vehicle.angular_velocity.x | rad/s | float | per tick | `vehicle.get_angular_velocity().x` | +| vehicle.angular_velocity.y | rad/s | float | per tick | `vehicle.get_angular_velocity().y` | +| vehicle.angular_velocity.z | rad/s | float | per tick | `vehicle.get_angular_velocity().z` | + + +### 1.3 Control Inputs (Actor = `carla.Vehicle`) +| Metric | Units | Type | Update | Source | +| ------------------------- | ----: | ----- | -------- | ----------------------------------------- | +| control.throttle | 0..1 | float | per tick | `vehicle.get_control().throttle` | +| control.steer | -1..1 | float | per tick | `vehicle.get_control().steer` | +| control.brake | 0..1 | float | per tick | `vehicle.get_control().brake` | +| control.hand_brake | bool | bool | per tick | `vehicle.get_control().hand_brake` | +| control.reverse | bool | bool | per tick | `vehicle.get_control().reverse` | +| control.manual_gear_shift | bool | bool | per tick | `vehicle.get_control().manual_gear_shift` | +| control.gear | - | int | per tick | `vehicle.get_control().gear` | + + +### 1.4 Physics & Geometry +| Metric | Units | Type | Update | Source | +| -------------------------------- | ----: | ------- | ---------- | --------------------------------------------------------- | +| vehicle.physics.mass | kg | float | static-ish | `vehicle.get_physics_control().mass` | +| vehicle.physics.drag_coefficient | - | float | static-ish | `vehicle.get_physics_control().drag_coefficient` | +| vehicle.physics.max_rpm | rpm | float | static-ish | `vehicle.get_physics_control().max_rpm` | +| vehicle.physics.moi | kg·m² | vector3 | static-ish | `vehicle.get_physics_control().moment_of_inertia` | +| vehicle.wheels.count | - | int | static-ish | `len(vehicle.get_physics_control().wheels)` | +| vehicle.wheel[i].radius | m | float | static-ish | `vehicle.get_physics_control().wheels[i].radius` | +| vehicle.wheel[i].max_steer_angle | deg | float | static-ish | `vehicle.get_physics_control().wheels[i].max_steer_angle` | + + +### 1.5 Autopilot / Traffic Manager +| Metric | Units | Type | Update | Source | +| --------------------------- | ----: | ---- | ---------- | ---------------------------------------- | +| vehicle.autopilot_enabled | bool | bool | on change | track calls to `vehicle.set_autopilot()` | +| vehicle.tm_port | port | int | static-ish | port used when enabling autopilot | +| vehicle.is_at_traffic_light | bool | bool | per tick | `vehicle.is_at_traffic_light()` | +| vehicle.traffic_light_state | - | enum | per tick | `vehicle.get_traffic_light_state()` | + + +> Note: Traffic Manager “state” is not broadly exposed as a queryable object; treat it as configuration + behavior observed via vehicle control/trajectory unless you find accessible TM getters. + +### 1.6 World / Simulation +| Metric | Units | Type | Update | Source | +| ---------------------------------- | -----: | ------------ | ---------- | --------------------------------------------------- | +| world.frame | frame | int | per tick | `world.get_snapshot().frame` | +| world.timestamp.elapsed_seconds | s | float | per tick | `world.get_snapshot().timestamp.elapsed_seconds` | +| world.timestamp.delta_seconds | s | float | per tick | `world.get_snapshot().timestamp.delta_seconds` | +| world.timestamp.platform_timestamp | s | float | per tick | `world.get_snapshot().timestamp.platform_timestamp` | +| world.map.name | - | string | static-ish | `world.get_map().name` | +| world.settings.synchronous_mode | bool | bool | on change | `world.get_settings().synchronous_mode` | +| world.settings.fixed_delta_seconds | s | float / None | on change | `world.get_settings().fixed_delta_seconds` | +| world.settings.no_rendering_mode | bool | bool | on change | `world.get_settings().no_rendering_mode` | +| world.weather.* | varies | object | on change | `world.get_weather()` | + +--- + +## 2) Sensor Streams (continuous data + metadata) + +### 2.1 Collision Sensor (Actor = `carla.Sensor`) +| Metric | Units | Type | Update | Source | +|---|---:|---|---|---| +| collision.event.count | - | int | per callback | increment in `sensor.listen(callback)` | + +### 2.2 Lane Invasion Sensor +| Metric | Units | Type | Update | Source | +|---|---:|---|---|---| +| lane_invasion.event.count | - | int | per callback | increment in `sensor.listen(callback)` | + +### 2.3 GNSS Sensor +| Metric | Units | Type | Update | Source | +|---|---:|---|---|---| +| gnss.lat | deg | float | per callback | `GNSSMeasurement.latitude` | +| gnss.lon | deg | float | per callback | `GNSSMeasurement.longitude` | +| gnss.alt | m | float | per callback | `GNSSMeasurement.altitude` | + +### 2.4 IMU Sensor +| Metric | Units | Type | Update | Source | +|---|---:|---|---|---| +| imu.accel.x | m/s² | float | per callback | `IMUMeasurement.accelerometer.x` | +| imu.accel.y | m/s² | float | per callback | `IMUMeasurement.accelerometer.y` | +| imu.accel.z | m/s² | float | per callback | `IMUMeasurement.accelerometer.z` | +| imu.gyro.x | rad/s | float | per callback | `IMUMeasurement.gyroscope.x` | +| imu.gyro.y | rad/s | float | per callback | `IMUMeasurement.gyroscope.y` | +| imu.gyro.z | rad/s | float | per callback | `IMUMeasurement.gyroscope.z` | +| imu.compass | rad | float | per callback | `IMUMeasurement.compass` | + +### 2.5 LiDAR Sensor (metadata only for v1) +| Metric | Units | Type | Update | Source | +|---|---:|---|---|---| +| lidar.points_per_frame | - | int | per callback | `len(LidarMeasurement)` or `measurement.raw_data` length logic | +| lidar.frame | frame | int | per callback | `measurement.frame` | +| lidar.timestamp | s | float | per callback | `measurement.timestamp` | + +### 2.6 Camera Sensor (metadata only for v1) +| Metric | Units | Type | Update | Source | +|---|---:|---|---|---| +| camera.image_width | px | int | static-ish | blueprint attribute (e.g., `image_size_x`) | +| camera.image_height | px | int | static-ish | blueprint attribute (e.g., `image_size_y`) | +| camera.fov | deg | float | static-ish | blueprint attribute `fov` | +| camera.frame | frame | int | per callback | `Image.frame` | +| camera.timestamp | s | float | per callback | `Image.timestamp` | + +--- + +## 3) Discrete Events (callbacks / lifecycle) + +### 3.1 Collision Event (`carla.CollisionEvent`) +| Field | Units | Type | Source | +| ---------------------------- | ----: | ------- | --------------------------------------------------- | +| event.frame | frame | int | `event.frame` | +| event.timestamp | s | float | `event.timestamp` | +| event.other_actor_id | - | int | `event.other_actor.id` | +| event.normal_impulse.(x,y,z) | N·s | vector3 | `event.normal_impulse` | +| event.intensity | N·s | float | `event.normal_impulse` *(computed magnitude)* | + + +### 3.2 Lane Invasion Event (`carla.LaneInvasionEvent`) +| Event Field | Units | Type | Source | +|---|---:|---|---| +| event.frame | frame | int | `event.frame` | +| event.timestamp | s | float | `event.timestamp` | +| event.crossed_lane_markings | - | list/enum | `event.crossed_lane_markings` | + +### 3.3 Actor Spawn / Destruction (World-level) +| Event | Trigger | Notes | +|---|---|---| +| actor_spawned | detected via world snapshot actor set diff OR custom spawn wrapper | requires tracking previous actor IDs | +| actor_destroyed | detected via world snapshot actor set diff OR custom destroy wrapper | same | + +### 3.4 Simulation / Run Lifecycle (Toolkit-level) +| Event | Trigger | Notes | +|---|---|---| +| run_started | when your RunManager starts | v2 sprint implements | +| run_stopped | when your RunManager stops | v2 sprint implements | +| sim_connected | when `carla.Client` connects | can log once per session | diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cbcee26 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# Core +carla diff --git a/src/cot/__init__.py b/src/cot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cot/carla_client.py b/src/cot/carla_client.py new file mode 100644 index 0000000..f03dd0a --- /dev/null +++ b/src/cot/carla_client.py @@ -0,0 +1,10 @@ +import carla + +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 2000 +DEFAULT_TIMEOUT_S = 10.0 # This is dependent on your server setup. With 2 seconds my client refused to connect. + +def make_client(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT, timeout_s: float = DEFAULT_TIMEOUT_S) -> carla.Client: + client = carla.Client(host, port) + client.set_timeout(timeout_s) + return client diff --git a/src/scripts/probe_world_and_vehicle.py b/src/scripts/probe_world_and_vehicle.py new file mode 100644 index 0000000..77ab69f --- /dev/null +++ b/src/scripts/probe_world_and_vehicle.py @@ -0,0 +1,98 @@ +from cot.carla_client import make_client +import math + + +def vec_mag(v) -> float: + return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) + + +def get_or_spawn_vehicle(world): + """Return an existing vehicle if present; otherwise spawn one safely and wait for stabilization.""" + vehicles = world.get_actors().filter("vehicle.*") + if len(vehicles) > 0: + return vehicles[0], False # (vehicle, spawned?) + + bp_lib = world.get_blueprint_library() + + preferred = bp_lib.filter("vehicle.tesla.model3") + bp = preferred[0] if preferred else bp_lib.filter("vehicle.*")[0] + + spawn_points = world.get_map().get_spawn_points() + if not spawn_points: + raise RuntimeError("No spawn points available on this map.") + + for sp in spawn_points[:30]: + v = world.try_spawn_actor(bp, sp) + if v is None: + continue + + # Let the simulator advance so the actor has a stable transform/physics state + settings = world.get_settings() + for _ in range(3): + if settings.synchronous_mode: + world.tick() + else: + world.wait_for_tick() + + # Re-fetch to ensure we have the fully-registered actor + v2 = world.get_actor(v.id) + return (v2 if v2 is not None else v), True + + raise RuntimeError("Failed to spawn a vehicle (first 30 spawn points blocked).") + + +def main(): + client = make_client(timeout_s=10.0) + world = client.get_world() + + snap = world.get_snapshot() + print("frame:", snap.frame) + print("elapsed_seconds:", snap.timestamp.elapsed_seconds) + print("delta_seconds:", snap.timestamp.delta_seconds) + + settings = world.get_settings() + print("sync:", settings.synchronous_mode) + print("fixed_dt:", settings.fixed_delta_seconds) + print("no_rendering:", settings.no_rendering_mode) + + vehicles = world.get_actors().filter("vehicle.*") + print("vehicles(before):", len(vehicles)) + + v = None + spawned = False + try: + v, spawned = get_or_spawn_vehicle(world) + print("vehicle.selected:", v.id, v.type_id, "| spawned:" , spawned) + print("vehicle.is_alive:", v.is_alive, "is_active:", v.is_active, "state:", v.actor_state) + print("vehicle.bounding_box.extent:", v.bounding_box.extent.x, v.bounding_box.extent.y, v.bounding_box.extent.z) + + # Vehicle/Actor metrics + t = v.get_transform() + vel = v.get_velocity() + acc = v.get_acceleration() + ang = v.get_angular_velocity() + ctrl = v.get_control() + + print("loc:", t.location.x, t.location.y, t.location.z) + print("rot:", t.rotation.pitch, t.rotation.yaw, t.rotation.roll) + print("speed(m/s):", vec_mag(vel)) + print("acc(m/s^2):", vec_mag(acc)) + print("ang(rad/s):", vec_mag(ang)) + print("control:", ctrl.throttle, ctrl.steer, ctrl.brake) + print("vel_vec(m/s):", vel.x, vel.y, vel.z) + print("acc_vec(m/s^2):", acc.x, acc.y, acc.z) + print("ang_vec(rad/s):", ang.x, ang.y, ang.z) + + # Traffic light related + is_at = v.is_at_traffic_light() + print("is_at_traffic_light:", is_at) + if is_at: + state = v.get_traffic_light_state() + name = getattr(state, "name", None) + print("traffic_light_state:", name if name else state) + finally: + if spawned and v is not None and getattr(v, "is_alive", False): + v.destroy() + +if __name__ == "__main__": + main() \ No newline at end of file