Simulating a Robot#

This tutorial shows you how to create and simulate a robot using SimulationManager. You’ll learn how to load a robot from URDF files, configure control systems, and run basic robot simulation with joint control.

The Code#

The tutorial corresponds to the create_robot.py script in the scripts/tutorials/sim directory.

Code for create_robot.py
  1# ----------------------------------------------------------------------------
  2# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
  3#
  4# Licensed under the Apache License, Version 2.0 (the "License");
  5# you may not use this file except in compliance with the License.
  6# You may obtain a copy of the License at
  7#
  8#     http://www.apache.org/licenses/LICENSE-2.0
  9#
 10# Unless required by applicable law or agreed to in writing, software
 11# distributed under the License is distributed on an "AS IS" BASIS,
 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13# See the License for the specific language governing permissions and
 14# limitations under the License.
 15# ----------------------------------------------------------------------------
 16
 17"""
 18This script demonstrates how to create and simulate a robot using SimulationManager.
 19It shows how to load a robot from URDF, set up control parts, and run basic simulation.
 20"""
 21
 22from __future__ import annotations
 23
 24import argparse
 25import time
 26from embodichain.cli.sim import add_sim_args_to_parser
 27
 28
 29def build_parser() -> argparse.ArgumentParser:
 30    """Build CLI options without initializing simulation resources."""
 31    parser = argparse.ArgumentParser(
 32        description="Create and simulate a robot in SimulationManager"
 33    )
 34    add_sim_args_to_parser(parser)
 35    parser.add_argument(
 36        "--max-steps",
 37        type=int,
 38        default=None,
 39        help="Stop after this many physics steps (default: run until interrupted).",
 40    )
 41    return parser
 42
 43
 44if __name__ == "__main__":
 45    # Parse before importing optional simulation/planning dependencies.
 46    _cli_args = build_parser().parse_args()
 47
 48
 49import numpy as np
 50import torch
 51
 52torch.set_printoptions(precision=4, sci_mode=False)
 53
 54from scipy.spatial.transform import Rotation as R
 55
 56from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
 57from embodichain.lab.visualization import visualization_cfg_from_args
 58from embodichain.lab.sim.objects import Robot
 59from embodichain.lab.sim.cfg import (
 60    RenderCfg,
 61    physics_cfg_for_backend,
 62    JointDrivePropertiesCfg,
 63    RobotCfg,
 64    URDFCfg,
 65)
 66from embodichain.data import get_data_path
 67
 68ACTION_SWITCH_INTERVAL = 100
 69ACTION_CYCLE_STEPS = 4 * ACTION_SWITCH_INTERVAL
 70
 71
 72def main(args: argparse.Namespace | None = None) -> None:
 73    """Main function to demonstrate robot simulation."""
 74
 75    # Parse command line arguments
 76    parser = build_parser()
 77    if args is None:
 78        args = parser.parse_args()
 79    if args.max_steps is not None and args.max_steps < 1:
 80        parser.error("--max-steps must be at least 1")
 81
 82    # Initialize simulation
 83    print("Creating simulation...")
 84    config = SimulationManagerCfg(
 85        headless=True,
 86        device=args.device,
 87        arena_space=3.0,
 88        render_cfg=RenderCfg(renderer=args.renderer),
 89        physics_cfg=physics_cfg_for_backend(args.physics),
 90        physics_dt=1.0 / 100.0,
 91        num_envs=args.num_envs,
 92        visualization=visualization_cfg_from_args(args),
 93    )
 94    sim = SimulationManager(config)
 95
 96    try:
 97        # Create robot configuration
 98        robot = create_robot(sim)
 99
