feat: huoyi M20 smooth/bridge training and stability docs

Current code snapshot: smooth/bridge task configs, smooth random terrain,
roll/pitch stability rewards, M20 URDF updates, eval profile and stability
training documentation.
This commit is contained in:
syfffffjj
2026-11-02 16:41:07 +08:00
commit 07027e8fde
91 changed files with 13047 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
[package]
# Semantic Versioning is used: https://semver.org/
version = "1.0.0"
# Description
category = "isaaclab"
readme = "README.md"
title = "RL Training Repo for DeepRobotics"
author = "Bo Peng"
maintainer = "Bo Peng"
description="RL Training Repo for DeepRobotic, Based on IsaacLab."
repository = "https://github.com/DeepRoboticsLab/rl_training.git"
keywords = ["extension", "rl_training", "isaaclab"]
[dependencies]
"isaaclab" = {}
"isaaclab_assets" = {}
"isaaclab_mimic" = {}
"isaaclab_rl" = {}
"isaaclab_tasks" = {}
# NOTE: Add additional dependencies here
[[python.module]]
name = "rl_training"
[isaaclab_settings]
# TODO: Uncomment and list any apt dependencies here.
# If none, leave it commented out.
# apt_deps = ["example_package"]
# TODO: Uncomment and provide path to a ros_ws
# with rosdeps to be installed. If none,
# leave it commented out.
# ros_ws = "path/from/extension_root/to/ros_ws"
+3
View File
@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel", "toml"]
build-backend = "setuptools.build_meta"
@@ -0,0 +1,15 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
"""
Python module serving as a project/extension template.
"""
# Register Gym environments.
from .tasks import *
# Register UI extensions.
from .ui_extension_example import *
@@ -0,0 +1,32 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Package containing asset and sensor configurations."""
import os
import toml
##
# Configuration for different assets.
##
# Conveniences to other module directories via relative paths
ISAACLAB_ASSETS_EXT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))
"""Path to the extension source directory."""
ISAACLAB_ASSETS_DATA_DIR = os.path.join(ISAACLAB_ASSETS_EXT_DIR, "../../deep_robotics_model")
"""Path to the extension data directory."""
ISAACLAB_ASSETS_METADATA = toml.load(os.path.join(ISAACLAB_ASSETS_EXT_DIR, "config", "extension.toml"))
"""Extension metadata dictionary parsed from the extension.toml file."""
# Configure the module-level variables
__version__ = ISAACLAB_ASSETS_METADATA["package"]["version"]
@@ -0,0 +1,125 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
import isaaclab.sim as sim_utils
from isaaclab.actuators import DCMotorCfg, DelayedPDActuatorCfg
from isaaclab.assets.articulation import ArticulationCfg
from rl_training.assets import ISAACLAB_ASSETS_DATA_DIR
DEEPROBOTICS_LITE3_CFG = ArticulationCfg(
spawn=sim_utils.UsdFileCfg(
usd_path=f"{ISAACLAB_ASSETS_DATA_DIR}/Lite3/Lite3_usd/Lite3.usd",
activate_contact_sensors=True,
rigid_props=sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
retain_accelerations=False,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=1000.0,
max_depenetration_velocity=1.0,
),
articulation_props=sim_utils.ArticulationRootPropertiesCfg(
enabled_self_collisions=False, solver_position_iteration_count=4, solver_velocity_iteration_count=1
),
),
init_state=ArticulationCfg.InitialStateCfg(
pos=(0.0, 0.0, 0.35),
joint_pos={
".*Knee_joint": 0.0,
},
joint_vel={".*": 0.0},
),
soft_joint_pos_limit_factor=0.99,
actuators={
# "Hip": DelayedPDActuatorCfg(
# joint_names_expr=[".*_Hip[X,Y]_joint"],
# effort_limit=24.0,
# velocity_limit=26.2,
# stiffness=30.0,
# damping=1.0,
# friction=0.0,
# armature=0.0,
# min_delay=0,
# max_delay=5,
# ),
"Knee": DelayedPDActuatorCfg(
joint_names_expr=[".*_Knee_joint"],
effort_limit=36.0,
velocity_limit=26.2,
stiffness=30.0,
damping=1.0,
friction=0.0,
armature=0.0,
min_delay=0,
max_delay=5,
),
},
)
DEEPROBOTICS_M20_CFG = ArticulationCfg(
spawn=sim_utils.UrdfFileCfg(
asset_path=f"{ISAACLAB_ASSETS_DATA_DIR}/M20/M20_urdf/urdf/Solenopsis.urdf",
usd_dir="/home/suyufei/tmp/IsaacLab_m20_wheel220",
fix_base=False,
merge_fixed_joints=True,
joint_drive=None,
activate_contact_sensors=True,
visual_material=sim_utils.PreviewSurfaceCfg(
diffuse_color=(0.42, 0.45, 0.50),
roughness=0.65,
metallic=0.15,
),
rigid_props=sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
retain_accelerations=False,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=1000.0,
max_angular_velocity=1000.0,
max_depenetration_velocity=1.0,
),
articulation_props=sim_utils.ArticulationRootPropertiesCfg(
enabled_self_collisions=False, solver_position_iteration_count=4, solver_velocity_iteration_count=1
),
),
init_state=ArticulationCfg.InitialStateCfg(
pos=(0.0, 0.0, 0.32),
joint_pos={
"f[l,r]_knee_joint": 0.5,
"h[l,r]_knee_joint": -0.5,
".*wheel_joint": 0.0,
},
joint_vel={".*": 0.0},
),
soft_joint_pos_limit_factor=0.9,
actuators={
"joint": DelayedPDActuatorCfg(
joint_names_expr=[".*knee_joint"],
effort_limit=75.0,
velocity_limit=17.8024,
stiffness=80.0,
damping=2.0,
friction=0.0,
armature=0.0,
min_delay=0,
max_delay=5,
),
"wheel": DelayedPDActuatorCfg(
joint_names_expr=[".*_wheel_joint"],
effort_limit=28.68,
velocity_limit=109.956,
stiffness=0.0,
damping=0.6,
friction=1.0,
armature=0.00243216,
min_delay=0,
max_delay=5,
),
},
)
@@ -0,0 +1,27 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Package containing task implementations for various robotic environments."""
import os
import toml
from isaaclab_tasks.utils import import_packages
##
# Register Gym environments.
##
# The blacklist is used to prevent importing configs from sub-packages
_BLACKLIST_PKGS = ["utils"]
# Import all configs in this package
import_packages(__name__, _BLACKLIST_PKGS)
@@ -0,0 +1,16 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""
Config-based workflow environments.
"""
import gymnasium as gym
@@ -0,0 +1,14 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Locomotion environments for legged robots."""
from .velocity import * # noqa
@@ -0,0 +1,18 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Locomotion environments with velocity-tracking commands.
These environments are based on the `legged_gym` environments provided by Rudin et al.
Reference:
https://github.com/leggedrobotics/legged_gym
"""
@@ -0,0 +1,15 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Configurations for velocity-based locomotion environments."""
# We leave this file empty since we don't want to expose any configs in this package directly.
# We still need this file to import the "config" module in the parent package.
@@ -0,0 +1,15 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Configurations for velocity-based locomotion environments."""
# We leave this file empty since we don't want to expose any configs in this package directly.
# We still need this file to import the "config" module in the parent package.
@@ -0,0 +1,36 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
import gymnasium as gym
from . import agents
##
# Register Gym environments.
##
gym.register(
id="Flat-Deeprobotics-Lite3-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.flat_env_cfg:DeeproboticsLite3FlatEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsLite3FlatPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsLite3FlatTrainerCfg",
},
)
gym.register(
id="Rough-Deeprobotics-Lite3-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.rough_env_cfg:DeeproboticsLite3RoughEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsLite3RoughPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsLite3RoughTrainerCfg",
},
)
@@ -0,0 +1,5 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,48 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.utils import configclass
from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg
@configclass
class DeeproboticsLite3RoughPPORunnerCfg(RslRlOnPolicyRunnerCfg):
num_steps_per_env = 24
max_iterations = 10000
save_interval = 100
experiment_name = "deeprobotics_lite3_rough"
empirical_normalization = False
clip_actions = 100
policy = RslRlPpoActorCriticCfg(
init_noise_std=1.0,
noise_std_type="log",
actor_hidden_dims=[512, 256, 128],
critic_hidden_dims=[512, 256, 128],
activation="elu",
)
algorithm = RslRlPpoAlgorithmCfg(
value_loss_coef=1.0,
use_clipped_value_loss=True,
clip_param=0.2,
entropy_coef=0.01,
num_learning_epochs=5,
num_mini_batches=4,
learning_rate=1.0e-3,
schedule="adaptive",
gamma=0.99,
lam=0.95,
desired_kl=0.01,
max_grad_norm=1.0,
)
@configclass
class DeeproboticsLite3FlatPPORunnerCfg(DeeproboticsLite3RoughPPORunnerCfg):
def __post_init__(self):
super().__post_init__()
self.max_iterations = 10000
self.experiment_name = "deeprobotics_lite3_flat"
@@ -0,0 +1,32 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.utils import configclass
from .rough_env_cfg import DeeproboticsLite3RoughEnvCfg
@configclass
class DeeproboticsLite3FlatEnvCfg(DeeproboticsLite3RoughEnvCfg):
def __post_init__(self):
# post init of parent
super().__post_init__()
# override rewards
# self.rewards.base_height_l2.params["sensor_cfg"] = None
# change terrain to flat
self.scene.terrain.terrain_type = "plane"
self.scene.terrain.terrain_generator = None
# no height scan
# self.scene.height_scanner = None
self.observations.policy.height_scan = None
# self.observations.critic.height_scan = None
# no terrain curriculum
self.curriculum.terrain_levels = None
# If the weight of rewards is 0, set rewards to None
if self.__class__.__name__ == "DeeproboticsLite3FlatEnvCfg":
self.disable_zero_weight_rewards()
@@ -0,0 +1,183 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.utils import configclass
from rl_training.tasks.manager_based.locomotion.velocity.velocity_env_cfg import LocomotionVelocityRoughEnvCfg
# from isaaclab.sensors.ray_caster import GridPatternCfg
##
# Pre-defined configs
##
from rl_training.assets.deeprobotics import DEEPROBOTICS_LITE3_CFG # isort: skip
@configclass
class DeeproboticsLite3RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
base_link_name = "TORSO"
foot_link_name = ".*_FOOT"
# fmt: off
joint_names = [
"FL_HipX_joint", "FL_HipY_joint", "FL_Knee_joint",
"FR_HipX_joint", "FR_HipY_joint", "FR_Knee_joint",
"HL_HipX_joint", "HL_HipY_joint", "HL_Knee_joint",
"HR_HipX_joint", "HR_HipY_joint", "HR_Knee_joint",
]
link_names = [
'TORSO',
'FL_HIP', 'FR_HIP', 'HL_HIP', 'HR_HIP',
'FL_THIGH', 'FR_THIGH', 'HL_THIGH', 'HR_THIGH',
'FL_SHANK', 'FR_SHANK', 'HL_SHANK', 'HR_SHANK',
'FL_FOOT', 'FR_FOOT', 'HL_FOOT', 'HR_FOOT',
]
# fmt: on
def __post_init__(self):
# post init of parent
super().__post_init__()
# ------------------------------Sence------------------------------
self.scene.robot = DEEPROBOTICS_LITE3_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
self.scene.height_scanner.prim_path = "{ENV_REGEX_NS}/Robot/" + self.base_link_name
self.scene.height_scanner_base.prim_path = "{ENV_REGEX_NS}/Robot/" + self.base_link_name
self.scene.height_scanner.pattern_cfg.resolution = 0.07 # = GridPatternCfg(resolution=0.07, size=[1.6, 1.0]),
# ------------------------------Observations------------------------------
self.observations.policy.base_lin_vel = None # type: ignore
self.observations.policy.height_scan = None # type: ignore
self.observations.policy.base_ang_vel.scale = 0.25
self.observations.policy.joint_pos.scale = 1.0
self.observations.policy.joint_vel.scale = 0.05
self.observations.policy.joint_pos.params["asset_cfg"].joint_names = self.joint_names
self.observations.policy.joint_vel.params["asset_cfg"].joint_names = self.joint_names
# ------------------------------Actions------------------------------
# reduce action scale
self.actions.joint_pos.scale = {".*_HipX_joint": 0.125, "^(?!.*_HipX_joint).*": 0.25}
self.actions.joint_pos.clip = {".*": (-100.0, 100.0)}
self.actions.joint_pos.joint_names = self.joint_names
# ------------------------------Events------------------------------
self.events.randomize_reset_base.params = {
"pose_range": {
"x": (-1.0, 1.0),
"y": (-1.0, 1.0),
"z": (0.0, 0.0),
"roll": (-0.3, 0.3),
"pitch": (-0.3, 0.3),
"yaw": (-3.14, 3.14),
},
"velocity_range": {
"x": (-0.2, 0.2),
"y": (-0.2, 0.2),
"z": (-0.2, 0.2),
"roll": (-0.05, 0.05),
"pitch": (-0.05, 0.05),
"yaw": (-0.0, 0.0),
},
}
self.events.randomize_rigid_body_mass.params["asset_cfg"].body_names = self.link_names # [self.base_link_name]
self.events.randomize_rigid_body_mass_base = None
self.events.randomize_com_positions.params["asset_cfg"].body_names = self.base_link_name # [self.base_link_name]
# self.events.randomize_com_positions = None
self.events.randomize_apply_external_force_torque = None
self.events.randomize_push_robot = None
self.events.randomize_actuator_gains.params["asset_cfg"].joint_names = self.joint_names
# set terrain generation probability to 0 for boxes and stairs
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].proportion = 0.4
self.scene.terrain.terrain_generator.sub_terrains["hf_pyramid_slope"].proportion = 0.3
self.scene.terrain.terrain_generator.sub_terrains["hf_pyramid_slope_inv"].proportion = 0.3
self.scene.terrain.terrain_generator.sub_terrains["boxes"].proportion = 0.0
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs"].proportion = 0.0
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs_inv"].proportion = 0.0
# scale down the terrains because the robot is small
# self.scene.terrain.terrain_generator.sub_terrains["boxes"].grid_height_range = (0.025, 0.1)
# self.scene.terrain.terrain_generator.sub_terrains["boxes"].grid_width = 0.8
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].noise_range = (0.01, 0.06)
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].noise_step = 0.01
# ------------------------------Rewards------------------------------
self.rewards.action_rate_l2.weight = -0.02 #-0.02
# self.rewards.smoothness_2.weight = -0.0075
self.rewards.base_height_l2.weight = -10.0
self.rewards.base_height_l2.params["target_height"] = 0.35
self.rewards.base_height_l2.params["asset_cfg"].body_names = [self.base_link_name]
self.rewards.feet_air_time.weight = 5.0 # 5.0
self.rewards.feet_air_time.params["threshold"] = 0.5
self.rewards.feet_air_time.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_air_time_variance.weight = -8.0 # -8.0
self.rewards.feet_air_time_variance.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_slide.weight = -0.05
self.rewards.feet_slide.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_slide.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.stand_still.weight = -0.5 # -1.0
self.rewards.stand_still.params["asset_cfg"].joint_names = self.joint_names
self.rewards.stand_still.params["command_threshold"] = 0.1
self.rewards.feet_height_body.weight = -2.5 # -2.5
self.rewards.feet_height_body.params["target_height"] = -0.35
self.rewards.feet_height_body.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_height.weight = -0.2 # -0.2
self.rewards.feet_height.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_height.params["target_height"] = 0.05
self.rewards.contact_forces.weight = -1e-1 # -2e-2
self.rewards.contact_forces.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.lin_vel_z_l2.weight = -2.0 #-2.0
self.rewards.ang_vel_xy_l2.weight = -0.05 # -0.05
self.rewards.track_lin_vel_xy_exp.weight = 3.0
self.rewards.track_ang_vel_z_exp.weight = 1.5
self.rewards.undesired_contacts.weight = -0.5
self.rewards.undesired_contacts.params["sensor_cfg"].body_names = [f"^(?!.*{self.foot_link_name}).*"]
self.rewards.joint_torques_l2.weight = -2.5e-5
self.rewards.joint_acc_l2.weight = -1e-8
self.rewards.joint_deviation_l1.weight = -0.5
self.rewards.joint_deviation_l1.params["asset_cfg"].joint_names = [".*HipX.*"]
self.rewards.joint_power.weight = -2e-5
self.rewards.flat_orientation_l2.weight = -5.0
# add the following rewards to improve the gait
self.rewards.feet_gait.weight = 0.5
self.rewards.feet_gait.params["synced_feet_pair_names"] = [
["FL_FOOT", "HR_FOOT"],
["FR_FOOT", "HL_FOOT"]
]
self.rewards.joint_mirror.weight = -0.05
self.rewards.joint_mirror.params["mirror_joints"] = [
["FL_(HipX|HipY|Knee).*", "HR_(HipX|HipY|Knee).*"],
["FR_(HipX|HipY|Knee).*", "HL_(HipX|HipY|Knee).*"],
]
self.rewards.joint_pos_limits.weight = -5.0
# self.rewards.joint_pos_penalty.weight = -1.0
self.rewards.feet_contact_without_cmd.weight = 0.1
self.rewards.feet_contact_without_cmd.params["sensor_cfg"].body_names = [self.foot_link_name]
# If the weight of rewards is 0, set rewards to None
if self.__class__.__name__ == "DeeproboticsLite3RoughEnvCfg":
self.disable_zero_weight_rewards()
# ------------------------------Terminations------------------------------
self.terminations.illegal_contact = None
# self.terminations.bad_orientation_2 = None
# ------------------------------Curriculums------------------------------
# self.curriculum.command_levels.params["range_multiplier"] = (0.2, 1.0)
self.curriculum.command_levels = None
# ------------------------------Commands------------------------------
self.commands.base_velocity.ranges.lin_vel_x = (-1.5, 1.5)
self.commands.base_velocity.ranges.lin_vel_y = (-0.8, 0.8)
self.commands.base_velocity.ranges.ang_vel_z = (-0.8, 0.8)
@@ -0,0 +1,15 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Configurations for velocity-based locomotion environments."""
# We leave this file empty since we don't want to expose any configs in this package directly.
# We still need this file to import the "config" module in the parent package.
@@ -0,0 +1,68 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
import gymnasium as gym
from . import agents
##
# Register Gym environments.
##
gym.register(
id="Flat-Deeprobotics-M20-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.flat_env_cfg:DeeproboticsM20FlatEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsM20FlatPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsM20FlatTrainerCfg",
},
)
gym.register(
id="Rough-Deeprobotics-M20-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.rough_env_cfg:DeeproboticsM20RoughEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsM20RoughPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsM20RoughTrainerCfg",
},
)
gym.register(
id="Rough-Deeprobotics-M20-Support-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.rough_env_cfg:DeeproboticsM20SupportRoughEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsM20SupportRoughPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsM20RoughTrainerCfg",
},
)
gym.register(
id="Rough-Deeprobotics-M20-Bridge-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.bridge_env_cfg:DeeproboticsM20BridgeEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsM20BridgePPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsM20RoughTrainerCfg",
},
)
gym.register(
id="Smooth-Deeprobotics-M20-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.smooth_env_cfg:DeeproboticsM20SmoothEnvCfg",
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DeeproboticsM20SmoothPPORunnerCfg",
"cusrl_cfg_entry_point": f"{agents.__name__}.cusrl_ppo_cfg:DeeproboticsM20RoughTrainerCfg",
},
)
@@ -0,0 +1,5 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,78 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.utils import configclass
from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg
@configclass
class DeeproboticsM20RoughPPORunnerCfg(RslRlOnPolicyRunnerCfg):
num_steps_per_env = 24
max_iterations = 20000
save_interval = 100
experiment_name = "deeprobotics_m20_rough"
empirical_normalization = False
clip_actions = 100
policy = RslRlPpoActorCriticCfg(
init_noise_std=1.0,
noise_std_type="log",
actor_hidden_dims=[512, 256, 128],
critic_hidden_dims=[512, 256, 128],
activation="elu",
)
algorithm = RslRlPpoAlgorithmCfg(
value_loss_coef=1.0,
use_clipped_value_loss=True,
clip_param=0.2,
entropy_coef=0.01,
num_learning_epochs=5,
num_mini_batches=4,
learning_rate=1.0e-3,
schedule="adaptive",
gamma=0.99,
lam=0.95,
desired_kl=0.01,
max_grad_norm=1.0,
)
@configclass
class DeeproboticsM20FlatPPORunnerCfg(DeeproboticsM20RoughPPORunnerCfg):
def __post_init__(self):
super().__post_init__()
self.max_iterations = 5000
self.experiment_name = "deeprobotics_m20_flat"
@configclass
class DeeproboticsM20SupportRoughPPORunnerCfg(DeeproboticsM20RoughPPORunnerCfg):
def __post_init__(self):
super().__post_init__()
self.experiment_name = "deeprobotics_m20_support"
@configclass
class DeeproboticsM20BridgePPORunnerCfg(DeeproboticsM20RoughPPORunnerCfg):
"""Single-side bridge fine-tuning, forward-only command."""
def __post_init__(self):
super().__post_init__()
self.max_iterations = 5000
self.experiment_name = "huoyi_bridge"
@configclass
class DeeproboticsM20SmoothPPORunnerCfg(DeeproboticsM20RoughPPORunnerCfg):
"""Smooth random-undulation fine-tuning, forward-only command."""
def __post_init__(self):
super().__post_init__()
self.max_iterations = 5000
self.experiment_name = "huoyi_smooth"
@@ -0,0 +1,76 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
"""Single-side bridge task: left legs on a long raised bridge, right legs on flat ground."""
from isaaclab.terrains import TerrainGeneratorCfg
from isaaclab.utils import configclass
import rl_training.tasks.manager_based.locomotion.velocity.mdp as mdp
from .rough_env_cfg import DeeproboticsM20RoughEnvCfg
@configclass
class DeeproboticsM20BridgeEnvCfg(DeeproboticsM20RoughEnvCfg):
"""Stage 1 policy on a long single-side bridge, forward-only command."""
def __post_init__(self):
super().__post_init__()
# ------------------------------Terrain------------------------------
# One long bridge cell type. The bridge runs along +x, so the robot can
# keep walking forward without leaving the split-level configuration.
self.scene.terrain.terrain_generator = TerrainGeneratorCfg(
size=(12.0, 4.0),
border_width=2.0,
num_rows=10,
num_cols=20,
horizontal_scale=0.1,
vertical_scale=0.005,
slope_threshold=0.75,
use_cache=False,
curriculum=True,
sub_terrains={
"single_side_bridge": mdp.SingleSideBridgeTerrainCfg(
proportion=1.0,
bridge_height_range=(0.06, 0.14),
bridge_width=1.5,
)
},
)
self.scene.terrain.max_init_terrain_level = 0
# ------------------------------Spawn------------------------------
# Spawn exactly on the bridge edge: y=0 and yaw=0 put the left wheels
# on the bridge and the right wheels on the flat ground.
self.events.randomize_reset_base.params = {
"pose_range": {
"x": (-1.0, 1.0),
"y": (0.0, 0.0),
"z": (0.30, 0.30),
"roll": (-0.03, 0.03),
"pitch": (-0.03, 0.03),
"yaw": (0.0, 0.0),
},
"velocity_range": {
"x": (-0.1, 0.1),
"y": (0.0, 0.0),
"z": (0.0, 0.0),
"roll": (0.0, 0.0),
"pitch": (0.0, 0.0),
"yaw": (0.0, 0.0),
},
}
# ------------------------------Commands------------------------------
# Forward-only: no lateral and no yaw command.
self.commands.base_velocity.heading_command = False
self.commands.base_velocity.rel_standing_envs = 0.0
self.commands.base_velocity.rel_heading_envs = 0.0
self.commands.base_velocity.ranges.lin_vel_x = (0.2, 0.8)
self.commands.base_velocity.ranges.lin_vel_y = (0.0, 0.0)
self.commands.base_velocity.ranges.ang_vel_z = (0.0, 0.0)
# Keep the same 33-dim policy / 8-dim action layout as the other tasks.
self.disable_zero_weight_rewards()
@@ -0,0 +1,32 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.utils import configclass
from .rough_env_cfg import DeeproboticsM20RoughEnvCfg
@configclass
class DeeproboticsM20FlatEnvCfg(DeeproboticsM20RoughEnvCfg):
def __post_init__(self):
# post init of parent
super().__post_init__()
# override rewards
self.rewards.base_height_l2.params["sensor_cfg"] = None
# change terrain to flat
self.scene.terrain.terrain_type = "plane"
self.scene.terrain.terrain_generator = None
# no height scan
self.scene.height_scanner = None
self.observations.policy.height_scan = None
self.observations.critic.height_scan = None
# no terrain curriculum
self.curriculum.terrain_levels = None
# If the weight of rewards is 0, set rewards to None
if self.__class__.__name__ == "DeeproboticsM20FlatEnvCfg":
self.disable_zero_weight_rewards()
@@ -0,0 +1,475 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from isaaclab.managers import CurriculumTermCfg as CurrTerm
from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.managers import RewardTermCfg as RewTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.managers import TerminationTermCfg as DoneTerm
from isaaclab.utils import configclass
import rl_training.tasks.manager_based.locomotion.velocity.mdp as mdp
from rl_training.tasks.manager_based.locomotion.velocity.velocity_env_cfg import (
ActionsCfg,
EventCfg,
LocomotionVelocityRoughEnvCfg,
RewardsCfg,
)
##
# Pre-defined configs
##
from rl_training.assets.deeprobotics import DEEPROBOTICS_M20_CFG # isort: skip
@configclass
class DeeproboticsM20ActionsCfg(ActionsCfg):
"""Action specifications for the MDP."""
joint_pos = mdp.JointPositionActionCfg(
asset_name="robot", joint_names=[""], scale=0.25, use_default_offset=True, clip=None, preserve_order=True
)
joint_vel = mdp.JointVelocityActionCfg(
asset_name="robot", joint_names=[""], scale=20.0, use_default_offset=True, clip=None, preserve_order=True
)
@configclass
class DeeproboticsM20EventCfg(EventCfg):
"""Domain randomization with tire-specific contact material."""
# Replace the inherited all-body material randomization with separate tire and support materials.
randomize_rigid_body_material = None
randomize_wheel_material = EventTerm(
func=mdp.randomize_rigid_body_material,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=".*_wheel"),
"static_friction_range": (0.9, 1.4),
"dynamic_friction_range": (0.6, 0.9),
"restitution_range": (0.0, 0.05),
"num_buckets": 1024,
},
)
randomize_support_material = EventTerm(
func=mdp.randomize_rigid_body_material,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=["base_link", ".*_knee"]),
"static_friction_range": (0.4, 0.8),
"dynamic_friction_range": (0.25, 0.4),
"restitution_range": (0.0, 0.05),
"num_buckets": 1024,
},
)
@configclass
class DeeproboticsM20RewardsCfg(RewardsCfg):
"""Reward terms for the MDP."""
joint_vel_wheel_l2 = RewTerm(
func=mdp.joint_vel_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names="")}
)
joint_acc_wheel_l2 = RewTerm(
func=mdp.joint_acc_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names="")}
)
joint_torques_wheel_l2 = RewTerm(
func=mdp.joint_torques_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names="")}
)
track_diff_wheel_yaw_exp = RewTerm(
func=mdp.track_diff_wheel_yaw_exp,
weight=1.0,
params={
"command_name": "base_velocity",
"std": 0.6,
"wheel_radius": 0.11,
"track_width": 0.50,
"left_wheel_indices": (0, 2),
"right_wheel_indices": (1, 3),
"command_threshold": 0.05,
"asset_cfg": SceneEntityCfg("robot", joint_names=""),
},
)
lin_vel_xy_yaw_command_l2 = RewTerm(
func=mdp.lin_vel_xy_yaw_command_l2,
weight=-0.4,
params={
"command_name": "base_velocity",
"yaw_command_threshold": 0.25,
"lin_command_threshold": 0.15,
"asset_cfg": SceneEntityCfg("robot"),
},
)
command_progress = RewTerm(
func=mdp.command_progress,
weight=0.0,
params={
"command_name": "base_velocity",
"min_command": 0.12,
"max_reward_speed": 0.8,
"asset_cfg": SceneEntityCfg("robot"),
},
)
support_contact_progress = RewTerm(
func=mdp.ObstacleSupportEscapeReward,
weight=0.0,
params={
"command_name": "base_velocity",
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"wheel_sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"asset_cfg": SceneEntityCfg("robot"),
"wheel_asset_cfg": SceneEntityCfg("robot", joint_names=""),
"wheel_radius": 0.11,
"support_force_threshold": 15.0,
"wheel_obstacle_force_threshold": 35.0,
"max_contact_time": 0.45,
"min_command": 0.15,
"stall_speed_ratio": 0.35,
"min_stall_speed": 0.12,
"slip_threshold": 0.30,
"obstacle_memory_time": 0.60,
"support_window_time": 0.80,
"recovery_speed_ratio": 0.60,
"min_recovery_speed": 0.18,
"recovery_reward_time": 0.25,
"cooldown_time": 0.60,
"recovery_bonus": 0.25,
"max_reward_speed": 0.8,
},
)
support_contact_excess = RewTerm(
func=mdp.support_contact_excess,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"force_threshold": 120.0,
"max_contact_time": 0.6,
},
)
knee_inward_fold_penalty = RewTerm(
func=mdp.knee_inward_fold_penalty,
weight=0.0,
params={
"front_asset_cfg": SceneEntityCfg("robot", joint_names=""),
"hind_asset_cfg": SceneEntityCfg("robot", joint_names=""),
"max_inward_tilt": 0.0,
},
)
@configclass
class DeeproboticsM20RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
"""Stage 1: blind tactile locomotion without body-support rewards."""
actions: DeeproboticsM20ActionsCfg = DeeproboticsM20ActionsCfg()
events: DeeproboticsM20EventCfg = DeeproboticsM20EventCfg()
rewards: DeeproboticsM20RewardsCfg = DeeproboticsM20RewardsCfg()
base_link_name = "base_link"
foot_link_name = ".*_wheel"
support_link_names = ["base_link", ".*_knee"]
# fmt: off
leg_joint_names = [
"fl_knee_joint",
"fr_knee_joint",
"hl_knee_joint",
"hr_knee_joint",
]
wheel_joint_names = [
"fl_wheel_joint", "fr_wheel_joint", "hl_wheel_joint", "hr_wheel_joint",
]
knee_joint_names = [
"fl_knee_joint", "fr_knee_joint", "hl_knee_joint", "hr_knee_joint",
]
front_knee_joint_names = ["fl_knee_joint", "fr_knee_joint"]
hind_knee_joint_names = ["hl_knee_joint", "hr_knee_joint"]
joint_names = leg_joint_names + wheel_joint_names
# fmt: on
def __post_init__(self):
# post init of parent
super().__post_init__()
# ------------------------------Sence------------------------------
self.scene.robot = DEEPROBOTICS_M20_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
self.scene.height_scanner.prim_path = "{ENV_REGEX_NS}/Robot/" + self.base_link_name
self.scene.height_scanner_base.prim_path = "{ENV_REGEX_NS}/Robot/" + self.base_link_name
self.scene.terrain.physics_material.restitution = 0.0
self.sim.physics_material = self.scene.terrain.physics_material
# ------------------------------Observations------------------------------
self.observations.policy.joint_pos.func = mdp.joint_pos_rel_without_wheel
self.observations.policy.joint_pos.params["wheel_asset_cfg"] = SceneEntityCfg(
"robot", joint_names=self.wheel_joint_names
)
self.observations.critic.joint_pos.func = mdp.joint_pos_rel_without_wheel
self.observations.critic.joint_pos.params["wheel_asset_cfg"] = SceneEntityCfg(
"robot", joint_names=self.wheel_joint_names
)
self.observations.policy.base_lin_vel.scale = 2.0
self.observations.policy.base_ang_vel.scale = 0.25
self.observations.policy.joint_pos.scale = 1.0
self.observations.policy.joint_vel.scale = 0.05
self.observations.policy.base_lin_vel = None
self.observations.policy.height_scan = None
self.observations.policy.joint_pos.params["asset_cfg"].joint_names = self.joint_names
self.observations.policy.joint_vel.params["asset_cfg"].joint_names = self.joint_names
# ------------------------------Actions------------------------------
# reduce action scale
self.actions.joint_pos.scale = 0.25
self.actions.joint_vel.scale = 5.0
self.actions.joint_pos.clip = {".*": (-100.0, 100.0)}
self.actions.joint_vel.clip = {".*": (-100.0, 100.0)}
self.actions.joint_pos.joint_names = self.leg_joint_names
self.actions.joint_vel.joint_names = self.wheel_joint_names
# ------------------------------Events------------------------------
self.events.randomize_reset_base.params = {
"pose_range": {
"x": (-1.0, 1.0),
"y": (-1.0, 1.0),
"z": (0.0, 0.0),
"roll": (-0.3, 0.3),
"pitch": (-0.3, 0.3),
"yaw": (-3.14, 3.14),
},
"velocity_range": {
"x": (-0.2, 0.2),
"y": (-0.2, 0.2),
"z": (-0.2, 0.2),
"roll": (-0.05, 0.05),
"pitch": (-0.05, 0.05),
"yaw": (-0.0, 0.0),
},
}
self.events.randomize_rigid_body_mass_base.params["asset_cfg"].body_names = [self.base_link_name]
self.events.randomize_rigid_body_mass.params["asset_cfg"].body_names = [
f"^(?!.*{self.base_link_name}).*"
]
self.events.randomize_com_positions.params["asset_cfg"].body_names = [self.base_link_name]
self.events.randomize_apply_external_force_torque.params["asset_cfg"].body_names = [self.base_link_name]
self.scene.terrain.max_init_terrain_level = 2
self.scene.terrain.terrain_generator.sub_terrains["boxes"].grid_height_range = (0.02, 0.14)
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs"].step_height_range = (0.03, 0.16)
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs_inv"].step_height_range = (0.03, 0.16)
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].noise_range = (0.01, 0.10)
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].noise_step = 0.01
# only randomize actuator gains for actuated joints (knees + wheels), exclude hipx/hipy completely
self.events.randomize_actuator_gains.params["asset_cfg"].joint_names = self.joint_names
# ------------------------------Rewards------------------------------
# General
self.rewards.is_terminated.weight = 0
# Root penalties
self.rewards.lin_vel_z_l2.weight = -2.0
self.rewards.ang_vel_xy_l2.weight = -0.05
self.rewards.flat_orientation_l2.weight = -0.35
self.rewards.roll_l2.weight = -0.45
self.rewards.base_height_l2.weight = -0.5
self.rewards.base_height_l2.params["target_height"] = 0.45
self.rewards.base_height_l2.params["asset_cfg"].body_names = [self.base_link_name]
self.rewards.body_lin_acc_l2.weight = 0
self.rewards.body_lin_acc_l2.params["asset_cfg"].body_names = [self.base_link_name]
# Joint penalties
self.rewards.joint_torques_l2.weight = -2.5e-5
self.rewards.joint_torques_l2.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.joint_torques_wheel_l2.weight = 0
self.rewards.joint_torques_wheel_l2.params["asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.joint_vel_l2.weight = 0
self.rewards.joint_vel_l2.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.joint_vel_wheel_l2.weight = 0
self.rewards.joint_vel_wheel_l2.params["asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.joint_acc_l2.weight = -3e-6
self.rewards.joint_acc_l2.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.joint_acc_wheel_l2.weight = -1e-7
self.rewards.joint_acc_wheel_l2.params["asset_cfg"].joint_names = self.wheel_joint_names
# self.rewards.create_joint_deviation_l1_rewterm("joint_deviation_hip_l1", -0.2, [".*_hip_joint"])
self.rewards.joint_pos_limits.weight = -5.0
self.rewards.joint_pos_limits.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.joint_vel_limits.weight = 0
self.rewards.joint_vel_limits.params["asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.joint_power.weight = -2e-5
self.rewards.joint_power.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.stand_still.weight = -2.0
self.rewards.stand_still.params["asset_cfg"].joint_names = self.leg_joint_names
self.rewards.knee_joint_pos_penalty.weight = -0.1
self.rewards.knee_joint_pos_penalty.params["asset_cfg"].joint_names = self.knee_joint_names
# Inward fold limit: with the knee link vertical at q=0, front knees fold inward for
# q < 0 and hind knees for q > 0. Do not allow the legs to pass the vertical
# (straight-down) pose on the inward side.
self.rewards.knee_inward_fold_penalty.weight = -1.0
self.rewards.knee_inward_fold_penalty.params["front_asset_cfg"] = SceneEntityCfg(
"robot", joint_names=self.front_knee_joint_names
)
self.rewards.knee_inward_fold_penalty.params["hind_asset_cfg"] = SceneEntityCfg(
"robot", joint_names=self.hind_knee_joint_names
)
self.rewards.knee_inward_fold_penalty.params["max_inward_tilt"] = 0.0
self.rewards.wheel_vel_penalty.weight = 0
self.rewards.wheel_vel_penalty.params["sensor_cfg"].body_names = self.foot_link_name
self.rewards.wheel_vel_penalty.params["asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.joint_mirror.weight = 0.0
self.rewards.joint_mirror.params["mirror_joints"] = [
["fl_knee_joint", "hr_knee_joint"],
["fr_knee_joint", "hl_knee_joint"],
]
# Action penalties
self.rewards.action_rate_l2.weight = -0.015
# Contact sensor
self.rewards.undesired_contacts.weight = -1.0
self.rewards.undesired_contacts.params["sensor_cfg"].body_names = [f"^(?!.*{self.foot_link_name}).*"]
self.rewards.undesired_contacts.params["threshold"] = 1.0
self.rewards.contact_forces.weight = -1.5e-4
self.rewards.contact_forces.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.support_contact_progress.weight = 0.0
self.rewards.support_contact_progress.params["sensor_cfg"].body_names = self.support_link_names
self.rewards.support_contact_progress.params["wheel_sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.support_contact_progress.params["wheel_asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.support_contact_excess.weight = 0.0
self.rewards.support_contact_excess.params["sensor_cfg"].body_names = self.support_link_names
# Velocity-tracking rewards
self.rewards.track_lin_vel_xy_exp.weight = 2.0 # 1.8
self.rewards.track_ang_vel_z_exp.weight = 1.5 # 1.2
self.rewards.track_ang_vel_z_exp.params["std"] = 0.5
self.rewards.track_diff_wheel_yaw_exp.weight = 1.0
self.rewards.track_diff_wheel_yaw_exp.params["asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.lin_vel_xy_yaw_command_l2.weight = -0.4
# Others
self.rewards.feet_air_time.weight = 0
self.rewards.feet_air_time.params["threshold"] = 0.5
self.rewards.feet_air_time.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_contact.weight = 0
self.rewards.feet_contact.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_contact_without_cmd.weight = 0.1
self.rewards.feet_contact_without_cmd.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_stumble.weight = 0
self.rewards.feet_stumble.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_slide.weight = 0
self.rewards.feet_slide.params["sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_slide.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_height.weight = 0
self.rewards.feet_height.params["target_height"] = 0.1
self.rewards.feet_height.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_height_body.weight = 0
self.rewards.feet_height_body.params["target_height"] = -0.4
self.rewards.feet_height_body.params["asset_cfg"].body_names = [self.foot_link_name]
self.rewards.feet_gait.weight = 0
self.rewards.feet_gait.params["synced_feet_pair_names"] = (("fl_wheel", "hr_wheel"), ("fr_wheel", "hl_wheel"))
self.rewards.upward.weight = 0.08
# If the weight of rewards is 0, set rewards to None
if self.__class__.__name__ == "DeeproboticsM20RoughEnvCfg":
self.disable_zero_weight_rewards()
# ------------------------------Terminations------------------------------
# self.terminations.illegal_contact.params["sensor_cfg"].body_names = [self.base_link_name]
self.terminations.illegal_contact = None
self.terminations.bad_orientation_2 = DoneTerm(
func=mdp.bad_orientation,
params={"limit_angle": 0.9, "asset_cfg": SceneEntityCfg("robot")},
)
# ------------------------------Curriculums------------------------------
# self.curriculum.command_levels.params["range_multiplier"] = (0.2, 1.0)
self.curriculum.command_levels = None
# ------------------------------Commands------------------------------
self.commands.base_velocity.debug_vis = False
self.commands.base_velocity.ranges.lin_vel_x = (-2.0, 2.0)
self.commands.base_velocity.ranges.lin_vel_y = (0.0, 0.0)
self.commands.base_velocity.ranges.ang_vel_z = (-1.5, 1.5)
@configclass
class DeeproboticsM20SupportRoughEnvCfg(DeeproboticsM20RoughEnvCfg):
"""Stage 2: resume from Stage 1 and enable brief body/knee support for obstacle crossing."""
def __post_init__(self):
super().__post_init__()
# Keep the same policy/action dimensions as Stage 1 for checkpoint resume.
# Only rewards, command distribution, and terrain curriculum are changed here.
self.scene.terrain.max_init_terrain_level = 1
self.scene.terrain.terrain_generator.sub_terrains["boxes"].proportion = 0.25
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs"].proportion = 0.25
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs_inv"].proportion = 0.15
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].proportion = 0.15
self.scene.terrain.terrain_generator.sub_terrains["hf_pyramid_slope"].proportion = 0.10
self.scene.terrain.terrain_generator.sub_terrains["hf_pyramid_slope_inv"].proportion = 0.10
self.scene.terrain.terrain_generator.sub_terrains["boxes"].grid_height_range = (0.03, 0.22)
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs"].step_height_range = (0.04, 0.24)
self.scene.terrain.terrain_generator.sub_terrains["pyramid_stairs_inv"].step_height_range = (0.04, 0.20)
self.scene.terrain.terrain_generator.sub_terrains["random_rough"].noise_range = (0.01, 0.14)
# Keep privileged terrain information for the critic only. The actor remains blind/tactile.
self.observations.policy.height_scan = None
self.observations.critic.height_scan.params["sensor_cfg"] = SceneEntityCfg("height_scanner")
self.observations.critic.height_scan.clip = (-1.0, 1.0)
self.observations.critic.height_scan.scale = 1.0
# Body/knee contact is allowed only as short support during forward progress.
self.rewards.undesired_contacts.weight = -0.10
self.rewards.undesired_contacts.params["sensor_cfg"].body_names = [f"^(?!.*{self.foot_link_name}).*"]
self.rewards.undesired_contacts.params["threshold"] = 35.0
self.rewards.command_progress.weight = 0.8
self.rewards.support_contact_progress.weight = 0.0
self.rewards.support_contact_progress.params["sensor_cfg"].body_names = self.support_link_names
self.rewards.support_contact_progress.params["wheel_sensor_cfg"].body_names = [self.foot_link_name]
self.rewards.support_contact_progress.params["wheel_asset_cfg"].joint_names = self.wheel_joint_names
self.rewards.support_contact_excess.weight = -0.60
self.rewards.support_contact_excess.params["sensor_cfg"].body_names = self.support_link_names
self.rewards.support_contact_excess.params["force_threshold"] = 100.0
self.rewards.support_contact_excess.params["max_contact_time"] = 0.45
# Stage 2 should still avoid the bad solution of crawling on the body.
self.rewards.track_lin_vel_xy_exp.weight = 1.5
self.rewards.track_ang_vel_z_exp.weight = 1.0
self.rewards.flat_orientation_l2.weight = -0.35
self.rewards.roll_l2.weight = -0.45
self.rewards.base_height_l2.weight = -0.35
self.rewards.base_height_l2.params["target_height"] = 0.29
# Stage 2 first focuses on forward obstacle crossing, then expands the speed range.
self.commands.base_velocity.heading_command = False
self.commands.base_velocity.rel_standing_envs = 0.0
self.commands.base_velocity.rel_heading_envs = 0.0
self.commands.base_velocity.ranges.lin_vel_x = (0.25, 1.2)
self.commands.base_velocity.ranges.lin_vel_y = (0.0, 0.0)
self.commands.base_velocity.ranges.ang_vel_z = (-0.6, 0.6)
self.curriculum.command_levels = CurrTerm(
func=mdp.command_levels_positive_x,
params={
"reward_term_name": "track_lin_vel_xy_exp",
"initial_max_ratio": 0.35,
"increment": 0.1,
},
)
self.disable_zero_weight_rewards()
@@ -0,0 +1,79 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
"""Smooth random-undulation task: train both pitch and roll to stay at zero."""
from isaaclab.terrains import TerrainGeneratorCfg
from isaaclab.utils import configclass
import rl_training.tasks.manager_based.locomotion.velocity.mdp as mdp
from .rough_env_cfg import DeeproboticsM20RoughEnvCfg
@configclass
class DeeproboticsM20SmoothEnvCfg(DeeproboticsM20RoughEnvCfg):
"""Stage 1 policy on a smooth, randomly undulating terrain, forward-only command."""
def __post_init__(self):
super().__post_init__()
# ------------------------------Terrain------------------------------
# Low-frequency sinusoids only: no steps, no sharp edges.
self.scene.terrain.terrain_generator = TerrainGeneratorCfg(
size=(8.0, 8.0),
border_width=2.0,
num_rows=10,
num_cols=20,
horizontal_scale=0.05,
vertical_scale=0.005,
slope_threshold=None,
use_cache=False,
curriculum=True,
sub_terrains={
"smooth_random": mdp.SmoothRandomTerrainCfg(
proportion=1.0,
border_width=0.25,
height_range=(0.10, 0.20),
wavelength_range=(1.0, 2.2),
num_waves=3,
)
},
)
self.scene.terrain.max_init_terrain_level = 9
# ------------------------------Spawn------------------------------
self.events.randomize_reset_base.params = {
"pose_range": {
"x": (-2.0, 2.0),
"y": (-2.0, 2.0),
"z": (0.30, 0.30),
"roll": (-0.05, 0.05),
"pitch": (-0.05, 0.05),
"yaw": (-3.14, 3.14),
},
"velocity_range": {
"x": (-0.1, 0.1),
"y": (0.0, 0.0),
"z": (0.0, 0.0),
"roll": (0.0, 0.0),
"pitch": (0.0, 0.0),
"yaw": (0.0, 0.0),
},
}
# Commands keep the original Stage-1 distribution inherited from
# DeeproboticsM20RoughEnvCfg: x in [-2, 2] m/s, y = 0, yaw in [-1.5, 1.5]
# rad/s, heading command enabled, 2% standing and 100% heading envs.
# ------------------------------Rewards------------------------------
# Split the flat-orientation penalty into true Euler pitch and roll in
# radians. Giving -0.4 to each keeps the total strength of the previous
# -0.8 flat_orientation_l2 term while letting the two axes be tuned
# independently.
self.rewards.flat_orientation_l2.weight = 0.0
self.rewards.pitch_l2.weight = -0.4
self.rewards.roll_l2.weight = -0.4
self.rewards.ang_vel_xy_l2.weight = -0.1
self.disable_zero_weight_rewards()
@@ -0,0 +1,22 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""This sub-module contains the functions that are specific to the locomotion environments."""
from isaaclab.envs.mdp import * # noqa: F401, F403
from isaaclab_tasks.manager_based.locomotion.velocity.mdp import * # noqa: F401, F403
from .commands import * # noqa: F401, F403
from .curriculums import * # noqa: F401, F403
from .events import * # noqa: F401, F403
from .observations import * # noqa: F401, F403
from .rewards import * # noqa: F401, F403
from .terrain import * # noqa: F401, F403
@@ -0,0 +1,130 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import torch
from typing import TYPE_CHECKING, Sequence
from isaaclab.managers import CommandTerm, CommandTermCfg
from isaaclab.utils import configclass
import rl_training.tasks.manager_based.locomotion.velocity.mdp as mdp
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedEnv
class UniformThresholdVelocityCommand(mdp.UniformVelocityCommand):
"""Command generator that generates a velocity command in SE(2) from uniform distribution with threshold."""
cfg: mdp.UniformThresholdVelocityCommandCfg
"""The configuration of the command generator."""
def _resample_command(self, env_ids: Sequence[int]):
super()._resample_command(env_ids)
# set small commands to zero
self.vel_command_b[env_ids, :2] *= (torch.norm(self.vel_command_b[env_ids, :2], dim=1) > 0.2).unsqueeze(1)
@configclass
class UniformThresholdVelocityCommandCfg(mdp.UniformVelocityCommandCfg):
"""Configuration for the uniform threshold velocity command generator."""
class_type: type = UniformThresholdVelocityCommand
class DiscreteCommandController(CommandTerm):
"""
Command generator that assigns discrete commands to environments.
Commands are stored as a list of predefined integers.
The controller maps these commands by their indices (e.g., index 0 -> 10, index 1 -> 20).
"""
cfg: DiscreteCommandControllerCfg
"""Configuration for the command controller."""
def __init__(self, cfg: DiscreteCommandControllerCfg, env: ManagerBasedEnv):
"""
Initialize the command controller.
Args:
cfg: The configuration of the command controller.
env: The environment object.
"""
# Initialize the base class
super().__init__(cfg, env)
# Validate that available_commands is non-empty
if not self.cfg.available_commands:
raise ValueError("The available_commands list cannot be empty.")
# Ensure all elements are integers
if not all(isinstance(cmd, int) for cmd in self.cfg.available_commands):
raise ValueError("All elements in available_commands must be integers.")
# Store the available commands
self.available_commands = self.cfg.available_commands
# Create buffers to store the command
# -- command buffer: stores discrete action indices for each environment
self.command_buffer = torch.zeros(self.num_envs, dtype=torch.int32, device=self.device)
# -- current_commands: stores a snapshot of the current commands (as integers)
self.current_commands = [self.available_commands[0]] * self.num_envs # Default to the first command
def __str__(self) -> str:
"""Return a string representation of the command controller."""
return (
"DiscreteCommandController:\n"
f"\tNumber of environments: {self.num_envs}\n"
f"\tAvailable commands: {self.available_commands}\n"
)
"""
Properties
"""
@property
def command(self) -> torch.Tensor:
"""Return the current command buffer. Shape is (num_envs, 1)."""
return self.command_buffer
"""
Implementation specific functions.
"""
def _update_metrics(self):
"""Update metrics for the command controller."""
pass
def _resample_command(self, env_ids: Sequence[int]):
"""Resample commands for the given environments."""
sampled_indices = torch.randint(
len(self.available_commands), (len(env_ids),), dtype=torch.int32, device=self.device
)
sampled_commands = torch.tensor(
[self.available_commands[idx.item()] for idx in sampled_indices], dtype=torch.int32, device=self.device
)
self.command_buffer[env_ids] = sampled_commands
def _update_command(self):
"""Update and store the current commands."""
self.current_commands = self.command_buffer.tolist()
@configclass
class DiscreteCommandControllerCfg(CommandTermCfg):
"""Configuration for the discrete command controller."""
class_type: type = DiscreteCommandController
available_commands: list[int] = []
"""
List of available discrete commands, where each element is an integer.
Example: [10, 20, 30, 40, 50]
"""
@@ -0,0 +1,95 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
"""Common functions that can be used to create curriculum for the learning environment.
The functions can be passed to the :class:`isaaclab.managers.CurriculumTermCfg` object to enable
the curriculum introduced by the function.
"""
from __future__ import annotations
import torch
from collections.abc import Sequence
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedRLEnv
def command_levels_vel(
env: ManagerBasedRLEnv,
env_ids: Sequence[int],
reward_term_name: str,
range_multiplier: Sequence[float] = (0.1, 1.0),
) -> None:
"""command_levels_vel"""
base_velocity_ranges = env.command_manager.get_term("base_velocity").cfg.ranges
# Get original velocity ranges (ONLY ON FIRST EPISODE)
if env.common_step_counter == 0:
env._original_vel_x = torch.tensor(base_velocity_ranges.lin_vel_x, device=env.device)
env._original_vel_y = torch.tensor(base_velocity_ranges.lin_vel_y, device=env.device)
env._initial_vel_x = env._original_vel_x * range_multiplier[0]
env._final_vel_x = env._original_vel_x * range_multiplier[1]
env._initial_vel_y = env._original_vel_y * range_multiplier[0]
env._final_vel_y = env._original_vel_y * range_multiplier[1]
# Initialize command ranges to initial values
base_velocity_ranges.lin_vel_x = env._initial_vel_x.tolist()
base_velocity_ranges.lin_vel_y = env._initial_vel_y.tolist()
# avoid updating command curriculum at each step since the maximum command is common to all envs
if env.common_step_counter % env.max_episode_length == 0:
episode_sums = env.reward_manager._episode_sums[reward_term_name]
reward_term_cfg = env.reward_manager.get_term_cfg(reward_term_name)
delta_command = torch.tensor([-0.1, 0.1], device=env.device)
# If the tracking reward is above 80% of the maximum, increase the range of commands
if torch.mean(episode_sums[env_ids]) / env.max_episode_length_s > 0.8 * reward_term_cfg.weight:
new_vel_x = torch.tensor(base_velocity_ranges.lin_vel_x, device=env.device) + delta_command
new_vel_y = torch.tensor(base_velocity_ranges.lin_vel_y, device=env.device) + delta_command
# Clamp to ensure we don't exceed final ranges
new_vel_x = torch.clamp(new_vel_x, min=env._final_vel_x[0], max=env._final_vel_x[1])
new_vel_y = torch.clamp(new_vel_y, min=env._final_vel_y[0], max=env._final_vel_y[1])
# Update ranges
base_velocity_ranges.lin_vel_x = new_vel_x.tolist()
base_velocity_ranges.lin_vel_y = new_vel_y.tolist()
return torch.tensor(base_velocity_ranges.lin_vel_x[1], device=env.device)
def command_levels_positive_x(
env: ManagerBasedRLEnv,
env_ids: Sequence[int],
reward_term_name: str,
initial_max_ratio: float = 0.35,
increment: float = 0.1,
) -> torch.Tensor:
"""Expand only the upper bound of a positive forward-speed command range.
Unlike :func:`command_levels_vel`, this curriculum preserves the configured minimum forward speed. This avoids
producing near-zero commands in obstacle-focused stages where the policy needs enough momentum to learn climbing.
"""
base_velocity_ranges = env.command_manager.get_term("base_velocity").cfg.ranges
if env.common_step_counter == 0:
original_x = torch.tensor(base_velocity_ranges.lin_vel_x, device=env.device)
env._positive_x_min = original_x[0]
env._positive_x_max = original_x[1]
initial_x_max = torch.maximum(env._positive_x_min, env._positive_x_max * initial_max_ratio)
base_velocity_ranges.lin_vel_x = [env._positive_x_min.item(), initial_x_max.item()]
if env.common_step_counter % env.max_episode_length == 0:
episode_sums = env.reward_manager._episode_sums[reward_term_name]
reward_term_cfg = env.reward_manager.get_term_cfg(reward_term_name)
mean_reward = torch.mean(episode_sums[env_ids]) / env.max_episode_length_s
if mean_reward > 0.8 * reward_term_cfg.weight:
current_x_max = torch.tensor(base_velocity_ranges.lin_vel_x[1], device=env.device)
new_x_max = torch.clamp(current_x_max + increment, max=env._positive_x_max)
base_velocity_ranges.lin_vel_x = [env._positive_x_min.item(), new_x_max.item()]
return torch.tensor(base_velocity_ranges.lin_vel_x[1], device=env.device)
@@ -0,0 +1,213 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import torch
from typing import TYPE_CHECKING, Literal
import isaaclab.utils.math as math_utils
from isaaclab.assets import Articulation, RigidObject
from isaaclab.managers import SceneEntityCfg
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedEnv
def randomize_rigid_body_inertia(
env: ManagerBasedEnv,
env_ids: torch.Tensor | None,
asset_cfg: SceneEntityCfg,
inertia_distribution_params: tuple[float, float],
operation: Literal["add", "scale", "abs"],
distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform",
):
"""Randomize the inertia tensors of the bodies by adding, scaling, or setting random values.
This function allows randomizing only the diagonal inertia tensor components (xx, yy, zz) of the bodies.
The function samples random values from the given distribution parameters and adds, scales, or sets the values
into the physics simulation based on the operation.
.. tip::
This function uses CPU tensors to assign the body inertias. It is recommended to use this function
only during the initialization of the environment.
"""
# extract the used quantities (to enable type-hinting)
asset: RigidObject | Articulation = env.scene[asset_cfg.name]
# resolve environment ids
if env_ids is None:
env_ids = torch.arange(env.scene.num_envs, device="cpu")
else:
env_ids = env_ids.cpu()
# resolve body indices
if asset_cfg.body_ids == slice(None):
body_ids = torch.arange(asset.num_bodies, dtype=torch.int, device="cpu")
else:
body_ids = torch.tensor(asset_cfg.body_ids, dtype=torch.int, device="cpu")
# get the current inertia tensors of the bodies (num_assets, num_bodies, 9 for articulations or 9 for rigid objects)
inertias = asset.root_physx_view.get_inertias()
# apply randomization on default values
inertias[env_ids[:, None], body_ids, :] = asset.data.default_inertia[env_ids[:, None], body_ids, :].clone()
# randomize each diagonal element (xx, yy, zz -> indices 0, 4, 8)
for idx in [0, 4, 8]:
# Extract and randomize the specific diagonal element
randomized_inertias = _randomize_prop_by_op(
inertias[:, :, idx],
inertia_distribution_params,
env_ids,
body_ids,
operation,
distribution,
)
# Assign the randomized values back to the inertia tensor
inertias[env_ids[:, None], body_ids, idx] = randomized_inertias
# set the inertia tensors into the physics simulation
asset.root_physx_view.set_inertias(inertias, env_ids)
def randomize_com_positions(
env: ManagerBasedEnv,
env_ids: torch.Tensor | None,
asset_cfg: SceneEntityCfg,
com_distribution_params: tuple[float, float],
operation: Literal["add", "scale", "abs"],
distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform",
):
"""Randomize the center of mass (COM) positions for the rigid bodies.
This function allows randomizing the COM positions of the bodies in the physics simulation. The positions can be
randomized by adding, scaling, or setting random values sampled from the specified distribution.
.. tip::
This function is intended for initialization or offline adjustments, as it modifies physics properties directly.
Args:
env (ManagerBasedEnv): The simulation environment.
env_ids (torch.Tensor | None): Specific environment indices to apply randomization, or None for all environments.
asset_cfg (SceneEntityCfg): The configuration for the target asset whose COM will be randomized.
com_distribution_params (tuple[float, float]): Parameters of the distribution (e.g., min and max for uniform).
operation (Literal["add", "scale", "abs"]): The operation to apply for randomization.
distribution (Literal["uniform", "log_uniform", "gaussian"]): The distribution to sample random values from.
"""
# Extract the asset (Articulation or RigidObject)
asset: RigidObject | Articulation = env.scene[asset_cfg.name]
# Resolve environment indices
if env_ids is None:
env_ids = torch.arange(env.scene.num_envs, device="cpu")
else:
env_ids = env_ids.cpu()
# Resolve body indices
if asset_cfg.body_ids == slice(None):
body_ids = torch.arange(asset.num_bodies, dtype=torch.int, device="cpu")
else:
body_ids = torch.tensor(asset_cfg.body_ids, dtype=torch.int, device="cpu")
# Get the current COM offsets (num_assets, num_bodies, 3)
com_offsets = asset.root_physx_view.get_coms()
for dim_idx in range(3): # Randomize x, y, z independently
randomized_offset = _randomize_prop_by_op(
com_offsets[:, :, dim_idx],
com_distribution_params,
env_ids,
body_ids,
operation,
distribution,
)
com_offsets[env_ids[:, None], body_ids, dim_idx] = randomized_offset[env_ids[:, None], body_ids]
# Set the randomized COM offsets into the simulation
asset.root_physx_view.set_coms(com_offsets, env_ids)
"""
Internal helper functions.
"""
def _randomize_prop_by_op(
data: torch.Tensor,
distribution_parameters: tuple[float | torch.Tensor, float | torch.Tensor],
dim_0_ids: torch.Tensor | None,
dim_1_ids: torch.Tensor | slice,
operation: Literal["add", "scale", "abs"],
distribution: Literal["uniform", "log_uniform", "gaussian"],
) -> torch.Tensor:
"""Perform data randomization based on the given operation and distribution.
Args:
data: The data tensor to be randomized. Shape is (dim_0, dim_1).
distribution_parameters: The parameters for the distribution to sample values from.
dim_0_ids: The indices of the first dimension to randomize.
dim_1_ids: The indices of the second dimension to randomize.
operation: The operation to perform on the data. Options: 'add', 'scale', 'abs'.
distribution: The distribution to sample the random values from. Options: 'uniform', 'log_uniform'.
Returns:
The data tensor after randomization. Shape is (dim_0, dim_1).
Raises:
NotImplementedError: If the operation or distribution is not supported.
"""
# resolve shape
# -- dim 0
if dim_0_ids is None:
n_dim_0 = data.shape[0]
dim_0_ids = slice(None) # type: ignore
else:
n_dim_0 = len(dim_0_ids)
if not isinstance(dim_1_ids, slice):
dim_0_ids = dim_0_ids[:, None]
# -- dim 1
if isinstance(dim_1_ids, slice):
n_dim_1 = data.shape[1]
else:
n_dim_1 = len(dim_1_ids)
# resolve the distribution
if distribution == "uniform":
dist_fn = math_utils.sample_uniform
elif distribution == "log_uniform":
dist_fn = math_utils.sample_log_uniform
elif distribution == "gaussian":
dist_fn = math_utils.sample_gaussian
else:
raise NotImplementedError(
f"Unknown distribution: '{distribution}' for joint properties randomization."
" Please use 'uniform', 'log_uniform', 'gaussian'."
)
# perform the operation
if operation == "add":
data[dim_0_ids, dim_1_ids] += dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device)
elif operation == "scale":
data[dim_0_ids, dim_1_ids] *= dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device)
elif operation == "abs":
data[dim_0_ids, dim_1_ids] = dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device)
else:
raise NotImplementedError(
f"Unknown operation: '{operation}' for property randomization. Please use 'add', 'scale', or 'abs'."
)
return data
def bad_orientation_2(
env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") # type: ignore
) -> torch.Tensor:
"""Terminate when the asset's orientation is too far from the desired orientation limits.
This is computed by checking the angle between the projected gravity vector and the z-axis.
"""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
return (asset.data.projected_gravity_b[:, 2] > 0) | (asset.data.projected_gravity_b[:, :2].abs() > 0.7).any(-1)
@@ -0,0 +1,105 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import torch
from typing import TYPE_CHECKING
from isaaclab.assets import Articulation
from isaaclab.managers import SceneEntityCfg
from isaaclab.sensors import ContactSensor
if TYPE_CHECKING:
from isaaclab.envs import ManagerBasedEnv, ManagerBasedRLEnv
# def joint_pos_rel_without_wheel(
# env: ManagerBasedEnv,
# asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
# wheel_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
# ) -> torch.Tensor:
# """The joint positions of the asset w.r.t. the default joint positions.(Without the wheel joints)"""
# # extract the used quantities (to enable type-hinting)
# asset: Articulation = env.scene[asset_cfg.name]
# joint_pos_rel = asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.default_joint_pos[:, asset_cfg.joint_ids]
# joint_pos_rel[:, wheel_asset_cfg.joint_ids] = 0
# return joint_pos_rel
def joint_pos_rel_without_wheel(
env: ManagerBasedEnv,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
wheel_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
asset: Articulation = env.scene[asset_cfg.name]
# 1. 先在“完整 joint 空间”算相对位置
joint_pos_rel_full = asset.data.joint_pos - asset.data.default_joint_pos
# 2. 把 wheel joint 清零(这里的 index 是全局的,合法)
joint_pos_rel_full[:, wheel_asset_cfg.joint_ids] = 0.0
# 3. 最后再裁剪成 policy 需要的 joint
joint_pos_rel = joint_pos_rel_full[:, asset_cfg.joint_ids]
return joint_pos_rel
def phase(env: ManagerBasedRLEnv, cycle_time: float) -> torch.Tensor:
if not hasattr(env, "episode_length_buf") or env.episode_length_buf is None:
env.episode_length_buf = torch.zeros(env.num_envs, device=env.device, dtype=torch.long)
phase = env.episode_length_buf[:, None] * env.step_dt / cycle_time
phase_tensor = torch.cat([torch.sin(2 * torch.pi * phase), torch.cos(2 * torch.pi * phase)], dim=-1)
return phase_tensor
def contact_state(
env: ManagerBasedRLEnv,
sensor_cfg: SceneEntityCfg,
threshold: float = 1.0,
) -> torch.Tensor:
"""Binary contact state for selected bodies."""
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
force_norm = torch.linalg.norm(contact_sensor.data.net_forces_w[:, sensor_cfg.body_ids, :], dim=-1)
return (force_norm > threshold).float()
def contact_force_norm(
env: ManagerBasedRLEnv,
sensor_cfg: SceneEntityCfg,
scale: float = 100.0,
clip: float = 2.0,
) -> torch.Tensor:
"""Normalized contact force magnitude for selected bodies."""
contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name]
force_norm = torch.linalg.norm(contact_sensor.data.net_forces_w[:, sensor_cfg.body_ids, :], dim=-1)
return torch.clamp(force_norm / scale, min=0.0, max=clip)
def wheel_slip_proxy(
env: ManagerBasedRLEnv,
wheel_radius: float,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
) -> torch.Tensor:
"""A proprioceptive slip proxy from wheel peripheral speed and base forward speed."""
asset: Articulation = env.scene[asset_cfg.name]
wheel_speed = torch.abs(asset.data.joint_vel[:, asset_cfg.joint_ids] * wheel_radius)
base_forward_speed = torch.abs(asset.data.root_lin_vel_b[:, 0:1])
return torch.clamp(torch.abs(wheel_speed - base_forward_speed), min=0.0, max=3.0)
def applied_torque(
env: ManagerBasedRLEnv,
asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
scale: float = 80.0,
clip: float = 2.0,
) -> torch.Tensor:
"""Normalized applied motor torques."""
asset: Articulation = env.scene[asset_cfg.name]
torque = asset.data.applied_torque[:, asset_cfg.joint_ids]
return torch.clamp(torque / scale, min=-clip, max=clip)
@@ -0,0 +1,107 @@
"""Custom terrains for the FireAnt locomotion tasks."""
from __future__ import annotations
import numpy as np
import trimesh
from isaaclab.terrains import HfTerrainBaseCfg, SubTerrainBaseCfg
from isaaclab.terrains.height_field.utils import height_field_to_mesh
from isaaclab.utils import configclass
def single_side_bridge_terrain(
difficulty: float, cfg: "SingleSideBridgeTerrainCfg"
) -> tuple[list[trimesh.Trimesh], np.ndarray]:
"""Long raised bridge on the +y half of the cell; flat ground on the -y half.
The bridge edge is exactly at the cell center line (local ``y = size[1] / 2``),
so a robot spawned at the cell center with zero yaw has its left wheels on the
bridge and its right wheels on the flat ground.
"""
height = cfg.bridge_height_range[0] + difficulty * (
cfg.bridge_height_range[1] - cfg.bridge_height_range[0]
)
# Ground slab: top surface at z=0, extends over the whole cell.
ground = trimesh.creation.box(
extents=(cfg.size[0], cfg.size[1], 0.05),
transform=trimesh.transformations.translation_matrix(
(cfg.size[0] / 2.0, cfg.size[1] / 2.0, -0.025)
),
)
# Bridge: full cell length along x, raised strip on the +y side.
bridge = trimesh.creation.box(
extents=(cfg.size[0], cfg.bridge_width, height),
transform=trimesh.transformations.translation_matrix(
(
cfg.size[0] / 2.0,
cfg.size[1] / 2.0 + cfg.bridge_width / 2.0,
height / 2.0,
)
),
)
origin = np.array((cfg.size[0] / 2.0, cfg.size[1] / 2.0, 0.0))
return [ground, bridge], origin
@configclass
class SingleSideBridgeTerrainCfg(SubTerrainBaseCfg):
"""Configuration for a long single-side bridge terrain."""
function = single_side_bridge_terrain
bridge_height_range: tuple[float, float] = (0.06, 0.14)
"""Bridge height at the start and end of the difficulty curriculum (m)."""
bridge_width: float = 1.5
"""Width of the raised bridge on the +y side of the cell (m)."""
@height_field_to_mesh
def smooth_random_terrain(difficulty: float, cfg: "SmoothRandomTerrainCfg") -> np.ndarray:
"""Smooth low-frequency random height field.
The surface is a sum of a few low-frequency sinusoids. It has no steps or
sharp edges, so the robot has to keep its body level over gentle, random
pitch and roll disturbances.
"""
width_pixels = int(cfg.size[0] / cfg.horizontal_scale)
length_pixels = int(cfg.size[1] / cfg.horizontal_scale)
x = np.arange(width_pixels, dtype=np.float64) * cfg.horizontal_scale
y = np.arange(length_pixels, dtype=np.float64) * cfg.horizontal_scale
grid_x, grid_y = np.meshgrid(x, y, indexing="ij")
amplitude = cfg.height_range[0] + difficulty * (cfg.height_range[1] - cfg.height_range[0])
heights = np.zeros_like(grid_x)
for _ in range(cfg.num_waves):
direction = np.random.uniform(0.0, 2.0 * np.pi)
wavelength = np.random.uniform(*cfg.wavelength_range)
phase = np.random.uniform(0.0, 2.0 * np.pi)
weight = np.random.uniform(0.5, 1.0)
projection = grid_x * np.cos(direction) + grid_y * np.sin(direction)
heights += weight * np.sin(2.0 * np.pi * projection / wavelength + phase)
heights -= heights.mean()
peak = float(np.max(np.abs(heights)))
if peak > 1e-9:
heights *= amplitude / peak
return np.rint(heights / cfg.vertical_scale).astype(np.int16)
@configclass
class SmoothRandomTerrainCfg(HfTerrainBaseCfg):
"""Configuration for a smooth random-undulation terrain."""
function = smooth_random_terrain
height_range: tuple[float, float] = (0.02, 0.05)
"""Peak height of the undulations at the start and end of the curriculum (m)."""
wavelength_range: tuple[float, float] = (1.5, 3.0)
"""Spatial wavelength range of the undulations (m)."""
num_waves: int = 3
"""Number of sinusoids summed to form the surface."""
@@ -0,0 +1,868 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
# Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import inspect
import math
import sys
from dataclasses import MISSING
import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg, AssetBaseCfg
from isaaclab.envs import ManagerBasedRLEnvCfg
from isaaclab.managers import CurriculumTermCfg as CurrTerm
from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.managers import ObservationGroupCfg as ObsGroup
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import RewardTermCfg as RewTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.managers import TerminationTermCfg as DoneTerm
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sensors import ContactSensorCfg, RayCasterCfg, patterns
from isaaclab.terrains import TerrainImporterCfg
from isaaclab.utils import configclass
from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise
import rl_training.tasks.manager_based.locomotion.velocity.mdp as mdp
##
# Pre-defined configs
##
from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG # isort: skip
##
# Scene definition
##
@configclass
class MySceneCfg(InteractiveSceneCfg):
"""Configuration for the terrain scene with a legged robot."""
# ground terrain
terrain = TerrainImporterCfg(
prim_path="/World/ground",
terrain_type="generator",
terrain_generator=ROUGH_TERRAINS_CFG,
max_init_terrain_level=5,
collision_group=-1,
physics_material=sim_utils.RigidBodyMaterialCfg(
friction_combine_mode="multiply",
restitution_combine_mode="multiply",
static_friction=1.0,
dynamic_friction=1.0,
restitution=1.0,
),
# Keep scene startup independent of NVIDIA's online asset server.
# The robot visual geometry is still loaded from the STL meshes in its URDF.
visual_material=sim_utils.PreviewSurfaceCfg(
diffuse_color=(0.18, 0.20, 0.22),
roughness=0.9,
),
debug_vis=False,
)
# robots
robot: ArticulationCfg = MISSING
# sensors
height_scanner = RayCasterCfg(
prim_path="{ENV_REGEX_NS}/Robot/base",
offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)),
ray_alignment="yaw",
pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=[1.6, 1.0]),
debug_vis=False,
mesh_prim_paths=["/World/ground"],
)
height_scanner_base = RayCasterCfg(
prim_path="{ENV_REGEX_NS}/Robot/base",
offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)),
ray_alignment="yaw",
pattern_cfg=patterns.GridPatternCfg(resolution=0.05, size=(0.1, 0.1)),
debug_vis=False,
mesh_prim_paths=["/World/ground"],
)
contact_forces = ContactSensorCfg(prim_path="{ENV_REGEX_NS}/Robot/.*", history_length=3, track_air_time=True)
# lights
sky_light = AssetBaseCfg(
prim_path="/World/skyLight",
spawn=sim_utils.DomeLightCfg(
intensity=1200.0,
color=(0.75, 0.75, 0.75),
),
)
##
# MDP settings
##
@configclass
class CommandsCfg:
"""Command specifications for the MDP."""
base_velocity = mdp.UniformThresholdVelocityCommandCfg(
asset_name="robot",
resampling_time_range=(10.0, 10.0),
rel_standing_envs=0.02,
rel_heading_envs=1.0,
heading_command=True,
heading_control_stiffness=0.5,
debug_vis=True,
ranges=mdp.UniformThresholdVelocityCommandCfg.Ranges(
lin_vel_x=(-1.0, 1.0), lin_vel_y=(-1.0, 1.0), ang_vel_z=(-1.0, 1.0), heading=(-math.pi, math.pi)
),
)
@configclass
class ActionsCfg:
"""Action specifications for the MDP."""
joint_pos = mdp.JointPositionActionCfg(
asset_name="robot", joint_names=[".*"], scale=0.5, use_default_offset=True, clip=None, preserve_order=True
)
@configclass
class ObservationsCfg:
"""Observation specifications for the MDP."""
@configclass
class PolicyCfg(ObsGroup):
"""Observations for policy group."""
# observation terms (order preserved)
base_lin_vel = ObsTerm(
func=mdp.base_lin_vel,
noise=Unoise(n_min=-0.1, n_max=0.1),
clip=(-100.0, 100.0),
scale=1.0,
)
base_ang_vel = ObsTerm(
func=mdp.base_ang_vel,
noise=Unoise(n_min=-0.2, n_max=0.2),
clip=(-100.0, 100.0),
scale=1.0,
)
projected_gravity = ObsTerm(
func=mdp.projected_gravity,
noise=Unoise(n_min=-0.05, n_max=0.05),
clip=(-100.0, 100.0),
scale=1.0,
)
velocity_commands = ObsTerm(
func=mdp.generated_commands,
params={"command_name": "base_velocity"},
clip=(-100.0, 100.0),
scale=1.0,
)
joint_pos = ObsTerm(
func=mdp.joint_pos_rel,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=True)},
noise=Unoise(n_min=-0.01, n_max=0.01),
clip=(-100.0, 100.0),
scale=1.0,
)
joint_vel = ObsTerm(
func=mdp.joint_vel_rel,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=True)},
noise=Unoise(n_min=-1.5, n_max=1.5),
clip=(-100.0, 100.0),
scale=1.0,
)
actions = ObsTerm(
func=mdp.last_action,
clip=(-100.0, 100.0),
scale=1.0,
)
height_scan = ObsTerm(
func=mdp.height_scan,
params={"sensor_cfg": SceneEntityCfg("height_scanner")},
noise=Unoise(n_min=-0.1, n_max=0.1),
clip=(-1.0, 1.0),
scale=1.0,
)
def __post_init__(self):
self.enable_corruption = True
self.concatenate_terms = True
@configclass
class CriticCfg(ObsGroup):
"""Observations for critic group."""
# observation terms (order preserved)
base_lin_vel = ObsTerm(
func=mdp.base_lin_vel,
clip=(-100.0, 100.0),
scale=1.0,
)
base_ang_vel = ObsTerm(
func=mdp.base_ang_vel,
clip=(-100.0, 100.0),
scale=1.0,
)
projected_gravity = ObsTerm(
func=mdp.projected_gravity,
clip=(-100.0, 100.0),
scale=1.0,
)
velocity_commands = ObsTerm(
func=mdp.generated_commands,
params={"command_name": "base_velocity"},
clip=(-100.0, 100.0),
scale=1.0,
)
joint_pos = ObsTerm(
func=mdp.joint_pos_rel,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=True)},
clip=(-100.0, 100.0),
scale=1.0,
)
joint_vel = ObsTerm(
func=mdp.joint_vel_rel,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=True)},
clip=(-100.0, 100.0),
scale=1.0,
)
actions = ObsTerm(
func=mdp.last_action,
clip=(-100.0, 100.0),
scale=1.0,
)
height_scan = ObsTerm(
func=mdp.height_scan,
params={"sensor_cfg": SceneEntityCfg("height_scanner")},
clip=(-1.0, 1.0),
scale=1.0,
)
# joint_effort = ObsTerm(
# func=mdp.joint_effort,
# clip=(-100, 100),
# scale=0.01,
# )
def __post_init__(self):
self.enable_corruption = False
self.concatenate_terms = True
# observation groups
policy: PolicyCfg = PolicyCfg()
critic: CriticCfg = CriticCfg()
@configclass
class EventCfg:
"""Configuration for events."""
# startup
randomize_rigid_body_material = EventTerm(
func=mdp.randomize_rigid_body_material,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=".*"),
"static_friction_range": (0.3, 1.0),
"dynamic_friction_range": (0.3, 0.8),
"restitution_range": (0.0, 0.4),
"num_buckets": 1024,
},
)
randomize_rigid_body_mass = EventTerm(
func=mdp.randomize_rigid_body_mass,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"mass_distribution_params": (0.85, 1.15),
"operation": "scale",
"recompute_inertia": True,
},
)
randomize_rigid_body_mass_base = EventTerm(
func=mdp.randomize_rigid_body_mass,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"mass_distribution_params": (-1.0, 3.0),
"operation": "add",
"recompute_inertia": True,
},
)
randomize_rigid_body_inertia = EventTerm(
func=mdp.randomize_rigid_body_inertia,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=".*"),
"inertia_distribution_params": (0.85, 1.15),
"operation": "scale",
},
)
randomize_com_positions = EventTerm(
func=mdp.randomize_rigid_body_com,
mode="startup",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=".*"),
"com_range": {"x": (-0.03, 0.03), "y": (-0.03, 0.03), "z": (-0.02, 0.02)},
},
)
# reset
randomize_apply_external_force_torque = EventTerm(
func=mdp.apply_external_force_torque,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"force_range": (-10.0, 10.0),
"torque_range": (-10.0, 10.0),
},
)
randomize_reset_joints = EventTerm(
func=mdp.reset_joints_by_scale,
# func=mdp.reset_joints_by_offset,
mode="reset",
params={
"position_range": (1.0, 1.0),
"velocity_range": (0.0, 0.0),
},
)
randomize_actuator_gains = EventTerm(
func=mdp.randomize_actuator_gains,
mode="reset",
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"stiffness_distribution_params": (0.85, 1.15),
"damping_distribution_params": (0.85, 1.15),
"operation": "scale",
"distribution": "uniform",
},
)
randomize_reset_base = EventTerm(
func=mdp.reset_root_state_uniform,
mode="reset",
params={
"pose_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5), "yaw": (-3.14, 3.14)},
"velocity_range": {
"x": (-0.5, 0.5),
"y": (-0.5, 0.5),
"z": (-0.5, 0.5),
"roll": (-0.5, 0.5),
"pitch": (-0.5, 0.5),
"yaw": (-0.5, 0.5),
},
},
)
# interval
randomize_push_robot = EventTerm(
func=mdp.push_by_setting_velocity,
mode="interval",
interval_range_s=(10.0, 15.0),
params={"velocity_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5)}},
)
@configclass
class RewardsCfg:
"""Reward terms for the MDP."""
# General
is_terminated = RewTerm(func=mdp.is_terminated, weight=0.0)
# Root penalties
lin_vel_z_l2 = RewTerm(func=mdp.lin_vel_z_l2, weight=0.0)
ang_vel_xy_l2 = RewTerm(func=mdp.ang_vel_xy_l2, weight=0.0)
flat_orientation_l2 = RewTerm(func=mdp.flat_orientation_l2, weight=0.0)
roll_l2 = RewTerm(func=mdp.roll_l2, weight=0.0)
pitch_l2 = RewTerm(func=mdp.pitch_l2, weight=0.0)
base_height_l2 = RewTerm(
func=mdp.base_height_l2,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"sensor_cfg": SceneEntityCfg("height_scanner_base"),
"target_height": 0.0,
},
)
body_lin_acc_l2 = RewTerm(
func=mdp.body_lin_acc_l2,
weight=0.0,
params={"asset_cfg": SceneEntityCfg("robot", body_names="")},
)
# Joint penalties
joint_torques_l2 = RewTerm(
func=mdp.joint_torques_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")}
)
joint_vel_l2 = RewTerm(
func=mdp.joint_vel_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")}
)
joint_acc_l2 = RewTerm(
func=mdp.joint_acc_l2, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")}
)
joint_deviation_l1 = RewTerm(
func=mdp.joint_deviation_l1,
weight=0.0,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")},
)
def create_joint_deviation_l1_rewterm(self, attr_name, weight, joint_names_pattern):
rew_term = RewTerm(
func=mdp.joint_deviation_l1,
weight=weight,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=joint_names_pattern)},
)
setattr(self, attr_name, rew_term)
joint_pos_limits = RewTerm(
func=mdp.joint_pos_limits, weight=0.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")}
)
joint_vel_limits = RewTerm(
func=mdp.joint_vel_limits,
weight=0.0,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*"), "soft_ratio": 1.0},
)
joint_power = RewTerm(
func=mdp.joint_power,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
},
)
stand_still_without_cmd = RewTerm(
func=mdp.stand_still_without_cmd,
weight=0.0,
params={
"command_name": "base_velocity",
"command_threshold": 0.1,
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
},
)
joint_pos_penalty = RewTerm(
func=mdp.joint_pos_penalty,
weight=0.0,
params={
"command_name": "base_velocity",
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"stand_still_scale": 5.0,
"velocity_threshold": 0.5,
"command_threshold": 0.1,
},
)
hipx_joint_pos_penalty = RewTerm(
func=mdp.joint_pos_penalty,
weight=0.0,
params={
"command_name": "base_velocity",
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"stand_still_scale": 5.0,
"velocity_threshold": 0.5,
"command_threshold": 0.1,
},
)
hipy_joint_pos_penalty = RewTerm(
func=mdp.joint_pos_penalty,
weight=0.0,
params={
"command_name": "base_velocity",
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"stand_still_scale": 5.0,
"velocity_threshold": 0.5,
"command_threshold": 0.1,
},
)
knee_joint_pos_penalty = RewTerm(
func=mdp.joint_pos_penalty,
weight=0.0,
params={
"command_name": "base_velocity",
"asset_cfg": SceneEntityCfg("robot", joint_names=".*"),
"stand_still_scale": 5.0,
"velocity_threshold": 0.5,
"command_threshold": 0.1,
},
)
wheel_vel_penalty = RewTerm(
func=mdp.wheel_vel_penalty,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=""),
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"command_name": "base_velocity",
"velocity_threshold": 0.5,
"command_threshold": 0.1,
},
)
joint_mirror = RewTerm(
func=mdp.joint_mirror,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot"),
"mirror_joints": [["FR.*", "RL.*"], ["FL.*", "RR.*"]],
},
)
action_mirror = RewTerm(
func=mdp.action_mirror,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot"),
"mirror_joints": [["FR.*", "RL.*"], ["FL.*", "RR.*"]],
},
)
action_sync = RewTerm(
func=mdp.action_sync,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot"),
"joint_groups": [
["FR_hip_joint", "FL_hip_joint", "RL_hip_joint", "RR_hip_joint"],
["FR_thigh_joint", "FL_thigh_joint", "RL_thigh_joint", "RR_thigh_joint"],
["FR_calf_joint", "FL_calf_joint", "RL_calf_joint", "RR_calf_joint"],
],
},
)
# Action penalties
applied_torque_limits = RewTerm(
func=mdp.applied_torque_limits,
weight=0.0,
params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*")},
)
action_rate_l2 = RewTerm(func=mdp.action_rate_l2, weight=0.0)
# smoothness_1 = RewTerm(func=mdp.smoothness_1, weight=0.0) # Same as action_rate_l2
# smoothness_2 = RewTerm(func=mdp.smoothness_2, weight=0.0) # Unvaliable now
# Contact sensor
undesired_contacts = RewTerm(
func=mdp.undesired_contacts,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"threshold": 1.0,
},
)
contact_forces = RewTerm(
func=mdp.contact_forces,
weight=0.0,
params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""), "threshold": 100.0},
)
# Velocity-tracking rewards
track_lin_vel_xy_exp = RewTerm(
func=mdp.track_lin_vel_xy_exp, weight=0.0, params={"command_name": "base_velocity", "std": math.sqrt(0.5)}
)
track_ang_vel_z_exp = RewTerm(
func=mdp.track_ang_vel_z_exp, weight=0.0, params={"command_name": "base_velocity", "std": math.sqrt(0.5)}
)
# Others
# feet_air_time = RewTerm(
# func=mdp.feet_air_time,
# weight=0.0,
# params={
# "command_name": "base_velocity",
# "threshold": 0.5,
# "sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
# },
# )
feet_air_time = RewTerm(
func=mdp.feet_air_time_including_ang_z,
weight=0.0,
params={
"command_name": "base_velocity",
"threshold": 0.5,
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
},
)
feet_air_time_variance = RewTerm(
func=mdp.feet_air_time_variance_penalty,
weight=0,
params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names="")},
)
feet_gait = RewTerm(
func=mdp.GaitReward,
weight=0.0,
params={
"std": math.sqrt(0.5),
"command_name": "base_velocity",
"max_err": 0.2,
"velocity_threshold": 0.5,
"command_threshold": 0.1,
"synced_feet_pair_names": (("", ""), ("", "")),
"asset_cfg": SceneEntityCfg("robot"),
"sensor_cfg": SceneEntityCfg("contact_forces"),
},
)
feet_contact = RewTerm(
func=mdp.feet_contact,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"command_name": "base_velocity",
"expect_contact_num": 2,
},
)
feet_contact_without_cmd = RewTerm(
func=mdp.feet_contact_without_cmd,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"command_name": "base_velocity",
},
)
feet_stumble = RewTerm(
func=mdp.feet_stumble,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
},
)
feet_slide = RewTerm(
func=mdp.feet_slide,
weight=0.0,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"asset_cfg": SceneEntityCfg("robot", body_names=""),
},
)
stand_still = RewTerm(
func=mdp.stand_still_joint_deviation_l1,
weight=0,
params={
"command_name": "base_velocity",
"asset_cfg": SceneEntityCfg("robot", joint_names=""),
},
) # negetive
feet_height = RewTerm(
func=mdp.feet_height,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"tanh_mult": 2.0,
"target_height": 0.05,
"command_name": "base_velocity",
},
)
feet_height_body = RewTerm(
func=mdp.feet_height_body,
weight=0.0,
params={
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"tanh_mult": 2.0,
"target_height": -0.3,
"command_name": "base_velocity",
},
)
feet_distance_y_exp = RewTerm(
func=mdp.feet_distance_y_exp,
weight=0.0,
params={
"std": math.sqrt(0.25),
"asset_cfg": SceneEntityCfg("robot", body_names=""),
"stance_width": float,
},
)
# feet_distance_xy_exp = RewTerm(
# func=mdp.feet_distance_xy_exp,
# weight=0.0,
# params={
# "std": math.sqrt(0.25),
# "asset_cfg": SceneEntityCfg("robot", body_names=""),
# "stance_length": float,
# "stance_width": float,
# },
# )
upward = RewTerm(func=mdp.upward, weight=0.0)
# lin_vel_xy_l2_with_ang_z_command = RewTerm(
# func=mdp.lin_vel_xy_l2_with_ang_z_command,
# weight=0,
# params={
# "command_name": "base_velocity",
# "command_threshold": 0.1,
# },
# ) # negetive
@configclass
class TerminationsCfg:
"""Termination terms for the MDP."""
# MDP terminations
time_out = DoneTerm(func=mdp.time_out, time_out=True)
# command_resample
terrain_out_of_bounds = DoneTerm(
func=mdp.terrain_out_of_bounds,
params={"asset_cfg": SceneEntityCfg("robot"), "distance_buffer": 3.0},
time_out=True,
)
# Contact sensor
illegal_contact = DoneTerm(
func=mdp.illegal_contact,
params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names=""), "threshold": 1.0},
)
bad_orientation_2 = DoneTerm(func=mdp.bad_orientation_2)
@configclass
class CurriculumCfg:
"""Curriculum terms for the MDP."""
terrain_levels = CurrTerm(func=mdp.terrain_levels_vel)
command_levels = CurrTerm(
func=mdp.command_levels_vel,
params={
"reward_term_name": "track_lin_vel_xy_exp",
"range_multiplier": (0.1, 1.0),
},
)
##
# Environment configuration
##
@configclass
class LocomotionVelocityRoughEnvCfg(ManagerBasedRLEnvCfg):
"""Configuration for the locomotion velocity-tracking environment."""
# Scene settings
scene: MySceneCfg = MySceneCfg(num_envs=4096, env_spacing=2.5)
# Basic settings
observations: ObservationsCfg = ObservationsCfg()
actions: ActionsCfg = ActionsCfg()
commands: CommandsCfg = CommandsCfg()
# MDP settings
rewards: RewardsCfg = RewardsCfg()
terminations: TerminationsCfg = TerminationsCfg()
events: EventCfg = EventCfg()
curriculum: CurriculumCfg = CurriculumCfg()
def __post_init__(self):
"""Post initialization."""
# general settings
self.decimation = 4
self.episode_length_s = 20.0
# simulation settings
self.sim.dt = 0.005
self.sim.render_interval = self.decimation
self.sim.physics_material = self.scene.terrain.physics_material
self.sim.physx.gpu_max_rigid_patch_count = 10 * 2**15
self.sim.physx.max_position_iteration_count = 4
self.sim.physx.max_velocity_iteration_count = 1
# update sensor update periods
# we tick all the sensors based on the smallest update period (physics update period)
if self.scene.height_scanner is not None:
self.scene.height_scanner.update_period = self.decimation * self.sim.dt
if self.scene.contact_forces is not None:
self.scene.contact_forces.update_period = self.sim.dt
# check if terrain levels curriculum is enabled - if so, enable curriculum for terrain generator
# this generates terrains with increasing difficulty and is useful for training
if getattr(self.curriculum, "terrain_levels", None) is not None:
if self.scene.terrain.terrain_generator is not None:
self.scene.terrain.terrain_generator.curriculum = True
else:
if self.scene.terrain.terrain_generator is not None:
self.scene.terrain.terrain_generator.curriculum = False
def disable_zero_weight_rewards(self):
"""If the weight of rewards is 0, set rewards to None"""
for attr in dir(self.rewards):
if not attr.startswith("__"):
reward_attr = getattr(self.rewards, attr)
if not callable(reward_attr) and reward_attr.weight == 0:
setattr(self.rewards, attr, None)
def create_obsgroup_class(class_name, terms, enable_corruption=False, concatenate_terms=True):
"""
Dynamically create and register a ObsGroup class based on the given configuration terms.
:param class_name: Name of the configuration class.
:param terms: Configuration terms, a dictionary where keys are term names and values are term content.
:param enable_corruption: Whether to enable corruption for the observation group. Defaults to False.
:param concatenate_terms: Whether to concatenate the observation terms in the group. Defaults to True.
:return: The dynamically created class.
"""
# Dynamically determine the module name
module_name = inspect.getmodule(inspect.currentframe()).__name__
# Define the post-init function
def post_init_wrapper(self):
setattr(self, "enable_corruption", enable_corruption)
setattr(self, "concatenate_terms", concatenate_terms)
# Dynamically create the class using ObsGroup as the base class
terms["__post_init__"] = post_init_wrapper
dynamic_class = configclass(type(class_name, (ObsGroup,), terms))
# Custom serialization and deserialization
def __getstate__(self):
state = self.__dict__.copy()
return state
def __setstate__(self, state):
self.__dict__.update(state)
# Add custom serialization methods to the class
dynamic_class.__getstate__ = __getstate__
dynamic_class.__setstate__ = __setstate__
# Place the class in the global namespace for accessibility
globals()[class_name] = dynamic_class
# Register the dynamic class in the module's dictionary
if module_name in sys.modules:
sys.modules[module_name].__dict__[class_name] = dynamic_class
else:
raise ImportError(f"Module {module_name} not found.")
# Return the class for external instantiation
return dynamic_class
@@ -0,0 +1,52 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import omni.ext
# Functions and vars are available to other extension as usual in python: `example.python_ext.some_public_function(x)`
def some_public_function(x: int):
print("[rl_training] some_public_function was called with x: ", x)
return x**x
# Any class derived from `omni.ext.IExt` in top level module (defined in `python.modules` of `extension.toml`) will be
# instantiated when extension gets enabled and `on_startup(ext_id)` will be called. Later when extension gets disabled
# on_shutdown() is called.
class ExampleExtension(omni.ext.IExt):
# ext_id is current extension id. It can be used with extension manager to query additional information, like where
# this extension is located on filesystem.
def on_startup(self, ext_id):
print("[rl_training] startup")
self._count = 0
self._window = omni.ui.Window("My Window", width=300, height=300)
with self._window.frame:
with omni.ui.VStack():
label = omni.ui.Label("")
def on_click():
self._count += 1
label.text = f"count: {self._count}"
def on_reset():
self._count = 0
label.text = "empty"
on_reset()
with omni.ui.HStack():
omni.ui.Button("Add", clicked_fn=on_click)
omni.ui.Button("Reset", clicked_fn=on_reset)
def on_shutdown(self):
print("[rl_training] shutdown")
+55
View File
@@ -0,0 +1,55 @@
# Copyright (c) 2025 Deep Robotics
# SPDX-License-Identifier: BSD 3-Clause
#
# # Copyright (c) 2024-2025 Ziqi Fan
# SPDX-License-Identifier: Apache-2.0
"""Installation script for the 'rl_training' python package."""
import os
import toml
from setuptools import setup
# Obtain the extension data from the extension.toml file
EXTENSION_PATH = os.path.dirname(os.path.realpath(__file__))
# Read the extension.toml file
EXTENSION_TOML_DATA = toml.load(os.path.join(EXTENSION_PATH, "config", "extension.toml"))
# Minimum dependencies required prior to installation
INSTALL_REQUIRES = [
# base
"psutil",
"colorama",
"xacrodoc",
# amp
"numpy",
"pandas",
"pinocchio",
# rl
"cusrl[all]",
]
# Installation operation
setup(
name="rl_training",
packages=["rl_training"],
author=EXTENSION_TOML_DATA["package"]["author"],
maintainer=EXTENSION_TOML_DATA["package"]["maintainer"],
url=EXTENSION_TOML_DATA["package"]["repository"],
version=EXTENSION_TOML_DATA["package"]["version"],
description=EXTENSION_TOML_DATA["package"]["description"],
keywords=EXTENSION_TOML_DATA["package"]["keywords"],
install_requires=INSTALL_REQUIRES,
license="Apache License 2.0",
include_package_data=True,
python_requires=">=3.10",
classifiers=[
"Natural Language :: English",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Isaac Sim :: 4.5.0",
"Isaac Sim :: 5.0.0",
],
zip_safe=False,
)