{"title":"Simulation","description":"Run your robot in simulation with horus-sim3d before deploying to hardware","slug":"getting-started/simulation","content":"# Simulation\n\nHORUS separates your robot logic from the hardware it runs on. The same node code that reads from a physical LiDAR or IMU works identically with simulated sensors, because both publish to the same shared-memory topics using the same message types.\n\n## Overview\n\n**horus-sim3d** is a 3D physics simulator distributed as a HORUS CLI plugin. It is not bundled with HORUS -- you install it separately:\n\n```bash\nhorus install horus-sim3d\n```\n\nOnce installed, one flag swaps between simulation and real hardware:\n\n```bash\nhorus run          # real hardware\nhorus run --sim    # simulated sensors via horus-sim3d\n```\n\nYour robot code does not change. Nodes subscribe to topics like `turtlebot.front_lidar.scan` regardless of whether the data comes from a physical RPLidar or a simulated laser scanner.\n\n## Quick Start\n\n### 1. Create a Project\n\n```bash\nhorus new my_robot\ncd my_robot\n```\n\n### 2. Add a Robot Description\n\nEdit `horus.toml` to point at your URDF file:\n\n```toml\n[robot]\nname = \"turtlebot\"\ndescription = \"robot.urdf\"\n```\n\nPlace your URDF file in the project root (or a subdirectory -- the path is relative to the project root).\n\n### 3. Define Hardware\n\nAdd a `[hardware]` section listing every device. Mark devices that should run in simulation with `sim = true`:\n\n```toml\n[hardware]\nlidar = { use = \"rplidar\", port = \"/dev/ttyUSB0\", sim = true, noise = 0.01 }\nimu = { use = \"bno055\", bus = \"/dev/i2c-1\", sim = true }\n```\n\n### 4. Run in Simulation\n\n```bash\nhorus run --sim\n```\n\nThis launches horus-sim3d, loads your URDF, and starts publishing simulated sensor data on the same topics your nodes already subscribe to. Only devices with `sim = true` are replaced by the simulator -- the rest connect to real hardware as usual.\n\n### 5. Run on Real Hardware\n\nWhen you are ready for the real robot:\n\n```bash\nhorus run\n```\n\nNo code changes. The scheduler loads the `[hardware]` section and connects to physical hardware. The `sim` field is ignored when running without `--sim`.\n\n## Configuration\n\n### The `[robot]` Section\n\nThe `[robot]` table in `horus.toml` tells HORUS about your robot model:\n\n```toml\n[robot]\nname = \"turtlebot\"            # used in topic naming\ndescription = \"robot.urdf\"    # URDF file path (passed to the simulator)\nsimulator = \"sim3d\"            # which simulator plugin (optional, default: sim3d)\n```\n\n| Key | Required | Description |\n|-----|----------|-------------|\n| `name` | Yes | Robot name. Used as the first segment in all topic names (e.g., `turtlebot.front_lidar.scan`) |\n| `description` | No | Path to URDF, Xacro, SDF, or MJCF file relative to project root. Passed to the simulator for model loading |\n| `simulator` | No | Simulator plugin name. Defaults to `\"sim3d\"`. HORUS launches `horus-{simulator}` when you run `--sim` |\n\n### The `[hardware]` Section\n\n`[hardware]` defines all your devices in one place. Each device can optionally include `sim = true` to indicate it should be replaced by the simulator when running in `--sim` mode.\n\nWhen you run `horus run --sim`, devices with `sim = true` are handed off to the simulator. Devices **without** `sim = true` (or with `sim = false`) stay connected to real hardware. This lets you mix real and simulated hardware in a single configuration block.\n\n```toml\n[hardware]\nlidar = { use = \"rplidar\", port = \"/dev/ttyUSB0\", sim = true, noise = 0.01 }\nimu = { use = \"bno055\", bus = \"/dev/i2c-1\", sim = true }\ncamera = { use = \"realsense\" }\n# camera has no sim = true -- stays real even in --sim mode\n```\n\n### Mixed Mode\n\nYou can selectively simulate individual devices while keeping others connected to real hardware:\n\n```bash\nhorus run --sim lidar          # only lidar is simulated, IMU + camera stay real\nhorus run --sim lidar camera   # lidar + camera simulated, IMU stays real\nhorus run --sim                # all devices with sim = true are simulated\n```\n\nThis is useful for testing a new perception algorithm against simulated LiDAR while the robot's IMU and motors are connected to real hardware.\n\n## Hardware Types\n\nEach entry in `[hardware]` specifies a device source:\n\n| Type | TOML Syntax | What It Does |\n|------|-------------|-------------|\n| **Terra** | `use = \"rplidar\"` | Uses a pre-built Terra hardware driver |\n| **Package** | `package = \"horus-driver-ati-netft\"` | Installs and runs a registry driver package |\n| **Node** | `node = \"ConveyorDriver\"` | Runs a local node registered via `register_driver!` |\n| **Crate** | `crate = \"rplidar-driver\"` | Adds a Rust crate from crates.io to `.horus/Cargo.toml` |\n| **PyPI** | `pip = \"adafruit-bno055\"` | Adds a Python package from PyPI to `.horus/pyproject.toml` |\n| **Exec** | `exec = \"./sensor_bridge.py\"` | Launches as a subprocess with health monitoring |\n\nAny device can include `sim = true` alongside its source to mark it as simulatable. When `--sim` is active for that device, the simulator takes over instead of loading the real driver.\n\nDevices can include additional parameters as key-value pairs:\n\n```toml\n[hardware.arm]\nuse = \"dynamixel\"\nport = \"/dev/ttyUSB0\"\nbaudrate = 1000000\nservo_ids = [1, 2, 3, 4, 5, 6]\nsim = true\n```\n\n## Topic Naming Convention\n\nHORUS uses a shared topic naming convention that both real hardware and the simulator follow. This is why your code works in both modes without changes.\n\n**Format**: `{robot_name}.{sensor_name}.{data_type}`\n\nTopics use dots as separators (not slashes). This is required because HORUS topics are backed by shared memory files, and `shm_open()` on macOS does not allow slashes in names.\n\n### Standard Data Type Suffixes\n\n| Suffix | Sensor / Use | Message Type |\n|--------|-------------|-------------|\n| `scan` | 2D/3D LiDAR | `LaserScan` |\n| `imu` | IMU (accel + gyro) | `Imu` |\n| `gps` | GPS receiver | `Gps` |\n| `image` | RGB camera | `Image` |\n| `depth` | Depth camera | `Image` |\n| `camera_info` | Camera intrinsics | `CameraInfo` |\n| `odom` | Wheel odometry | `Odometry` |\n| `cmd_vel` | Velocity commands | `CmdVel` |\n| `joint_state` | Joint positions/velocities | `JointState` |\n| `joint_cmd` | Joint commands | `JointCommand` |\n| `wrench` | Force/torque sensor | `Wrench` |\n| `sonar` | Ultrasonic sonar | `Sonar` |\n| `encoder` | Rotary encoder | `Encoder` |\n| `radar` | Radar with Doppler | `Radar` |\n| `segmentation` | Semantic segmentation | `Image` |\n| `thermal` | Thermal/IR camera | `Image` |\n| `event_camera` | Dynamic vision sensor | `EventCamera` |\n| `pointcloud` | 3D point cloud | `PointCloud` |\n\n### Examples\n\n```text\nturtlebot.front_lidar.scan       # 2D LiDAR scan data\nturtlebot.imu_sensor.imu         # IMU readings\nturtlebot.rgb_camera.image       # RGB camera image\nturtlebot.rgb_camera.depth       # Depth from an RGB-D camera\nturtlebot.rgb_camera.camera_info # Camera intrinsics\nturtlebot.odom                   # Robot-level odometry (no sensor_name)\nturtlebot.cmd_vel                # Velocity commands (no sensor_name)\nturtlebot.joint_state            # Joint positions (no sensor_name)\n```\n\nRobot-level topics (odometry, velocity commands, joint state) omit the sensor name segment -- they apply to the whole robot, not a specific sensor.\n\n## Sim Control Services\n\nWhen horus-sim3d is running, your nodes can control the simulation at runtime through service topics. Each service has a `.request` topic (you write to) and a `.response` topic (you read from).\n\n| Service | Topic Name | Request Type | What It Does |\n|---------|-----------|-------------|-------------|\n| **Spawn** | `sim.spawn` | `SpawnRequest` | Spawn a model (URDF, SDF) or primitive (box, sphere, cylinder) |\n| **Despawn** | `sim.despawn` | `DespawnRequest` | Remove an entity by ID |\n| **Teleport** | `sim.teleport` | `TeleportRequest` | Instantly move an entity to a new pose |\n| **Pause** | `sim.pause` | `PauseRequest` | Pause physics and sensor updates |\n| **Resume** | `sim.resume` | `ResumeRequest` | Resume a paused simulation |\n| **Raycast** | `sim.raycast` | `RaycastRequest` | Cast a ray and get the hit point, normal, and entity ID |\n| **Get State** | `sim.state.get` | `GetStateRequest` | Query sim time, entity count, paused status, physics dt |\n| **Set Param** | `sim.param.set` | `SetParamRequest` | Change gravity or physics timestep at runtime |\n\nThese service names are defined in `horus_robotics::convention::service` and are **simulator-agnostic** -- any simulator plugin that implements them will work with `horus run --sim`.\n\n### Example: Spawning an Obstacle\n\nRust:\n\n```rust\n// simplified\nuse horus::prelude::*;\nuse horus_robotics::messages::simulation::*;\n\n// Create request/response topics\nlet req: Topic<SpawnRequest> = Topic::new(\"sim.spawn.request\")?;\nlet resp: Topic<SpawnResponse> = Topic::new(\"sim.spawn.response\")?;\n\n// Spawn a box at position (2, 0.5, 0) with size 1x1x1\nreq.send(SpawnRequest {\n    model: \"box\".into(),\n    name: \"obstacle_1\".into(),\n    position: [2.0, 0.5, 0.0],\n    scale: [1.0, 1.0, 1.0],\n    ..Default::default()\n});\n\n// Wait for response\nstd::thread::sleep(std::time::Duration::from_millis(50));\nif let Some(response) = resp.recv() {\n    if response.success {\n        println!(\"Spawned entity {} with ID {}\", response.name, response.entity_id);\n    }\n}\n```\n\nPython:\n\n```python\n# simplified\n# NOTE: SpawnRequest/SpawnResponse are not yet available in Python bindings.\n# Use dict-based topics for simulation control from Python:\nimport horus\n\nspawn_req = horus.Topic(\"sim.spawn.request\")\nspawn_req.send({\n    \"model\": \"box\",\n    \"name\": \"obstacle_1\",\n    \"position\": [2.0, 0.5, 0.0],\n    \"scale\": [1.0, 1.0, 1.0],\n})\n```\n\n## sim.toml (Optional)\n\n`sim.toml` is an optional configuration file for horus-sim3d. It provides fine-grained control over physics, sensors, actuators, world setup, and rendering. Place it in your project root and pass it with `--config`:\n\n```bash\nhorus sim3d --config sim.toml --robot robot.urdf\n```\n\n\n### Physics Configuration\n\n```toml\n[physics]\ndt = 0.001                      # timestep in seconds (default: 1/240)\nsolver = \"newton\"                # \"newton\", \"lcp\", or \"smooth\"\nintegrator = \"semi_implicit_euler\" # \"semi_implicit_euler\", \"rk4\", \"velocity_verlet\", \"implicit_euler\"\nmax_iterations = 100\nsubsteps = 1\n\n[physics.contact]\ndamping = 0.5\nstiffness = 100000.0\n```\n\n### World Configuration\n\n```toml\n[world]\nground = true                    # enable ground plane\ngravity = [0.0, -9.81, 0.0]     # gravity vector [x, y, z] in m/s^2\nscene = \"warehouse.yaml\"         # optional scene file\n\n[[world.objects]]\nname = \"wall_1\"\nmodel = \"box\"\nposition = [5.0, 1.0, 0.0]\nscale = [0.2, 2.0, 10.0]\nis_static = true\n\n[world.terrain]\ntype = \"heightfield\"\nsource = \"terrain.png\"\nsize = [100.0, 100.0]\nheight_scale = 5.0\n```\n\n### Sensor Overrides\n\n```toml\n[sensors.front_lidar]\ntype = \"lidar\"\nlink = \"lidar_link\"              # URDF link to attach to\nrate_hz = 10                     # publish rate (overrides URDF value)\nrays = 360\nrange = [0.1, 30.0]\nfov = 360.0\nnoise = { type = \"gaussian\", std = 0.01 }\n\n[sensors.imu_sensor]\ntype = \"imu\"\nlink = \"imu_link\"\nrate_hz = 100\nnoise = { type = \"gaussian\", std = 0.005 }\n```\n\n### Actuator Configuration\n\n```toml\n[actuators.drive]\ntype = \"differential_drive\"\ntopic = \"cmd_vel\"\nwheel_radius = 0.033\nwheel_separation = 0.16\nmax_speed = 1.0\nmax_torque = 2.0\n\n[actuators.drive.motor_model]\ntype = \"dc\"                       # DC motor model for sim-to-real fidelity\n\n[actuators.drive.latency]\ncommand_ms = 5                    # simulated command latency\nsensor_ms = 2                    # simulated sensor feedback latency\n```\n\n### Full sim.toml Structure\n\n| Section | Purpose |\n|---------|---------|\n| `[robot]` | Robot name and URDF path |\n| `[physics]` | Timestep, solver, integrator, contact parameters |\n| `[world]` | Ground plane, gravity, terrain, static/dynamic objects |\n| `[sensors.*]` | Per-sensor configuration: type, rate, noise, link attachment |\n| `[actuators.*]` | Per-actuator configuration: type, motor model, latency |\n| `[controllers.*]` | Controller configuration (PID, etc.) |\n| `[materials.*]` | Physics material definitions (friction, restitution) |\n| `[topics]` | Topic naming and remapping |\n| `[visual]` | Rendering settings and camera modes |\n| `[recording]` | Trajectory and sensor data recording |\n| `[rl]` | Reinforcement learning training configuration |\n\n## URDF Sensors\n\nhorus-sim3d reads sensor definitions from your URDF file's `<gazebo>` elements. This is the primary source for sensor placement and configuration:\n\n```xml\n<robot name=\"turtlebot\">\n  <link name=\"lidar_link\">\n    <!-- link geometry -->\n  </link>\n\n  <gazebo reference=\"lidar_link\">\n    <sensor type=\"ray\" name=\"front_lidar\">\n      <ray>\n        <scan>\n          <horizontal>\n            <samples>360</samples>\n            <min_angle>-3.14159</min_angle>\n            <max_angle>3.14159</max_angle>\n          </horizontal>\n        </scan>\n        <range>\n          <min>0.1</min>\n          <max>30.0</max>\n        </range>\n      </ray>\n      <update_rate>10</update_rate>\n    </sensor>\n  </gazebo>\n</robot>\n```\n\nThe `[sensors]` section in sim.toml **overlays on top** of what is in the URDF. You do not need to duplicate sensor definitions -- only specify what you want to override:\n\n```toml\n# Override just the rate and add noise -- geometry comes from the URDF\n[sensors.front_lidar]\ntype = \"lidar\"\nrate_hz = 20                     # override URDF's 10 Hz to 20 Hz\nnoise = { type = \"gaussian\", std = 0.02 }\n```\n\nIf a sensor name in sim.toml matches a URDF sensor name, the sim.toml values are merged onto the URDF values. If the name does not match any URDF sensor, sim3d spawns it as an additional sensor.\n\n### Supported Sensor Types\n\nhorus-sim3d supports 16 sensor types:\n\n| Type | sim.toml `type` | Data Suffix | Description |\n|------|----------------|-------------|-------------|\n| 2D/3D LiDAR | `lidar` | `scan` | Ray-based laser scanner |\n| IMU | `imu` | `imu` | Accelerometer + gyroscope |\n| RGB Camera | `rgb_camera` | `image` | Color camera with render pipeline |\n| Depth Camera | `depth_camera` | `depth` | Depth-only camera |\n| Stereo Camera | `stereo_camera` | `image` | Stereo camera pair |\n| GPS | `gps` | `gps` | GNSS receiver |\n| Force/Torque | `force_torque` | `wrench` | 6-axis force/torque on a joint |\n| Contact | `contact` | `contact` | Binary contact detection |\n| Encoder | `encoder` | `encoder` | Rotary or absolute encoder on a joint |\n| Sonar | `sonar` | `sonar` | Ultrasonic distance sensor |\n| Radar | `radar` | `radar` | Doppler radar |\n| Thermal Camera | `thermal_camera` | `thermal` | Infrared camera |\n| Event Camera | `event_camera` | `event_camera` | Dynamic vision sensor (neuromorphic) |\n| Barometer | `barometer` | `barometer` | Atmospheric pressure sensor |\n| Magnetometer | `magnetometer` | `magnetometer` | 3-axis compass |\n| Altimeter | `altimeter` | `altimeter` | Altitude sensor |\n\n## Complete Example\n\nHere is a complete project setup for a differential-drive robot with LiDAR, IMU, and a camera.\n\n**horus.toml**:\n\n```toml\n[package]\nname = \"my-robot\"\nversion = \"0.1.0\"\n\n[robot]\nname = \"turtlebot\"\ndescription = \"robot.urdf\"\n\n[hardware]\nlidar = { use = \"rplidar\", port = \"/dev/ttyUSB0\", sim = true, noise = 0.01 }\nimu = { use = \"bno055\", bus = \"/dev/i2c-1\", sim = true }\ncamera = { use = \"realsense\", sim = true }\n```\n\n**src/main.rs** (works in both sim and real):\n\n```rust\n// simplified\nuse horus::prelude::*;\nuse horus_robotics::prelude::*;\n\nfn main() -> anyhow::Result<()> {\n    let scan: Topic<LaserScan> = Topic::new(\"turtlebot.front_lidar.scan\")?;\n    let imu_data: Topic<Imu> = Topic::new(\"turtlebot.imu_sensor.imu\")?;\n    let cmd: Topic<CmdVel> = Topic::new(\"turtlebot.cmd_vel\")?;\n\n    Scheduler::new()\n        .add(\"navigator\", |ctx| {\n            // Read latest sensor data\n            if let Some(scan) = scan.recv() {\n                let min_range = scan.ranges.iter().copied().fold(f32::MAX, f32::min);\n\n                if min_range < 0.5 {\n                    // Obstacle close -- turn\n                    cmd.send(CmdVel { linear_x: 0.0, angular_z: 0.5, ..Default::default() });\n                } else {\n                    // Clear path -- drive forward\n                    cmd.send(CmdVel { linear_x: 0.3, angular_z: 0.0, ..Default::default() });\n                }\n            }\n        })\n        .rate(10.hz())\n        .build()?;\n\n    Ok(())\n}\n```\n\nRun it:\n\n```bash\n# Test in simulation first\nhorus run --sim\n\n# Deploy to real hardware\nhorus run\n```\n\n## sim3d Deep Dive\n\nThe horus-sim3d plugin has extensive documentation covering every aspect of the simulator. These guides go deeper than this overview page.\n\n### References\n\n| Guide | What it covers |\n|-------|---------------|\n| [CLI Reference](https://github.com/softmata/horus-sim3d/blob/main/docs/cli-reference.md) | All sim3d flags: `--mode`, `--world`, `--speed`, `--no-gui`, `--namespace`, `--driver-mode` |\n| [sim.toml Configuration](https://github.com/softmata/horus-sim3d/blob/main/docs/configuration.md) | Complete reference for physics, world, sensors, actuators, materials, visual, recording, RL (1300+ lines) |\n| [Scene Format](https://github.com/softmata/horus-sim3d/blob/main/docs/scene-format.md) | YAML world file schema — static objects, terrain, lighting, spawn points |\n| [Robot Loading](https://github.com/softmata/horus-sim3d/blob/main/docs/robot-loading.md) | URDF, MJCF, SDF parsing — how robot models are loaded and configured |\n| [Sensor Reference](https://github.com/softmata/horus-sim3d/blob/main/docs/sensors.md) | All 16 sensor types — configuration, noise models, rate tuning, URDF overlay |\n| [Actuators Reference](https://github.com/softmata/horus-sim3d/blob/main/docs/actuators.md) | Motor models, latency simulation, traction, battery |\n| [Topic Wiring](https://github.com/softmata/horus-sim3d/blob/main/docs/topic-wiring.md) | How sim3d topics map to horus hardware topics — the sim/real swap mechanism |\n| [Physics Engine](https://github.com/softmata/horus-sim3d/blob/main/docs/physics.md) | Featherstone ABA/RNEA/CRBA, LCP contacts, solver selection, integrators |\n| [Performance Tuning](https://github.com/softmata/horus-sim3d/blob/main/docs/performance.md) | Substeps, dt, headless mode, GPU acceleration, profiling |\n| [Multi-Robot](https://github.com/softmata/horus-sim3d/blob/main/docs/multi-robot.md) | Namespace isolation, multi-sim coordination |\n| [Recording](https://github.com/softmata/horus-sim3d/blob/main/docs/recording.md) | Trajectory capture, sensor data recording, video export |\n| [RL Training](https://github.com/softmata/horus-sim3d/blob/main/docs/reinforcement-learning.md) | GymVecEnv, domain randomization, curriculum, Python bindings |\n| [Cloud Deployment](https://github.com/softmata/horus-sim3d/blob/main/docs/deployment/cloud.md) | Running sim3d headless in cloud/CI environments |\n| [Editor](https://github.com/softmata/horus-sim3d/blob/main/docs/editor.md) | Entity inspector, visual debugging tools |\n\n### Tutorials\n\n| Tutorial | What you build |\n|----------|---------------|\n| [1: Basic Simulation](https://github.com/softmata/horus-sim3d/blob/main/docs/tutorials/01_basic_simulation.md) | Load a world, spawn objects, control physics |\n| [2: Robot Simulation](https://github.com/softmata/horus-sim3d/blob/main/docs/tutorials/02_robot_simulation.md) | Load URDF, wire sensors, drive a robot |\n| [3: Sensors](https://github.com/softmata/horus-sim3d/blob/main/docs/tutorials/03_sensors.md) | Configure all sensor types, add noise, tune rates |\n| [4: Reinforcement Learning](https://github.com/softmata/horus-sim3d/blob/main/docs/tutorials/04_reinforcement_learning.md) | Set up RL training with vectorized environments |\n\n### How `horus run --sim` Works\n\nWhen you run `horus run --sim`, the CLI:\n\n1. Reads `[robot].name` and `[robot].description` from `horus.toml`\n2. Sets `HORUS_SIM_MODE=1` so the hardware system identifies devices with `sim = true`\n3. Launches sim3d as a background process: `horus-sim3d --driver-mode --robot robot.urdf --robot-name turtlebot`\n4. The `--driver-mode` flag tells sim3d to use the shared topic naming convention (`{robot}.{sensor}.{type}`) without a namespace prefix -- so topics match what real hardware produces\n5. sim3d parses the URDF, creates physics world, attaches sensors, and starts publishing to horus SHM topics\n6. Your code runs normally -- nodes subscribe to the same topics regardless of sim or real\n7. On exit, horus kills the sim3d process\n\nIf sim3d is not installed, you get a helpful message: `Install with: horus install horus-sim3d`\n\n## Next Steps\n\n- [Deterministic Mode](/advanced/deterministic-mode) -- reproducible execution with virtual time for simulation and testing\n- [Package Management](/package-management/package-management) -- install plugins and packages from the HORUS registry\n- [Standard Messages](/stdlib/messages/cmd-vel) -- message types used by topics","headings":["Simulation","Overview","Quick Start","1. Create a Project","2. Add a Robot Description","3. Define Hardware","4. Run in Simulation","5. Run on Real Hardware","Configuration","The [robot] Section","The [hardware] Section","camera has no sim = true -- stays real even in --sim mode","Mixed Mode","Hardware Types","Topic Naming Convention","Standard Data Type Suffixes","Examples","Sim Control Services","Example: Spawning an Obstacle","simplified","NOTE: SpawnRequest/SpawnResponse are not yet available in Python bindings.","Use dict-based topics for simulation control from Python:","sim.toml (Optional)","Physics Configuration","World Configuration","Sensor Overrides","Actuator Configuration","Full sim.toml Structure","URDF Sensors","Override just the rate and add noise -- geometry comes from the URDF","Supported Sensor Types","Complete Example","Test in simulation first","Deploy to real hardware","sim3d Deep Dive","References","Tutorials","How horus run --sim Works","Next Steps"],"wordCount":2845}