100        # Materialize the declared scene before accessing robot metadata.
101        sim.prepare()
102        print(f"Robot created successfully with {robot.dof} joints")
103
104        # Open visualization window if not headless
105        if not args.headless:
106            sim.open_window()
107
108        # Run simulation loop
109        run_simulation(sim, robot, max_steps=args.max_steps)
110    finally:
111        print("Cleaning up...", flush=True)
112        sim.destroy(exit_process=False)
113
114
115def create_robot(sim):
116    """Create and configure a robot in the simulation."""
117
118    print("Loading robot...")
119
120    # Get SR5 arm URDF path
121    sr5_urdf_path = get_data_path("Rokae/SR5/SR5.urdf")
122
123    # Get hand URDF path
124    hand_urdf_path = get_data_path(
125        "BrainCoHandRevo1/BrainCoLeftHand/BrainCoLeftHand.urdf"
126    )
127
128    # Define control parts for the robot
129    # Joint names in control_parts can be regex patterns
130    CONTROL_PARTS = {
131        "arm": [
132            "joint[1-6]",  # Matches JOINT1, JOINT2, ..., JOINT6
133        ],
134        "eef": ["LEFT_.*"],  # Matches all joints starting with L_
135    }
136
137    # Define transformation for hand attachment
138    hand_attach_xpos = np.eye(4)
139    hand_attach_xpos[:3, :3] = R.from_rotvec([90, 0, 0], degrees=True).as_matrix()
140    hand_attach_xpos[2, 3] = 0.02
141
142    cfg = RobotCfg(
143        uid="sr5_with_brainco",
144        urdf_cfg=URDFCfg(
145            components=[
146                {
147                    "component_type": "arm",
148                    "urdf_path": sr5_urdf_path,
149                },
150                {
151                    "component_type": "hand",
152                    "urdf_path": hand_urdf_path,
153                    "transform": hand_attach_xpos,
154                },
155            ]
156        ),
157        control_parts=CONTROL_PARTS,
158        joint_drive_props=JointDrivePropertiesCfg(
159            drive_type="force",
160            stiffness={"joint[1-6]": 1e4, "LEFT_.*": 1e3},
161            damping={"joint[1-6]": 1.5e3, "LEFT_.*": 1e2},
162            max_effort={"joint[1-6]": 1e4, "LEFT_.*": 1e4},
163            # Demo stabilization, not calibrated motor inertia: the hand's
164            # small link inertias make high-gain, coupled PIP motion unstable.
165            # Include mimic joints and use the same armature on both backends.
166            armature={"LEFT_.*": 0.01},
167        ),
168    )
169
170    # Add robot to simulation
171    robot: Robot = sim.add_robot(cfg=cfg)
172
173    return robot
174
175
176def _expand_mimic_targets(
177    robot: Robot, joint_ids: list[int], joint_targets: torch.Tensor
178) -> torch.Tensor:
179    """Expand active-joint targets into mimic-consistent articulation targets."""
180
181    targets = robot.get_qpos(target=True).clone()
182    targets[:, joint_ids] = joint_targets
183
184    for mimic_id, parent_id, multiplier, offset in zip(
185        robot.mimic_ids,
186        robot.mimic_parents,
187        robot.mimic_multipliers,
188        robot.mimic_offsets,
189    ):
190        if mimic_id is None or parent_id is None:
191            continue
192        targets[:, mimic_id] = offset + multiplier * targets[:, parent_id]
193
194    limits = robot.body_data.qpos_limits
195    return targets.clamp(min=limits[..., 0], max=limits[..., 1])
196
197
198def run_simulation(
199    sim: SimulationManager, robot: Robot, max_steps: int | None = None
200) -> None:
201    """Run the simulation loop with robot control."""
202
203    print("Starting simulation...")
204    print("Robot will move through different poses")
205    print("Press Ctrl+C to stop")
206
207    step_count = 0
208
209    arm_joint_ids = robot.get_joint_ids("arm")
210    # Define some target joint positions for demonstration
211    arm_position1 = (
212        torch.tensor(
213            [0.0, -0.5, 0.5, -1.0, 0.5, 0.0], dtype=torch.float32, device=sim.device
214        )
215        .unsqueeze_(0)
216        .repeat(sim.num_envs, 1)
217    )
218
219    arm_position2 = (
220        torch.tensor(
221            [0.5, 0.0, -0.5, 0.5, -0.5, 0.5], dtype=torch.float32, device=sim.device
222        )
223        .unsqueeze_(0)
224        .repeat(sim.num_envs, 1)
225    )
226
227    # Get joint IDs for the hand.
228    hand_joint_ids = robot.get_joint_ids("eef")
229    active_hand_joint_ids = robot.get_joint_ids("eef", remove_mimic=True)
230    # Drive mimic joints toward the pose implied by their active parent instead of
231    # sending each joint to its independent limit. Newton keeps drives on mimic
232    # joints, so inconsistent targets otherwise compete with the mimic constraints.
233    hand_position_open = _expand_mimic_targets(
234        robot,
235        active_hand_joint_ids,
236        robot.body_data.qpos_limits[:, active_hand_joint_ids, 1],
237    )[:, hand_joint_ids]
238    hand_position_close = _expand_mimic_targets(
239        robot,
240        active_hand_joint_ids,
241        robot.body_data.qpos_limits[:, active_hand_joint_ids, 0],
242    )[:, hand_joint_ids]
243
244    # The reset pose is zero for every DOF, but this hand has non-zero mimic
245    # offsets. Start from a valid closed pose so the initial state and drive
246    # targets satisfy the same mimic equations.
247    robot.set_qpos(qpos=hand_position_close, joint_ids=hand_joint_ids, target=False)
248    robot.set_qpos(qpos=hand_position_close, joint_ids=hand_joint_ids)
249
250    try:
251        while max_steps is None or step_count < max_steps:
252            cycle_step = step_count % ACTION_CYCLE_STEPS
253
254            if cycle_step == 0:
255                robot.set_qpos(qpos=arm_position1, joint_ids=arm_joint_ids)
256                print(f"Moving to arm position 1")
257
258            if cycle_step == ACTION_SWITCH_INTERVAL:
259                robot.set_qpos(qpos=arm_position2, joint_ids=arm_joint_ids)
260                print(f"Moving to arm position 2")
261
262            if cycle_step == 2 * ACTION_SWITCH_INTERVAL:
263                robot.set_qpos(qpos=hand_position_close, joint_ids=hand_joint_ids)
264                print(f"Closing hand")
265
266            if cycle_step == 3 * ACTION_SWITCH_INTERVAL:
267                robot.set_qpos(qpos=hand_position_open, joint_ids=hand_joint_ids)
268                print(f"Opening hand")
269
270            # Apply commands before advancing physics so both backends observe the
271            # target change on the same simulation step.
272            sim.update(step=1)
273            step_count += 1
274
275    except KeyboardInterrupt:
276        print("Stopping simulation...")
277
278
279if __name__ == "__main__":
280    try:
281        main(_cli_args)
282    finally:
283        SimulationManager.flush_cleanup_queue()

