Generated banner for Matlab Use Scenario Builder
Generated logo for Matlab Use Scenario Builder

Matlab Use Scenario Builder

MathWorksOfficialCreated by MathWorks

  • Skill
  • Workflow
  • Software development
  • BSD-2-Clause
  • Low risk

From the author

Generate driving scenes, scenarios, road surfaces, and 3D content from scenariobuilder.* sensor data (GPS, camera, lidar, actor tracks) using Scenario Builder for Automated Driving Toolbox. BUILD, EXPORT, or AUGMENT a virtual scenario/scene/map: ego or actor trajectories, trajectory smoothing, OpenCRG road-surface extraction, 3D asset generation, static-object placement, point-cloud georeferencing + elevation, lane-based ego localization, sensor-fusion tracking, scenario-event extraction (cut-ins, hard brakes, near-misses, ADAS disengagements), or export to RoadRunner, drivingScenario, OpenDRIVE, OpenCRG, OpenSCENARIO, or Unreal Engine. Also: log-to-scenario, scenario harvesting, accident/near-miss reconstruction, SOTIF (ISO 21448) and ISO 26262 scenario coverage, USGS-aerial-lidar augmentation, traffic-sign placement, vision-based vehicle classification for actor assets. NOT for raw-data import or multi-sensor sync/crop/offset/timestamp normalization — route those to matlab-import-driving-data.
— the description by MathWorks

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Scenario Builder for MATLAB is a specialized tool for converting raw or processed driving data—including GPS, camera, lidar, and actor tracks—into simulation-ready scenarios. It enables engineers to reconstruct real-world driving environments for testing autonomous systems. The tool supports exporting to industry-standard formats like RoadRunner, OpenSCENARIO, OpenDRIVE, and Unreal Engine, facilitating SOTIF and ISO 26262 compliance by turning recorded logs into reproducible simulation test cases.

What it does

  • Converts GPS, lidar, and camera logs into ego and actor trajectories.
  • Extracts road surfaces and elevation data for high-fidelity simulation.
  • Automates the placement of static objects and traffic signs from sensor detections.
  • Supports scenario harvesting and reconstruction of critical events like near-misses.
  • Exports scenarios to RoadRunner, OpenSCENARIO, OpenDRIVE, and Unreal Engine.
  • Provides tools for lane-based ego localization and point-cloud georeferencing.

When to use it

  • When you need to convert recorded driving logs into a simulation-ready scenario.
  • When performing SOTIF or ISO 26262 scenario coverage analysis.
  • When you need to extract road surfaces or elevation data from lidar recordings.
  • When you need to export real-world driving data to RoadRunner or other simulation environments.
  • When you need to augment existing maps with static objects detected from sensor data.

Advantages

  • Enables data-driven scenario generation for improved simulation coverage.
  • Streamlines the pipeline from raw sensor logs to validated simulation environments.
  • Supports industry-standard formats for broad compatibility with simulation tools.
  • Facilitates compliance with safety standards through reproducible scenario testing.
  • Reduces manual effort in creating complex 3D driving environments.

Usage examples

“Build an ego trajectory from my recorded GPS data.”
The agent will process the GPS coordinates, apply trajectory smoothing, and generate a trajectory object ready for scenario integration.
“Export my recorded scenario to RoadRunner.”
The agent will guide you through the RoadRunner export workflow, ensuring roads are imported and trajectories are correctly mapped to the scene.
“Extract critical events like hard brakes from my sensor logs.”
The agent will analyze the actor tracks and ego data to identify and extract specific scenario events for further simulation testing.
“Improve my OSM scene with elevation data from my lidar.”
The agent will apply height correction to the road scene using the lidar-derived elevation data to ensure accurate road geometry.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/matlab/matlab-use-scenario-builder/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip matlab-use-scenario-builder.zip -d .agents/skills

Documentation by MathWorks

Scenario Builder for MATLAB

When to Use

  • User has recorded driving data (GPS/GNSS, camera, lidar, actor tracks) and needs to convert it into a simulation-ready scenario

  • User asks to export trajectories or scenarios to RoadRunner, drivingScenario, ASAM OpenSCENARIO, OpenDRIVE, ASAM OpenCRG, or Unreal Engine. Default target is RoadRunner — only generate a standalone drivingScenario object and open Driving Scenario Designer when the user explicitly asks for "DSD", "Driving Scenario Designer", drivingScenarioDesigner, or "build a drivingScenario object" (Workflow 15).

  • User mentions safety standards (SOTIF / ISO 21448, ISO 26262) and scenario coverage from real-world data

  • User needs to extract a road surface (OpenCRG) from lidar for vehicle-dynamics or chassis testing

  • User needs to add elevation to an HD map, georeference point clouds, or extract per-frame point clouds along an ego path

  • User needs to localize an ego trajectory on a map using lane detections (RVLD preferred, CLRNet fallback)

  • User needs to add static objects (signs, trees, poles, buildings, barriers) to a RoadRunner HD Map from cuboid detections

  • User wants to augment / enhance / improve a road scene from aerial lidar — adding trees + buildings (Variant A) or improving OSM elevation / banking / gradient / height only (Variant B) — see Workflow 16

  • User wants to add traffic signs from recorded camera + lidar logs with pre-detected sign bounding boxes — see Workflow 17

  • User needs to generate 3D mesh assets from a single camera image

  • User needs to extract critical scenario events (cut-ins, hard brakes, near-misses) from recorded drives

  • User needs accurate non-ego tracks via sensor fusion (multiSensorTargetTracker) before scenario building

  • User mentions multi-sensor preprocessing: synchronization, alignment, offset correction, cropping, timestamp normalization

When NOT to Use

  • User has raw dataset files and wants to load / inspect / visualize / explore / analyze them, synchronize / crop / offset / normalize multi-sensor timestamps, or use the drivingLogAnalyzer (DLA) app or its CLI equivalents — use the matlab-import-driving-data skill, then return here for trajectory smoothing and scenario generation

  • User has multi-sensor data and asks "visualize this dataset", "inspect my recording", "open in DLA", "drivingLogAnalyzer", "Crop & Sync", "offset correction", or "normalize timestamps" — matlab-import-driving-data

  • User is debugging general MATLAB code unrelated to Scenario Builder — use matlab-debug-code

  • User wants to install MATLAB toolboxes or check installed products — use matlab-list-products or matlab-install-products

  • User wants to reconstruct an IMU/tracking simulation scenario from scratch (no recorded data) — use waypointTrajectory from Sensor Fusion and Tracking Toolbox; this skill targets scenario reconstruction from recorded data

  • Task is about non-driving sensor data (medical imaging, audio, etc.) — out of scope

Scenario Builder is a support package that ships with MATLAB as part of the Automated Driving Toolbox. It generates realistic driving scenarios, high-resolution road surfaces, and 3D scene content from raw or processed sensor data — including GPS, camera, lidar, and actor track recordings. The generated outputs can be exported to RoadRunner, drivingScenario objects, ASAM OpenSCENARIO, ASAM OpenCRG (for vehicle dynamics/chassis testing), OpenDRIVE, and Unreal Engine for simulation and validation of autonomous driving systems.

Safety Standards Context: When users ask about improving scenario coverage, traceability, or compliance with SOTIF (ISO 21448) or ISO 26262, guide them toward real-world data-driven scenario generation using Scenario Builder. SOTIF defines an "unknown unsafe" quadrant — hazardous scenarios not yet identified — and converting recorded driving logs (especially critical events, ADAS disengagements, and long-tail situations) into simulation-ready scenarios is a key strategy to reduce that unknown unsafe area. Scenario Builder provides the pipeline from recorded data → reproducible simulation scenario.

Prerequisite: The MATLAB session must have the Scenario Builder for Automated Driving Toolbox support package installed. Verify with:

isfile(which("scenariobuilder.Trajectory"))

Workflow Catalog

This skill supports 18 workflows. Workflows 1–3 and 6 (the core happy path) are inline below. Workflow 4 has a minimal inline pattern with a pointer to its detailed reference. Workflows 5, 7–18 live in references/ and are loaded on demand.

#WorkflowWhereLoad when user says
1Import Raw Sensor Datainline"load my GPS/camera/lidar/actor data"
2Build GPS Data and Extract Trajectoryinline"build ego trajectory from GPS"
3Import Actor Tracks and Create Trajectoriesinline"actor tracks", "non-ego trajectories"
4Export Trajectories to RoadRunnerinline (minimal) + workflow-04-roadrunner-export-detail.md"export to RoadRunner", "RR scene", "simulate scenario"
5Inspect Multi-Sensor Data (drivingLogAnalyzer)see matlab-import-driving-data skill"visualize / inspect / explore / analyze this dataset", "multi-sensor data", "drivingLogAnalyzer", "DLA" — route to matlab-import-driving-data, not handled here
6Preprocess, Synchronize, Crop, Offsetinline"sync", "crop", "normalize timestamps"
7Height Correction for Scenes with Elevationworkflow-07-height-correction.md"Z=0 but roads have elevation", "adjustHeight", "HERE HD scene + GPS", "OpenDRIVE scene + GPS", "pre-built scene with terrain"
8Localize Ego Using Lane Detectionsworkflow-08-lane-localization.md"lane localization", "snap to lane center", "localizeEgoUsingLanes"
9Add Static Objects to RoadRunner HD Mapworkflow-09-static-objects.md"add trees/signs/poles/buildings to RR"
10Road Surface (OpenCRG)workflow-10-road-surface-opencrg.md"road surface", "OpenCRG", "vehicle dynamics from lidar"
11Point Cloud Georeferencing & Elevationworkflow-11-point-cloud-georef.md"addElevation", "georeferenced point cloud", "per-frame lidar"
123D Asset Generation from Imagesworkflow-12-3d-asset-generation.md"imageAssetGenerator", "TripoSR", "3D asset from photo"
13Extract Key Scenario Eventsworkflow-13-event-extraction.md"cut-ins", "near-miss", "hard brake", "ADAS disengagement"
14Sensor Fusion Trackingworkflow-14-sensor-fusion-tracking.md"noisy detections", "ID switches", "multiSensorTargetTracker"
15Driving Scenario Designer (drivingScenario object)workflow-15-driving-scenario-designer.mdexplicit only: "Driving Scenario Designer", "DSD", "open in drivingScenarioDesigner", "build a drivingScenario object"
16Road Scene Augmentation from Aerial Lidarworkflow-16-aerial-lidar-augmentation.md"augment / enhance / improve the scene with trees / buildings", "single lat/lon US — generate scene", "USGS aerial lidar", "improve OSM elevation / banking / gradient / height", .las / .laz aerial input
17Traffic Signs from Recorded Camera + Lidarworkflow-17-traffic-signs-from-sensor-data.md"add traffic signs", "place signs on the map", "signs from camera detections + lidar" (pre-detected sign boxes required)
18Vehicle Classification from Cameraworkflow-18-vehicle-classification.md"classify vehicles", "vehicle color", "vehicle type", "actor asset type", "what kind of car", "realistic actors", "use real colors"
19Ego Lane Inference from Cameraworkflow-19-ego-lane-inference.md"which lane am I in", "predict lane index", "ego lane", "lane count", auto-fires before startLaneIdx question in Step 7 when vision is available

