# Adding a New Robot to LeggedGym-Ex This guide walks you through the complete process of adding a new robot to the LeggedGym-Ex framework. By following these steps, you'll be able to train locomotion policies for any custom quadrupedal or bipedal robot. ```{note} This guide assumes you have a working installation of LeggedGym-Ex. If not, please refer to the installation instructions first. ``` ## Overview Adding a new robot involves six key steps: 1. **Create Robot Class** - Inherit from `LeggedRobot` and customize behavior 2. **Create Configuration Class** - Define robot parameters, rewards, and training settings 3. **Define Asset Paths** - Configure URDF/XML paths for each simulator 4. **Register the Task** - Add to the task registry 5. **Test Training** - Verify the setup with a minimal training run 6. **Tune Parameters** - Refine rewards, commands, and domain randomization ```{tip} The fastest way to add a new robot is to copy an existing robot implementation and modify it. The Go2 implementation is a good starting point for quadrupeds, while G1 works well for bipeds. ``` --- ## Step 1: Create Robot Class ### 1.1 Directory Structure Create a new directory for your robot under `legged_gym/envs/`: ``` legged_gym/envs/ ├── myrobot/ │ ├── __init__.py │ ├── myrobot.py # Robot environment class │ └── myrobot_config.py # Configuration class ``` ### 1.2 Basic Robot Class Create `myrobot.py` with a class inheriting from `LeggedRobot`: ```python # legged_gym/envs/myrobot/myrobot.py from legged_gym import * import torch from legged_gym.envs.base.legged_robot import LeggedRobot from legged_gym.utils.math_utils import torch_rand_float class MyRobot(LeggedRobot): """Custom robot environment extending LeggedRobot. This class handles: - DOF state resets - Observation computation - Root state resets - Noise scaling for observations """ def _reset_dofs(self, env_ids): """Reset DOF positions and velocities for specified environments. DOF positions are initialized near default standing pose with small random offsets to encourage robustness. Args: env_ids: Tensor of environment indices to reset """ dof_pos = torch.zeros( (len(env_ids), self.num_actions), dtype=torch.float, device=self.device, requires_grad=False ) dof_vel = torch.zeros( (len(env_ids), self.num_actions), dtype=torch.float, device=self.device, requires_grad=False ) # Initialize near default positions with randomization # Adjust indices based on your robot's DOF structure dof_pos[:, :] = self.simulator.default_dof_pos[:] + \ torch_rand_float(-0.2, 0.2, (len(env_ids), self.num_actions), self.device) self.simulator.reset_dofs(env_ids, dof_pos, dof_vel) def compute_observations(self): """Compute observations for all environments. The observation vector includes: - Commands (3): lin_vel_x, lin_vel_y, ang_vel_yaw - Projected gravity (3): body z-axis in world frame - Base angular velocity (3): scaled by obs_scales.ang_vel - DOF positions (num_dofs): deviation from default - DOF velocities (num_dofs): scaled by obs_scales.dof_vel - Actions (num_actions): previous action for history [NOTE]: Must update _get_noise_scale_vec() when changing observations """ self.obs_buf = torch.cat(( self.commands[:, :3] * self.commands_scale, # 3 self.simulator.projected_gravity, # 3 self.simulator.base_ang_vel * self.obs_scales.ang_vel, # 3 (self.simulator.dof_pos - self.simulator.default_dof_pos) * self.obs_scales.dof_pos, # num_dofs self.simulator.dof_vel * self.obs_scales.dof_vel, # num_dofs self.actions # num_actions ), dim=-1) # Add height measurements if enabled if self.cfg.terrain.measure_heights: heights = torch.clip( self.simulator.base_pos[:, 2].unsqueeze(1) - 0.5 - self.measured_heights, -1, 1. ) * self.obs_scales.height_measurements self.obs_buf = torch.cat((self.obs_buf, heights), dim=-1) # Add noise if enabled if self.add_noise: self.obs_buf += (2 * torch.rand_like(self.obs_buf) - 1) * self.noise_scale_vec # Compute privileged observations for asymmetric actor-critic if self.num_privileged_obs is not None: self._compute_privileged_observations() def _compute_privileged_observations(self): """Compute privileged observations for critic network. Includes domain randomization parameters that are not available on real robots: - Friction coefficient - Added base mass - CoM displacement - External push velocities """ self.privileged_obs_buf = torch.cat(( self.simulator.base_lin_vel * self.obs_scales.lin_vel, self.simulator.base_ang_vel * self.obs_scales.ang_vel, self.simulator.projected_gravity, self.commands[:, :3] * self.commands_scale, (self.simulator.dof_pos - self.simulator.default_dof_pos) * self.obs_scales.dof_pos, self.simulator.dof_vel * self.obs_scales.dof_vel, self.actions, self.last_actions, self.simulator._friction_values, # 1 self.simulator._added_base_mass, # 1 self.simulator._base_com_bias, # 3 self.simulator._rand_push_vels[:, :2], # 2 ), dim=-1) if self.cfg.terrain.measure_heights: heights = torch.clip( self.simulator.base_pos[:, 2].unsqueeze(1) - 0.5 - self.measured_heights, -1, 1. ) * self.obs_scales.height_measurements self.privileged_obs_buf = torch.cat( (self.privileged_obs_buf, heights), dim=-1 ) def _get_noise_scale_vec(self): """Set noise scales for observation corruption. [NOTE]: Must be updated when observation structure changes! The indices here must match the observation order in compute_observations(). Returns: Tensor of noise scales for each observation dimension """ noise_vec = torch.zeros_like(self.obs_buf[0]) self.add_noise = self.cfg.noise.add_noise noise_scales = self.cfg.noise.noise_scales noise_level = self.cfg.noise.noise_level # Match indices to observation structure noise_vec[:3] = 0. # commands (no noise) noise_vec[3:6] = noise_scales.gravity * noise_level noise_vec[6:9] = noise_scales.ang_vel * noise_level * self.obs_scales.ang_vel noise_vec[9:9+self.num_actions] = noise_scales.dof_pos * \ noise_level * self.obs_scales.dof_pos noise_vec[9+self.num_actions:9+2*self.num_actions] = noise_scales.dof_vel * \ noise_level * self.obs_scales.dof_vel noise_vec[9+2*self.num_actions:9+3*self.num_actions] = 0. # actions (no noise) if self.cfg.terrain.measure_heights: height_start = 9 + 3 * self.num_actions noise_vec[height_start:] = noise_scales.height_measurements * \ noise_level * self.obs_scales.height_measurements return noise_vec def _reset_root_states(self, env_ids): """Reset base position and velocity for specified environments. Args: env_ids: Tensor of environment indices to reset """ # Base position if self.simulator.custom_origins: base_pos = self.simulator.base_init_pos.reshape(1, -1).repeat(len(env_ids), 1) base_pos += self.simulator.env_origins[env_ids] base_pos[:, :2] += torch_rand_float( -0.5, 0.5, (len(env_ids), 2), device=self.device ) else: base_pos = self.simulator.base_init_pos.reshape(1, -1).repeat(len(env_ids), 1) base_pos += self.simulator.env_origins[env_ids] # Base orientation (quaternion) base_quat = self.simulator.base_init_quat.reshape(1, -1).repeat(len(env_ids), 1) # Base velocities (small random initial velocities) base_lin_vel = torch_rand_float(-0.0, 0.0, (len(env_ids), 3), self.device) base_ang_vel = torch_rand_float(-0.0, 0.0, (len(env_ids), 3), self.device) self.simulator.reset_root_states( env_ids, base_pos, base_quat, base_lin_vel, base_ang_vel ) ``` ### 1.3 Key Methods to Override | Method | Purpose | Override When | |--------|---------|---------------| | `_reset_dofs()` | Initialize joint positions | Custom standing pose needed | | `compute_observations()` | Build observation vector | Different sensor suite | | `_get_noise_scale_vec()` | Define observation noise | Observation structure changes | | `_reset_root_states()` | Initialize base pose | Custom spawn behavior | ```{warning} If you modify `compute_observations()`, you MUST update `_get_noise_scale_vec()` to match the new observation indices. Mismatched indices cause silent training failures! ``` --- ## Step 2: Create Configuration Class ### 2.1 Basic Configuration Create `myrobot_config.py` with nested configuration classes: ```python # legged_gym/envs/myrobot/myrobot_config.py from legged_gym import * from legged_gym.envs.base.legged_robot_config import LeggedRobotCfg, LeggedRobotCfgPPO from legged_gym.envs.base.common_cfgs import get_simulator_suffix class MyRobotCfg(LeggedRobotCfg): """Configuration for MyRobot locomotion training. Inherits from LeggedRobotCfg and overrides specific parameters. """ class env(LeggedRobotCfg.env): num_envs = 4096 # Number of parallel environments num_observations = 45 # Observation dimension (adjust based on your robot) num_privileged_obs = None # Set to int for asymmetric training num_actions = 12 # Number of actuated joints episode_length_s = 20 # Episode duration in seconds class init_state(LeggedRobotCfg.init_state): pos = [0.0, 0.0, 0.4] # Initial spawn position [x, y, z] in meters default_joint_angles = { # Default standing pose 'FL_hip_joint': 0.0, 'FL_thigh_joint': 0.8, 'FL_calf_joint': -1.5, # ... add all joints } class control(LeggedRobotCfg.control): control_type = 'P' # Position control stiffness = {'joint': 20.0} # PD stiffness [N*m/rad] damping = {'joint': 0.5} # PD damping [N*m*s/rad] action_scale = 0.25 # Action to angle mapping dt = 0.02 # Control frequency: 50 Hz decimation = 4 # Sim steps per control step class asset(LeggedRobotCfg.asset): name = "myrobot" file = '{LEGGED_GYM_ROOT_DIR}/resources/robots/myrobot/myrobot.urdf' xml_file = '{LEGGED_GYM_ROOT_DIR}/resources/robots/myrobot/myrobot.xml' foot_name = "foot" # Substring to identify foot links penalize_contacts_on = ["thigh", "calf"] # Penalize these contacts terminate_after_contacts_on = ["base"] # Terminate on these contacts base_link_name = "base" dof_names = [ # Joint names in action order "FR_hip_joint", "FR_thigh_joint", "FR_calf_joint", "FL_hip_joint", "FL_thigh_joint", "FL_calf_joint", "RR_hip_joint", "RR_thigh_joint", "RR_calf_joint", "RL_hip_joint", "RL_thigh_joint", "RL_calf_joint", ] dof_armature = [0.01] * 12 # Joint armature for stability class rewards(LeggedRobotCfg.rewards): only_positive_rewards = True tracking_sigma = 0.25 # Tracking reward bandwidth base_height_target = 0.36 # Target standing height [m] class scales(LeggedRobotCfg.rewards.scales): # Penalty rewards (negative) dof_pos_limits = -1.0 collision = -1.0 lin_vel_z = -0.5 base_height = -2.0 ang_vel_xy = -0.05 orientation = -1.0 dof_vel = -5.e-4 dof_acc = -2.e-7 action_rate = -0.01 action_smoothness = -0.01 torques = -2.e-4 # Positive rewards tracking_lin_vel = 1.0 tracking_ang_vel = 0.5 feet_air_time = 1.0 foot_clearance = 0.5 class commands(LeggedRobotCfg.commands): curriculum = True # Increase command difficulty over time max_curriculum = 1.0 num_commands = 4 resampling_time = 10.0 # Command change interval [s] heading_command = True # Use heading instead of angular velocity class ranges(LeggedRobotCfg.commands.ranges): lin_vel_x = [-0.5, 0.5] # [min, max] m/s lin_vel_y = [-1.0, 1.0] ang_vel_yaw = [-1, 1] # rad/s heading = [-3.14, 3.14] # rad class domain_rand(LeggedRobotCfg.domain_rand): randomize_friction = True friction_range = [0.5, 1.25] randomize_base_mass = True added_mass_range = [-1.0, 1.0] push_robots = True push_interval_s = 15 max_push_vel_xy = 1.0 randomize_com_displacement = True class MyRobotCfgPPO(LeggedRobotCfgPPO): """PPO training configuration for MyRobot.""" class runner(LeggedRobotCfgPPO.runner): run_name = 'simple_rl' + get_simulator_suffix() experiment_name = 'myrobot' save_interval = 200 max_iterations = 1500 ``` ### 2.2 Configuration Hierarchy The configuration system uses nested class inheritance: ``` LeggedRobotCfg ├── env # Environment settings ├── terrain # Terrain configuration ├── init_state # Initial robot state ├── control # Control parameters ├── asset # Robot asset paths ├── rewards # Reward function settings ├── commands # Command velocity ranges ├── domain_rand # Domain randomization ├── normalization # Observation/action scaling ├── noise # Observation noise └── sim # Simulation parameters ``` ### 2.3 Key Configuration Parameters #### Environment Settings | Parameter | Description | Typical Values | |-----------|-------------|----------------| | `num_envs` | Parallel environments | 4096 (GPU dependent) | | `num_observations` | Observation dimension | Robot-specific | | `num_actions` | Actuated joints | 12 (quadruped), 12+ (biped) | | `episode_length_s` | Episode duration | 10-20 seconds | #### Control Settings | Parameter | Description | Typical Values | |-----------|-------------|----------------| | `control_type` | Control mode | 'P' (position), 'V' (velocity), 'T' (torque) | | `stiffness` | PD stiffness | 20-100 N*m/rad | | `damping` | PD damping | 0.5-2.0 N*m*s/rad | | `action_scale` | Action magnitude | 0.25-0.5 | | `dt` | Control period | 0.02s (50Hz) | | `decimation` | Sim steps per control | 4 (for 200Hz sim) | #### Reward Scaling Strategy ```{tip} Start with balanced reward scales and tune based on training curves: 1. Command tracking rewards (positive): 0.5-2.0 2. Smoothness penalties (negative): -0.001 to -0.1 3. Safety penalties (negative): -1.0 to -10.0 ``` --- ## Step 3: Define Asset Paths for Each Simulator ### 3.1 Multi-Simulator Support LeggedGym-Ex supports three simulators, each with different asset requirements: | Simulator | Asset Format | Configuration Key | |-----------|--------------|-------------------| | IsaacGym | URDF | `asset.file` | | IsaacLab | URDF | `asset.file` | | Genesis | XML | `asset.xml_file` | ### 3.2 Directory Structure for Assets Place your robot assets in the resources directory: ``` resources/robots/ └── myrobot/ ├── myrobot.urdf # URDF for IsaacGym/IsaacLab ├── myrobot.xml # XML for Genesis └── meshes/ # Visual and collision meshes ├── base.stl ├── thigh.stl └── ... ``` ### 3.3 Configuration Example ```python class asset(LeggedRobotCfg.asset): name = "myrobot" # URDF path for IsaacGym and IsaacLab file = '{LEGGED_GYM_ROOT_DIR}/resources/robots/myrobot/myrobot.urdf' # XML path for Genesis xml_file = '{LEGGED_GYM_ROOT_DIR}/resources/robots/myrobot/myrobot.xml' # Additional Genesis-specific settings links_to_keep = ['FL_foot', 'FR_foot', 'RL_foot', 'RR_foot'] dof_vel_limits = [30.1, 30.1, 15.7] * 4 # rad/s for each joint ``` ### 3.4 URDF Requirements ```{important} Your URDF must satisfy these requirements: 1. All joints must be revolute (no prismatic joints for legs) 2. Joint limits must be defined 3. Inertial parameters must be realistic 4. Mesh files must be referenced with relative paths ``` Example URDF structure: ```xml ``` ### 3.5 Genesis XML Requirements Genesis requires a MJCF-style XML file: ```xml ``` --- ## Step 4: Register in envs/__init__.py ### 4.1 Import and Register Add your robot to the task registry in `legged_gym/envs/__init__.py`: ```python # legged_gym/envs/__init__.py # ... existing imports ... # MyRobot - Add these lines from legged_gym.envs.myrobot.myrobot import MyRobot from legged_gym.envs.myrobot.myrobot_config import MyRobotCfg, MyRobotCfgPPO # ... in the registration section ... task_registry.register("myrobot", MyRobot, MyRobotCfg(), MyRobotCfgPPO()) ``` ### 4.2 Registration Parameters ```python task_registry.register( task_name="myrobot", # Unique task identifier env_class=MyRobot, # Environment class env_cfg=MyRobotCfg(), # Environment config (instantiated) train_cfg=MyRobotCfgPPO() # Training config (instantiated) ) ``` ### 4.3 Naming Conventions Follow these conventions for task names: | Pattern | Example | Use Case | |---------|---------|----------| | `{robot}` | `go2` | Basic locomotion | | `{robot}_ts` | `go2_ts` | Teacher-Student | | `{robot}_ee` | `go2_ee` | Explicit Estimator | | `{robot}_wtw` | `go2_wtw` | Walk These Ways | | `{robot}_deepmimic` | `g1_deepmimic` | DeepMimic | | `{robot}_amp` | `k1_amp` | Adversarial Motion Priors | --- ## Step 5: Test with Minimal Training Run ### 5.1 Quick Verification Before full training, verify your setup: ```bash # Set the simulator (choose one) export SIMULATOR=genesis # export SIMULATOR=isaacgym # export SIMULATOR=isaaclab # Run a short training test python -m legged_gym.scripts.train --task=myrobot --headless --max_iterations=10 ``` ### 5.2 Expected Output A successful test should show: ``` [INFO] Creating environment: myrobot [INFO] num_envs: 4096 [INFO] num_observations: 45 [INFO] num_actions: 12 [INFO] Starting training... iteration 0: mean_reward=0.123, mean_episode_length=45.2 iteration 1: mean_reward=0.156, mean_episode_length=52.1 ... ``` ### 5.3 Common Verification Checklist - [ ] Environment initializes without errors - [ ] Robot spawns at correct height - [ ] Observations have expected shape - [ ] Rewards are being computed - [ ] Robot doesn't immediately fall over - [ ] No CUDA memory errors ### 5.4 Debug Mode For visual debugging: ```bash # Run without headless to see the robot python -m legged_gym.scripts.train --task=myrobot --num_envs=16 ``` Enable debug visualization in config: ```python class env(LeggedRobotCfg.env): debug = True debug_draw_height_points_around_base = True ``` --- ## Step 6: Tune Rewards and Commands ### 6.1 Reward Tuning Process 1. **Start simple**: Begin with command tracking rewards only 2. **Add smoothness**: Penalize jerky motions 3. **Add safety**: Penalize collisions and limits 4. **Iterate**: Adjust scales based on observed behavior ### 6.2 Common Reward Functions ```python # In your config class rewards(LeggedRobotCfg.rewards): class scales(LeggedRobotCfg.rewards.scales): # Command tracking (encourage goal achievement) tracking_lin_vel = 1.0 # Track x/y velocity commands tracking_ang_vel = 0.5 # Track yaw velocity command # Smoothness (penalize erratic behavior) lin_vel_z = -2.0 # Penalize vertical velocity ang_vel_xy = -0.05 # Penalize roll/pitch rates dof_acc = -2.5e-7 # Penalize joint acceleration action_rate = -0.01 # Penalize action changes # Energy efficiency torques = -1e-4 # Penalize large torques dof_power = -2e-4 # Penalize power consumption # Safety collision = -1.0 # Penalize body collisions dof_pos_limits = -10.0 # Penalize joint limit violations termination = -0.0 # Penalty for termination # Gait quality feet_air_time = 1.0 # Reward proper swing phase foot_clearance = 0.5 # Reward foot lift during swing ``` ### 6.3 Command Tuning ```python class commands(LeggedRobotCfg.commands): curriculum = True # Enable progressive difficulty max_curriculum = 1.5 # Maximum velocity multiplier class ranges(LeggedRobotCfg.commands.ranges): # Start conservative, curriculum will expand lin_vel_x = [-0.3, 0.3] # Forward/backward lin_vel_y = [-0.3, 0.3] # Lateral ang_vel_yaw = [-0.5, 0.5] # Turning ``` ### 6.4 Domain Randomization Tuning ```python class domain_rand(LeggedRobotCfg.domain_rand): # Physics randomization randomize_friction = True friction_range = [0.5, 1.5] # Wider for robustness randomize_base_mass = True added_mass_range = [-2.0, 2.0] # kg variation # External disturbances push_robots = True push_interval_s = 10 max_push_vel_xy = 0.5 # m/s push velocity # Modeling errors randomize_com_displacement = True com_pos_x_range = [-0.02, 0.02] com_pos_y_range = [-0.02, 0.02] com_pos_z_range = [-0.01, 0.01] ``` ### 6.5 Monitoring Training Progress Use tensorboard or wandb: ```bash # View training curves tensorboard --logdir logs/myrobot/ # Or enable wandb in config class runner(LeggedRobotCfgPPO.runner): sync_wandb = True ``` Key metrics to monitor: | Metric | Target | Action if Poor | |--------|--------|----------------| | `rew_tracking_lin_vel` | > 0.8 | Increase scale or reduce velocity range | | `rew_tracking_ang_vel` | > 0.7 | Increase scale or reduce yaw range | | `episode_length` | Near max | Check termination conditions | | `terrain_level` | Increasing | Training is progressing | --- ## Working Example: Clone Go2 → MyRobot This section provides a complete working example showing how to create a new robot by cloning and modifying the Go2 implementation. ### Step-by-Step Clone Process ```bash # 1. Create directory structure mkdir -p legged_gym/envs/myrobot # 2. Copy Go2 files as template cp legged_gym/envs/go2/go2.py legged_gym/envs/myrobot/myrobot.py cp legged_gym/envs/go2/go2_config.py legged_gym/envs/myrobot/myrobot_config.py # 3. Create __init__.py touch legged_gym/envs/myrobot/__init__.py ``` ### Modified myrobot.py ```python # legged_gym/envs/myrobot/myrobot.py from legged_gym import * import torch from legged_gym.envs.base.legged_robot import LeggedRobot from legged_gym.utils.math_utils import torch_rand_float class MyRobot(LeggedRobot): """Custom quadruped robot based on Go2 template.""" def _reset_dofs(self, env_ids): """Reset DOFs with MyRobot-specific initialization.""" dof_pos = torch.zeros((len(env_ids), self.num_actions), dtype=torch.float, device=self.device) dof_vel = torch.zeros((len(env_ids), self.num_actions), dtype=torch.float, device=self.device) # Customize for MyRobot's joint structure # Assuming similar FR, FL, RR, RL ordering hip_indices = [0, 3, 6, 9] thigh_indices = [1, 4, 7, 10] calf_indices = [2, 5, 8, 11] # Hips: small randomization around 0 dof_pos[:, hip_indices] = self.simulator.default_dof_pos[:, hip_indices] + \ torch_rand_float(-0.1, 0.1, (len(env_ids), 4), self.device) # Thighs: larger randomization dof_pos[:, thigh_indices] = self.simulator.default_dof_pos[:, thigh_indices] + \ torch_rand_float(-0.3, 0.3, (len(env_ids), 4), self.device) # Calves: moderate randomization dof_pos[:, calf_indices] = self.simulator.default_dof_pos[:, calf_indices] + \ torch_rand_float(-0.3, 0.3, (len(env_ids), 4), self.device) self.simulator.reset_dofs(env_ids, dof_pos, dof_vel) # Use default compute_observations from LeggedRobot # or customize as needed ``` ### Modified myrobot_config.py ```python # legged_gym/envs/myrobot/myrobot_config.py from legged_gym import * from legged_gym.envs.base.legged_robot_config import LeggedRobotCfg, LeggedRobotCfgPPO from legged_gym.envs.base.common_cfgs import get_simulator_suffix class MyRobotCfg(LeggedRobotCfg): """Configuration for MyRobot.""" class env(LeggedRobotCfg.env): num_envs = 4096 num_observations = 45 # Adjust based on your sensor setup num_privileged_obs = None num_actions = 12 # Adjust for your robot's DOFs episode_length_s = 20 class init_state(LeggedRobotCfg.init_state): pos = [0.0, 0.0, 0.35] # Adjust spawn height for your robot default_joint_angles = { # Customize for your robot's joint names 'FR_hip_joint': 0.0, 'FR_thigh_joint': 0.7, 'FR_calf_joint': -1.4, 'FL_hip_joint': 0.0, 'FL_thigh_joint': 0.7, 'FL_calf_joint': -1.4, 'RR_hip_joint': 0.0, 'RR_thigh_joint': 0.7, 'RR_calf_joint': -1.4, 'RL_hip_joint': 0.0, 'RL_thigh_joint': 0.7, 'RL_calf_joint': -1.4, } class control(LeggedRobotCfg.control): control_type = 'P' stiffness = {'joint': 25.0} # Tune for your robot damping = {'joint': 0.5} action_scale = 0.25 dt = 0.02 decimation = 4 class asset(LeggedRobotCfg.asset): name = "myrobot" # Point to your URDF/XML files file = '{LEGGED_GYM_ROOT_DIR}/resources/robots/myrobot/myrobot.urdf' xml_file = '{LEGGED_GYM_ROOT_DIR}/resources/robots/myrobot/myrobot.xml' foot_name = "foot" penalize_contacts_on = ["thigh", "calf"] terminate_after_contacts_on = ["base"] base_link_name = "base" dof_names = [ "FR_hip_joint", "FR_thigh_joint", "FR_calf_joint", "FL_hip_joint", "FL_thigh_joint", "FL_calf_joint", "RR_hip_joint", "RR_thigh_joint", "RR_calf_joint", "RL_hip_joint", "RL_thigh_joint", "RL_calf_joint", ] dof_armature = [0.01] * 12 class rewards(LeggedRobotCfg.rewards): soft_dof_pos_limit = 0.9 base_height_target = 0.32 # Adjust for your robot only_positive_rewards = True class scales(LeggedRobotCfg.rewards.scales): tracking_lin_vel = 1.0 tracking_ang_vel = 0.5 lin_vel_z = -2.0 base_height = -2.0 ang_vel_xy = -0.05 orientation = -1.0 dof_vel = -5.e-4 dof_acc = -2.e-7 action_rate = -0.01 action_smoothness = -0.01 torques = -2.e-4 feet_air_time = 1.0 foot_clearance = 0.5 dof_pos_limits = -1.0 collision = -1.0 class commands(LeggedRobotCfg.commands): curriculum = True max_curriculum = 1.0 num_commands = 4 resampling_time = 10.0 heading_command = True class ranges(LeggedRobotCfg.commands.ranges): lin_vel_x = [-0.5, 0.5] lin_vel_y = [-1.0, 1.0] ang_vel_yaw = [-1, 1] heading = [-3.14, 3.14] class domain_rand(LeggedRobotCfg.domain_rand): randomize_friction = True friction_range = [0.5, 1.25] randomize_base_mass = True added_mass_range = [-1., 1.] push_robots = True push_interval_s = 15 max_push_vel_xy = 1. randomize_com_displacement = True class MyRobotCfgPPO(LeggedRobotCfgPPO): class runner(LeggedRobotCfgPPO.runner): run_name = 'simple_rl' + get_simulator_suffix() experiment_name = 'myrobot' save_interval = 200 max_iterations = 1500 ``` ### Register and Test ```python # Add to legged_gym/envs/__init__.py from legged_gym.envs.myrobot.myrobot import MyRobot from legged_gym.envs.myrobot.myrobot_config import MyRobotCfg, MyRobotCfgPPO task_registry.register("myrobot", MyRobot, MyRobotCfg(), MyRobotCfgPPO()) ``` ```bash # Test the new robot export SIMULATOR=genesis python -m legged_gym.scripts.train --task=myrobot --headless --max_iterations=100 ``` --- ## Troubleshooting ### Common Issues and Solutions #### Robot Falls Immediately After Spawn **Symptoms**: Robot collapses instantly, episode terminates quickly. **Solutions**: 1. Check `init_state.pos[2]` - spawn height may be too low 2. Verify `default_joint_angles` match a stable pose 3. Increase `dof_armature` for numerical stability 4. Check URDF inertia values are realistic ```python # Debug spawn height class init_state(LeggedRobotCfg.init_state): pos = [0.0, 0.0, 0.5] # Try higher spawn ``` #### Observation Shape Mismatch **Symptoms**: Error like "expected shape (4096, 45) but got (4096, 48)". **Solutions**: 1. Count observation dimensions manually 2. Ensure `num_observations` matches `obs_buf.shape[1]` 3. Check if `measure_heights` adds extra dimensions ```python # Count observations: # commands (3) + gravity (3) + ang_vel (3) + dof_pos (12) + dof_vel (12) + actions (12) = 45 ``` #### CUDA Out of Memory **Symptoms**: RuntimeError: CUDA out of memory. **Solutions**: 1. Reduce `num_envs` 2. Reduce `terrain.num_rows` and `num_cols` 3. Disable `measure_heights` if not needed 4. Use smaller observation history ```python class env(LeggedRobotCfg.env): num_envs = 2048 # Reduce from 4096 ``` #### Training Not Progressing **Symptoms**: Rewards plateau at low values, robot doesn't learn to walk. **Solutions**: 1. Increase `tracking_lin_vel` reward scale 2. Reduce command velocity ranges 3. Check termination conditions aren't too strict 4. Verify domain randomization isn't too aggressive ```python class commands(LeggedRobotCfg.commands): class ranges(LeggedRobotCfg.commands.ranges): lin_vel_x = [-0.2, 0.2] # Start with smaller commands class rewards(LeggedRobotCfg.rewards): class scales(LeggedRobotCfg.rewards.scales): tracking_lin_vel = 2.0 # Increase tracking reward ``` #### Joint Limit Violations **Symptoms**: Robot jerks at joint limits, unnatural motion. **Solutions**: 1. Check URDF joint limits match real robot 2. Adjust `soft_dof_pos_limit` 3. Increase `dof_pos_limits` penalty ```python class rewards(LeggedRobotCfg.rewards): soft_dof_pos_limit = 0.9 # Penalize at 90% of limit class scales(LeggedRobotCfg.rewards.scales): dof_pos_limits = -10.0 # Increase penalty ``` #### Genesis-Specific Issues **Symptoms**: XML loading errors, missing links. **Solutions**: 1. Ensure `links_to_keep` includes all foot links 2. Check XML uses correct quaternion convention (wxyz) 3. Verify mesh paths are relative to XML location ```python class asset(LeggedRobotCfg.asset): links_to_keep = ['FL_foot', 'FR_foot', 'RL_foot', 'RR_foot'] dof_vel_limits = [30.0] * 12 # Required for Genesis ``` #### IsaacGym-Specific Issues **Symptoms**: Reset bugs, incorrect state after reset. **Solutions**: 1. Call `simulator.forward()` after reset 2. Check `collapse_fixed_joints` setting 3. Verify `default_dof_drive_mode` is correct ```python # In _reset_dofs or similar self.simulator.reset_dofs(env_ids, dof_pos, dof_vel) self.simulator.forward() # Required for IsaacGym ``` #### IsaacLab-Specific Issues **Symptoms**: Domain randomization errors, device mismatch. **Solutions**: 1. Ensure DR tensors are on CPU for IsaacLab 2. Use correct `collapse_fixed_joints` setting 3. Check `measure_heights` compatibility ```python # IsaacLab requires CPU tensors for DR # This is handled internally, but be aware ``` ### Debugging Commands ```bash # List all registered tasks python -c "from legged_gym.envs import task_registry; print(task_registry.task_classes.keys())" # Check config values python -c "from legged_gym.envs.myrobot.myrobot_config import MyRobotCfg; cfg = MyRobotCfg(); print(f'obs: {cfg.env.num_observations}, actions: {cfg.env.num_actions}')" # Test single environment python -m legged_gym.scripts.train --task=myrobot --num_envs=1 --headless ``` --- ## Summary Checklist Before submitting your new robot implementation: - [ ] Robot class inherits from `LeggedRobot` - [ ] All required methods implemented or using defaults - [ ] Configuration class inherits from `LeggedRobotCfg` - [ ] Asset paths correct for all simulators - [ ] Joint names match URDF/XML exactly - [ ] Default pose is stable - [ ] Task registered in `__init__.py` - [ ] Basic training runs without errors - [ ] Robot learns to walk (even poorly) - [ ] No CUDA memory issues with default settings --- ## Next Steps After successfully adding your robot: 1. **Terrain Training**: Add rough terrain for robustness ```python class terrain(LeggedRobotCfg.terrain): mesh_type = "trimesh" curriculum = True measure_heights = True ``` 2. **Sim-to-Real**: Prepare for deployment - Remove `base_lin_vel` from observations - Add observation noise - Train with domain randomization 3. **Advanced Methods**: Implement specialized algorithms - Teacher-Student for privileged information - Explicit Estimator for velocity estimation - Walk These Ways for gait control --- ## References - {doc}`../parameter_reference/legged_robot_config` - Complete parameter reference - {doc}`../api_reference/legged_robot` - API documentation - [IsaacGym Documentation](https://developer.nvidia.com/isaac-gym) - [Genesis Documentation](https://genesis-world.readthedocs.io/) - [IsaacLab Documentation](https://isaac-sim.github.io/IsaacLab/)