Data collection
August 17, 2026 ยท View on GitHub
Local collection
The expert drives CARLA routes and writes a py123d dataset to PY123D_DATA_ROOT. Start CARLA and run:
user@host:~/lead$ python -m lead --expert --routes src/lead/routes/data_routes/lead/Accident/route_001761.xml
Parallel collection on SLURM
For collecting many routes in parallel on a SLURM cluster:
user@host:~/lead$ python scripts/slurm/collect_data.py
Every route XML becomes its own SLURM job with a private CARLA instance. The launcher
keeps a fixed pool of jobs running and retries failed routes; pool size, retry limit,
and SLURM resources are set in .env (see .env.example). A route counts as finished
once its result file shows a completed run with a nonzero route score, so re-running
the launcher only collects what is missing.
Change the sensor rig
The rig is SensorRigConfig in
sensor_rig_config.py: two lidars, a
list of cameras, a list of radars, edited in place. It both spawns the CARLA sensors and
becomes the calibration written into the log.
Mounting a different camera rig
A single front camera instead of the six-camera surround rig:
cameras: list[CameraSpec] = [
{ # front
"pos": [0.25, 0.0, 2.25],
"rot": [0.0, 0.0, 0.0],
"width": 1024,
"height": 512,
"fov": 90,
},
]
Camera i is stored under the ID that CAMERA_ID_BY_LEAD_INDEX in
py123d_log_api.py maps it to; more than the six
mapped cameras (or four radars) needs an entry each. All cameras share the resolution of
cameras[0], and the rig is always two lidars. Depth, instance and perturbated cameras
are derived from the RGB list; a policy's camera selection
(policy.transfuser.camera.input_cameras) is not. Scalar knobs also work via the
LEAD_CONFIG dotlist, e.g. export LEAD_CONFIG="expert.sensor_rig.use_radars=false".
Training reads <PY123D_DATA_ROOT>/config.yaml as its expert section, so store the
config with the data. scripts/slurm/collect_data.py does this; after a local run:
import yaml
from lead.config import load_lead_config, yaml_filtered
expert_config = yaml_filtered(load_lead_config().expert.to_dict())
with open("data/lead/123D/config.yaml", "w", encoding="utf-8") as f:
yaml.safe_dump(expert_config, f, sort_keys=False)
Add a modality offline
A log is one Arrow file per modality stream plus sync.arrow, which holds one row per
tick with the row index each stream sits at. To add a modality after collection
(reasoning traces, captions, auto-labels), write custom.<id>.arrow next to the
others; closing the writer rebuilds the sync table from every *.arrow in the
directory. It reads back like any other stream:
scene.get_custom_modality_at_iteration(0, "reasoning").data.
Example code
from pathlib import Path
import pyarrow as pa
from py123d.api.scene.arrow.arrow_log_writer import ArrowLogWriter, SyncConfig
from py123d.api.scene.arrow.arrow_scene_builder import ArrowSceneBuilder
from py123d.api.scene.arrow.utils.log_writer_config import LogWriterConfig
from py123d.api.scene.scene_filter import SceneFilter
from py123d.common.execution.thread_pool_executor import ThreadPoolExecutor
from py123d.datatypes import CustomModality, CustomModalityMetadata, Timestamp
logs_root = Path("data/lead/123D/logs")
log_dir = logs_root / "normal_view/Accident/Town03_Rep0_route_001783_route0"
scenes = ArrowSceneBuilder(
logs_root=str(logs_root),
maps_root="data/lead/123D/maps",
).get_scenes(
SceneFilter(
split_names=["normal_view/Accident"],
log_names=[log_dir.name],
future_num_iterations=0,
),
ThreadPoolExecutor(),
)
log_metadata = scenes[0].get_log_metadata()
sync = pa.ipc.open_file(pa.memory_map(str(log_dir / "sync.arrow"))).read_all()
timestamps = sync.column("sync.timestamp_us").to_pylist()
writer = ArrowLogWriter(
log_writer_config=LogWriterConfig(force_log_conversion=True),
logs_root=logs_root,
sensors_root=Path("data/lead/123D/sensors"),
sync_config=SyncConfig(
reference_column="custom.driving_meta.timestamp_us",
direction="backward",
),
)
writer.reset(log_metadata)
metadata = CustomModalityMetadata(modality_id="reasoning", metadata={"model": "..."})
for time_us in timestamps:
writer.write_async(
CustomModality(
data={"text": your_caption_for(time_us)},
metadata=metadata,
timestamp=Timestamp.from_us(time_us),
),
)
writer.close()
Values go through msgpack, so numpy arrays are fine and numpy scalars such as np.bool_
are not. Timestamps must be non-decreasing and need not cover every tick.
reference_column has to stay the stream the log was written against, and the rebuild
overwrites sync.arrow in place.