Related references (not workflows): visualization-patterns.md — full code for camera-playback video saving (loaded after Rule 2 decision). osm-flat-scene-gotchas.md — common pitfalls on OSM flat scenes (Z handling, importScene options, stale sim, image-frame datasets).

STOP — Common Agent Failures (check BEFORE writing code)

  1. Missing enableOverlapGroupsOptions(IsEnabled=false) on importScene for RRHD

  2. Skipping lane localization when scene=OSM + Raw GPS + camera (REQUIRED per matrix)

  3. Skipping comparison video gate before reporting task complete

  4. Exporting ego to RoadRunner BEFORE importing roads (OSM scene must exist first)

  5. Using importScene on a saved .rrscene — use openScene(rrApp, file) instead (importScene requires a format string and is for RRHD/OpenDRIVE)

  6. Rebuilding CameraData without SensorParameters= — loses intrinsics, silently downgrades Mode 1 → Mode 3

  7. Comparison video uses raw camera on left — use the track-overlaid video (or BEV+Camera) when available

Mandatory Execution Order (evaluate BEFORE writing code)

When the prompt is "generate scenario from data" (short or long), execute in this order:

  1. addpath(scripts/) — skill helper functions (openFile, plotActorCircles, etc.)

  2. Load data → create objects → Rule 5 timestamp scale detection + normalize

  3. Plot GPS + trajectory side-by-side (VALIDATION — confirm data loaded correctly)

  4. Camera validation video (Rule 2 decision tree) → questdlg popup (BEFORE any RR export)

  5. Multi-GPS gate (if >1 GPS series) → compare video → ASK which series

  6. Establish scene: OSM download + importScene WITH enableOverlapGroupsOptions(IsEnabled=false) — Roads MUST exist before any exportToRoadRunner call

  7. Build ego trajectory → localization decision matrix (Rule 4 Step 7) — OSM + Raw GPS + camera = REQUIRED, no ASK needed — Otherwise: ASK or Skip per matrix

  8. Export ego + actors to RoadRunner (no Orientation=, flatten Z on flat scenes, preserve Orientation= on ego rebuild)

  9. Simulate (setCameraMode(rrApp, "Front"), no Pacing=, EnableLogging=true) → exportVideo — exportVideo VideoFolder MUST NOT contain spaces — use tempdir staging + copyfile if needed

  10. Side-by-side comparison video (track-overlaid left | sim right) → questdlg popup → DONE — Use the Mode 1/2 video as the left panel, NOT raw camera frames

Every step that saves a video MUST end with questdlg+openFile popup — no exceptions.

Skip-if-done: Every expensive file-producing step (websave, write(rrMap,...), importScene+saveScene) MUST check if ~isfile(output) before re-running. See execution-rules-detail.md Rule 7 MCP Chunking.

CAN-bus data: When dataset has OEM CAN signals (multiplexed slots, ego speed/yaw): negate Y, prefer IaEBA over MRR, use GPS+CAN heading fusion for ego.

IMPORTANT — Execution Rules

Rule 1: Show Steps Taken with Progress Table

Always show a progress table that updates after each major step completes. Use ✅ for completed, ⬜ for pending. Mark each step ✅ immediately after it completes in sequential order. Re-print the FULL table after EVERY step. Aim for ~8 rows; combine closely related operations into one row. Adapt the step list to the task.

The progress table does NOT replace informational tables — always ALSO show the data format/mapping table (Rule 6) and any other diagnostic tables.

Output paths: Save all videos to dataDir (the folder containing sensorData.mat), NOT pwd or a temp dir. After saving any video, print the full path in the Claude session AND in MATLAB via fprintf. Read-only dataDir fallback: when dataDir is on a read-only network share (streamed VideoWriter writes fail with Permission Denied even though exportgraphics PNG writes succeed), route MP4 outputs to a local writable outDir = fullfile(scriptDir, "out", "<dataset-tag>") and print BOTH paths.

For the standard table format example and full rules, see references/execution-rules-detail.md (Rule 1 section).

Rule 2: Visualization

Generate these visualizations:

1. GPS vs. Ego Trajectory (side by side):

%% Visualize GPS Data vs. Ego Trajectory
fTraj = figure(Position=[500 500 1000 500]);
gpsPanel = uipanel(Parent=fTraj,Position=[0 0 0.5 1],Title="GPS");
plot(gpsData,Parent=gpsPanel,Basemap="satellite")
trajPanel = uipanel(Parent=fTraj,Position=[0.5 0 0.5 1],Title="Ego Trajectory");
plot(egoTrajectory,ShowHeading=true,Parent=trajPanel)
drawnow;

2. Camera-playback validation video — STRICT priority order:

This rule fires when you are inside a scenario-building pipeline (trajectory, RoadRunner export, lane localization compare) and you need a saved single-camera validation artifact. For general "visualize / inspect / explore this dataset" requests, hand off to the matlab-import-driving-data skill (which uses drivingLogAnalyzer) — do not roll your own dashboard here.

You MUST verify camera intrinsics (fx, fy, cx, cy) and CameraHeight are explicitly in the dataset (MAT field, metadata, calibration file). Never fabricate or approximate these values.

ModeTriggerWhat to do
1 — track-overlay videoTracks + intrinsics + CameraHeight ALL present (single-camera focus)plotActorCircles save-video — see visualization-patterns.md. PRIMARY single-camera viz when intrinsics exist; do NOT downgrade to BEV+Camera.
2 — BEV + Camera side-by-sideTracks present, NO intrinsicsplotBEVAndCamera save-video — see visualization-patterns.md. Fallback only.
3 — raw camera playbackNeither tracks nor intrinsicsraw play(cameraData) save-video — see visualization-patterns.md.

Per-frame 3D detections without track IDs (CubeRCNN / KITTI / nuScenes-style corners3D) belong on DLA's native camera Actors overlay (in matlab-import-driving-data). If a saved overlay artifact is genuinely needed (e.g., shareable mp4 outside the app), convert detections to ActorTrackData with placeholder per-frame IDs ("det_<frameIdx>_<i>") — full conversion in per-frame-detections-to-actortrackdata.md.

Mandatory popup-and-open after every saved video (HARD RULE — no exceptions). Every close(vw) and every exportVideo(...) call in the entire pipeline MUST be immediately followed by a questdlg("<purpose> video saved to:\n%s\n\nOpen it now?", ..., "Yes", "No", "Yes") block whose Yes branch calls openFile(videoPath). This applies to every single video the pipeline saves — track-overlay (mode 1), BEV+Camera (mode 2), raw camera (mode 3), GPS-source compare, localization compare, input-vs-sim final compare, and any other intermediate save. There is no "informational" or "intermediate" exemption: if the script writes an MP4, the very next non-comment line must be the questdlg block. Wiring the popup only on the final comparison video while leaving earlier saves with just fprintf("Saved: %s\n", path) is a HARD RULE violation. The popup is the consent gate that proves the file is viewable; never bypass with a direct winopen/openFile call (the popup lets the user decline), and never skip with just a fprintf of the path. Do NOT also call interactive play(cameraData) / plotBEVAndCamera(...) for the same content — that would double the UI prompts.

Cross-platform open: The save-video patterns and workflow comparison videos use openFile(path) (in scripts/openFile.m) instead of winopen so the same script runs on Windows, macOS, and Linux. Make sure scripts/ is on the MATLAB path (addpath once near the top of the generated script).

plotBEVAndCamera synchronization requirement: Camera and track data must have the same number of samples. If sample counts differ after synchronize(), inform the user:

"The camera and actor track data have different sample rates (camera: N samples, tracks: M samples). Please use the Crop & Sync tab in the Driving Log Analyzer app to apply offset correction and resample to matching rates, then re-import the data."

Rule 3: Ask User About Available Map/Scene Before Choosing Road Source

Before generating a scenario, always ask the user:

"Do you have an existing road map or scene file (e.g., .rrscene, .rrhd, or OpenDRIVE .xodr) available for this data? If so, please provide the path. If not, I will download roads from OpenStreetMap."

Based on the answer:

  • User has a scene file → Use RoadRunnerScene parameter directly with their file path. Keep original altitude from GPS data. See "Scene Portability" note below.

  • User has no scene file → Use the OpenStreetMap → getRoadRunnerHDMap pipeline. Always zero altitude for OpenStreetMap. Validate OSM quality using the cascade below.

IMPORTANT: When asking the user, always say "OpenStreetMap" (the full name), not "OSM".

Scene Portability Warning: .rrscene files created in a different RoadRunner project may lose their geo-reference context when opened in a new project. The scene's WorldOrigin may show [0, 0] even though roads have real-world coordinates. If vehicles appear off-road after export, use the height correction workflow (workflow-07-height-correction.md).

Pre-built scene + GPS (HERE HD / OpenDRIVE / vendor .rrscene): When the user's scene was not built from their GPS, do NOT use getRoadRunnerHDMap(rrApp) (it doesn't exist) and do NOT use roadrunnerHDMap(file) positionally (errors). Pull LocalOrigin from worldSettings(rrApp) (parsing the Projection WKT for full precision), exportScene to .rrhd, then rrMap = roadrunnerHDMap; read(rrMap, file). Always run adjustHeight(traj, rrMap) on terrain-aware scenes. Full pattern in workflow-04-roadrunner-export-detail.md Option B.1.

Multi-GPS / multi-GNSS gate: When the dataset has more than one ego-position series (e.g., GPSData_Raw + GPSData_Corrected, GPS + GNSS, or two GNSS receivers), do NOT silently pick by name — "Corrected" might mean smoothed, RTK, lever-arm-compensated, or dead-reckoned. Build one trajectory per series with the same LocalOrigin, export them to a fresh scenario with red/green colors (use red/green only for 2-series; distinct colors for 3+), follow-cam on Ego_A, then build a raw-camera-left + RR-with-legend-right comparison video. Popup opens the video; ask the user in chat which series to use. Same compare-then-ask pattern as Workflow 8 lane localization. Full pattern in workflow-04-roadrunner-export-detail.md Step 2.5.

OpenStreetMap Quality Validation & Ego-Trajectory Fallback: after downloading OSM roads, always validate map quality (zero-lane check, localizeEgoUsingLanes locInfo metrics, post-localization improvement check) and fall back to an ego-trajectory road if the map doesn't fit. Full cascade with thresholds + fallback code in workflow-08-lane-localization.md (sections "OSM quality cascade" and "Ego-trajectory fallback"). Inform the user before falling back: "The OpenStreetMap roads don't align well with your GPS data — I'll use the raw GPS trajectory instead of localizing."

Rule 4: Follow the RoadRunner Scenario Creation Workflow

Default target = RoadRunner. When a user says "build a scenario", "generate a scenario", or "scenario from this data" without naming a target, follow this RoadRunner workflow — RoadRunner is the canonical Scenario Builder target. DSD is opt-in: only switch to workflow-15-driving-scenario-designer.md when the user explicitly says "Driving Scenario Designer", "DSD", drivingScenarioDesigner, or "build a drivingScenario object". exportToDrivingScenario calls inside this Rule 4 are intermediate steps for road geometry, not a DSD handoff. Lowercase "driving scenario" (generic English) means a scenario built from driving data — RoadRunner. When ambiguous, ask: "RoadRunner, or drivingScenario opened in DSD?"