The Code Explained#

Similar to the previous tutorial on creating a simulation scene, we use the SimulationManager class to set up the simulation environment. If you haven’t read that tutorial yet, please refer to Creating a simulation scene first.

Loading Robot URDF#

SimulationManager supports loading robots from URDF (Unified Robot Description Format) files. You can load either a single URDF file or compose multiple URDF components into a complete robot system.

For a simple two-component robot (arm + hand):

    sr5_urdf_path = get_data_path("Rokae/SR5/SR5.urdf")

    # Get hand URDF path
    hand_urdf_path = get_data_path(
        "BrainCoHandRevo1/BrainCoLeftHand/BrainCoLeftHand.urdf"
    )

    # Define control parts for the robot
    # Joint names in control_parts can be regex patterns
    CONTROL_PARTS = {
        "arm": [
            "joint[1-6]",  # Matches JOINT1, JOINT2, ..., JOINT6
        ],
        "eef": ["LEFT_.*"],  # Matches all joints starting with L_
    }

    # Define transformation for hand attachment
    hand_attach_xpos = np.eye(4)
    hand_attach_xpos[:3, :3] = R.from_rotvec([90, 0, 0], degrees=True).as_matrix()
    hand_attach_xpos[2, 3] = 0.02

    cfg = RobotCfg(
        uid="sr5_with_brainco",
        urdf_cfg=URDFCfg(
            components=[
                {
                    "component_type": "arm",
                    "urdf_path": sr5_urdf_path,
                },
                {
                    "component_type": "hand",
                    "urdf_path": hand_urdf_path,
                    "transform": hand_attach_xpos,
                },
            ]
        ),
        control_parts=CONTROL_PARTS,
        joint_drive_props=JointDrivePropertiesCfg(
            drive_type="force",
            stiffness={"joint[1-6]": 1e4, "LEFT_.*": 1e3},
            damping={"joint[1-6]": 1.5e3, "LEFT_.*": 1e2},
            max_effort={"joint[1-6]": 1e4, "LEFT_.*": 1e4},
            # Demo stabilization, not calibrated motor inertia: the hand's
            # small link inertias make high-gain, coupled PIP motion unstable.
            # Include mimic joints and use the same armature on both backends.
            armature={"LEFT_.*": 0.01},
        ),
    )

    # Add robot to simulation
    robot: Robot = sim.add_robot(cfg=cfg)

