# SimForge documentation > SimForge is a driving simulator for autonomous-vehicle research: scenarios in SimForge Worlds (real-world HD maps with their digital twins), closed-loop simulation with reactive traffic, 2D, schematic and photoreal 3D sensor rendering with the Helios Engine, Recast (a recorded drive turned into an editable scenario), and SimForge Gym, a Gymnasium environment for reinforcement learning. One command line, `simforge`, runs everything on your machine, or on SimForge's hosted cloud with `--cloud`. # SimForge docs > Install the simforge CLI, download the runtime and a map, render a scenario in 2D, schematic or 3D, train a policy with reinforcement learning, and run the same commands in the cloud. Source: https://simforge.ai/docs SimForge simulates driving scenarios in SimForge Worlds: real-world HD maps with their digital twins. Traffic reacts to your vehicle, the renderer produces camera images, and SimForge Gym, a Gymnasium environment, lets a learning policy drive. One command line, `simforge`, does all of it. ## How the pieces fit | Piece | What it is | Licence | |---|---|---| | `simforge` CLI | The one command line. It prints JSON, so people, scripts and AI agents use it the same way. | Apache-2.0 | | `simforge-rl` | SimForge Gym: the Python Gymnasium client for reinforcement learning. | Apache-2.0 | | Runtime | The simulation engine and the renderer the CLI drives. A separate download after you sign in. | Closed binary under PolyForm Noncommercial 1.0.0, downloaded under the SimForge account and download terms | | SimForge Worlds | Each World is an HD map of a real-world road network and its digital twin, the 3D environment the renderer draws. Downloaded after you sign in. | SimForge account and download terms: non-commercial, no redistribution | | Cloud | SimForge Cloud: the same commands with `--cloud`, run on SimForge's machines. New accounts start with $10 in free credits. | Paid, with a cost cap on every run | Commercial licences and questions about the license and terms: [contact@simforge.ai](mailto:contact@simforge.ai). Commands run on your machine by default. Add `--cloud` and the same command runs hosted. Nothing ever switches between the two on its own: a missing runtime or a refused cloud run is an error that names the fix. ## From install to first render The [quickstart](/docs/quickstart) does all of it in four steps. In more detail: 1. [Install the CLI](/docs/install). 2. [Sign in and install the runtime](/docs/sign-in). 3. [Download a map](/docs/maps). 4. [Author a scenario](/docs/scenarios) with typed operations. 5. [Render it](/docs/render) in 2D, schematic or 3D. Then [train a policy](/docs/rl) locally, or [run renders in the cloud](/docs/cloud). ## For AI agents Agents use the same CLI. Start at [For AI agents](/docs/agents): it lists the machine-readable command surface, the [skills pack](/docs/skills) for Claude Code, Codex and AGENTS.md agents, and [/llms.txt](/llms.txt). --- # Quickstart > From nothing to rendered frames on your own Linux machine - install the CLI, sign in, install the runtime and a map, and render a scenario in 3D and 2D. Source: https://simforge.ai/docs/quickstart This takes you from an empty machine to camera frames, instance-id, depth and semantic passes, and a video, rendered locally. Check [what the machine needs](/docs/install#what-the-machine-needs) first: Linux x86_64 (Ubuntu 24.04 or later), an Apple silicon Mac or Windows x64; for 3D an NVIDIA GPU with at least 8 GiB, or Apple M3 or newer. In Docker, start the container with the NVIDIA runtime's graphics capability ([Running in Docker](/docs/install#running-in-docker)); plain `--gpus all` cannot start Vulkan. ## 1. Install the CLI ```sh curl -fsSL https://simforge.ai/install.sh | sh ``` ## 2. Sign in, accept the agreement, install the runtime ```sh simforge login --device # prints a URL and a code; approve it in your browser simforge runtime license # read the agreement; nothing is accepted yet simforge runtime install --accept-license # accepts that exact version, then downloads the runtime simforge doctor # what is installed, and the fix for anything missing ``` Accepting the agreement is what entitles your account to the runtime and the maps. Every download is signed by SimForge: the CLI checks the signature and the sha256 of each archive before it installs anything. ## 3. A scenario and its map ```sh simforge maps pull easterbrook-discovery-school --accept-license simforge scenario new --map easterbrook-discovery-school --out s.json ``` Then add actors with [typed authoring operations](/docs/scenarios). A scenario from the SIMLOG-25k dataset works the same way: download its whole folder and pull the map it names. The SIMLOG-25k dataset is on Hugging Face: `huggingface.co/datasets/SIMFORGE-AI/simlog-25k`. ## 4. Render ```sh simforge render s.json --mode 3d --out frames # photoreal, one front camera simforge render s.json --mode 2d --out topdown # top-down video, no GPU needed ``` The 3D render uses one front camera on the ego: 1280x720 at 24 fps, the `showcase` preset, locally and with `--cloud`. [Preview and render](/docs/render) covers other rigs, presets, texture tiers and the [output format](/docs/render#output-format); add `--cloud --max-cost ` to render the same scenario on SimForge's GPUs instead. --- # Install the CLI > Install the simforge command line on Linux, macOS or Windows and check the machine with simforge doctor. Source: https://simforge.ai/docs/install `simforge` is a single binary for Linux, macOS and Windows. It contains no engine or renderer: those come with the runtime, a separate download after you [sign in](/docs/sign-in). Linux and macOS: ```sh curl -fsSL https://simforge.ai/install.sh | sh ``` Windows (PowerShell): ```powershell irm https://simforge.ai/install.ps1 | iex ``` Use these terminal installation commands. The current CLI binaries are not signed with a macOS Developer ID or Windows Authenticode certificate, so browser-downloaded, quarantined copies are not qualified for Gatekeeper or SmartScreen. Do not disable operating-system security or change PowerShell execution policy to run a downloaded installer file. The installer picks the build for your machine, downloads it, checks the archive's and the binary's sha256, and installs `simforge`: into `~/.local/bin` on Linux and macOS, into `%LOCALAPPDATA%\SimForge\bin` on Windows (set `SIMFORGE_INSTALL_DIR` to change either). It reads no credentials. `install.sh` edits no shell files: when `~/.local/bin` is not on your `PATH` it prints the line to add (for zsh, the macOS default, to `~/.zshrc`). `install.ps1` adds its folder to your user `PATH`. When it finishes it prints the next steps: [sign in](/docs/sign-in), accept the agreement, install the runtime, and install `ffmpeg`, which renders need to write their videos (`sudo apt-get install ffmpeg` on Ubuntu, `brew install ffmpeg` on macOS, `winget install Gyan.FFmpeg` on Windows). ## Check the install ```sh simforge --version ``` Then [sign in](/docs/sign-in). From there, two paths: - **Cloud only.** Add `--cloud` to commands and they run on SimForge's machines; this machine needs no runtime, GPU or ffmpeg. `simforge auth status` checks the sign-in. See [Run in the cloud](/docs/cloud). - **On this machine.** [Install the runtime](/docs/sign-in#install-the-runtime), then check the machine with `simforge doctor`. To pin a published runtime release, use `simforge runtime install --version VERSION --accept-license`. This selects the runtime version; `simforge --version` reports the CLI version. ## Check the machine ```sh simforge doctor ``` `simforge doctor` runs in the runtime, so it needs the runtime installed; before that it stops with `runtime_not_installed` and says what to do. It checks the machine: the GPU adapter, ffmpeg, the cache folders, free disk and whether the map registry can be reached. Each check reports `ok`, or what is missing and how to fix it. A machine without a GPU gets a warning, not a failure: simulation and 2D renders run on the CPU. Pass `--offline` to skip the network check; it is then reported as skipped, never as ok. ## What the machine needs | | Linux | macOS | Windows | |---|---|---|---| | CLI and `--cloud` | x86_64, glibc 2.39 or newer (Ubuntu 24.04 or later) | macOS 13 or later, Apple silicon or Intel | Windows 10 (1803) or 11, x64 or Arm64 | | Local runtime: simulation, 2D | x86_64 | Apple silicon or Intel | x64 | | Local 3D and schematic | NVIDIA, Vulkan, 8 GiB, driver 570 or newer | Apple M3 or newer with 16 GB of memory or more | NVIDIA, Vulkan, 8 GiB, driver 570 or newer | - On Windows on Arm the CLI runs every command with `--cloud`; a local command stops with `runtime_platform_unsupported` and says so. An Intel Mac runs simulation and 2D renders locally and 3D with `--cloud`. - Outside the 3D column, render commands stop with `gpu_unsupported`, `gpu_memory_unsupported` or `gpu_driver_unsupported` before any work. An Apple silicon Mac gives three quarters of its memory to the GPU, so a 16 GB Mac renders with 512 px textures and 24 GB or more with full textures. 2D rendering and simulation run on the CPU everywhere. - Video encodes with `ffmpeg` (libx264) on macOS and Windows; the NVIDIA hardware encoder is used on Linux. GPU lidar needs Vulkan ray queries, so on macOS lidar uses the CPU reference. - `ffmpeg` with libx264 on the `PATH` to encode videos: `sudo apt-get install ffmpeg` (Ubuntu), `brew install ffmpeg` (macOS), `winget install Gyan.FFmpeg` (Windows), or point `SIMFORGE_FFMPEG_BINARY` at one. `simforge doctor` checks it and prints the command; without it renders still write their frames and warn `video_encoder_missing`. - Disk for the runtime and the maps you download. The runtime is about 2.7 GB and a map with its 3D environment 5–19 GB. ## Running in Docker Run the container with the NVIDIA container runtime and its graphics capability, and install the EGL and OpenGL libraries in the image: ```sh docker run --runtime nvidia -e NVIDIA_DRIVER_CAPABILITIES=graphics,compute,utility ... apt-get install -y libegl1 libgl1 # in the image ``` Plain `--gpus all` exposes the GPU for compute only: Vulkan cannot start, and 3D and schematic renders fail with `render_failed`. 2D renders and simulation need no GPU in the container. ## Remove it Run `simforge logout` first if you also want to sign this machine out and delete its stored tokens. Linux and macOS: ```sh curl -fsSL https://simforge.ai/install.sh | sh -s -- --uninstall ``` Windows (PowerShell): ```powershell & ([scriptblock]::Create((irm https://simforge.ai/install.ps1))) -Uninstall ``` Use the same `SIMFORGE_INSTALL_DIR` if you customized it. The uninstall removes only the CLI and its previous binary, not the runtime, maps, credentials or unrelated files. Windows removes its user PATH entry only if the installer originally added it; pre-existing PATH entries and the registry value's type remain unchanged. Reinstall before uninstalling a Windows installation that predates the install receipt. The PowerShell commands run the script in memory; they do not require changing execution policy. On macOS and Linux, remove any PATH line you added yourself if you no longer need it. --- # Sign in and install the runtime > Sign in with simforge login, accept the runtime's PolyForm Noncommercial license and SimForge's account and download terms, and install the closed runtime that simulates and renders. Source: https://simforge.ai/docs/sign-in Downloads need a SimForge account: the runtime and the maps are tied to the account that accepted their agreement. Create one at [simforge.ai](https://simforge.ai) with your email, or continue with Google or GitHub, then sign in from the CLI. Downloads and cloud runs need a verified email: open the link SimForge emails you after sign-up (Google and GitHub accounts are already verified). To get a new link, sign in at [simforge.ai/auth/verify-email](https://simforge.ai/auth/verify-email) and resend it. ## Sign in ```sh simforge login # opens a browser to approve this machine simforge login --device # no browser here (SSH, CI): prints a URL and a short code simforge whoami # account, organization, workspace and scopes ``` `login` waits until you approve the request in a browser. The CLI keeps its tokens in the operating system's keychain, or in a file only you can read when there is no keychain; the output says which and why. `simforge auth status` checks the login with the host without printing any token: `tokenValid: true` means the host accepted it, and `emailVerification` says separately whether your email is verified (with the page that resends the link when it is not). A login acts in the workspace of the organization you chose when approving it. Sign in again to switch. `simforge logout` revokes this machine's session and deletes its tokens. ## Read and accept the license and terms The runtime is licensed under the [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0). Your SimForge account, downloads and the maps are covered by the SimForge account and download terms. You accept both together. Read the current version first: ```sh simforge runtime license ``` In short: PolyForm Noncommercial lets you use, change and pass on the runtime for noncommercial purposes as it defines them (personal use, and use by noncommercial organizations such as universities, charities and public research bodies), keeping its license and the `Required Notice:` line (both ship in the runtime's `share/licenses/`). The account and download terms require signing in to download from SimForge and make [usage telemetry](/docs/telemetry) a condition of using SimForge's services and downloads. The maps are free for non-commercial use under the SimForge account and download terms and may not be redistributed. Commercial use needs a licence from SimForge. For a commercial licence or questions about the license and terms, write to [contact@simforge.ai](mailto:contact@simforge.ai). The text `runtime license` prints is what you accept; this summary is not. ## Install the runtime ```sh simforge runtime install --accept-license simforge runtime status ``` `--accept-license` accepts exactly the agreement version and digest the host serves now; without it the install stops with `agreement_acceptance_required`. The runtime is about 2.7 GB; the download prints no progress, is checked against its sha256 and only then installed. `runtime status` reports the installed runtime's version, protocol and capabilities (for example `capabilities.renderModes`). Without a runtime, local commands fail with `runtime_not_installed`. Commands with `--cloud` do not need it. ## Errors you may see | Code | Meaning | Do | |---|---|---| | `not_logged_in` | No login for this host | `simforge login` | | `session_invalid` | The login expired or was revoked | `simforge login` again | | `email_unverified` | The account's email is not verified yet | Open the link in your email, or resend it from the page the hint names, then retry | | `agreement_unavailable` | This host publishes no agreement for downloads yet | Check the host (`simforge.ai` by default) | | `agreement_acceptance_required` | The install was not told to accept the agreement | Read `simforge runtime license`, then add `--accept-license` | | `entitlement_required` | This download is not available to your account | Pick another map or version; do not retry | --- # SimForge Worlds and assets > Download SimForge Worlds (the public maps) with simforge maps pull, fetch the actor models, and manage the local cache. Source: https://simforge.ai/docs/maps SimForge Worlds are where scenarios happen. Each World is an HD map of a real-world road network (lanes, signals, the traffic network) with its digital twin, the 3D environment the renderer draws. The CLI calls a World a map. Scenarios are built on an installed map, and the installed version is the one they pin. ## Download a map ```sh simforge maps pull easterbrook-discovery-school@v9 --accept-license ``` `maps pull` downloads a map your signed-in account is entitled to, checks the download against its sha256, and installs it into the local maps cache. Pin a version with `name@version`; without one you get the current release. `--accept-license` accepts the current agreement, as for the runtime. `--cache-root DIR` puts the cache somewhere else. An interrupted download resumes when you run the command again. Accepting the agreement is all a public map needs: the first download entitles your workspace. The public maps are `richmond-field-station`, `easterbrook-discovery-school`, `el-camino-road`, `belmont-research-center`, `yale-street`, `san-ramon-phase-1` and `di-rosa-sf`. A map that is not public fails with `entitlement_required` (pick another map; retrying does not help), and one an operator withdrew from your workspace with `entitlement_revoked`. Every map download is signed, and the CLI refuses an unsigned or untrusted one (`signature_*`). A map is 5–19 GB and the download prints no progress. Pulling a map again downloads it again, so pull each version once. After the download, commands take the map's name (`easterbrook-discovery-school`), not `name@version`. ## Maps in the cloud ```sh simforge maps list simforge maps show ``` These list the hosted maps your workspace can use with `--cloud`. ## Actor models ```sh simforge assets pull ``` `assets pull` downloads the actor models the renderer draws (cars, people, props) and their `ATTRIBUTION.json`, checking every file's sha256. The public actor set is the CARLA asset library under CC BY 4.0: keep the attribution file with anything you publish that shows them. ## When a map is missing A local command on a map that is not installed fails with `map_not_installed` and names the map. Pull it, then run the command again. `simforge doctor` shows the cache folders and their free space. --- # Author a scenario > Create a scenario on an installed map and change it with typed, atomic authoring operations instead of editing JSON by hand. Source: https://simforge.ai/docs/scenarios A scenario is a small JSON file: the map, the actors and their routes, what triggers what, the weather and the clip length. It holds no video or sensor data. Never write it by hand: create it on an installed map and change it with authoring operations, which check every change against the map. Scenario files use UTF-8, with or without a byte-order mark. Windows CRLF line endings are supported; save UTF-16 files as UTF-8 before running them. ## Find places on the map ```sh simforge locations find --map easterbrook-discovery-school --limit 20 simforge scenario actors ``` `locations find` returns real lanes and junctions with their lane address (`roadAnchor.rsl`, as `road:section:lane`) and distance along the lane (`s`). `scenario actors` lists the actors you can place, such as `vehicle.sedan`. ## Create and edit ```sh simforge scenario new --map easterbrook-discovery-school --out s.json simforge scenario apply s.json add-actor --id ego --catalog-id vehicle.sedan --lane-rsl --s 20 \ -- set-ego --role-id ego -- set-clip --clip-seconds 20 simforge scenario check s.json --strictness warnings --preview ``` - Operations are chained with `--`; the whole chain applies atomically or not at all. - `--expect-version` and `--expect-digest`, given right after the file and before the first operation, refuse a change when the file changed since you last read it. - `scenario check` validates the scenario on its map and exits 2 on findings; `--preview` also drives every actor on the engine. - `simforge scenario ops list` lists every operation: actors, ego, speed, lane changes, junction turns, routes, event timing, weather, clock and clip length. ## Packages A scenario package (`.scenario.zip`) carries a scenario with what it pins, for moving it between machines. ```sh simforge package inspect scenario.zip simforge package verify scenario.zip ``` `package verify` checks the manifest and every member's digest and exits 2 on a mismatch. Both need the runtime. > **Note.** `simforge package import` resolves a package's map from SimForge's internal registry; it does not read the maps you pulled. ## For agents The `simforge-authoring` skill in the [skills pack](/docs/skills) covers every operation, its arguments and the refusals, with worked examples. --- # Preview and render > Render a scenario as a 2D top-down video, a schematic view or photoreal 3D sensor data, on your machine or in the cloud. Source: https://simforge.ai/docs/render One renderer, the Helios Engine, in three modes: | Mode | What you get | Runs on | |---|---|---| | `2d` | Top-down map video of the scenario | CPU | | `schematic` | A clean schematic view through the rig's cameras | GPU | | `3d` | Photoreal camera images and sensor passes | GPU | ## Pick a sensor rig Schematic and 3D renders look through a rig: the cameras on the vehicle. Without `--rig` they use the `basic-dash-camera` rig on the ego, a front camera at the preset's capture (1280x720 at 24 fps for the default `showcase`); write your own to choose others. ```sh simforge rig init --preset basic-dash-camera --out rig.json simforge rig show rig.json ``` `rig init` writes the cameras at the `--profile` capture: `showcase` (1280x720 at 24 fps) unless you pass `--profile training` (512x384 at 20 fps). The basic dash camera's camera is `dashcam-front`. Other presets include `alpamayo-2cam`, `alpamayo-6cam`, `tesla-hw3` and `waymo-5th-gen`; `simforge rig init --help` lists them all, and the vehicle classes. ## Preview a frame ```sh simforge preview frame s.json --mode 2d --at 5 --out plan.png simforge preview frame s.json --mode 3d --rig rig.json --camera dashcam-front --at 5 --out frame.png simforge preview clip s.json --mode 3d --rig rig.json --camera dashcam-front --at 5 --seconds 3 --out clip.mp4 ``` ## Render ```sh simforge render s.json --mode 2d --out out-2d simforge render s.json --mode schematic --rig rig.json --out out-schematic simforge render s.json --mode 3d --rig rig.json --out out-3d ``` `--out` is a folder that must not exist or be empty. 3D renders take `--preset showcase` or `--preset training` (the dataset look at a fixed cost per frame); without `--preset`, a 3D render uses `showcase`, locally and with `--cloud`. They also take `--passes` to choose among `rgb`, `id`, `depth` and `semantic`. ## GPU memory and texture tiers How much GPU memory a 3D render needs depends on the map and the texture tier much more than on the cameras. The CLI picks the tier from your GPU: full textures (`uastc-full`) on 16 GiB and more, 512 px textures (`bc7-512`) below that, and says so in the result (`localDefaults`). `--textures uastc-full` or `--textures bc7-512` overrides it. On an Apple silicon Mac the GPU memory is three quarters of the Mac's memory: a 24 GB Mac gets full textures, a 16 GB Mac 512 px textures. | GPU memory | What fits (measured peak) | |---|---| | 8 GiB | 512 px textures on every public map, up to six 1920x1080 cameras (4.5 GiB) | | 12 GiB | 512 px textures everywhere; full textures on every public map except san-ramon-phase-1, one 1280x720 camera (8.1 GiB) | | 16 GiB | Full textures on every public map, up to six 1920x1080 cameras (11.9 GiB) | Peak GPU memory of one 3D render (showcase preset, one 1280x720 camera unless stated): | Map | Full textures | 512 px textures | |---|---|---| | san-ramon-phase-1 | 11.3 GiB | 3.8 GiB | | san-ramon-phase-1, six 1920x1080 cameras | 11.9 GiB | 4.5 GiB | | di-rosa-sf | 8.1 GiB | 2.8 GiB | | richmond-field-station | 7.3 GiB | 2.1 GiB | | belmont-research-center | 7.2 GiB | 2.3 GiB | | yale-street | 6.5 GiB | 2.3 GiB | | el-camino-road | 3.5 GiB | 1.9 GiB | | easterbrook-discovery-school | 2.0 GiB | 1.8 GiB | The training preset needs about the same; a schematic render needs 0.2 GiB. Measured on an RTX 5080 (driver 595.84). The machine-readable table is `sdk/contracts/runtime-support.v1.json` in the toolkit. ## Output format A 3D render writes: | Path | Format | |---|---| | `render.json` | The render's summary, also printed by the command: preset, rig, texture tier, map and actor-pack identities, gates, timings, videos, warnings | | `results.json` | Every captured file with its sensor, tick, pass, byte count and sha256 | | `/.rgb.png` | The camera image, RGBA8 PNG, sRGB | | `/.id.png` | Instance ids, RGBA8 PNG: id = R + 256·G + 65536·B, 0 = sky or background | | `legend.json` | Instance id to the static map mesh it is | | `/.semantic.png` | Semantic class in the B channel of an RGBA8 PNG, CARLA CityScapes ids: 0 unlabeled, 1 building, 2 fence, 4 pedestrian, 5 pole, 7 road, 9 vegetation, 10 vehicle, 12 traffic sign, 18 traffic light | | `/.depth.f32.bin` | Raw little-endian float32, width x height, row-major, reverse-Z: distance along the camera axis in metres = `nearM / value` (`nearM` is the camera's near plane, 0.05 m by default); 0 = nothing hit | | `observed-frames.jsonl` | One line per frame: every actor's position, rotation and visibility as rendered | | `parity.json` | How closely the rendered poses match the simulated trace | | `video/.mp4` | The RGB frames as H.264 video (needs `ffmpeg`; `--video off` skips it) | | `job/` | The exact inputs the renderer ran with (timeline, scene state, texture plan) | Ticks count from the clip start at the rig's frame rate. Lidar sources write `/.ply` and radar sources `.csv`. A 2D render writes `render.json` and `video/topdown.mp4`. Traces are identical on every machine; pixels are identical run to run on the same GPU model and driver, and differ slightly between GPUs. Without ffmpeg, a 2D render that requests video preserves its images under `frames/topdown/.png` and reports that directory in `frame.directory`; it warns that no video was encoded. `--video off` remains a render-and-measure operation without retained frames. ## Before you render Check the GPU with `simforge doctor` and the runtime's render modes with `simforge runtime status`. A render that cannot run on this machine fails with a named error; it never falls back to another device or to the cloud. A scene that does not fit in GPU memory fails with `native_gpu_out_of_memory`: use `--textures bc7-512`, fewer or smaller cameras, or `--cloud`. A 3D render whose GPU memory cannot be measured warns `gpu_memory_unmeasured`; `--vram-budget` sets the budget explicitly. ## Render in the cloud The same command runs on SimForge's machines with `--cloud` and a cost cap in cents: ```sh simforge render s.json --mode 3d --rig rig.json --out out-3d --cloud --max-cost 200 --wait --download ``` Cloud downloads include every captured sensor file named by `results.json`, the videos and output metadata. The local renderer's diagnostic `job/` directory is not exported in full; its authoritative `job/timeline.json` is included. See [Run in the cloud](/docs/cloud) for pricing, caps and following a run. --- # Custom sensor rigs > Bring your own cameras, lidar and radar: the rig file, the vehicle frame and rotation conventions, render profiles, and how to use one rig locally, in the cloud and across scenarios. Source: https://simforge.ai/docs/sensors A sensor rig is a JSON file that lists what a render captures: the cameras, lidars and radars, where each one sits on the vehicle, which way it looks and what it records. The format is `simforge.render-rig/v1`. A rig is not part of a scenario: you pass it to each render, so one rig works with every scenario, and one scenario can be rendered through many rigs. Schematic and 3D renders look through a rig. Without `--rig` they use the built-in `basic-dash-camera` on the scenario's ego. 2D renders are a top-down map view and take no rig. ## The quick path ```sh simforge rig init --preset basic-dash-camera --class car --out rig.json simforge rig validate rig.json simforge rig show rig.json simforge render s.json --mode 3d --rig rig.json --out out-3d ``` `rig init` writes a built-in rig fitted to a vehicle class's default box, which is the easiest starting point to edit. `rig validate` checks the file exactly as a render reads it and exits 2 with the problem. `rig show` prints what the rig captures: each sensor's mount, the direction it faces, its field of view, and how much of the horizon the cameras cover together, with any gaps. Every field is in the [rig file reference](/docs/sensor-reference), and three complete, rendered rigs are in [example rigs](/docs/sensor-examples). ## How a rig attaches to the vehicle Every scenario names an ego: the vehicle that carries the sensors (a car, truck, bus, van, motorcycle or drone). A render mounts every source of the rig on that ego, whatever `actorId` the file names, and says so with the warning `render_rig_rehosted` when it moves one. So `actorId: "ego"` in a rig works for any scenario. A scenario without an ego is refused (`render_ego_missing`). Mounts are rigid: a sensor moves with the vehicle body, including its pitch and roll over bumps and on slopes, and it never sees the vehicle that carries it. The one exception is a drone camera with `mountFrame: "gimbal"`, whose rotation follows the drone's stabilized gimbal while its position rides the body. ## The vehicle frame Positions are metres in the vehicle's own frame. The origin is the centre of the vehicle's box at ground level. | Axis | Points | Notes | |---|---|---| | +X | forward, the direction of travel | the front bumper of a 4.8 m car is at x = 2.4 | | +Y | up | the roof of a 1.5 m tall car is at y = 1.5 | | +Z | left | the left side of a 1.9 m wide car is at z = 0.95 | ```text Top view (looking down) Side view (from the left) +X forward +Y up ^ ^ | | o-> camera, looking +X +------+------+ +-----+----------+ | | | | | | +Z <----+------o | | | | left | vehicle | ---+-----o----------+---> +X forward | box | ground (y = 0); o = origin +-------------+ o = origin (box centre, at ground level) ``` X forward, Y up and Z left make a left-handed frame. Keep that in mind when you bring mounts from a right-handed convention: see [converting calibrations](#converting-a-calibration-you-already-have) below. > **Note.** Lidar point clouds (PLY) use a different frame: +Z is to the right (CARLA parity). See [sensor outputs](/docs/sensor-outputs#lidar-files). Default boxes, used to fit the built-in rigs: car 4.8 x 1.9 x 1.5 m, van 5.5 x 2.0 x 2.2 m, truck 9.5 x 2.5 x 3.5 m, drone 1.0 x 1.0 x 0.45 m (length x width x height). A scenario actor with its own dimensions keeps its own box, and your rig's positions are used as written. ## Rotations A sensor's rotation is three angles in radians, all optional and 0 by default. At zero rotation a sensor looks straight ahead along +X, level, with its image upright. | Field | Turns the sensor | Positive means | Range | |---|---|---|---| | `yawRad` | about the vehicle's up axis | to the left (counter-clockwise seen from above) | -pi to pi | | `pitchRad` | about the sensor's sideways axis | nose up | -pi/2 to pi/2 | | `rollRad` | about the sensor's forward axis | right side down | -pi to pi | They apply in that order: yaw first, then pitch about the turned sensor, then roll about its own line of sight. There is no quaternion or matrix input. Worked examples: | Sensor | yawRad | pitchRad | rollRad | |---|---|---|---| | Forward, level | 0 | 0 | 0 | | Forward, tilted 5 degrees down | 0 | -0.0873 | 0 | | Left-facing (+90 degrees) | 1.5708 | 0 | 0 | | Right-facing (-90 degrees) | -1.5708 | 0 | 0 | | Front-left at 60 degrees | 1.0472 | 0 | 0 | | Rear-facing | 3.14159 | 0 | 0 | | Straight down (a mapping camera) | 0 | -1.5708 | 0 | A rear camera's yaw must stay inside the range: write 3.14159 or -3.14159, not 3.1416, which `rig validate` refuses (`transform.rotation is outside yaw/roll [-pi, pi], pitch [-pi/2, pi/2]`). ## Converting a calibration you already have Many calibrations use a right-handed vehicle frame with x forward, y left and z up (FLU), or x forward, y right and z down (FRD). To move a mount into a SimForge rig: | Your frame | SimForge x | SimForge y | SimForge z | yaw | pitch | roll | |---|---|---|---|---|---|---| | FLU (x fwd, y left, z up), origin at ground under the box centre | x | z | y | yaw | -pitch (FLU pitch is nose down) | roll | | FRD (x fwd, y right, z down), origin at ground under the box centre | x | -z | -y | -yaw | pitch | roll | If your origin is somewhere else, such as the rear axle, subtract that point's position in the SimForge frame first: for a rear axle 1.4 m behind the box centre, x = x_axle - 1.4. Check every converted rig with `simforge rig show`: its `facing` column names the side each sensor looks at (front, front-left, left, rear-left and so on), which catches a flipped sign at once. ## Render profiles The `--preset` flag picks how a 3D render looks; the rig picks what it captures. | Profile | Look | Built-in rigs record | |---|---|---| | `showcase` | photoreal, the default for a 3D render (local and `--cloud`) | 1280x720 at 24 fps | | `training` | the dataset look at a fixed cost per frame | 512x384 at 20 fps | | `schematic` | a clean schematic view (`--mode schematic`) | 1920x1080 at 30 fps | Without `--rig`, the default dash camera gets the profile's capture size and rate. `rig init` writes its cameras at a profile's capture (`--profile`, default `showcase`): fit a rig to the profile you render with, such as `--profile training` for a training render. A rig file keeps its own: each camera renders at its own `width`, `height` and `fps` whatever the profile. Give every camera of a rig the same `fps`; cameras at different rates are scheduled on the union of their frame times. ## Use a rig Locally, with the [runtime installed](/docs/sign-in): ```sh simforge rig validate rig.json simforge preview frame s.json --mode 3d --rig rig.json --camera cam-front --at 5 --out frame.png simforge render s.json --mode 3d --rig rig.json --out out-3d simforge render s.json --mode 3d --rig rig.json --preset training --passes rgb,depth --out out-train ``` `preview frame` renders one camera at one moment, which is the fastest way to check a mount; `--camera` takes a camera's `outputName`. `--passes` chooses the camera passes (`rgb`, `id`, `depth`, `semantic`; all four by default). In the cloud, the same file goes with the request: ```sh simforge render s.json --mode 3d --rig rig.json --out out-3d --cloud --dry-run simforge render s.json --mode 3d --rig rig.json --out out-3d --cloud --max-cost 100 --wait --download ``` A cloud render sends the rig as part of the run (the `rig` of a `render` run in the [Cloud API](/docs/cloud)) and renders it with the same runtime. It returns each sensor's video plus `render.json` and `results.json`, not the per-frame images, depth or point clouds: render locally when you need those. See [sensor outputs](/docs/sensor-outputs). In Studio, the Render panel's sensor step offers the built-in rigs, a single front camera and the rig this scenario last rendered with. Studio has no rig upload yet: to render your own rig file, use the CLI, locally or with `--cloud`. ## Reuse a rig across scenarios A rig names no map, scenario or actor of its own, so keep one file per sensor setup and pass it to every render: ```sh for s in scenarios/*.json; do simforge render "$s" --mode 3d --rig rigs/surround.rig.json --out "out/$(basename "$s" .json)" done ``` Positions are absolute metres on the vehicle, so a rig written for a car sits in the same place on a truck: the cameras are not moved to the truck's roof. Write one rig per vehicle class you render, or start each from `simforge rig init --preset --class `, which fits a built-in rig to the class's box. ## What a rig cannot describe yet These are not supported today. A field for any of them is refused as unknown: - Pinhole intrinsics as fx, fy, cx, cy, or a shifted principal point: a pinhole camera is `horizontalFovDeg` with square pixels and the principal point at the image centre. - Brown-Conrady (OpenCV radial-tangential), Kannala-Brandt or equidistant fisheye distortion: the only lens model is the f-theta polynomial (`cameraModel`). - Exposure, shutter speed, rolling shutter, motion blur per camera, noise or lens flare settings. - IMU, GNSS, ultrasonic, thermal or event cameras. The vehicle's pose at every frame is in `observed-frames.jsonl`. - Separate depth, semantic or instance camera sources: those are passes of an RGB camera (`--passes`); a source with `modality` `depth`, `semantic` or `instance` validates but the renderer refuses it (`native_modality_unsupported`). - A lidar whose vertical field is not symmetric about its mount (for example +15 to -25 degrees), lidar noise or dropout, and radar noise or clutter. --- # Rig file reference > Every field of a simforge.render-rig/v1 sensor rig, with its type, range, default and units, and what the renderer supports on top. Source: https://simforge.ai/docs/sensor-reference A rig is one JSON object. Unknown fields are refused everywhere, so a typo fails `simforge rig validate` instead of being ignored. The JSON Schema is [/docs/render-rig.schema.json](/docs/render-rig.schema.json), and the CLI checks exactly the same rules. ## The file | Field | Type | Required | What | |---|---|---|---| | `schema` | string | yes | Always `simforge.render-rig/v1` | | `sources` | array of 1 to 64 sources | yes | The cameras, lidars and radars | | `clip` | object | no | `{startSeconds, endSeconds}`: render only this part of the scenario, `[start, end)` in seconds from the scenario's start. Default: the whole scenario (warning `render_clip_whole_timeline`) | | `video` | object | only with a radar | `{width, height, fps, container, codec, quality}`. The renderer uses `fps` as the radar sample rate; `container` must be `mp4`, `codec` `h264`, and `quality` one of `draft`, `standard`, `high` (`lossless` is refused). Each video takes its size from the cameras | | `sensorHosts` | array | no | Which vehicle carries each source: `{sourceId, actorId, vehicleAsset: {catalogAssetId}}`. Leave it out: every source is then carried by the scenario's ego | ## Every source | Field | Type | Required | What | |---|---|---|---| | `actorId` | string | yes | The vehicle that carries it. Any value works: a render mounts every source on the scenario's ego. Use `ego` | | `sensorId` | string | yes | Your name for the sensor | | `sensorLabel` | string, 1 to 200 characters | no | A human-readable name, shown by `rig show` and in Studio | | `outputName` | string | yes | The sensor's folder and file prefix in the output: a letter or digit, then up to 63 letters, digits, `-` or `_`. Unique in the rig | | `modality` | string | yes | `rgb` for a camera, `lidar`, or `radar` | | `transform.position` | `{x, y, z}`, metres | yes | The mount in the [vehicle frame](/docs/sensors#the-vehicle-frame): x forward, y up, z left, from the box centre at ground level | | `transform.rotation` | `{yawRad, pitchRad, rollRad}`, radians | no | Each 0 by default. [Rotations](/docs/sensors#rotations): yaw left-positive in -pi to pi, pitch nose-up in -pi/2 to pi/2, roll right-side-down in -pi to pi | | `mountFrame` | `body` or `gimbal` | no, cameras only | `gimbal` follows a drone's stabilized gimbal; default `body` | | `attributes` | object | yes | By modality, below | Two sources may not share an `outputName`, or the same `actorId`, `sensorId` and `modality`. > **Note.** Lidar point clouds (PLY) use a different frame: +Z is to the right (CARLA parity). See [sensor outputs](/docs/sensor-outputs#lidar-files). ## Camera (`modality: rgb`) | Attribute | Type | Range | What | |---|---|---|---| | `width` | integer, pixels | 64 to 8192 (the renderer draws at most 4096) | Image width | | `height` | integer, pixels | 64 to 8192 (the renderer draws at most 4096) | Image height | | `fps` | number, frames per second | above 0, up to 240 | Frame rate | | `horizontalFovDeg` | number, degrees | above 0, up to 180 | Pinhole camera: the horizontal field of view. The vertical field follows from the aspect ratio | | `cameraModel` | object | | Lens camera: an f-theta lens (below) | | `nearM` | number, metres | above 0 | Near clip plane; also the depth pass's scale. Typical: 0.05 | | `farM` | number, metres | above `nearM` | Far clip plane. Typical: 1000 | A camera has exactly one of `horizontalFovDeg` and `cameraModel`. A pinhole camera's intrinsics follow from these fields: the principal point is the image centre, pixels are square, and the focal length in pixels is fx = fy = (width / 2) / tan(horizontalFovDeg / 2). A 1280x720 camera with a 90 degree field of view has fx = fy = 640, cx = 640 and cy = 360, and a vertical field of 58.7 degrees; `rig show` prints each camera's vertical field. Every camera writes the passes you ask for with `--passes`: `rgb` (the image), `depth`, `id` (instances) and `semantic`. See [sensor outputs](/docs/sensor-outputs). ## Lens camera (`cameraModel`) For wide-angle and fisheye cameras, an f-theta lens maps the distance of a pixel from the principal point to the angle of its ray from the optical axis. | Field | Type | What | |---|---|---| | `type` | `ftheta` | The only lens model | | `referenceResolution` | `[width, height]`, pixels | The calibration image's size. The camera's `width` and `height` must keep its aspect ratio (within 1%); the image is a centre-aligned resize of it | | `principalPoint` | `[x, y]`, reference pixels | The optical centre; integer values are pixel centres | | `pixeldistToAnglePoly` | 2 to 8 numbers | theta(r) = c0 + c1 r + c2 r^2 + ..., r in reference pixels from the principal point, theta in radians. It must rise steadily out to the image corners and stay below 90 degrees there | | `maxAngleRad` | number, radians, above 0 | The calibration's valid half-angle; informational | | `angleToPixeldistPoly` | numbers | The inverse fit (radians to pixels); informational, for your own projections | | `linearCde` | `[c, d, e]` | Linear skew: pixel = principal point + `[[c, d], [e, 1]]` x the polynomial's offset. Default `[1, 0, 0]` | ```json { "schema": "simforge.render-rig/v1", "sources": [ { "actorId": "ego", "sensorId": "cam-front-fisheye", "outputName": "cam-front-fisheye", "transform": { "position": { "x": 2.3, "y": 0.8, "z": 0 }, "rotation": { "pitchRad": -0.2 } }, "modality": "rgb", "attributes": { "width": 960, "height": 600, "fps": 20, "nearM": 0.05, "farM": 1000, "cameraModel": { "type": "ftheta", "referenceResolution": [1920, 1200], "principalPoint": [959.5, 599.5], "pixeldistToAnglePoly": [0, 0.00105, 0, 0], "maxAngleRad": 1.2 } } } ] } ``` ```sh simforge preview frame s.json --mode 3d --rig fisheye.rig.json --camera cam-front-fisheye --at 5 --out fisheye.png ``` ![The f-theta camera above, previewed at 5 s on easterbrook-discovery-school](/docs/images/sensors-ftheta-preview.jpg) The open CLI checks a lens's shape; the installed runtime plans it, so `rig validate` and `rig show` of a rig with a lens need the runtime. Only the Helios Engine draws f-theta lenses. ## Lidar (`modality: lidar`) | Attribute | Type | Range | What | |---|---|---|---| | `channels` | integer | 1 to 256 | Laser rings, spread evenly over the vertical field | | `rangeM` | number, metres | above 0, up to 1000 | Maximum range | | `pointsPerSecond` | integer | above 0 | Points across all channels per second | | `rotationFrequencyHz` | number, Hz | above 0, up to 240 | Revolutions per second; sets how dense a scan is. A full scan is written at every rendered frame | | `upperFovDeg` | number, degrees | -180 to 180, above `lowerFovDeg` | Highest ring's elevation | | `lowerFovDeg` | number, degrees | -180 to 180 | Lowest ring's elevation. The renderer needs `upperFovDeg = -lowerFovDeg` (`native_lidar_asymmetric_fov_unsupported`) | | `horizontalFovDeg` | number, degrees | 5 to 360, default 360 | Azimuth coverage, centred on the sensor's forward direction | Each ring takes pointsPerSecond / (channels x rotationFrequencyHz) samples per revolution, rounded and kept between 64 and 4096. A beam that hits nothing within `rangeM` returns no point. ## Radar (`modality: radar`) | Attribute | Type | Range | What | |---|---|---|---| | `horizontalFovDeg` | number, degrees | above 0, up to 180 | Horizontal field, centred on the sensor's forward direction | | `verticalFovDeg` | number, degrees | above 0, up to 180 | Vertical field | | `rangeM` | number, metres | above 0, up to 1000 | Maximum range | | `pointsPerSecond` | integer | above 0 | Rays cast per second; at the rig's `video.fps` this must give at least one ray per frame (`native_radar_budget_invalid`) | A rig with a radar needs `video` (its `fps` is the radar's sample rate). Each frame lists the detections with their range, angles and radial velocity. ## What the renderer adds on top `rig validate` checks the format. A render also checks what the Helios Engine can draw, and fails with a named error before any work: | Rule | Error | |---|---| | At least one `rgb` camera | `native_render_camera_missing` | | Cameras at most 4096x4096 | `native_camera_size_unsupported` | | Only `rgb` cameras, `lidar` and `radar` sources | `native_modality_unsupported` | | A symmetric lidar field (`upperFovDeg = -lowerFovDeg`) | `native_lidar_asymmetric_fov_unsupported` | | A rig with a radar names `video` | `render_schedule_invalid` | | Enough radar rays per frame | `native_radar_budget_invalid` | | `video` is mp4, h264, not lossless | `native_video_profile_unsupported`, `native_video_quality_unsupported` | --- # Example rigs > Three complete sensor rigs, each rendered end to end on a public map - a front camera, a seven-camera surround rig, and a camera with lidar and radar. Source: https://simforge.ai/docs/sensor-examples Each rig below passes `simforge rig validate` and was rendered with the runtime on easterbrook-discovery-school, exactly as shown, on an RTX 5080. Copy one into a file, change what you need, and check it with `simforge rig show`. All three use the [vehicle frame](/docs/sensors#the-vehicle-frame) of a car: x forward, y up, z left, in metres from the centre of the car's box at ground level. ## Front camera One forward camera on the windscreen, 1280x720 at 20 fps with a 90 degree field of view, tilted 3 degrees down (`pitchRad` -0.0524). ```json { "schema": "simforge.render-rig/v1", "sources": [ { "actorId": "ego", "sensorId": "cam-front", "sensorLabel": "Front camera", "outputName": "cam-front", "modality": "rgb", "transform": {"position": {"x": 1.6, "y": 1.45, "z": 0}, "rotation": {"yawRad": 0, "pitchRad": -0.0524, "rollRad": 0}}, "attributes": {"width": 1280, "height": 720, "fps": 20, "horizontalFovDeg": 90, "nearM": 0.05, "farM": 1000} } ] } ``` ```sh simforge render s.json --mode 3d --rig front-camera.rig.json --out out-front ``` Every camera writes four passes per frame unless you narrow them with `--passes`: the image, metric depth, semantic classes and instance ids (here colourised for display; the files hold the raw values, see [sensor outputs](/docs/sensor-outputs)). ![Front camera at 5 s: the RGB image, depth, semantic classes and instance ids](/docs/images/sensors-front-camera-passes.jpg) ## Seven-camera surround rig A layout like the camera arrays of common driving datasets: a wide and a telephoto camera looking forward, two front-corner cameras at 60 degrees, two rear-corner cameras at 120 degrees, and one camera looking back. Together they cover the whole horizon; `simforge rig show` reports `"coveredDeg": 360.0` and no gaps. ```json { "schema": "simforge.render-rig/v1", "sources": [ { "actorId": "ego", "sensorId": "cam-front", "sensorLabel": "Front wide", "outputName": "cam-front", "modality": "rgb", "transform": {"position": {"x": 1.7, "y": 1.5, "z": 0}, "rotation": {"yawRad": 0, "pitchRad": 0, "rollRad": 0}}, "attributes": {"width": 1280, "height": 720, "fps": 20, "horizontalFovDeg": 120, "nearM": 0.05, "farM": 1000} }, { "actorId": "ego", "sensorId": "cam-front-tele", "sensorLabel": "Front telephoto", "outputName": "cam-front-tele", "modality": "rgb", "transform": {"position": {"x": 1.72, "y": 1.52, "z": 0}, "rotation": {"yawRad": 0, "pitchRad": 0, "rollRad": 0}}, "attributes": {"width": 1280, "height": 720, "fps": 20, "horizontalFovDeg": 30, "nearM": 0.05, "farM": 1000} }, { "actorId": "ego", "sensorId": "cam-front-left", "sensorLabel": "Front left", "outputName": "cam-front-left", "modality": "rgb", "transform": {"position": {"x": 1.5, "y": 1.46, "z": 0.45}, "rotation": {"yawRad": 1.0472, "pitchRad": 0, "rollRad": 0}}, "attributes": {"width": 1280, "height": 720, "fps": 20, "horizontalFovDeg": 120, "nearM": 0.05, "farM": 1000} }, { "actorId": "ego", "sensorId": "cam-front-right", "sensorLabel": "Front right", "outputName": "cam-front-right", "modality": "rgb", "transform": {"position": {"x": 1.5, "y": 1.46, "z": -0.45}, "rotation": {"yawRad": -1.0472, "pitchRad": 0, "rollRad": 0}}, "attributes": {"width": 1280, "height": 720, "fps": 20, "horizontalFovDeg": 120, "nearM": 0.05, "farM": 1000} }, { "actorId": "ego", "sensorId": "cam-rear-left", "sensorLabel": "Rear left", "outputName": "cam-rear-left", "modality": "rgb", "transform": {"position": {"x": -0.6, "y": 1.44, "z": 0.45}, "rotation": {"yawRad": 2.0944, "pitchRad": 0, "rollRad": 0}}, "attributes": {"width": 1280, "height": 720, "fps": 20, "horizontalFovDeg": 90, "nearM": 0.05, "farM": 1000} }, { "actorId": "ego", "sensorId": "cam-rear-right", "sensorLabel": "Rear right", "outputName": "cam-rear-right", "modality": "rgb", "transform": {"position": {"x": -0.6, "y": 1.44, "z": -0.45}, "rotation": {"yawRad": -2.0944, "pitchRad": 0, "rollRad": 0}}, "attributes": {"width": 1280, "height": 720, "fps": 20, "horizontalFovDeg": 90, "nearM": 0.05, "farM": 1000} }, { "actorId": "ego", "sensorId": "cam-rear", "sensorLabel": "Rear", "outputName": "cam-rear", "modality": "rgb", "transform": {"position": {"x": -2.2, "y": 1.2, "z": 0}, "rotation": {"yawRad": 3.14159, "pitchRad": 0, "rollRad": 0}}, "attributes": {"width": 1280, "height": 720, "fps": 20, "horizontalFovDeg": 90, "nearM": 0.05, "farM": 1000} } ] } ``` | Camera | Faces | yawRad | Field of view | |---|---|---|---| | `cam-front` | forward | 0 | 120 degrees | | `cam-front-tele` | forward | 0 | 30 degrees | | `cam-front-left` | front left | 1.0472 (60 degrees) | 120 degrees | | `cam-front-right` | front right | -1.0472 (-60 degrees) | 120 degrees | | `cam-rear-left` | rear left | 2.0944 (120 degrees) | 90 degrees | | `cam-rear-right` | rear right | -2.0944 (-120 degrees) | 90 degrees | | `cam-rear` | back | 3.14159 (180 degrees) | 90 degrees | The rear camera's yaw is 3.14159, inside the allowed range of -pi to pi; 3.1416 is just outside it and fails validation. ```sh simforge render s.json --mode 3d --rig surround-7cam.rig.json --preset training --out out-surround ``` ![The seven cameras at 5 s, training preset: front left, front, front right, rear left (top row); front telephoto, rear right, rear (bottom row)](/docs/images/sensors-surround-7cam.jpg) ## Camera, lidar and radar The front camera, a 64-channel roof lidar and a forward radar 0.2 m ahead of the front of the box. The lidar spins at 10 Hz with a symmetric vertical field of +15 to -15 degrees and 1.2 million points per second, so each scan has up to 64 x 1875 points. The radar casts 1500 rays per second over a 90 x 10 degree field. A rig with a radar needs `video`: its `fps` (20 here) is the radar's sample rate. ```json { "schema": "simforge.render-rig/v1", "sources": [ { "actorId": "ego", "sensorId": "cam-front", "sensorLabel": "Front camera", "outputName": "cam-front", "modality": "rgb", "transform": {"position": {"x": 1.6, "y": 1.45, "z": 0}, "rotation": {"yawRad": 0, "pitchRad": -0.0524, "rollRad": 0}}, "attributes": {"width": 1280, "height": 720, "fps": 20, "horizontalFovDeg": 90, "nearM": 0.05, "farM": 1000} }, { "actorId": "ego", "sensorId": "lidar-roof", "sensorLabel": "Roof lidar", "outputName": "lidar-roof", "modality": "lidar", "transform": {"position": {"x": 0, "y": 1.9, "z": 0}}, "attributes": {"channels": 64, "rangeM": 120, "pointsPerSecond": 1200000, "rotationFrequencyHz": 10, "upperFovDeg": 15, "lowerFovDeg": -15, "horizontalFovDeg": 360} }, { "actorId": "ego", "sensorId": "radar-front", "sensorLabel": "Front radar", "outputName": "radar-front", "modality": "radar", "transform": {"position": {"x": 2.6, "y": 0.5, "z": 0}}, "attributes": {"horizontalFovDeg": 90, "verticalFovDeg": 10, "rangeM": 150, "pointsPerSecond": 1500} } ], "video": {"width": 1280, "height": 720, "fps": 20, "container": "mp4", "codec": "h264", "quality": "standard"} } ``` ```sh simforge render s.json --mode 3d --rig camera-lidar-radar.rig.json --out out-lidar ``` The render writes the camera's passes, one `.ply` point cloud for the lidar and one `.csv` of detections for the radar at every frame, and a review video for each sensor. ![The camera, and the lidar and radar review videos at 5 s](/docs/images/sensors-camera-lidar-radar.jpg) Mount a radar outside the vehicle's body. A radar inside it, for example at the box's front edge where the body's nose is, sees only the body: every detection is a few centimetres away. ## The scenario used ```sh simforge maps pull easterbrook-discovery-school --accept-license simforge scenario new --map easterbrook-discovery-school --clip-seconds 10 --out s.json simforge scenario apply s.json add-actor --id ego --catalog-id vehicle.sedan --lane-rsl 10:0:-1 --s 6 \ -- set-ego --role-id ego -- add-actor --id lead --catalog-id vehicle.sedan --lane-rsl 10:0:-1 --s 30 -- set-clock --minutes 780 ``` A sedan ego following a second sedan down road 10 at about 13 m/s, at 13:00, for 10 seconds: 200 frames at 20 fps. --- # Sensor outputs > What a render writes for each camera, lidar and radar - the folder layout, file formats, timestamps, the camera and vehicle poses - and how to load it in Python. Source: https://simforge.ai/docs/sensor-outputs A local 3D render writes everything into its `--out` folder, which must not exist yet or be empty. This is the layout for the [camera, lidar and radar example](/docs/sensor-examples#camera-lidar-and-radar): ```text out-lidar/ render.json the render's summary: rig, preset, render settings, timings, videos, warnings results.json every captured file: sensor, tick, pass, bytes, sha256, and each camera frame's projection and time observed-frames.jsonl one line per frame: every actor's position, rotation and visibility as rendered legend.json instance id -> the static map mesh it is parity.json how closely the rendered poses match the simulated trace render.log cam-front/00000000.rgb.png one file per frame and pass cam-front/00000000.depth.f32.bin cam-front/00000000.semantic.png cam-front/00000000.id.png lidar-roof/00000000.ply one point cloud per frame radar-front/00000000.csv one detection list per frame video/cam-front.mp4 one video per sensor (lidar and radar as review videos) job/render-job.json the exact inputs, including every camera's pose at every frame ``` Folder and file names come from each source's `outputName`. Ticks are zero-padded to eight digits. A lidar or radar review video shows the sensor's right on the right of the picture. Review videos rendered before 13 October 2026 are mirrored left to right; the point clouds and radar files of those renders are correct. ## Camera files | File | Format | |---|---| | `.rgb.png` | The image, RGBA8 PNG, sRGB | | `.depth.f32.bin` | Little-endian float32, width x height, row-major, reverse-Z: distance along the camera axis in metres = `nearM / value`; 0 means nothing was hit (sky) | | `.semantic.png` | The class in the blue channel of an RGBA8 PNG, CARLA CityScapes ids: 0 unlabeled, 1 building, 2 fence, 4 pedestrian, 5 pole, 7 road, 9 vegetation, 10 vehicle, 12 traffic sign, 18 traffic light | | `.id.png` | The instance id, RGBA8 PNG: id = R + 256 x G + 65536 x B, 0 = sky or background. `legend.json` names the static map meshes; ids missing from it are actors | `--passes rgb,depth` (any of `rgb`, `id`, `depth`, `semantic`) writes only the passes you need. A 1280x720 frame takes about 1 MB as RGB and 3.7 MB as depth, so 7 cameras x 200 frames with all four passes come to 6.3 GB. The RGB image carries the preset's camera look: exposure, tone mapping, bloom, a vignette and a slight lens distortion (0.008). Depth, id and semantic are drawn without them, so their pixels line up with an undistorted pinhole. For pixel-exact geometry in the RGB image too, turn the distortion off: `--set lens.distortion=0`. `render.json` records every setting in `renderConfig`. ## Lidar files `.ply` is an ASCII PLY point cloud with one row per point: ```text ply format ascii 1.0 element vertex 91524 property float x property float y property float z property float intensity property uint instance_id end_header ``` `x`, `y` and `z` are metres in the lidar's own frame: x forward along the sensor's line of sight, y up, and **z to the right** (unlike the rig's vehicle frame, whose z points left). `intensity` is a reflectance proxy in 0 to 1, and `instance_id` uses the same ids as the cameras' id pass. The renderer writes one full scan at every rendered frame, the camera rate: the example's 10 Hz lidar gives 200 point clouds in 10 seconds at 20 fps, not 100. Every point of a scan is taken at the frame's instant (no motion during the sweep), and `rotationFrequencyHz` sets how dense a scan is: each ring has pointsPerSecond / (channels x rotationFrequencyHz) samples, kept between 64 and 4096. Beams that hit nothing within `rangeM` return no point, so the count varies from frame to frame. ## Radar files `.csv` has one detection per row: | Column | Unit | Meaning | |---|---|---| | `depth_m` | metres | Range along the beam | | `azimuth_rad` | radians | Beam direction; 0 is the radar's forward axis, positive to the right | | `altitude_rad` | radians | Beam elevation; positive up | | `velocity_mps` | m/s | Radial velocity relative to the radar; negative when the target closes in. Static objects ahead of a vehicle driving at 13 m/s read about -13 | ## Time and synchronization Every sensor is captured at the same instants. Frame `n` is at time clipStart + n / fps, in seconds from the scenario's start: `results.json` gives each camera frame's `sampleTime.seconds`, and `observed-frames.jsonl` each frame's `time`. The rig's `clip` sets clipStart (0 by default). A frame is an instantaneous snapshot: rolling shutter, exposure time and sensor clock drift are not modelled (`results.json` says so per frame under `camera.sampling`). ## Poses and calibration There is no separate calibration file: the rig you rendered with is the calibration. - **Intrinsics.** A pinhole camera has fx = fy = (width / 2) / tan(horizontalFovDeg / 2) and its principal point at the image centre. `results.json` gives each frame's `projection`: `kind`, `fovYDeg`, `nearM`, `farM` and the depth convention. - **Extrinsics.** Each source's `transform` in the rig: its position and rotation on the vehicle. - **The vehicle in the world.** `observed-frames.jsonl` gives every actor's pose at every frame: `position` `[x, y, z]` in metres and `rotation` as a quaternion `[x, y, z, w]`, in the map's scene frame (x east, y up, z south). - **Each camera in the world.** `job/render-job.json` lists every camera at every frame (`rig.cameraSchedule`): its mount as the renderer applied it (`attach`), and its position (`eye`) and a point 50 m along its line of sight (`target`) in the scene frame. ## Load it in Python The toolkit has no reader for these files; NumPy and Pillow are enough: ```python import json from pathlib import Path import numpy as np from PIL import Image out = Path("out-lidar") results = json.loads((out / "results.json").read_text()) # Every captured file: its sensor, tick, pass and, for cameras, the projection and sample time. for artifact in results["artifacts"][:3]: print(artifact["sensorId"], artifact["tick"], artifact["pass"], artifact["path"]) # One camera frame: the image, instance ids, semantic classes and metric depth. cam, tick = "cam-front", 40 rgb = np.asarray(Image.open(out / cam / f"{tick:08d}.rgb.png"))[..., :3] idx = np.asarray(Image.open(out / cam / f"{tick:08d}.id.png")).astype(np.uint32) instance = idx[..., 0] + 256 * idx[..., 1] + 65536 * idx[..., 2] semantic = np.asarray(Image.open(out / cam / f"{tick:08d}.semantic.png"))[..., 2] height, width = rgb.shape[:2] raw = np.fromfile(out / cam / f"{tick:08d}.depth.f32.bin", dtype=" 0, near_m / np.maximum(raw, 1e-12), np.inf) # inf: nothing hit # Pinhole intrinsics from the rig: horizontalFovDeg 90 at 1280x720. fx = fy = (width / 2) / np.tan(np.radians(90) / 2) cx, cy = width / 2, height / 2 # One lidar scan: x forward, y up, z right of the sensor, metres. def read_ply(path): lines = path.read_text().splitlines() start = lines.index("end_header") + 1 return np.loadtxt(lines[start:], ndmin=2) # columns: x y z intensity instance_id points = read_ply(out / "lidar-roof" / "00000004.ply") # One radar frame: range, azimuth, elevation, radial velocity. radar = np.genfromtxt(out / "radar-front" / "00000040.csv", delimiter=",", names=True) ``` ## In the cloud A cloud 3D render (`--cloud`) returns each sensor's video (`video/.mp4`, lidar and radar as review videos) with `render.json` and `results.json`. It does not return the per-frame images, depth, id or semantic passes, point clouds or radar files: render locally when you need them. `simforge preview frame --cloud` returns the RGB frame as a PNG. --- # Sensor performance and troubleshooting > How sensor count, resolution and passes change render time, GPU memory and cloud price; the limits; and the rig errors you can hit, with their fixes. Source: https://simforge.ai/docs/sensor-troubleshooting ## Render time A 3D render draws every camera at every one of its frames, so its time grows with the number of cameras x pixels x frames, plus a fixed cost to load the map. Lidar and radar add ray casts and a review video each. Measured on an RTX 5080 with the three [example rigs](/docs/sensor-examples) on easterbrook-discovery-school, a 10 second scenario: | Rig | Preset | Frames written | Render time | Of which drawing | Output on disk | |---|---|---|---|---|---| | Front camera, 1280x720 | showcase | 200 x 4 passes | 15.3 s | 9.1 s | 0.9 GB | | Seven cameras, 1280x720 | training | 1400 x 4 passes | 36.0 s | 23.8 s | 6.3 GB | | Camera, lidar and radar | showcase | 200 x 4 passes, 200 scans, 200 radar frames | 25.6 s | 14.8 s | 1.8 GB | Peak GPU memory stayed under 2.5 GiB for all three, with full textures. Most of the rest of the time is encoding the videos (10.9 s for the seven cameras) and loading the map and actors (about 3 s). A single `simforge preview frame` took 9 s. To render faster: use `--preset training`, lower `width` and `height`, render a part of the scenario with the rig's `clip`, ask only for the passes you need (`--passes rgb`), and skip videos you do not need (`--video off`). Check a mount with `simforge preview frame` before a full render. ## GPU memory GPU memory depends far more on the map and the texture tier than on the cameras. On san-ramon-phase-1 with full textures, one 1280x720 camera peaks at 11.3 GiB and six 1920x1080 cameras at 11.9 GiB; with 512 px textures (`--textures bc7-512`), 3.8 and 4.5 GiB. The full table per map is in [Preview and render](/docs/render#gpu-memory-and-texture-tiers). A render that does not fit fails with `native_gpu_out_of_memory`: use `--textures bc7-512`, fewer or smaller cameras, or `--cloud`. ## Cloud price A cloud render's [price](/docs/cloud#prices) does not depend on the rig: more cameras, lidar or radar do not change it, and 2D renders are free. `--dry-run` shows the price before anything runs, and `--max-cost` caps it ([Run in the cloud](/docs/cloud)). A cloud render returns each sensor's video, `render.json` and `results.json`; render locally for the per-frame images, depth and point clouds. ## Limits | What | Limit | |---|---| | Sources in a rig | 1 to 64 | | Camera size | 64 to 8192 pixels in the file; the renderer draws at most 4096x4096 | | Camera frame rate | above 0, up to 240 fps | | Pinhole field of view | above 0, up to 180 degrees | | Lidar | 1 to 256 channels, range up to 1000 m, up to 240 revolutions per second, symmetric vertical field | | Radar | fields up to 180 degrees, range up to 1000 m, at least one ray per frame | | Rotation | yaw and roll -pi to pi, pitch -pi/2 to pi/2, in radians | ## Rig errors and fixes `simforge rig validate` prints one JSON error and exits 2. These are its real messages: | Message (`render_rig_invalid`) | Fix | |---|---| | `sources[0].attributes: unknown field fx, expected one of width, height, fps, horizontalFovDeg, nearM, farM, cameraModel` | The rig has no fx/fy/cx/cy: give `horizontalFovDeg` = 2 x atan(width / (2 x fx)) in degrees | | `sources[0]: a camera declares exactly one of horizontalFovDeg and cameraModel` | Keep the field of view or the lens, not both and not neither | | `sources[0]: camera size 10000x720 is outside 64..8192` | Lower `width` or `height` (the renderer draws at most 4096) | | `sources[0]: fps 0 is outside (0, 240]` | Give a frame rate above 0 | | `sources[0]: nearM must be positive and farM greater than nearM` | Use `nearM` 0.05 and `farM` 1000 unless you need others | | `sources[6]: transform.rotation is outside yaw/roll [-pi, pi], pitch [-pi/2, pi/2]` | A rear camera is yaw 3.14159, not 3.1416; a downward camera is pitch -1.5708 | | `duplicate source outputName "cam-front"` | Give every source its own `outputName` | | `sources[0]: outputName "front cam" must match ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$` | Use letters, digits, `-` and `_`, no spaces | | `sources[0].modality "thermal" (rgb\|depth\|semantic\|instance\|lidar\|radar)` | Thermal is not supported; use `rgb`, `lidar` or `radar` | | `rig schema "simforge.render-rig/v2" (expected simforge.render-rig/v1)` | Set `schema` to `simforge.render-rig/v1` | A render then checks what the renderer supports; its errors are listed in the [rig file reference](/docs/sensor-reference#what-the-renderer-adds-on-top). The most common: | Error | Fix | |---|---| | `native_modality_unsupported` | A `depth`, `semantic` or `instance` source: make it `rgb` and choose passes with `--passes` | | `native_lidar_asymmetric_fov_unsupported` | Make the lidar's field symmetric: `upperFovDeg = -lowerFovDeg` | | `render_schedule_invalid` (a radar) | Add `video` to the rig; its `fps` is the radar's sample rate | | `native_camera_size_unsupported` | Keep cameras at 4096x4096 or smaller | | `render_ego_missing` | The scenario has no ego: `simforge scenario apply s.json set-ego --role-id ` | | `preview_camera_unknown` | `--camera` takes a camera's `outputName`; the error lists the rig's cameras | | `out_not_empty` | `--out` must be a new or empty folder; a failed render can leave `.input` behind: remove it too | ## FAQ **My images are mirrored left to right, or a side camera shows the wrong side.** The frame is x forward, y up, z left, and yaw is positive to the left. A calibration in a right-handed frame needs [converting](/docs/sensors#converting-a-calibration-you-already-have). `simforge rig show` names each sensor's `facing`. **Can I give fx, fy, cx and cy?** Not yet. A pinhole camera is square-pixelled with the principal point at the centre: set `horizontalFovDeg` = 2 x atan(width / (2 x fx)). For a lens with distortion, fit an f-theta polynomial. **Which distortion models are supported?** The f-theta polynomial only. Brown-Conrady (OpenCV), Kannala-Brandt and equidistant fisheye are not supported yet. **Do I need one rig per scenario?** No. A rig names no scenario: pass the same file to every render. Every source is mounted on the scenario's ego. **Do I need one rig per vehicle?** Positions are absolute metres, so a car rig on a truck keeps the car's mount heights. Write one rig per vehicle class, or start from `simforge rig init --preset --class `. **Can Studio use my rig file?** Not yet: Studio offers the built-in rigs and the rig a scenario last rendered with. Render your rig with the CLI, locally or with `--cloud`. **Why does my cloud render have no PNGs or point clouds?** A cloud 3D render returns the videos, `render.json` and `results.json`. Render locally for the per-frame files. **Every radar detection is a few centimetres away.** The radar is inside the vehicle's body and sees only the body. Move it just outside: the example's front radar sits 0.2 m ahead of the box's front edge. **My lidar wrote more scans than it made revolutions.** It writes one full scan per rendered frame, at the camera rate, not one per revolution. See [lidar files](/docs/sensor-outputs#lidar-files). **Are IMU or GNSS supported?** Not as sensors. The ego's exact pose at every frame is in `observed-frames.jsonl`. --- # Reinforcement learning with SimForge Gym > Serve SimForge episodes with simforge env serve and train a policy with SimForge Gym (the open simforge-rl Gymnasium client), on your own machine. Source: https://simforge.ai/docs/rl `simforge env serve` runs episodes of a scenario on the local runtime, with reactive traffic and optional rendered cameras. SimForge Gym, the open `simforge-rl` Python package, is the Gymnasium client: it never contains a simulator of its own, so without a running server there is no episode. Reinforcement learning runs on your machine; the cloud does not run it. Install it from PyPI: ```sh python -m pip install 'simforge-rl[ppo]' ``` ## Serve episodes ```sh simforge env serve s.json --socket /tmp/simforge-env.sock --no-sensors --max-sessions 4 ``` The server prints a ready document and runs until you stop it. `--no-sensors` serves state only and needs no GPU. For camera observations, serve with `--rig rig.json` instead (see [Preview and render](/docs/render) for rigs). `--max-sessions` must cover every environment you open, including the evaluation one. On Windows use `simforge-rl` 0.1.3 or newer and a runtime advertising `capabilities.envSocketTransport: "named-pipe"`. The same `--socket` path selects a secure local named pipe with a bounded hashed name, so long profile paths do not hit the Unix socket path limit. Older Windows runtimes fail with an upgrade instruction. macOS and Linux use Unix sockets. ## Drive it from Python ```python from simforge_rl import SimForgeEnv, SocketTransport, WeightedTerms with SimForgeEnv( SocketTransport("/tmp/simforge-env.sock"), action_mode="setpoint", observation="state", reward_fn=WeightedTerms({"progress": 1.0, "proximity": 2.0}), ) as env: observation, info = env.reset(seed=7) observation, reward, terminated, truncated, info = env.step([5.0, 1.0]) ``` - A `setpoint` action is `[target_speed_mps, target_accel_mps2]`; `action_mode="control"` takes `[throttle, brake, steer]`; `None` keeps the scenario's scripted control. - `observation="state"` is privileged engine state. `"sensors"` returns only rendered camera passes keyed `/`, with no engine state in `info`; `"state+sensors"` returns both. - `reward_fn` replaces the reward; `info["native_reward"]` keeps the runtime's own. - Reset after an episode terminates or truncates; nothing resets on its own. ## Train with PPO ```sh python -m simforge_rl.examples.ppo --tasks richmond-field-station --runtime --num-envs 8 --total-timesteps 200000 --out run/ ``` State observations need no GPU. `--tasks` picks a bundled task set: one per released map (`belmont-research-center`, `easterbrook-discovery-school`, `el-camino-road`, `richmond-field-station`, `yale-street`), each 24 to 29 weighted, seeded scenario templates (car following, cut-ins, lead braking, crossing pedestrians, unprotected turns, red-light runners) at sites verified on that map. A set is pinned to the map version it was verified on: pull that version. `--runtime` runs each worker's episodes in its own runtime process; without it the example starts `simforge env serve` processes instead, with the same observations and rewards. `run/` then holds `curve.csv`, `curve.svg`, `metadata.json` and `policy.pt`, and each evaluation also scores the scenarios' scripted driving as a baseline. `python -m simforge_rl.examples.ppo --help` lists every flag. ## Train on a pulled dataset ```sh simforge datasets pull --include scenarios --out data/my-dataset python -m simforge_rl.examples.ppo --dataset data/my-dataset --split train --runtime --max-open 16 --eval-limit 8 --out run/ ``` `--dataset` takes a pulled folder or a hosted dataset id, which it pulls first (see [Getting data out](/docs/datasets)); every file is checked against its digest, and each episode runs exactly the input its scenario's simulation ran, on the installed map the scenario names. `--max-open` bounds the sessions each worker keeps open, so a split of thousands of scenarios trains with a bounded number of sessions. In Python, `ScenarioSet.from_dataset(...)` and `ScenarioSet.bundled(...)` give the same task sets. ## Persistent worlds `simforge world serve` hosts one shared, realtime world that several clients join and drive, rather than isolated episodes. It authenticates clients with a token from a file you create first and keep private (for example `umask 077; head -c 32 /dev/urandom | xxd -p > world.token`), passed as `--token-file`; see `simforge world serve --help`. --- # Run in the cloud > Run the same simforge commands on SimForge's machines with --cloud, price them with --dry-run, cap them with --max-cost, and download the results. Source: https://simforge.ai/docs/cloud Add `--cloud` to a command and it runs on SimForge's hosted API instead of your runtime: the same command, the same scenario file, the same output. Use it for what your machine cannot do, such as photoreal renders without a large GPU, or many jobs at once. > **Beta.** SimForge Cloud is open in beta: the Studio is free to explore and 2D renders are free; paid compute (3D renders, evaluations, transfers, Recast, asset generation, `--cloud`) runs on SimForge's machines. [Sign up](https://simforge.ai/?auth=sign-up) and start with $10 in free credits; credit purchases aren't open during the beta. Work the cloud does not run yet (3D renders, evaluations, transfers and Recast until their workers arrive) is refused with `cloud_not_open`: run the command without `--cloud`. `--dry-run` still shows the price. ## Prices | What you run | Price | |---|---| | 3D render | $1.00 per render | | Open-loop evaluation | $0.25 per evaluation | | Closed-loop evaluation | $1.00 per evaluation | | Closed-loop evaluation of a model | $2.00 per evaluation | | Transfer to another map | $0.04 per scenario | | Recast (log to scenario) | $1.00 per log | | 3D asset generation | $1.00 per asset | | 2D render | Free | Each price is fixed before a run starts, and `--dry-run` shows it before anything runs. Prices are in cents on the command line (`--max-cost 100` is one dollar). Authoring, simulation, templates, OpenSCENARIO import and export, and validation are free. - A new account starts with $10 in free credits. A paid run the workspace's credits do not cover is refused before anything starts (`insufficient_credits`), with a link to the workspace's credits page. Credits are checked first, so this is the answer even for work the cloud does not run yet. - Free 2D renders run one at a time per account, up to 30 a day (UTC); past that the host answers `rate_limited`. - A run that fails costs nothing. A partial or cancelled run is charged only for the jobs that finished, and Recast only when the log became a scenario. ## Price, cap, run, download ```sh simforge render s.json --mode 3d --rig rig.json --out out-3d --cloud --dry-run simforge render s.json --mode 3d --rig rig.json --out out-3d --cloud --max-cost 200 --wait --download simforge simulate s.json --out sim --cloud --max-cost 50 --wait --download ``` - `--dry-run` returns the estimate (`costEstimateCents`) and starts nothing. On a scenario file it first saves the file to your workspace as an immutable revision (free), and the run that follows reuses exactly that revision. With `--max-cost` it still prices the run and adds `maxCost: {limitCents, exceeded}`: `exceeded: true` means the same command without `--dry-run` would be refused. - `--max-cost` (in cents) is required with `--cloud`. A run whose estimate is above it or above the workspace's daily spending cap is refused before anything starts (`budget_exceeded`); one the workspace's credits do not cover, with `insufficient_credits`. `--max-cost 0` runs only free work. - `--wait` follows the run to `succeeded`, `failed`, `partial` or `cancelled`; `--download` then writes every artifact into `--out`, sha256-checked. Giving `--out` also waits and downloads; without it the command returns as soon as the run is submitted. While it waits, stderr gets a `run.progress` line with the run id straight away, then the run's status and its jobs on each change and at least every 30 seconds; the result still comes on stdout at the end. - `--idempotency-key` makes resending a lost submission safe: the host answers with the original run and charges once. The cloud runs `render`, `simulate`, `preview frame` and `preview clip`, Recast (log ingestion), transfer (below), and downloads hosted datasets as files ([Getting data out](/docs/datasets)). Reinforcement learning stays local. Hosted evaluation launches are disabled for everyone, including workspace owners and staff. `closed-loop`, `open-loop run` and cloud `evaluate` return `403 evaluation_launch_disabled`, including dry runs. Use the **Request evaluation** form on the Model Comparison page instead. Existing evaluation results and downloads stay available. ## Recast: bring your own logs ```sh simforge import log drive.recording.json --map auto --name "left turn" --cloud --max-cost 100 --out ./drive simforge import log drive.mcap --map auto --cloud --dry-run ``` Recast turns a recorded drive into an editable scenario: a recording (`simforge.recording/v1`) or a ROS 2 MCAP lidar log becomes a scenario on one of SimForge's maps. `--map auto` takes the recording's own map, or the map its GNSS path lies in. An MCAP log is perceived on a GPU worker first; its detector's weights are CC BY-NC-SA 4.0, so scenarios made from MCAP logs are for non-commercial use. ## The same event on another map ```sh simforge transfer s.json --map yale-street --cloud --max-cost 20 --out ./yale ``` A transfer that lands creates a new scenario in the workspace; one no site on the map accepts creates nothing and exits 2 with its verdict. > Run transfer with `--cloud` for now. A local transfer needs the target map's `odr` derivative, which `simforge maps pull` does not install yet, so it stops with `map_facts_missing`. ## Follow a run ```sh simforge runs list --limit 20 simforge runs watch simforge runs cancel simforge results download --out ./results simforge usage ``` A partial or cancelled run keeps the artifacts it produced. ## Hosted scenarios A scenario stored in your workspace runs at an immutable revision, which pins the exact map version, actors and recordings: ```sh simforge scenarios list --limit 20 simforge render --revision --mode 2d --out ./render --cloud --max-cost 50 --wait --download ``` To change a scenario and keep the original, copy it. The copy is a new scenario with the same map, actors, timeline and settings, named ` (copy)`; the original's renders and runs stay with the original. It lands in the original's dataset, or in your default dataset when you cannot change that one (`--dataset` picks another): ```sh simforge scenarios copy --name "Left turn, wet" ``` For a scenario shared from another workspace, the copy keeps its pinned recordings and provenance. If that original already has a copy in your target dataset, the existing copy opens instead of making another; the shared original stays read-only. ## Sign-in, scopes and API keys `simforge login` asks for scopes that cover downloads and cloud runs. A CI job can use an API key instead: set `SIMFORGE_API_KEY` (`sfk_…`) and `SIMFORGE_API_URL`. The key acts in the workspace it was created in, and your workspace role still applies: a viewer only reads, runs need a member. Managing keys needs the `keys:admin` scope, which a plain login does not ask for. `--scope` adds a scope to the default ones, so sign in again with it, then create the key: ```sh simforge login --scope keys:admin # the default scopes plus keys:admin simforge keys create --name ci --expires-in-days 90 simforge keys list simforge keys revoke ``` A new key's secret (`sfk_…`) is printed once. You can also create and revoke keys in the browser, on your workspace's [API keys page](https://simforge.ai/dashboard/workspace/settings?view=api-keys). If `simforge auth status` then lists only `keys:admin` under `scopes`, the CLI is an older one that replaced the defaults: reinstall it (`curl -fsSL https://simforge.ai/install.sh | sh`), or name every scope (`simforge login --scope keys:admin --scope runs:write …`) as its `insufficient_scope` hint prints. An AI agent that calls the API without the CLI signs in through [auth.md](/auth.md): you approve it on simforge.ai with a code it shows you, for one organization and the scopes it asks for. ## Errors | Code (HTTP) | Meaning | Do | |---|---|---| | `cloud_not_open` (403) | SimForge Cloud is not open on this host yet (beta), or does not run this work yet: the reason names it ("3D renders aren't available yet on SimForge Cloud."), `detail.code` is `run_kind_unavailable` and `detail.kinds` its job kinds | Run the command without `--cloud` | | `email_unverified` (403) | Runs, free 2D renders included, need a verified email; `detail.resendUrl` is the page that resends the link | Open the link in your email, or resend it, then retry | | `insufficient_credits` (402) | The workspace's credits do not cover the run's price; `detail.creditsUrl` is its credits page. During the beta credit purchases are closed (`detail.checkout` is `closed`) | Run it locally, or choose free work such as a 2D render | | `rate_limited` (429) | A free 2D render over the account's limit: one at a time (`detail.limit` `free_2d_concurrent`) or 30 a day (`free_2d_daily`) | Start it when the running one finishes, or after 00:00 UTC | | `max_cost_required` (the CLI's) | `--cloud` without a cost cap; nothing was sent | Pass `--max-cost` deliberately | | `budget_exceeded` (402) | Estimate above `--max-cost` or the workspace's daily spending cap (`detail.limit` is `max_cost` or `daily_cap`). A `--dry-run` is never refused for `--max-cost`: it answers the price with `maxCost.exceeded` | Raise the cap within your budget, or stop | | `insufficient_scope` (403) | A scope (`detail.required`) or role (`detail.requiredRole`) is missing, or `--workspace` names another workspace than the login's (`detail.code` `workspace_mismatch`) | Run the login the hint names (`simforge login --scope keys:admin` for API keys), sign in for that workspace's organization, or ask an admin | | `not_found` (404) | The object does not exist or is not visible to you | Check the id and the workspace | | `conflict` (409) | A stale version or digest, or a duplicate | Read the object again, then decide | The hosted API is described by its [OpenAPI document](/docs/openapi.json). --- # Getting data out > Pull a hosted dataset, a filter of it or chosen scenarios as files with the CLI, an agent, a link list or the API; resumable, checked and verifiable offline. Source: https://simforge.ai/docs/datasets A dataset is a collection of scenarios in your hosted workspace, or one shared with it. Getting it out means downloading the files SimForge already stores for each scenario (its document, OpenSCENARIO export, trace and simulation record, and, if you ask for them, renders, labels, ground truth and recordings) straight from storage, through signed links. Nothing is packed or zipped for you, so a pull starts at once and an interrupted one picks up where it stopped. The public SimForge scenario dataset, SIMLOG-25k, is on Hugging Face: `huggingface.co/datasets/SIMFORGE-AI/simlog-25k`. Route recordings contain trajectory data, not original camera or sensor video. Renders are generated by SimForge. Public SIMLOG files omit source dataset names, log IDs and collection provenance. ## With the CLI ```sh simforge datasets list simforge datasets pull --dry-run simforge datasets pull --out ./data simforge datasets verify ./data ``` `--dry-run` prints how many scenarios and files the pull has and their size, and downloads nothing. Narrow the pull with `--filter 'grade:A tag:cut-in'` (the library's filters), `--q `, `--source-log ` or `--ids a,b,c`. `--include` chooses what comes with each scenario: `renders[:training|showcase|schematic]`, `labels`, `gt`, `recordings`, or `scenarios` for nothing else (default `renders:training`). `--passes rgb,depth` keeps only some render modalities. `--jobs` sets the parallel downloads (default 8). Every file is checked against its sha256 before it is put in place. Run the same command again to resume: it fetches only what is missing or changed. A file SimForge is still preparing (an OpenSCENARIO export, ground truth) is waited for, up to `--wait-pending` seconds (default 1800); if some are still not ready, the pull lists them and you run it again later. When everything is in place the CLI writes `manifest.json`, `scenarios.jsonl` and `SHA256SUMS`; `simforge datasets verify DIR` checks the folder against them, offline. The folder: ``` manifest.json, scenarios.jsonl, SHA256SUMS recordings/.rec.json.zst scenarios//// document.json, scenario.xosc, trace.json.gz, resolution.json.gz labels/labels.json, gt/ego.parquet, gt/actors.parquet renders/-/... ``` Scenarios or files that cannot be had (a scenario never saved, an OpenSCENARIO export that failed) are listed with their reason in `manifest.json` under `excluded`, never dropped silently. ## With an agent Give your coding agent the setup and the task, for example: ```text Install the SimForge CLI (curl -fsSL https://simforge.ai/install.sh | sh), sign in with `simforge login` and install the SimForge skills for yourself (`simforge skills install --claude` or `--codex`). Then pull dataset , only grade A scenarios with their training renders and labels: first `simforge datasets pull --filter 'grade:A' --include renders,labels --dry-run` to tell me the size, then the same command with `--out ./data`, then `simforge datasets verify ./data`. ``` Studio's Export panel shows this prompt, and the exact command, for the selection you are looking at, after checking that your CLI is installed and signed in. ## A link list, without the CLI ```sh curl -H "Authorization: Bearer $SIMFORGE_API_KEY" \ "https://simforge.ai/api/v1/datasets//files?format=urls&include=renders,labels" -o links.txt aria2c -i links.txt -d ./data -j 8 ``` The list holds a link per file, valid for 24 hours, with the file's place in the folder and its sha256, which aria2c checks. With wget instead (it does not check digests): `grep '^http' links.txt | wget -x -nH --cut-dirs=5 -i - -P ./data`. Files still being prepared are listed as comments at the end of the list; download the list again later for them. A link list has no `manifest.json`: use the CLI when you want the folder verified. ## From CI, with an API key Create a key with `datasets:read` and `artifacts:read` (`simforge keys create --name ci-pull --scope datasets:read --scope artifacts:read`, which needs a login with `keys:admin`, or the API keys page in workspace settings) and set it as `SIMFORGE_API_KEY`; the CLI and the API then need no login: ```sh SIMFORGE_API_KEY=sfk_... simforge datasets pull --include scenarios --out ./data ``` The API itself: `GET /api/v1/datasets//files` pages the selection's files (signed links valid for an hour; ask for the page again for fresh ones), `dryRun=true` counts them, and `POST` takes the same fields as a JSON body for long id lists. See the API reference. ## Copy SIMLOG-25k scenarios into your workspace Every workspace lists SIMLOG-25k read-only. Browse it, open a scenario and watch it in 2D; to edit or run scenarios, copy them into one of your datasets. ```sh simforge datasets copy --filter 'map:yale-street split:test' --new 'SIMLOG-25k copy' simforge datasets copy --ids --into ``` In Studio, **Copy to dataset…** copies the selected scenarios, current filter or whole dataset into an existing or new dataset. Large copies run in the background. A scenario already copied into that dataset is skipped, and each copy identifies its SIMLOG-25k source. --- # JSON output, errors and exit codes > The contract every simforge command keeps - one JSON document on stdout, a structured error on stderr, and exit codes 0, 1 and 2 - so scripts and agents can rely on it. Source: https://simforge.ai/docs/output Every `simforge` command keeps the same contract, so a script or an agent can drive it without parsing prose. ## stdout: one JSON document A command prints exactly one JSON document on stdout. JSON is the default; `--json` asks for it explicitly, and `--pretty` indents the same document for people. Nothing else is ever written to stdout. ## stderr: a structured error When a command fails it prints one JSON line on stderr: ```json {"code": "max_cost_required", "reason": "…", "hint": "Pass --max-cost CENTS to explicitly cap this cloud job."} ``` - `code` is stable: key your handling on it. Never parse `reason`, which is for people. - `hint` says what to do next, often the exact command. - `path` names the input at fault, and `detail` carries structured context, when there are any. - `httpStatus` and `requestId` are there when the SimForge host refused a request: quote the `requestId` to support, which traces the request by it. `login` also writes event lines (`{"event": …}`, never a `code`) on stderr while it waits: the URL and code a person must approve. `runtime install` and `maps pull` write `{"event": "distribution.download.progress", "bytes": {"done": …, "total": …}, "percent": …}` lines as they download, at each tenth of the archive and at least every 30 seconds. A command that waits on a cloud run (`--cloud --wait`, `runs watch`) writes `{"event": "run.progress", "runId": …, "status": …, "jobs": [{"kind", "status", "progress", "queue"}], "elapsedSeconds": …, "message": …}` when it first reads the run, whenever the run or a job changes, and at least every 30 seconds. ## Exit codes | Exit | Meaning | What to do | |---|---|---| | 0 | OK | Read stdout | | 1 | The command could not run: bad flags, a missing file, the runtime or a map not installed, not signed in | Fix the invocation, or install what the error names | | 2 | The command ran and found something wrong with its input: a failed check, a refused operation, a digest mismatch | Report or repair the input; retrying the same input gives the same answer | ## The command surface as data ```sh simforge --help --json simforge render --help ``` `simforge --help --json` prints every command with its arguments and flags: types, defaults, allowed values and which ones are required. `simforge --help` prints one command. An unknown flag is an error (`unknown_flag`), except after the scenario of `render`, `simulate` and `preview`, whose trailing arguments are runtime options: check those against the command's help. The same document is published at [/docs/cli.json](/docs/cli.json) and rendered as the [command reference](/docs/cli). A request the hosted API refuses (an HTTP 4xx answer, such as `budget_exceeded` or `insufficient_scope`) exits 2 with the API's own code; a server failure exits 1. ## Common error codes | Code | Meaning | |---|---| | `unknown_command`, `unknown_flag` | Not a command or flag of this CLI; read `--help` | | `missing_argument`, `bad_value` | A required argument is missing, or a value is invalid | | `not_logged_in`, `session_invalid` | Sign in with `simforge login` | | `runtime_not_installed` | Install the runtime: `simforge runtime install --accept-license` | | `runtime_platform_unsupported` | This platform has no local runtime (Windows on Arm): add `--cloud` | | `map_not_installed` | Pull the map: `simforge maps pull @ --accept-license` | | `agreement_acceptance_required` | Read `simforge runtime license`, then pass `--accept-license` | | `entitlement_required` | This download is not available to your account | | `entitlement_revoked` | SimForge revoked this account's right to the download; it is not re-granted by accepting again | | `gpu_unsupported`, `gpu_memory_unsupported`, `gpu_driver_unsupported` | The GPU is outside the [supported range](/docs/install#what-the-machine-needs) (NVIDIA with at least 8 GiB and driver 570 or newer, or Apple M3 or newer); 2D renders, simulation and `env serve --no-sensors` still work | | `native_gpu_out_of_memory` | The scene did not fit in GPU memory: use `--textures bc7-512`, fewer or smaller cameras, or `--cloud` | | `signature_missing`, `signature_invalid`, `signature_untrusted`, `signature_wrong_host`, `signature_mismatch` | The download is not one SimForge signed for this host; nothing was installed | | `runtime_install_invalid` | The runtime found is not an installed SimForge runtime release (no `runtime-manifest.json` beside it): run `simforge runtime install --accept-license` | | `recording_missing` | A scenario that replays a recording cannot find `recordings/.rec.json.gz` beside its `document.json` | | `plan2d_map_missing` | The map's 2D derivative is not installed: pull the map again | | `max_cost_required`, `budget_exceeded` | A cloud run needs a cost cap, or its estimate is over it | | `insufficient_credits` | The workspace's credits do not cover a cloud run's [price](/docs/cloud#prices) | | `cloud_not_open` | SimForge Cloud is not open on this host yet, or does not run that work yet (`detail.code` `run_kind_unavailable`): run without `--cloud` | | `email_unverified` | Downloads and cloud runs need a verified email: open the link in your email, or resend it from the page the hint names | | `file_exists` | The output exists; pass `--force` to overwrite it | Codes for a specific area are on its page: [sign-in and runtime](/docs/sign-in), [cloud](/docs/cloud). --- # Usage telemetry > What the SimForge runtime reports about how it is used, what it never collects, and how to see the data before it is sent. Source: https://simforge.ai/docs/telemetry Usage telemetry is a condition of the SimForge account and download terms, which cover SimForge's services and downloads; the runtime's own license is PolyForm Noncommercial 1.0.0. The runtime reports how it is used, tied to the account you signed in with, so we can see which GPUs people use, how many jobs they run and where the local stack breaks. There is no off switch; commercial licences can agree otherwise. ## Collected - Your account id (from sign-in) and an id for this installation. - The CLI and runtime versions, the operating system and architecture. - The GPU model, its memory, the driver version and the GPU count. - For each job: its kind (simulate, render 2D, schematic or 3D, preview, RL episodes, evaluate), the render profile, the sensor count, frames, wall-clock seconds, GPU-busy seconds, and the outcome (ok, or the error code). - For reinforcement learning: episodes and environment steps. - The name of a public map, when one is used. ## Never collected - Your email, name, username or host name. The account id is enough; your email stays in the account system. - Your IP address: the server drops it. - File paths, scenario content or names, environment variables. - Anything about a private or customer map. ## See what is sent ```sh simforge telemetry show ``` prints the batches waiting to be sent, exactly as they will be sent. When you are signed out it shows the batches sealing would make, with no account id. The runtime records each finished job (and, for reinforcement learning, a session's episodes and steps) in `~/.simforge/usage/`. The runtime, not the open CLI, sends the records in the background, at most once a day after a job and also after `simforge login`, `maps pull` and `runtime install`. A job never waits on the network, and records that cannot be sent yet stay on the machine until the next attempt. `DO_NOT_TRACK` does not apply. --- # For AI agents > Where an AI agent finds the SimForge docs, the machine-readable command surface and API, and the skills pack, and how to drive the CLI safely. Source: https://simforge.ai/docs/agents SimForge is built to be driven by agents. The CLI an agent uses is the one people use: every command prints JSON and fails with a stable error code. ## Where things are | What | Where | |---|---| | Docs index for agents | [/llms.txt](/llms.txt) | | Every page and the command reference in one file | [/llms-full.txt](/llms-full.txt) | | Any page as Markdown | add `.md`: [/docs/install.md](/docs/install.md) | | The command surface as JSON (`simforge.cli-surface/v1`) | [/docs/cli.json](/docs/cli.json), or `simforge --help --json` | | The hosted API (`--cloud`) as OpenAPI | [/docs/openapi.json](/docs/openapi.json) | | Agent skills for Claude Code, Codex and AGENTS.md agents | [Skills pack](/docs/skills), or `simforge skills install` | | How an agent signs in to the hosted API without the CLI (auth.md) | [/auth.md](/auth.md) | ## Driving the CLI 1. Read the surface first: `simforge --help --json`. Never guess a flag. 2. Parse stdout as one JSON document. On failure, key on `code` in the stderr JSON and follow its `hint`. 3. Exit 1 means fix the invocation or install what is missing. Exit 2 means the input itself is wrong: report or repair it, do not retry it unchanged. 4. Sign-in needs a person: `simforge login --device` prints a URL and code. Relay them to the user and wait. Never print or store tokens. 5. Cloud runs cost money. Always price with `--dry-run` first and pass a `--max-cost` no higher than the budget you were given. 6. Long-running servers (`simforge env serve`, `simforge world serve`, `simforge mcp`) run until stopped: start them in their own terminal, not inside a call that waits for them to finish. ## Signing in without the CLI An agent that calls the hosted API (`/api/v1`) directly signs in through [auth.md](/auth.md), the recipe for agent sign-in that every `/api/v1` 401 points to (`WWW-Authenticate: Bearer resource_metadata=…`). The agent registers with the email of the person's SimForge account and shows the person a link and a code. The person signs in on simforge.ai, enters the code, chooses the organization and approves. The agent then gets a one-hour access token and an identity assertion that renews it. Only that account can approve, and only once its email is verified. The person sees the agent under Account → CLI sessions and can revoke it there. A backend with no person at hand uses an API key instead. ## MCP ```sh simforge mcp ``` `simforge mcp` serves the CLI over the Model Context Protocol (stdio): one tool per command, with its arguments and flags as typed inputs. Every call runs the same binary and returns the command's JSON result, exit code and error. `--only render --only scenario` limits the tools; `--cwd` sets the working folder. Local runs are the default; a tool call with `cloud: true` and a `max-cost` runs hosted. ## Skills The [skills pack](/docs/skills) gives an agent task-level instructions: the quickstart, authoring, rendering, the cloud, RL, maps, scenario packages and troubleshooting. It ships inside the CLI: ```sh simforge skills install --claude ``` --- # Agent skills pack > Install the SimForge agent skills into Claude Code, Codex or any agent that reads AGENTS.md, from the CLI or as a zip download. Source: https://simforge.ai/docs/skills The skills teach a coding agent to use SimForge: which command comes next, what each error means, and what never to do (such as spending past a cost cap). Each skill is a folder with a `SKILL.md`, in the Agent Skills format. ## Install from the CLI The skills ship inside the `simforge` binary, so they always match its commands: ```sh simforge skills list simforge skills install --claude # ~/.claude/skills simforge skills install --codex # $CODEX_HOME/skills, else ~/.codex/skills simforge skills install --dir ./skills # any folder ``` `skills install` records what it wrote. Installing again updates those files; a file you changed by hand is refused (exit 2, `skills_modified`) unless you pass `--force`. ## Download the pack [simforge-skills.zip](/docs/simforge-skills.zip) holds the same skills, an `AGENTS.md` index and a README: ```sh curl -fsSLO https://simforge.ai/docs/simforge-skills.zip unzip simforge-skills.zip ``` | Agent | Install | |---|---| | Claude Code | `cp -R simforge-skills/skills/* ~/.claude/skills/`, or a project's `.claude/skills/` | | Codex | `cp -R simforge-skills/skills/* "${CODEX_HOME:-$HOME/.codex}/skills/"` | | Agents that read AGENTS.md | Copy `simforge-skills/skills/` into the repository and add `simforge-skills/AGENTS.md` to its AGENTS.md | | Any other agent | Give it [/llms.txt](/llms.txt), or the `SKILL.md` files as instructions | ## The skills | Skill | Use it for | |---|---| | `simforge-quickstart` | A first task: install, sign in, download a map, author, render, RL | | `simforge-cli` | The JSON contract, sign-in, cost caps and the MCP server | | `simforge-authoring` | Building and editing scenarios with typed operations | | `simforge-render` | Previews and renders in 2D, schematic and 3D | | `simforge-cloud` | Hosted runs with `--cloud`, caps, runs and results | | `simforge-closed-loop` | Local reinforcement learning with SimForge Gym (`simforge-rl`) | | `simforge-maps-assets` | Map and actor downloads, caches and licences | | `simforge-scenario-package` | Verifying and importing `.scenario.zip` packages | | `simforge-troubleshoot` | Reading `simforge doctor` and error codes | --- # simforge command reference CLI version 0.2.0-rc.5. Generated from `simforge --help --json`; the same document is at /docs/cli.json. Output: stdout: one JSON document (indented with --pretty); stderr: {code, path?, reason, detail?} on failure. ## Global flags | Flag | Notes | What | |---|---|---| | `--pretty` | | Indent the JSON result for humans (the same document, never a different one) | | `--json` | | Explicit machine-readable JSON output (also the default) | | `--cloud` | | Execute locally by default; submit the same input as an authenticated cloud job | | `--workspace cloud_workspace` | env SIMFORGE_WORKSPACE_ID | With --cloud: the workspace you expect the login to act in; a different one is refused (workspace_mismatch) | | `--idempotency-key IDEMPOTENCY_KEY` | | With --cloud: a key that makes resending a lost submission safe (the host answers with the original run and charges once) | | `--dry-run` | | With --cloud: price the job (costEstimateCents) and start nothing | | `--api-url API_URL` | | Hosted API base URL for API-key use. Default: SIMFORGE_API_URL, then the signed-in host's /api/v1 | | `--map-version MAP_VERSION` | | Exact hosted map version for a scenario file. Default: the installed release's, else the one hosted version carrying the file's OpenDRIVE digest | | `--help` | | Print machine-readable command help. | ## Exit codes | Exit | Meaning | |---|---| | 0 | ok | | 1 | command error: the command could not run | | 2 | findings: the command ran and found something wrong with its input | ## assets ### simforge assets pull Pull the actor-asset closure and its ATTRIBUTION.json, verifying every blob's sha256 Usage: `simforge assets pull [OPTIONS]` Runs on: local runtime. | Argument / flag | Notes | What | |---|---|---| | `--only ONLY` | one of actors, sky | Pull only this closure. Default: both the actor closure and the sky plates | | `--closure SHA256` | | The actor closure digest (sha256). Default: the closure this build is pinned to | | `--base-url URL` | | Asset store base URL (https:// or file://). Default: SIMFORGE_ACTOR_ASSETS_BASE_URL, then the public store | | `--root DIR` | | Local asset root. Default: SIMFORGE_ACTOR_ASSETS_ROOT, then $XDG_DATA_HOME/simforge/actor-assets | | `--timeout SECONDS` | default 60 | Seconds to wait for a connection or for a response to start | ## auth ### simforge auth status The account, organization, scopes, expiry, host and credential store of this machine's login, checked with the host (tokenValid), and the account's email verification Usage: `simforge auth status [OPTIONS]` | Argument / flag | Notes | What | |---|---|---| | `--host HOST` | | SimForge host. Default: SIMFORGE_HOST, then the host of the last login, then simforge.ai | ## authoring ### simforge authoring capabilities What this credential may do on this host: operation, host-operation, feature and worker status Usage: `simforge authoring capabilities [OPTIONS]` ### simforge authoring catalogue The host's operation catalogue (simforge.authoring-operations/v1) Usage: `simforge authoring catalogue [OPTIONS]` ### simforge authoring discover Bounded map discovery without the map package: locations, lanes, movements or the actor catalogue Usage: `simforge authoring discover [OPTIONS] --map --kind ` | Argument / flag | Notes | What | |---|---|---| | `--map MAP_VERSION_ID` | required | | | `--kind KIND` | required; one of locations, lanes, movements, catalogue | | | `--limit N` | | | ### simforge authoring operations Every operation with its `drafts apply` flags, result schema and the host's status for this credential Usage: `simforge authoring operations [OPTIONS] [OP_ID]` | Argument / flag | Notes | What | |---|---|---| | `` | | One operation id (camelCase or its kebab-case command); absent, every operation | ### simforge authoring spawn Whether an actor can be placed at a lane position or scene point, optionally against a draft's actors Usage: `simforge authoring spawn [OPTIONS] --map --catalog-id ` | Argument / flag | Notes | What | |---|---|---| | `--map MAP_VERSION_ID` | required | | | `--catalog-id ID` | required | | | `--lane-rsl RSL` | | A lane handle from `authoring discover --kind lanes` (with --s) | | `--s S` | | | | `--x X` | | A scene point (with --z) | | `--z Z` | | | | `--heading-rad HEADING_RAD` | | | | `--lateral-m LATERAL_M` | | | | `--opposing` | | | | `--static` | | | | `--draft DRAFT_ID` | | Check overlap against this draft's actors (needs --expected-version and --expected-digest) | | `--expected-version N` | | | | `--expected-digest SHA256` | | | ## batch ### simforge batch Sites x draws matrix of a template: instantiate -> simulate -> evaluate per cell, resumable Usage: `simforge batch [OPTIONS] --out