At a high level, always follow this end-to-end pipeline (skip steps that don't apply):

  1. Load raw sensor data — GPS, actor tracks, camera, lidar

  2. Create sensor data objects — GPSData, ActorTrackData, CameraData

  3. Preprocess — normalizeTimestamps, crop, synchronize

  4. Download roads (actor-aware) — Compute actor lateral extent, pass Extent=buffer to getMapROI. NEVER call getMapROI without Extent. When the OSM-derived .rrhd is imported into RoadRunner via importScene(rrApp, ..., "RoadRunner HD Map", ImportOptions=iOpts), always build iOpts with enableOverlapGroupsOptions(IsEnabled=false). Never call importScene without these options. Only re-enable (and re-import) after the user flags junction/overpass artifacts. After importScene, acquire the map object: rrMap = roadrunnerHDMap(); read(rrMap, rrhdFile); — or pass rrApp directly as the 2nd arg to localizeEgoUsingLanes (both paths documented). HARD RULE — getRoadRunnerHDMap(rrApp) does NOT exist on the roadrunner app class (it's a drivingScenario method); calling it errors. Do NOT pass the .rrhd file path as a string to localizeEgoUsingLanes — the 2nd arg must be an object.

  5. Build ego trajectory — trajectory(gpsData, "LocalOrigin", localOrigin) then smooth

  6. Validate sensors — Show GPS vs. Ego Trajectory plot AND BEV+Camera video (or track-overlay when intrinsics exist). Save the video to disk. Mandatory popup-and-open step: every saved validation video MUST end with a questdlg "Open it now? (Yes/No)" that calls openFile(path) on Yes — even when the next step doesn't need a user decision. The popup is the consent gate that confirms the file is viewable before the agent moves on. See [feedback-saved-video-popup] memory rule and visualization-patterns.md.

  7. Localize ego (decision matrix — read BEFORE writing any lane-detection code) — Localization is NOT one-size-fits-all. Before writing a single laneBoundaryDetector line, classify the situation against this matrix and act accordingly. The agent has been observed correctly identifying the case (e.g., "user chose Corrected GPS + pre-built HERE HD scene, this is Option B.1 — no localization") and then auto-running localization anyway. That is a HARD RULE violation. The classification IS the gate — once you've named the case, you MUST follow its action without re-deriving from scratch.

    Scene sourceGPS sourceCamera + intrinsics?Localization action
    OSM (downloaded)Raw GPSYesREQUIRED — run Workflow 8 unconditionally; raw GPS on OSM lanes needs the snap.
    OSM (downloaded)Raw GPSNoSkip — no detector available.
    OSM (downloaded)Corrected / RTK GPSYesASK — corrected GPS may already be lane-accurate.
    Pre-built .rrscene (HERE HD, OpenDRIVE, vendor)any GPSYesASK — scene's lane geometry may not match OSM-trained detector; localization can degrade trajectory. Default offer = "use GPS as-is".
    Pre-built .rrsceneany GPSNoSkip — no detector available.
    Ego-trajectory fallback (no road download)any GPSanySkip — there are no map lanes to snap to.

    ASK protocol (whenever the matrix says ASK): After building the ego trajectory and (for pre-built scenes) opening the scene + extracting LocalOrigin, stop and ask before any laneBoundaryDetector / detect / laneData / localizeEgoUsingLanes call:

    "Do you want to apply lane localization (snaps the trajectory to lane center using camera detections), or use the {Corrected GPS / pre-built scene's GPS} trajectory directly?"

    The question must reference the actual GPS series the user picked and (for pre-built scenes) the scene file. Do not phrase it as a preference question — phrase it as a fork: localize vs use as-is. Wait for the user's answer before writing or running the lane-detection block. If the user picks "use as-is", the script must skip laneBoundaryDetector, laneBoundaryTracker, laneData, and localizeEgoUsingLanes entirely and proceed directly to scenario export.

    HARD RULE — naming the case is the gate. If you write a sentence like "the user chose Corrected GPS and has a pre-built .rrscene, so I'll use Option B.1 (no localization)", you have already classified the case. The very next code block MUST NOT be lane detection. Either ask (matrix → ASK), or skip (matrix → Skip), or proceed without localization (Option B.1). Auto-running localization after stating you wouldn't is the failure mode this rule exists to prevent.

    If localization runs (REQUIRED or user opted in): Build monoCamera first → lane detection (RVLD first, CLRNet fallback when RVLD detects boundaries on only one side of ego — i.e., all LaneBoundary lateral offsets share the same sign — so the localizer would error with "ego lane detections missing") → imageToVehicle(monoCamera, ...) → findParabolicLaneBoundaries → laneBoundaryTracker → laneData → ASK startLaneIdx → localizeEgoUsingLanes(egoTrajectory, rrMap, laneData, startLaneIdx) (POSITIONAL args — StartLaneIndex=N is rejected). Acquire rrMap via rrMap = roadrunnerHDMap(); read(rrMap, rrhdFile); (the empty constructor + read pattern). HARD RULE — getRoadRunnerHDMap(rrApp) does NOT exist on the roadrunner app object (it's a drivingScenario method). Calling it errors with "Undefined function ... for input arguments of type 'roadrunner'". localizeEgoUsingLanes also accepts rrApp directly as the second argument if you prefer not to construct the map; both paths are documented. HARD RULE — imageToVehicle REQUIRES a monoCamera object, NOT a bare cameraIntrinsics. Build cameraParams = monoCamera(intrinsics, camHeight, Pitch=..., Yaw=..., Roll=...) BEFORE Step 1 of the detection chain — it carries the camera-to-vehicle geometry the bare intrinsics object lacks. Skipping imageToVehicle + findParabolicLaneBoundaries and feeding pixel coordinates straight into laneBoundaryTracker ALSO fails — the tracker expects parabolicLaneBoundary rows in vehicle coordinates. startLaneIdx — predict then confirm (Workflow 19). When the gate is reached: (a) confirm the track-overlay (Step 6) video popup has fired, (b) IF the agent can view images, run Workflow 19 (sample 5 frames, classify lane count + ego lane, bisect transitions if any) — see workflow-19-ego-lane-inference.md. (c) Present prediction for confirmation: "From the dashcam I see N lanes, ego in lane M (leftmost=1, no shoulders/ramps). Confirm or correct?" (d) If agent cannot view images OR confidence is low, fall back to plain ASK: "Watching the dashcam, which lane is the ego in? Count from the leftmost driving lane = 1 (no shoulders / parking)." (e) Pass the user's confirmed/corrected integer to localizeEgoUsingLanes. Do NOT generate a lane_count_vs_frame.png plot, do NOT print a frames-per-lane-count summary table, and do NOT print "Detection-based HINT: ego in lane X of Y tracked boundaries". No variable-lane gate question: after the user provides startLaneIdx, just call localizeEgoUsingLanes — it interpolates through frames with fewer lanes. Do NOT ask "the map narrows to K lanes, want to crop or split?" The silent lane-count probe is still computed internally for diagnostics, but never shown to the user and never blocks localization. Only report a lane-count problem if localizeEgoUsingLanes actually throws an error. Verification video = raw camera (left) | RR follow-cam on Ego_Localized + red/green legends (right), gated by the mandatory questdlg+openFile popup. Ask the user in chat which trajectory to use. Full code in workflow-08-lane-localization.md.

  8. Generate final scenario — Same RR instance: newScenario(rrApp) → re-export only localizedTrajectory with Color="auto" → actorprops(trackObjNorm, localizedTrajectory) (accepts scenariobuilder.Trajectory directly — do NOT convert to waypointTrajectory). MUST normalizeTimestamps(copy(trackData)) + set localizedTrajectory.TimeOrigin=0 first or every actor is rejected. Do NOT relaunch RR. Skip the GPS-vs-Trajectory plot — the red/green compare already covers it. Non-ego actors: run Workflow 18 (vehicle classification) automatically — build best-crop grid, view it, classify by color + type, map to AssetPath + RR named Color. If prerequisites are missing (no intrinsics / no camera / text-only agent) or confidence is low, fall back to AssetPath="Vehicles/Sedan.fbx", Color="auto". HARD RULE — do NOT pass Orientation= when constructing scenariobuilder.Trajectory for non-ego actors. actorprops returns yaw in compass convention (e.g., ~340° for northbound) which is incompatible with RoadRunner's heading convention; passing it as Orientation produces actors facing sideways or yaw-flickering across frames. Omit the parameter entirely and let RR auto-derive heading from the waypoint sequence: scenariobuilder.Trajectory(t, wp, Name=tid) — no Orientation, no Speed. On flat scenes (OSM, OpenDRIVE without elevation), flatten waypoint Z to 0 before constructing the actor Trajectory — actorInfo.Waypoints(:,3) carries sensor-mount offset (~1 m), not road height, so without flattening every actor floats above the road. Keep Z and use adjustHeight on ego only when the rrhd has real terrain elevation (HERE HD, Zenrin, OpenDRIVE+CRG, point-cloud-derived). See workflow-04-roadrunner-export-detail.md Step 4. Post-export asset replacement (when iterating on classification without re-exporting trajectories): rrAPI = roadrunnerAPI(rrApp); allActors = rrAPI.Scenario.Actors; then allActors(k).ActorAsset = "<PROJECT>/Assets/Vehicles/Suv.fbx_rrx"; — CRITICAL: always use <PROJECT>/Assets/... relative paths, NEVER absolute paths (absolute silently produces white placeholder boxes). See osm-flat-scene-gotchas.md for common flat-scene pitfalls.

  9. Simulate & export video — Disable collision (setFailCondition("DurationCondition")), setCameraMode(rrApp, "front", FocusActorID=1) (NEVER follow for the final scenario), simulateScenario(rrApp, EnableLogging=true) — HARD RULE: do NOT pass Pacing= to simulateScenario for the final capture; Pacing caps the logged simulation duration and silently produces a 1-second video instead of the full clip. exportVideo, build the comparison video. All four are mandatory.

Camera-mode rule (do NOT mix up): Step 7 (localization compare) = "follow" on localized ego (keeps red/green both in frame). Step 9 (final scenario) = "front" on ego ID=1 (mirrors the dashcam). Export-order HARD RULE — FocusActorID=1 follows whatever was exported FIRST. RoadRunner assigns ActorID in exportToRoadRunner call order (1st→ID=1, 2nd→ID=2, …). Export the actor the camera should track FIRST: localization compare = Ego_Localized first then Ego_Raw; multi-GPS compare (workflow-04 Step 2.5) = the series whose perspective matters first; final scenario = ego first then non-egos. Inverting silently produces a video that follows the wrong actor — looks like a "video bug" but root cause is export sequence.

Ego localization priority: When the decision matrix above says REQUIRED (OSM + Raw GPS + camera-with-intrinsics) — i.e., the agent downloaded roads from OSM, the user did NOT specify a corrected/RTK GPS series, and cameraData.SensorParameters carries fx, fy, cx, cy + CameraHeight — run Workflow 8 unconditionally, immediately after building the ego trajectory and before any export. Do NOT ask in this case; localization on raw GPS over OSM lanes is the canonical happy path. Skipping Workflow 8 here (e.g., going straight from trajectory(gpsData,...) to exportToRoadRunner without ever calling laneBoundaryDetector / detect / laneData / localizeEgoUsingLanes) is a regression — it produces a scenario where the ego drifts off the lane it was actually in. Common false-skip patterns to avoid:

  • Building cameraData but never calling laneBoundaryDetector(...) on it.

  • Treating intrinsics + raw GPS as "good enough" because the GPS plot looks reasonable on a satellite basemap. The OSM lane offset can be 1–3 m even when GPS looks correct at 2-D map zoom.

  • Confusing this case with the pre-built-scene case (matrix row 4) — pre-built scenes are ASK; OSM + Raw GPS is REQUIRED.

Rule 5: Ensure Timestamps Are in Seconds and All Sensors Are Synchronized

Before any trajectory creation or export: (1) detect raw timestamp scale and convert to seconds, (2) normalize so all sensors share t=0, (3) synchronize to a reference sensor's timeline.

Timestamp scale detection (always convert to seconds BEFORE creating sensor objects):

Raw value magnitudeUnitConversion
~1e9 (e.g., 1.557e9)POSIX secondsnone
~1e12milliseconds/1e3
~1e15microseconds/1e6
~1e18nanoseconds/1e9

Span check (catch relative timestamps): Some datasets (notably HERE HD / OpenDRIVE exports) store timestamps in microseconds-since-trip-start, so the first sample is small but the span is huge. After the magnitude check above, also guard on span: if ts(end) - ts(1) > 1e6, ts = ts / 1e6; end (microseconds) or > 1e3 for milliseconds. Without the span check you silently get a 23-million-second trajectory. Apply the same conversion to every sensor's raw timestamps before constructing the scenariobuilder.* objects.

normalizeTimestamps pitfall: convertTimestamps(obj, "numeric") may internally normalize some sensors but not others. Check whether timestamps already start near 0 before calling normalizeTimestamps with a shared reference. If one is normalized and another still has POSIX values, normalize each independently first, then synchronize. Start from the earliest sensor: normalizeTimestamps(sensorB, timeRef) errors with "Time origin must be less than or equal to the minimum timestamp" if sensorB starts before the reference sensor. Always call normalizeTimestamps first on the sensor with the smallest Timestamps(1), capture its timeRef, then pass that to all others.

For the full conversion + sync code block, see references/execution-rules-detail.md (Rule 5 section).

Rule 6: Explain Expected Data Format Before Importing

Before importing any sensor data, always tell the user the exact format/structure their data must be in. Inspect the user's data first (load it, check variable names, sizes, types), then present a sensor-format mapping table: Sensor | Required Fields | Expected Format | Your Data | Raw Magnitude / Scale. Fill in what you detect and flag mismatches or required transformations (e.g., "timestamps are POSIX — will convert", "positions are 2D — will add z=0").

HARD RULE — Raw Magnitude column is mandatory. Run max(rawTs) (and span rawTs(end)-rawTs(1) if the first sample is small) BEFORE constructing any scenariobuilder.* object; write the magnitude AND Rule 5 scale factor (/1, /1e3, /1e6, /1e9) into the table. A blank magnitude cell is not acceptable — Polysync (~1.46e15 µs) silently builds million-second trajectories that only fail at simulateScenario. Full table template + known-dataset quick reference (Pandaset, Polysync, nuScenes, KITTI, HERE HD) in references/execution-rules-detail.md (Rule 6 section).

Field name case sensitivity: MATLAB table variable names are case-sensitive. Datasets vary (timestamp vs timeStamp, TrackID vs trackID). Always disp(data.X.Properties.VariableNames') before accessing any field.

Y-axis convention (CAN/OEM data — HARD RULE): OEM radar (MRR_*) and camera AEB (IaEBA_*) signals use +Y = right. MATLAB/SAE/ISO use +Y = left. ALWAYS negate Y from CAN object sources: posY = -rawLatDist. Without negation actors on the left appear on the right — silent, no error. Add a "Coordinate flip needed?" column to the format table when CAN data is detected.

Rule 7: Generate and Run a MATLAB Script

Always generate a well-documented .m script and run it (via run_matlab_file or evaluate_matlab_code) — do not execute snippets piecemeal. Include %% section headers, comments, and a header block stating purpose, inputs, and outputs. Always set a dataDir variable for video outputs and clean up rrApp before re-launching. For the standard header template, see references/execution-rules-detail.md (Rule 7 section).

Rule 8: Ask User for RoadRunner Paths

Always ask the user for the RoadRunner installation folder (containing AppRoadRunner.exe) and project folder. Auto-discovery is unreliable. If you already have the paths from earlier in this conversation, reuse without re-asking. Single-instance guard: never call roadrunner(...) unconditionally — always check first:

if ~exist('rrApp', 'var') || ~isvalid(rrApp)
    rrApp = roadrunner(rrProjectPath, InstallationFolder=rrAppPath);
end

This prevents opening multiple RoadRunner windows when the script is re-run or when Part 2 follows Part 1 in the same MATLAB session.

Multi-script pipeline pitfall: the default pipeline shape is single-script — every reference template opens RoadRunner once and keeps it alive through export. If you split a pipeline across scripts to checkpoint between user-gated pauses, do NOT start each part with clear; clc; then roadrunner(...) — clear wipes the handle but not the GUI process, so each part spawns a new RoadRunner window. Either keep one script with inline uiwait/questdlg pauses, or close(rrApp); clear rrApp at the end of each part and skip the outright clear at the top of the next. Full patterns in references/execution-rules-detail.md (Rule 8 — Multi-Script Pipeline Pitfall).

Clean-slate before export: After the guard, always call newScenario(rrApp) before exporting trajectories — this ensures a previous run's actors/scenario don't accumulate. When loading a pre-built scene (openScene), call newScenario(rrApp) after the scene is loaded (scene stays, scenario clears). When importing an HD map (importScene), the import itself opens a new scene so no explicit newScenario is needed before import — but call it after import and before exportToRoadRunner.

Rule 9: Create Your Own Helper Functions When Needed

Do not rely on pre-existing helper functions (e.g., helperSmoothWaypoints, helperPlotActors). Write helpers as local functions or separate .m files alongside the script.

Rule 10: Always Use scenariobuilder.* APIs for Timestamped Sensor Data

Read the matching reference file in references/ BEFORE the first call. The references/*-api.md and workflow-*.md files have the exact constructor signatures. Do NOT guess from memory — scenariobuilder.ActorTrackData(timestamps, trackID, position) has 3 positional args (no ClassID), and similar pitfalls exist for the other constructors. Open the reference once at the top of the session, then proceed.

Whenever the user's data has timestamps + a sensor type, use the corresponding scenariobuilder.* object:

User hasUse
Timestamps + lat/lon/GPS/GNSSscenariobuilder.GPSData
Timestamps + camera images/videoscenariobuilder.CameraData
Timestamps + lidar point cloudsscenariobuilder.LidarData
Timestamps + actor/object tracksscenariobuilder.ActorTrackData
Timestamps + waypoints (local XY)scenariobuilder.Trajectory

Whenever the user mentions these operations, use the built-in methods:

User saysUse
Synchronize, sync, align sensors, multi-sensor alignmentsynchronize(sensorObj, referenceSensor)
Crop, trim, cut, extract segmentcrop(sensorObj, startTime, endTime)
Offset correction, time offset, shift timestampsnormalizeTimestamps(sensorObj) or normalizeTimestamps(sensorObj, timeRef)
Normalize, common time base, t=0 referencenormalizeTimestamps(sensorObj)
Convert timestamps, datetime to numericconvertTimestamps(sensorObj, "numeric")
Visualize, plot, play, replayplot(sensorObj) or play(sensorObj)

Do NOT implement these manually (e.g., interp1 for sync, manual indexing for crop). The scenariobuilder.* objects handle edge cases that manual code often gets wrong. OpenStreetMap roads: always use getMapROI for the bounding box, URL, and georeference — never manually construct OSM API URLs. Standard OSM import pattern in references/execution-rules-detail.md (Rule 11).

Rule 11: Preferred APIs — Use / Don't Use

NEVER use these functions:

Don't UseUse InsteadReason
roadrunnerLaneInfolaneBoundarySegment → laneBoundaryGroup → getLanesInRoadRunnerHDMapStruct-heavy I/O, error-prone
actorTracklistscenariobuilder.ActorTrackDataLegacy — no sync/crop/normalize/viz support

waypointTrajectory vs scenariobuilder.Trajectory: For scenario building, always use scenariobuilder.Trajectory. waypointTrajectory is a Sensor Fusion System object for IMU simulation — NOT for scenario reconstruction.

Preferred pipelines (full code in references/execution-rules-detail.md):

  • Programmatic roads: laneBoundarySegment → laneBoundaryGroup → (optional smoothBoundaries) → getLanesInRoadRunnerHDMap → write(rrMap, filePath).

  • Static objects: Always roadrunnerStaticObjectInfo. Never manually build roadrunner.hdmap.StaticObject/Sign structs. See workflow-09-static-objects.md.

  • GPS/GNSS data: GPSData → trajectory(...) → smooth(...) → exportToDrivingScenario or exportToRoadRunner. Never combine latlon2local + vehicle() + trajectory() on a drivingScenario.

  • Legacy migration: Convert actorTracklist/objectTrack via importFromObjectTrack() on an ActorTrackData.

Rule 12: Minimum-Change Reruns

When fixing a defect, change ONLY what the defect requires. Do not rebuild the scene/scenario/actors when only a color, orientation, camera mode, or a scene-flag changed. Each unnecessary rerun wastes 30-60s and risks file-lock issues. Before re-running anything, write a one-line plan: "the defect is X; the minimum change is Y." For the matrix of defect → minimum change, see references/execution-rules-detail.md (Rule 12 section).

Quick Reference — Key APIs

APIPurpose
Sensor Data Objects
scenariobuilder.GPSDataStore GPS coordinates with timestamps
scenariobuilder.ActorTrackDataStore actor track detections with timestamps
scenariobuilder.CameraDataStore camera image data with timestamps
scenariobuilder.LidarDataStore lidar point cloud data with timestamps
scenariobuilder.TrajectoryCreate trajectory from timestamps + waypoints
recordedSensorDataConvenience factory to create any sensor data object
drivingLogAnalyzerLaunch the interactive app for sensor data analysis
Trajectory & Actor Processing
actorpropsExtract actor properties and trajectories from track data
importFromObjectTrackImport objectTrack array (sensor fusion) into ActorTrackData
writeCSVExport trajectory to CSV (world-coord waypoints + optional orientation)
selectActorRoadsFilter roads in an actor's path from roadprops output
Road Network & Map
getMapROICompute geographic bounding box from GPS for OSM download
roadpropsExtract road properties and geo-reference from OSM/OpenDRIVE file
updateLaneSpecUpdate lane widths/markings using lane detections
Lane Detection & Localization
laneBoundaryDetectorDetect lane boundaries. Constructor: laneBoundaryDetector(Model="RVLD") — RVLD is preferred. Fall back to laneBoundaryDetector() (CLRNet default) only if the RVLD constructor errors. WRONG: checking which("rvldLaneDetector") or exist("rvldLaneDetector") — no such function exists. RVLD is a model option on laneBoundaryDetector, not a separate class. Also fall back to CLRNet if tracker output has boundaries on only one side of ego (all same-sign lateral offsets).
laneBoundaryTrackerTrack lane boundaries across frames
laneDataStore recorded lane boundary data with timestamps
egoToWorldLaneBoundarySegmentsConvert tracked ego-frame lane boundaries → world-frame laneBoundarySegment
laneBoundarySegmentStore lane boundary info for a road segment (world coords)
laneBoundaryGroupConnect multiple lane boundary segments
getLanesInRoadRunnerHDMapCreate RoadRunner HD Map from lane boundary objects
localizeEgoUsingLanesSnap ego trajectory to lane center using lane detections
Static Objects & 3D Assets
roadrunnerStaticObjectInfoConvert cuboid detections to RR HD Map static objects/signs
imageAssetGeneratorGenerate 3D mesh assets from a single camera image (TripoSR)
Point Cloud & Elevation
egoPointCloudExtractorExtract local point clouds along ego trajectory
addElevationAdd elevation to roadrunnerHDMap, 2D lane points, or cuboids
roadSurfaceExtract high-res road surface from lidar → OpenCRG, elevation, mesh, boundaries
Export
exportToDrivingScenarioExport trajectory + road network → drivingScenario
exportToRoadRunnerExport trajectory + scene directly to RoadRunner
exportScenario(rrApp,file,"OpenSCENARIO XML")Export RR scenario to ASAM OpenSCENARIO XML
exportScene(rrApp,file,"OpenDRIVE")Export RR scene to ASAM OpenDRIVE
openScenarioExportOptionsConfigure OpenSCENARIO export (version, OpenDRIVE pairing, catalogs)
openDriveExportOptionsConfigure OpenDRIVE export (version, signals, objects, CRG)
exportToASAMOpenCRGExport roadSurface to ASAM OpenCRG .crg file
enableOverlapGroupsOptionsHard default IsEnabled=false for every importScene(rrApp, ..., "RoadRunner HD Map", ...) call. Flip to true and re-import (asking the user) ONLY if junctions/overpasses look wrong.

Workflow 1 — Import Raw Sensor Data

Use recordedSensorData (factory) or construct directly: scenariobuilder.GPSData(timestamps, lat, lon, alt), scenariobuilder.ActorTrackData(timestamps, trackID, position, ...), scenariobuilder.CameraData(camTimestamps, imageFileNames, ...), scenariobuilder.LidarData(lidarTimestamps, pcdFiles, ...). For per-schema field-mapping see the matlab-import-driving-data skill. No wrapper exists for radar or IMU — do NOT fabricate scenariobuilder.RadarData / scenariobuilder.IMUData.


Workflow 2 — Build GPS Data and Extract Trajectory

altitude = zeros(size(latitude));  % OSM has no elevation — zero it
gpsData = scenariobuilder.GPSData(timestamps, latitude, longitude, altitude);
[roadProperties, localOrigin] = roadprops(OpenStreetMap="drive_map.osm");
egoTrajectory = trajectory(gpsData, "LocalOrigin", localOrigin);
smooth(egoTrajectory);   % remove GPS noise

Workflow 3 — Import Actor Track Data and Create Trajectories

%% Step 1: Create ActorTrackData
trackData = scenariobuilder.ActorTrackData(timestamps, trackID, position, ...
    Category=category, Dimension=dimension, Orientation=orientation);

%% Step 2: Create ego trajectory (needed as reference)
egoTrajectory = trajectory(gpsData, "LocalOrigin", localOrigin);
smooth(egoTrajectory);

%% Step 3: Extract actor properties using actorprops
actorInfo = actorprops(trackData, egoTrajectory);                  % basic
actorInfo = actorprops(trackData, egoTrajectory, AgeThreshold=20); % filter short tracks
actorInfo = actorprops(trackData, egoTrajectory, SaveAs="none");   % in-memory only

actorprops returns a table with Age, TrackID, ClassID, EntryTime, ExitTime, Mesh, Time, Waypoints, Speed, Roll, Pitch, Yaw, IsStationary.

Lightweight off-ramp: world-coordinate trajectories as CSV

If the user just wants trajectories in world coords (no RoadRunner, no drivingScenario), use actorprops → Trajectory → writeCSV(traj, FileName="ego_trajectory.csv", IncludeOrientation=true).

Step 4: Ask User About Actor Track Smoothness

After generating actor trajectories, always ask:

"Are the actor trajectories smooth and consistent? If tracks appear jittery or IDs are not persistent, I can run a JPDA smoother (smootherJIPDA) on the raw detections for better track quality."

If the user reports jittery/noisy tracks, apply offline smoothing using trackerJPDA + smootherJIPDA, then importFromObjectTrack into a fresh ActorTrackData. Full pattern in workflow-14-sensor-fusion-tracking.md.


Workflow 4 — Export Trajectories to RoadRunner (minimal inline)

This is the minimal happy-path pattern. For the full workflow (4 scene-source options including OpenStreetMap with actor-aware buffer, HERE HD Live Map, OpenDRIVE; stale-cosim-fix; advanced simulation control; side-by-side comparison video composition; OpenSCENARIO/OpenDRIVE export), load workflow-04-roadrunner-export-detail.md.

%% Step 1: Connect to RoadRunner (single-instance guard — Rule 8)
if ~exist('rrApp', 'var') || ~isvalid(rrApp)
    rrApp = roadrunner(rrProjectPath, InstallationFolder=rrAppPath);
end

%% Step 2: Get scene from OSM (most common path) — full version in reference
mapROI = getMapROI(gpsData.Latitude, gpsData.Longitude, Extent=buffer);
osmFile = fullfile(tempdir, "drive_map.osm");
websave(osmFile, mapROI.osmUrl, weboptions(ContentType="xml"));
[roadProperties, localOrigin] = roadprops(OpenStreetMap=osmFile);
egoTrajectory = trajectory(gpsData, "LocalOrigin", localOrigin);
smooth(egoTrajectory);

scenario = exportToDrivingScenario(egoTrajectory, ...
    RoadNetworkSource="OpenStreetMap", FileName=osmFile, Name="Ego");
rrMap = getRoadRunnerHDMap(scenario);

%% Step 3: Import HD Map into RoadRunner — overlap groups DISABLED by default
% (user can re-enable later if junctions/overpasses look wrong — see workflow-04)
rrhdFile = fullfile(tempdir, "roads.rrhd");
write(rrMap, rrhdFile);
copyfile(rrhdFile, fullfile(rrProjectPath, "Assets", "RoadFromOSM.rrhd"));
oOpts = enableOverlapGroupsOptions(IsEnabled=false);
bOpts = roadrunnerHDMapBuildOptions(EnableOverlapGroupsOptions=oOpts);
iOpts = roadrunnerHDMapImportOptions(BuildOptions=bOpts);
importScene(rrApp, "RoadFromOSM.rrhd", "RoadRunner HD Map", ImportOptions=iOpts);
tempSceneFile = fullfile(tempdir, "osm_roads_temp.rrscene");
saveScene(rrApp, tempSceneFile);

%% Step 4: Clean slate + export ego (with the saved scene)
newScenario(rrApp);
exportToRoadRunner(egoTrajectory, rrApp, ...
    RoadRunnerScene=tempSceneFile, Name="Ego", SetupSimulation=true);

%% Step 5: Classify vehicles (Workflow 18 — automatic when intrinsics exist)
% Builds best-crop grid, agent views + classifies, maps to AssetPath + Color.
% See references/workflow-18-vehicle-classification.md for full pipeline.
% Self-gating: skipped if no intrinsics/CameraHeight or agent can't view images.
classificationMap = containers.Map();  % populated by agent vision pass

%% Step 6: Export non-ego actors with classified asset types
for i = 1:height(actorInfo)
    actorTraj = scenariobuilder.Trajectory( ...
        actorInfo.Time{i}, actorInfo.Waypoints{i}, Name=actorInfo.TrackID(i));
    smooth(actorTraj);
    tid = char(actorInfo.TrackID(i));
    if isKey(classificationMap, tid)
        cls = classificationMap(tid);
        assetPath = cls.AssetPath;  rrColor = cls.Color;
    else
        assetPath = "Vehicles/Sedan.fbx"; rrColor = "auto";
    end
    exportToRoadRunner(actorTraj, rrApp, ...
        Name=actorInfo.TrackID(i), AssetPath=assetPath, ...
        Color=rrColor, SetupSimulation=false);
end

%% Step 6: Disable collision fail condition + simulate (see Rule 11 snippet)
%% Step 7: exportVideo + side-by-side comparison — see workflow-04 reference

exportToRoadRunner name-value args (full list in reference): Name, Color, AssetPath, SetupSimulation, RoadRunnerScene.

Comparison video requirements: insertText timestamp burn on BOTH halves, questdlg popup after close(vw), defensive field-name inspect on input struct. Full pattern in workflow-04-roadrunner-export-detail.md.


Workflow 5 — handled in matlab-import-driving-data

DLA (drivingLogAnalyzer) routing for "visualize / inspect / explore / analyze this dataset" → matlab-import-driving-data.

Workflow 6 — Preprocess, Synchronize, Crop, and Correct Offset

Use normalizeTimestamps, crop, synchronize, convertTimestamps on the scenariobuilder data objects. Route complex preprocessing to matlab-import-driving-data.

Workflows 7–17, Object APIs, End-to-End Examples

Workflows 7–18 are reference-only — see the Workflow Catalog above for triggers and references/workflow-NN-<slug>.md. Workflow 15 (DSD) is opt-in. Workflows 16–17 cover scene augmentation. Workflow 18 (vehicle classification) runs automatically inside Workflow 4 Step 4 when the prerequisites are met.

Workflow 15 (DSD) actor safeguards — NEVER use smoothTrajectory on recorded actor waypoints. Use trajectory(veh, wpu, spdu) instead. smoothTrajectory's accumulateDistance is brittle on sparse, near-stationary, or noisy paths and throws "Unable to create smooth trajectory." Also: dedup consecutive identical (x,y) waypoints, compute per-waypoint speed via finite differences, clamp speed to max(spdu, 0.1) (zero stalls DSD), and flatten actor Z to 0 on OSM scenes. Full pattern in workflow-15-driving-scenario-designer.md.

Object APIs (load on demand): trajectory-api.md, actortrackdata-api.md, gpsdata-api.md. For raw-dataset wrapping → matlab-import-driving-data.

End-to-end examples (read one before writing scripts): example-roadrunner-scenario.md (direct RR export, Pandaset) · example-scenario-from-gps-and-tracks.md (OSM roads + getMapROI + roadprops).


Files

references/workflow-04-roadrunner-export-detail.md


name: workflow-04-roadrunner-export-detail description: Detailed RoadRunner export workflow — connecting to RoadRunner, obtaining a scene (OSM/file/HERE/OpenDRIVE), exporting ego + actor trajectories, running the simulation, exporting and comparing simulation video. Loaded when user needs full RR export detail beyond the inline minimal pattern.

Workflow 4 — Export Trajectories to RoadRunner (Detailed)

Parent skill: SKILL.md — Workflow 4 inline section has the minimal happy-path pattern. This file has the complete workflow including all four scene-source options, simulation control, and side-by-side comparison video composition.

Related references: workflow-08-lane-localization.md when intrinsics + lane detection are available. workflow-09-static-objects.md when adding static objects to the HD map.

Step 1: Connect to RoadRunner

% Use paths provided by the user (see Rule 8) — single-instance guard
rrAppPath = "<user-provided installation path>";
rrProjectPath = "<user-provided project path>";
if ~exist('rrApp', 'var') || ~isvalid(rrApp)
    rrApp = roadrunner(rrProjectPath, InstallationFolder=rrAppPath);
end

RoadRunner crash recovery (use at chunk boundaries or after timeout): If RoadRunner has crashed or become unresponsive mid-pipeline, the handle is stale. Always wrap RR calls after a pause/chunk boundary in a try-catch:

try
    worldSettings(rrApp);  % lightweight probe — errors if RR died
catch
    clear rrApp;
    rrApp = roadrunner(rrProjectPath, InstallationFolder=rrAppPath);
end

Stale cosimulation connection fix: If you get the error "MATLAB is already connected to RoadRunner Scenario cosimulation server", close and relaunch RoadRunner to clear it:

% Close and relaunch RoadRunner to clear stale cosim connection
close(rrApp); clear rrApp; pause(2);
rrApp = roadrunner(rrProjectPath, InstallationFolder=rrAppPath);

If that alone doesn't work (connection persists), also clear the Simulink client (requires Simulink):

try
    rrSim = Simulink.ScenarioSimulation.getSimulationClient('localhost:60731');
    delete(rrSim); clear rrSim;
catch
end

Step 2: Obtain a RoadRunner Scene

A scene provides the road network for the scenario. Use one of the following options:

Option A: Download from OpenStreetMap (most common)

Important: Always zero altitude for OSM (no elevation data). Use getRoadRunnerHDMap (not OpenDRIVE export) to avoid lateral misalignment.

IMPORTANT — Actor-aware OSM download: Do NOT use only ego GPS for the bounding box. Actors may be on parallel roads outside the ego's GPS spread. Use a single-pass approach:

  1. Scan raw actor track positions for max lateral offset (ego-relative Y coordinate)

  2. Add 50m margin (for road width) and pass as Extent to getMapROI

  3. Extent is in meters

This avoids a double-pass (no need to build trajectories first) and ensures roads are downloaded for all actors.

% Step 1: Estimate buffer from raw track positions (instant — no trajectory needed)
maxLateral = 0;
for i = 1:numel(trackPositions)
    pos = trackPositions{i};
    if ~isempty(pos)
        maxLateral = max(maxLateral, max(abs(pos(:,2))));
    end
end
buffer = maxLateral + 50;  % 50m margin for road width

% Step 2: Download OSM with actor-aware buffer (skip-if-exists + timeout), convert to RoadRunner HD Map
mapROI = getMapROI(gpsData.Latitude, gpsData.Longitude, Extent=buffer);
osmFile = fullfile(tempdir, "drive_map.osm");
if ~isfile(osmFile)
    websave(osmFile, mapROI.osmUrl, weboptions(ContentType="xml", Timeout=30));
end
ds =  drivingScenario;
roadNetwork(ds,OpenStreetMap=osmFile)
rrMap = getRoadRunnerHDMap(ds);
rrhdFile = fullfile(tempdir, "roads.rrhd");
write(rrMap, rrhdFile);

% Step 3: Build ego trajectory with OSM local origin
localOrigin = rrMap.GeoReference;
egoTrajectory = trajectory(gpsData, "LocalOrigin", localOrigin);
smooth(egoTrajectory);

% Step 4: Export to drivingScenario with OSM roads
scenario = exportToDrivingScenario(egoTrajectory, ...
    RoadNetworkSource="OpenStreetMap", FileName=osmFile, Name="Ego");

% Step 5: Import HD Map — overlap groups MUST BE DISABLED by default.
% This is a hard rule, not an optimization: importing with the default
% (IsEnabled=true) produces false-positive overlap groups at junctions/overpasses
% on OSM-derived maps, which makes the scene look wrong even when geometry is fine.
% Always pass these three options together. Only re-enable later if the user
% reports junction/overpass artifacts AFTER reviewing the scene (see below).
copyfile(rrhdFile, fullfile(rrProjectPath, "Assets", "RoadFromOSM.rrhd"));
changeWorldSettings(rrApp, WorldOrigin=localOrigin(1:2));
oOpts = enableOverlapGroupsOptions(IsEnabled=false);              % MANDATORY default
bOpts = roadrunnerHDMapBuildOptions(EnableOverlapGroupsOptions=oOpts);
iOpts = roadrunnerHDMapImportOptions(BuildOptions=bOpts);
importScene(rrApp, "RoadFromOSM.rrhd", "RoadRunner HD Map", ImportOptions=iOpts);
tempSceneFile = fullfile(tempdir, "osm_roads_temp.rrscene");
saveScene(rrApp, tempSceneFile);

% Step 6: Export ego with scene (ensures coordinate alignment)
newScene(rrApp);
exportToRoadRunner(egoTrajectory, rrApp, ...
    RoadRunnerScene=tempSceneFile, Name="Ego", SetupSimulation=true);
Re-enabling overlap groups (when default disabled doesn't look right)

Default behaviour above imports with EnableOverlapGroups=false. If, after scenario generation, the user reports that roads/scene look wrong at junctions or overpasses (e.g., crossing roads that should be connected aren't, or overpasses look flat), re-import the same .rrhd with overlap groups enabled, then ask the user whether the result looks better:

% Conditional fix — only when user flags scene problems at junctions/overpasses
oOpts = enableOverlapGroupsOptions(IsEnabled=true);
bOpts = roadrunnerHDMapBuildOptions(EnableOverlapGroupsOptions=oOpts);
iOpts = roadrunnerHDMapImportOptions(BuildOptions=bOpts);
importScene(rrApp, "RoadFromOSM.rrhd", "RoadRunner HD Map", ImportOptions=iOpts);

% Then ask the user if this version looks better
choice = questdlg("Re-imported scene with overlap groups enabled. Does this look better at junctions/overpasses?", ...
    "Overlap groups", "Yes — keep this", "No — revert to disabled", "Yes — keep this");

Re-importing the scene only — keep the actor list and simulation settings untouched (Rule 12: minimum-change reruns).

Option B: Import a pre-existing scene file

IMPORTANT: The .rrscene file must be inside the RoadRunner project's Scenes/ folder. If the user's scene file is located elsewhere, copy it into the project first:

% Copy scene file into the RoadRunner project if it's external
scenesDir = fullfile(rrProjectPath, "Scenes");
if ~isfolder(scenesDir)
    mkdir(scenesDir);
end
[~, sceneName, sceneExt] = fileparts(userSceneFile);
destScene = fullfile(scenesDir, sceneName + sceneExt);
if ~isfile(destScene)
    copyfile(userSceneFile, destScene);
end

% Now export using the scene inside the project
exportToRoadRunner(egoTrajectory, rrApp, ...
    RoadRunnerScene=destScene, Name="Ego", SetupSimulation=true);
Option B.1: Pre-built scene + GPS (HERE HD, OpenDRIVE, or any externally-built .rrscene)

When the user supplies a .rrscene that was not built from their GPS (e.g. a HERE HD export, a vendor OpenDRIVE scene, a manually-authored RoadRunner scene), you cannot get localOrigin from roadprops and getRoadRunnerHDMap(rrApp) does not exist. Use this pattern instead:

%% (1) Open the scene in RoadRunner (after copying into project Scenes/)
openScene(rrApp, sceneName + sceneExt);

%% (2) Read the scene's geo-anchor — this is the LocalOrigin for trajectories.
%   - worldSettings(rrApp).WorldOrigin gives [lat lon] (reduced precision, ~5 dp)
%   - For full precision, parse latitude_of_origin / central_meridian from the
%     Projection WKT string. Always do this for HERE HD / OpenDRIVE scenes —
%     the 5-dp WorldOrigin can be off by several meters at the equator.
ws   = worldSettings(rrApp);
proj = ws.Projection;
latStr = regexp(proj, 'latitude_of_origin",([\-\d\.]+)', "tokens", "once");
lonStr = regexp(proj, 'central_meridian",([\-\d\.]+)',   "tokens", "once");
localOrigin = [str2double(latStr{1}), str2double(lonStr{1}), 0];

%% (3) Build trajectory using the scene's anchor
egoTrajectory = trajectory(gpsData, "LocalOrigin", localOrigin);
smooth(egoTrajectory);

%% (4) HEREHD/OpenDRIVE scenes have terrain — apply height correction.
%   Even when GPS altitude is non-zero, surveyed altitude (geoid) and the
%   scene's terrain (often EGM96 or a local datum) can disagree by several
%   meters. Run adjustHeight unconditionally for terrain-aware scenes — it is
%   a no-op when alignment is already good. See workflow-07-height-correction.md.
rrhdOut = fullfile(tempdir, "scene_export.rrhd");
exportScene(rrApp, rrhdOut, "RoadRunner HD Map");
rrMap = roadrunnerHDMap;
read(rrMap, rrhdOut);                 % NOTE: positional ctor errors — must use read()
adjustHeight(egoTrajectory, rrMap);

%% (5) Export ego with the in-project scene file
exportToRoadRunner(egoTrajectory, rrApp, ...
    RoadRunnerScene=destScene, Name="Ego", SetupSimulation=true);

Why these calls are different from Option A:

  • getRoadRunnerHDMap(rrApp) — does NOT exist. getRoadRunnerHDMap only takes a drivingScenario. To get a roadrunnerHDMap from a scene already loaded in rrApp, export the HD map to a .rrhd first, then read() it into a fresh roadrunnerHDMap.

  • roadrunnerHDMap(file) — does NOT accept a positional file path. Must construct empty (rrMap = roadrunnerHDMap) then call read(rrMap, file).

  • The exported .rrhd's GeoReference property is [0 0] (does not carry the scene's anchor). Always pull LocalOrigin from worldSettings(rrApp), not from rrMap.GeoReference.

Option C: Download from HERE HD Live Map (requires HERE HD credentials)
% Requires valid HERE HD Live Map credentials configured in MATLAB
% User must have a HERE account and provide credentials
scenario = exportToDrivingScenario(egoTrajectory, ...
    RoadNetworkSource="HEREHDLiveMap", ...
    GeoCoordinates=[minLat minLon maxLat maxLon], Name="Ego");

Note: Ask the user for their HERE HD credentials before attempting this option. They must be configured via hereHDLMCredentials in MATLAB.

Option D: Import from OpenDRIVE file
scenario = exportToDrivingScenario(egoTrajectory, ...
    RoadNetworkSource="OpenDRIVE", FileName="road_network.xodr", Name="Ego");

Step 2.5: Multi-GPS / multi-GNSS — pick one with a comparison video gate

When the dataset contains more than one ego position series — common variants include GPSData_Raw/GPSData_Corrected, GPS/GNSS, or two GNSS receivers with different fix qualities — do not silently pick one. Field names are not always trustworthy ("Corrected" can mean smoothed, RTK-corrected, lever-arm-compensated, dead-reckoned, etc.) and "best" depends on what the scene needs.

Hard rule — show the user a comparison video before asking which to use. This mirrors the lane-localization compare-then-ask gate (workflow-08 Step 4): the user must see all candidates simulated against the same scene before they can make a sensible call. Do not pick by smaller offset, smaller residual, or any quantitative heuristic — visual judgement against the actual road wins every time.

%% Build one scenariobuilder.Trajectory per GPS series (same LocalOrigin)
egoA = trajectory(gpsA, "LocalOrigin", localOrigin); smooth(egoA);
egoB = trajectory(gpsB, "LocalOrigin", localOrigin); smooth(egoB);
% (height correction if scene has terrain — see Option B.1 step 4)

%% Stage them side-by-side in a fresh scenario — red and green only when 2 series.
% For 3+ series, use distinct named colors (red/green/blue/yellow) and
% always burn the legend onto the right-side panel.
%
% HARD RULE — export-order matters. RoadRunner assigns ActorID in
% exportToRoadRunner call order (1st → ID=1). setCameraMode(..., "follow",
% FocusActorID=1) tracks whichever actor was exported first. Pick the
% series whose perspective matters MOST (usually the "Corrected" or
% RTK-quality one) and export it FIRST so the follow-cam locks onto it.
% Inverting silently produces a video that follows the wrong GPS series.
newScenario(rrApp);
exportToRoadRunner(egoA, rrApp, ...
    RoadRunnerScene=destScene, Name="Ego_A", Color="red",   SetupSimulation=true);
exportToRoadRunner(egoB, rrApp, ...
    Name="Ego_B", Color="green", SetupSimulation=false);

%% Disable collision fail and simulate (follow camera on Ego_A so both stay in frame)
% SetupSimulation=true creates a sim object — delete it before simulateScenario
rrSim = createSimulation(rrApp); delete(rrSim); clear rrSim;
rootPhase = roadrunnerAPI(rrApp).Scenario.PhaseLogic.RootPhase;
rootPhase.setFailCondition("DurationCondition").Duration = egoA.Duration + 10;
setCameraMode(rrApp, "follow", FocusActorID=1);
simulateScenario(rrApp, EnableLogging=true);

%% Export the follow-cam video; build raw-camera-on-left / RR-on-right composite
%  with red/green legend burnt in (same composition as workflow-08 Step 4).
%  Save to dataDir, popup ONLY for opening the video.
gpsCompareOut = fullfile(dataDir, "ego_gps_compare_labeled.mp4");
% ... (use the same composer used in workflow-08 Step 4) ...

ans1 = questdlg(sprintf("GPS-source compare video saved to:\n%s\n\nOpen it now?", gpsCompareOut), ...
    "Open GPS-source compare video", "Yes", "No", "Yes");
if strcmp(ans1, "Yes"), openFile(gpsCompareOut); end  % cross-platform; scripts/openFile.m

%% AFTER the popup: ask the user in the chat channel which GPS series to use.
%  Only after they pick do you proceed to lane localization (workflow-08) or
%  scenario generation. Never pick automatically.

When NOT to apply this gate:

  • Only one GPS/GNSS series exists in the dataset — pick it without ceremony.

  • The user explicitly told you which series to use up-front (stated as a default for this dataset, not as a permanent preference).

When to apply 3-way:

  • If the dataset has two GPS series and lane localization will run, the simplest sequencing is: (1) GPS compare-and-pick (this gate), (2) lane-localization compare-and-pick (workflow-08 Step 4). Two separate gates, two separate questions. Don't try to combine into one 3-way video — users can't compare three trajectories at a glance.

Step 3: Export ego trajectory

% Export with a scene file — sets World Origin from LocalOrigin, creates simulation
exportToRoadRunner(egoTrajectory, rrApp, ...
    RoadRunnerScene="myScene.rrscene", Name="Ego", SetupSimulation=true);

Step 4: Export non-ego actor trajectories

Always smooth actor trajectories before exporting — raw track data is often noisy/jittery.

Vehicle classification (Workflow 18 — automatic). Before the export loop, run the vehicle classification pipeline from workflow-18-vehicle-classification.md. When camera intrinsics + CameraHeight + actor tracks are all present, the agent:

  1. Calls buildBestCropGrid (in scripts/) to project tracks → image crops → composite grid

  2. Views the grid and classifies each track by color + type

  3. Maps type → AssetPath (e.g. "Vehicles/SUV.fbx") and color → RR named color

If prerequisites are missing (no intrinsics, no camera, text-only agent), the classification step is skipped and actors export with the default AssetPath="Vehicles/Sedan.fbx", Color="auto".

Use classified Color/AssetPath when available; otherwise Color="auto". Reserve named colors ("red", "green", "blue") for ego comparison scenarios (raw vs localized, GPS_A vs GPS_B) where colour carries meaning.

Flatten waypoint Z to 0 on flat scenes (OSM, OpenDRIVE without elevation, Pandaset). actorInfo.Waypoints carries each actor's Z in world frame, but for scenes that lack terrain elevation the per-track Z is dominated by the sensor mount offset (camera height ≈ 1–1.5 m), not the road surface. Without flattening, every actor floats ~1 m above the road. Flatten only on flat scenes — on terrain-aware scenes (HERE HD, Zenrin, OpenDRIVE+CRG, point-cloud-derived rrhd) keep Z and apply the terrain offset described below.

Terrain-aware actor Z offset (HERE HD, Zenrin, point-cloud scenes). On terrain-aware scenes, do NOT flatten Z to 0 and do NOT use raw Z as-is. The ego actor (SetupSimulation=true) is snapped to the terrain surface by RoadRunner's simulation engine, but non-ego actors (SetupSimulation=false) render at their exact CSV Z value — which is typically 0.5–1.0 m below the rendered terrain mesh. This causes actors to clip into or disappear below the road surface.

Fix: Add +0.75 m (half the standard sedan height) to non-ego actor Z before export. This compensates for the terrain mesh sitting above the HD map geometry:

wp(:,3) = wp(:,3) + 0.75;  % terrain compensation — actors render ON road surface

This offset was empirically validated: Z+0 → actors invisible below terrain; Z+0.75 → actors sit naturally on road; Z+3/+5 → actors float above road.

%% Vehicle classification — Workflow 18 (self-gating)
% Run buildBestCropGrid → agent views grid → classifies → builds map.
% Full pipeline: references/workflow-18-vehicle-classification.md
classificationMap = containers.Map();  % populated below by agent vision pass
hasIntrinsics = exist('intrinsics','var') && isfield(intrinsics,'fx') && exist('camHeight','var');
if hasIntrinsics && trackData.NumSamples > 0 && cameraData.NumSamples > 0
    addpath(fullfile(fileparts(mfilename("fullpath")), "scripts"));
    classifyDir = fullfile(dataDir, "classification");
    [gridPath, cropManifest] = buildBestCropGrid(trackData, cameraData, ...
        intrinsics, camHeight, classifyDir);
    fprintf("Classification grid: %s (%d tracks)\n", gridPath, height(cropManifest));
    % >>> AGENT: View gridPath. Classify each labeled track by color + type.
    % >>> Build classificationMap entries per workflow-18 Step 2–4.
end

%% Export non-ego actors with classified assets
% SetupSimulation=false for non-ego actors to avoid overwriting simulation params
egoLocalOrigin = egoTrajectory.LocalOrigin;
isTerrainScene = true;  % Set false for flat OSM scenes — see "How to tell" note below
for i = 1:height(actorInfo)
    wp = actorInfo.Waypoints{i};
    if isTerrainScene
        wp(:,3) = wp(:,3) + 0.75;  % terrain compensation for non-ego actors
    else
        wp(:,3) = 0;               % flat scene — zero Z
    end
    % HARD RULE — do NOT pass Orientation= for non-ego actors. actorprops yaw
    % uses a compass convention (~340° for northbound) incompatible with RR's
    % world frame; RR auto-derives heading from waypoint deltas correctly.
    % Also do NOT pass Speed= — it is not a valid Trajectory N-V argument.
    actorTraj = scenariobuilder.Trajectory( ...
        actorInfo.Time{i}, wp, ...
        Name=actorInfo.TrackID(i), LocalOrigin=egoLocalOrigin);
    smooth(actorTraj);  % Remove noise from actor tracks

    tid = char(actorInfo.TrackID(i));
    if isKey(classificationMap, tid)
        cls = classificationMap(tid);
        exportToRoadRunner(actorTraj, rrApp, ...
            Name=actorInfo.TrackID(i), AssetPath=cls.AssetPath, ...
            Color=cls.Color, SetupSimulation=false);
    else
        exportToRoadRunner(actorTraj, rrApp, ...
            Name=actorInfo.TrackID(i), Color="auto", SetupSimulation=false);
    end
end

How to tell if the scene is "flat": read the rrhd's geometry Z range. On OSM-derived rrhd, range(geometryZ) < ~2 m (essentially noise from getRoadRunnerHDMap). On terrain-aware rrhd it spans tens to hundreds of metres along the route. Compare against range(gpsData.Altitude) — if both are near zero, flatten; if the rrhd has real elevation, run adjustHeight on ego instead.

Ego Z on flat scenes: The ego trajectory also needs Z=0 on flat OSM scenes — GPS altitude puts ego 30–50 m above the road. When rebuilding the ego Trajectory with Z=0 after localization, MUST preserve Orientation=localizedTrajectory.Orientation — without it, heading is re-derived from waypoints and loses the lane-aligned orientation from localizeEgoUsingLanes. Full pattern in osm-flat-scene-gotchas.md Gotcha 1.

exportToRoadRunner Name-Value Arguments

ArgumentDescriptionDefault
NameVehicle name in RoadRunner"auto"
ColorActor color"auto"
AssetPathActor asset path (relative to Assets/ or absolute)"auto"
SetupSimulationCreate simulation with time/step from trajectorytrue
RoadRunnerScene.rrscene or .rrhd file to import[]

Vehicle Assets

Vehicle assets are located at <rrProjectPath>/Assets/Vehicles/. To use a specific vehicle asset:

% List available vehicle assets
assetDir = fullfile(rrProjectPath,"Assets","Vehicles");
dir(assetDir)

% Use a specific asset when exporting
exportToRoadRunner(egoTrajectory, rrApp, ...
    AssetPath="Vehicles/Sedan.fbx", Name="Ego");

Asset filename casing matters. RR ships filenames with TitleCase stems (Sedan.fbx, Suv.fbx, Hatchback.fbx, Box_Truck.fbx). Lowercase forms (sedan.fbx) silently resolve to white placeholder boxes on Windows because <PROJECT>/Assets/... resolution is case-sensitive even on case-insensitive filesystems. Confirm spelling with dir(fullfile(rrProjectPath,"Assets","Vehicles")) before composing AssetPath.

Post-Export Asset Replacement via API

When modifying actor assets after exportToRoadRunner (e.g., applying classification results iteratively), use <PROJECT>/ relative paths — never absolute paths:

rrAPI = roadrunnerAPI(rrApp);
allActors = rrAPI.Scenario.Actors;  % index 1 = ego
allActors(k).ActorAsset = "<PROJECT>/Assets/Vehicles/Suv.fbx_rrx";
allActors(k).Color = [0.15 0.15 0.15 1];  % [R G B A] normalized

CRITICAL: Absolute paths silently break the asset reference — actors render as white placeholder boxes. The <PROJECT>/ prefix is required for RoadRunner to resolve the asset correctly.

Running the Scenario Simulation

After exporting ego + actors, run the simulation in RoadRunner.

Duration-Aware Simulation & Actor Count Caps

Long datasets (>30s) and dense actor lists (>15 actors) cause MCP timeouts. Use these caps:

simDuration = egoTrajectory.Duration;
if simDuration <= 30
    nExportActors = height(actorInfo);       % all actors
else
    nExportActors = min(15, height(actorInfo)); % top 15 by Age for validation
    % Sort by track age (longest-lived actors are most important)
    [~, ageOrder] = sort(actorInfo.Age, "descend");
    actorInfo = actorInfo(ageOrder(1:nExportActors), :);
end

For datasets >30s: validate at 30s first (confirm Y-axis, roads, actors look correct), then trigger the full-duration simulation. If the full sim times out via MCP, the simulation continues in RoadRunner — export the video in the next MCP call:

% After timeout: sim is still running in RR — wait for it
pause(2);  % RR finishes async
exportVideo(rrApp, VideoFolder=simVideoFolder, FileName="sim_frontCamera", VideoResolution="HD");

MANDATORY PRE-FLIGHT CHECKLIST — verify ALL before proceeding:

  • [ ] Actor-aware OSM buffer used (getMapROI(..., Extent=buffer))

  • [ ] GPS vs Trajectory plot shown to user

  • [ ] BEV+Camera or track-overlay video saved to disk

  • [ ] Collision fail condition disabled (setFailCondition("DurationCondition"))

  • [ ] simulateScenario(rrApp, EnableLogging=true) — NOT without EnableLogging

  • [ ] exportVideo called after simulation

  • [ ] Side-by-side comparison video (input vs sim) created and saved

CRITICAL — Always disable collision fail condition: RoadRunner's default fail condition stops the simulation when vehicles collide. Since recorded data often has close passes that trigger this, you MUST always replace it with a duration-based condition. Never skip this step.

% MANDATORY: Delete stale sim object from SetupSimulation=true, then set fail condition
rrSim = createSimulation(rrApp); delete(rrSim); clear rrSim;
rootPhase = roadrunnerAPI(rrApp).Scenario.PhaseLogic.RootPhase;
failCond = rootPhase.setFailCondition("DurationCondition");
failCond.Duration = egoTrajectory.Duration + 10;  % ensure it won't trigger

% Set camera to front (dashcam) view — shows ego perspective with all actors visible ahead.
% Available modes: "default", "front", "follow", "orbit"
%   "front"  — dashcam view from ego looking forward (recommended for driving-log-like replay)
%   "follow" — third-person chase camera behind ego (FollowHeight, FollowDistance adjustable)
%   "orbit"  — free-rotating overview of entire scene
setCameraMode(rrApp, "front", FocusActorID=1);

% HARD RULE — do NOT pass Pacing= here. Pacing throttles real-time playback
% but truncates/desyncs the EnableLogging capture for long clips. For full-
% length recording, omit Pacing entirely and let the simulation run as fast
% as the host allows.
% EnableLogging=true is REQUIRED for exportVideo to work after simulation
simulateScenario(rrApp, EnableLogging=true);

Export Simulation Video and Create Comparison

After simulation completes, export the simulation video and create a side-by-side comparison with the input camera video.

Step 1: Export simulation video (uses exportVideo — available in R2026a+):

%% Export simulation video from RoadRunner
% exportVideo takes ONLY name-value pairs (rrApp is positional). Passing the
% output path positionally — `exportVideo(rrApp, fullPath)` — errors with
% "Name-value pair arguments require a name followed by a value."
% FileName must be a STEM (no extension) — exportVideo appends .mp4 / .avi.
%
% HARD RULE — VideoFolder must be a path WITHOUT SPACES. RR's video exporter
% silently fails (or returns "permission denied") when the target path
% contains spaces — common on Windows OneDrive paths like
% "C:\Users\<user>\OneDrive - <Organization>\...". If dataDir contains a space,
% redirect VideoFolder to a no-space staging folder (e.g. tempdir or C:\rr_out)
% and copyfile the result back to dataDir afterwards.
simVideoFolder = dataDir;  % Always save in dataset folder
if contains(simVideoFolder, " ")
    simVideoFolder = fullfile(tempdir, "rr_video_export");
    if ~isfolder(simVideoFolder); mkdir(simVideoFolder); end
end
exportVideo(rrApp, VideoFolder=simVideoFolder, FileName="sim_frontCamera", VideoResolution="HD");

% exportVideo may save as .avi depending on codecs — always convert to .mp4
simVideoPath = fullfile(simVideoFolder, "sim_frontCamera.mp4");
aviPath = fullfile(simVideoFolder, "sim_frontCamera.avi");
if ~isfile(simVideoPath) && isfile(aviPath)
    aviReader = VideoReader(aviPath);
    mp4Writer = VideoWriter(simVideoPath, "MPEG-4");
    mp4Writer.FrameRate = aviReader.FrameRate;
    open(mp4Writer);
    while hasFrame(aviReader)
        writeVideo(mp4Writer, readFrame(aviReader));
    end
    close(mp4Writer);
    delete(aviPath);
end

Step 2: Create side-by-side comparison (Input Camera | Simulation). Three requirements are MANDATORY and have failed in past evals when omitted — none of them are optional:

  1. insertText timestamp burn on BOTH halves — every frame of the comparison video must have a black-box label like Input Camera (t=X.Xs) on the left half and RoadRunner Sim (t=X.Xs) on the right half. The user uses these to spot drift between the recorded drive and the simulation; without them the video is much harder to interpret. Skipping insertText (just concatenating raw frames) FAILS the comparison-video assertion, even when the rest of the composition is correct.

  2. questdlg + openFile popup AFTER close(vw) — the comparison video is NOT exempt from the saved-video popup HARD RULE. Every script that writes a comparison MP4 must immediately follow close(vw) with the questdlg("...saved to...\n\nOpen it now?", "Yes", "No") block whose Yes branch calls openFile(compVideoFile). fprintf("Saved: %s", ...) alone is not acceptable.

  3. Defensive field-name inspect on the input recording's struct/MAT — datasets vary in case (Timestamp vs timestamp, ImagePath vs imagePath, TrackID vs trackID). Before referencing fields off the loaded S = load(...) struct or any table, print disp(fieldnames(S)) (or disp(S.<Sensor>.Properties.VariableNames') for a table) and use the discovered name. Hard-coding a guess produces a silent Reference to non-existent field mid-script and the comparison video never gets built.

%% Side-by-side comparison video: Input (track-overlaid) vs Simulation
% PREFERRED: compose overlay on-the-fly from original frames to avoid
% double-compression blur (encode trackOverlay.mp4 → decode → resize → re-encode).
% Reading the pre-saved overlay video introduces generation loss.
simReader = VideoReader(simVideoPath);

targetH = 540; targetW = 960;
maxFrames = 100;  % side-by-side = heavier I/O per frame
step = max(1, ceil(cameraData.NumSamples / maxFrames));
frameIndices = 1:step:cameraData.NumSamples;
effectiveFPS = numel(frameIndices) / cameraData.Duration;

% Determine left-panel mode (same priority as Rule 2)
hasIntrinsics = ~isempty(cameraData.SensorParameters) ...
    && isstruct(cameraData.SensorParameters) ...
    && isfield(cameraData.SensorParameters, 'Intrinsics');
hasTracks = exist('trackData','var') && ~isempty(trackData.UniqueTrackIDs);

compVideoFile = fullfile(dataDir, "comparison_input_vs_sim.mp4");
vidWriter = VideoWriter(compVideoFile, "MPEG-4");
vidWriter.FrameRate = round(effectiveFPS);
open(vidWriter);

for frameIdx = frameIndices
    % Left panel: overlay from original frames (no double-compression)
    img = imread(cameraData.Frames(frameIdx));
    if hasIntrinsics && hasTracks
        imgL = plotActorCircles(img, frameIdx, cameraData, trackData, intrinsics);
    else
        imgL = img;
    end
    imgL = imresize(imgL, [targetH targetW]);
    t = cameraData.Timestamps(frameIdx);
    imgL = insertText(imgL, [10 10], sprintf("Input+Tracks (t=%.1fs)", t), ...
        FontSize=20, BoxColor="black", TextColor="white", BoxOpacity=0.6);

    % Right panel: RR sim
    simReader.CurrentTime = min(t, simReader.Duration - 0.01);
    imgR = readFrame(simReader);
    imgR = imresize(imgR, [targetH targetW]);
    imgR = insertText(imgR, [10 10], sprintf("RR Sim Front (t=%.1fs)", t), ...
        FontSize=20, BoxColor="black", TextColor="white", BoxOpacity=0.6);

    writeVideo(vidWriter, [imgL, imgR]);
end
close(vidWriter);

%% MATLAB popup
answer = questdlg(sprintf("Comparison video (Input vs Simulation) saved to:\n%s\n\nWould you like to open it?", ...
    compVideoFile), "Comparison Video", "Yes", "No", "Yes");
if strcmp(answer, "Yes")
    openFile(compVideoFile);
end

Alternative: Export to drivingScenario

scenario = exportToDrivingScenario(egoTrajectory, ...
    Name="Ego", RoadNetworkSource="OpenStreetMap", FileName=osmFile);

Alternative: Export to ASAM OpenSCENARIO & OpenDRIVE (via RoadRunner)

Always export OpenSCENARIO and OpenDRIVE from RoadRunner — it produces higher-accuracy geometry than the drivingScenario export path.

% After building the scene in RoadRunner (exportToRoadRunner with SetupSimulation=true):
outDir = fullfile(tempdir, "exports");
if ~isfolder(outDir), mkdir(outDir); end

% Export OpenSCENARIO XML (automatically generates paired OpenDRIVE)
% NOTE: For "OpenSCENARIO XML", the options class is openScenarioXMLExportOptions.
% Using openScenarioExportOptions here is REJECTED by exportScenario with:
%   "Invalid export options object specified for 'OpenSCENARIO XML' format."
% Use openScenarioDSLExportOptions for "OpenSCENARIO DSL".
xoscFile = fullfile(outDir, "scenario.xosc");
xoscOpts = openScenarioXMLExportOptions( ...
    OpenScenarioVersion=1.1, ...
    OpenDriveOptions=openDriveExportOptions(ExportSignals=true, ExportObjects=true));
exportScenario(rrApp, xoscFile, "OpenSCENARIO XML", xoscOpts);

% Or export OpenDRIVE only (scene/road network without scenario logic)
xodrFile = fullfile(outDir, "road_network.xodr");
xodrOpts = openDriveExportOptions(ExportSignals=true, ExportObjects=true);
exportScene(rrApp, xodrFile, "OpenDRIVE", xodrOpts);

Note: exportScenario generates both .xosc and a paired .xodr automatically. Use openScenarioExportOptions to control version (OpenScenarioVersion), OpenDRIVE file name, and catalog options. The RoadRunner path preserves full lane geometry, elevation, and signal placement that may be simplified in the drivingScenario export path.

For full API details of both export methods, read trajectory-api.md.


Copyright 2026 The MathWorks, Inc.


Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

What our scanner noticed· Low risk

  • Phrasing that asks for actions without confirmation

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
MathWorks
Publisher
MathWorks (Official)
Source
matlab/matlab-agentic-toolkit · skills-catalog/automotive/matlab-use-scenario-builder · commit 2a7f215
Licence
BSD 2-Clause License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE.md)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by MathWorks.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download matlab-use-scenario-builder.zip