The cfg.URDFCfg allows you to compose multiple URDF files with specific transformations, enabling complex robot assemblies.

Configuring Control Parts#

Control parts define how the robot’s joints are grouped for control purposes. This is useful for organizing complex robots with multiple subsystems.

    # Define control parts for the robot
    # Joint names in control_parts can be regex patterns
    CONTROL_PARTS = {
        "arm": [
            "joint[1-6]",  # Matches JOINT1, JOINT2, ..., JOINT6
        ],
        "eef": ["LEFT_.*"],  # Matches all joints starting with L_
    }

Joint names in control parts can use regex patterns for flexible matching. For example:

  • "JOINT[1-6]" matches JOINT1, JOINT2, …, JOINT6

  • "L_.*" matches all joints starting with “L_”

Setting Drive Properties#

Drive properties control how the robot’s joints behave during simulation, including stiffness, damping, and force limits.

        joint_drive_props=JointDrivePropertiesCfg(
            drive_type="force",
            stiffness={"joint[1-6]": 1e4, "LEFT_.*": 1e3},
            damping={"joint[1-6]": 1.5e3, "LEFT_.*": 1e2},
            max_effort={"joint[1-6]": 1e4, "LEFT_.*": 1e4},
            # Demo stabilization, not calibrated motor inertia: the hand's
            # small link inertias make high-gain, coupled PIP motion unstable.
            # Include mimic joints and use the same armature on both backends.
            armature={"LEFT_.*": 0.01},
        ),

You can set different stiffness values for different joint groups using regex patterns. More details on drive properties can be found in cfg.JointDrivePropertiesCfg.

For more robot configuration options, refer to cfg.RobotCfg.

Robot Control#

For the basic control of robot joints, you can set position targets using objects.Robot.set_qpos(). The control action should be created as a torch.Tensor with shape (num_envs, num_joints), where num_joints is the total number of joints in the robot or the number of joints in a specific control part.

  • If you can control all joints, use:

    robot.set_qpos(qpos=target_positions)
    
  • If you want to control a subset of joints, specify the joint IDs:

    robot.set_qpos(qpos=target_positions, joint_ids=subset_joint_ids)
    

Getting Robot State#

You can query the robot’s current joint positions and velocities via objects.Robot.get_qpos() and objects.Robot.get_qvel(). For more robot API details, see objects.Robot.

The Code Execution#

To run the robot simulation script:

python scripts/tutorials/sim/create_robot.py

You can customize the simulation with various command-line options:

# Run with GPU physics
python scripts/tutorials/sim/create_robot.py --device cuda

# Run multiple environments
python scripts/tutorials/sim/create_robot.py --num_envs 4

# Run in headless mode
python scripts/tutorials/sim/create_robot.py --headless

# Select a renderer backend
python scripts/tutorials/sim/create_robot.py --renderer rt

The simulation will show the robot moving through different poses, demonstrating basic joint control capabilities.

Key Features Demonstrated#

This tutorial demonstrates several key features of robot simulation in SimulationManager:

  1. URDF Loading: Both single-file and multi-component robot loading

  2. Control Parts: Organizing joints into logical control groups

  3. Drive Properties: Configuring joint stiffness and control behavior

  4. Joint Control: Setting position targets and reading joint states

  5. Multi-Environment: Running multiple robot instances in parallel

Next Steps#

After mastering basic robot simulation, you can explore:

  • End-effector control and inverse kinematics

  • Sensor integration (cameras, force sensors)

  • Robot-object interaction scenarios

This tutorial provides the foundation for creating sophisticated robotic simulation scenarios with SimulationManager.