feat: add smooth/bridge tasks and fire ant stability training docs

- register Smooth/Bridge M20 tasks and add env configs
- add smooth random terrain and roll/pitch orientation rewards
- update M20 URDF mass and wheel geometry, refresh prototype specs
- add fire ant smooth training task document
This commit is contained in:
syfffffjj
2026-11-02 16:36:38 +08:00
parent ee8963cf4c
commit 9eab8830a4
14 changed files with 1016 additions and 109 deletions
@@ -65,6 +65,7 @@ DEEPROBOTICS_LITE3_CFG = ArticulationCfg(
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,
@@ -88,7 +89,7 @@ DEEPROBOTICS_M20_CFG = ArticulationCfg(
),
),
init_state=ArticulationCfg.InitialStateCfg(
pos=(0.0, 0.0, 0.306),
pos=(0.0, 0.0, 0.32),
joint_pos={
"f[l,r]_knee_joint": 0.5,
"h[l,r]_knee_joint": -0.5,
@@ -44,3 +44,25 @@ gym.register(
"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",
},
)
@@ -54,3 +54,25 @@ class DeeproboticsM20SupportRoughPPORunnerCfg(DeeproboticsM20RoughPPORunnerCfg):
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()
@@ -7,8 +7,8 @@
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 ObservationTermCfg as ObsTerm
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
@@ -92,7 +92,7 @@ class DeeproboticsM20RewardsCfg(RewardsCfg):
params={
"command_name": "base_velocity",
"std": 0.6,
"wheel_radius": 0.096,
"wheel_radius": 0.11,
"track_width": 0.50,
"left_wheel_indices": (0, 2),
"right_wheel_indices": (1, 3),
@@ -132,7 +132,7 @@ class DeeproboticsM20RewardsCfg(RewardsCfg):
"wheel_sensor_cfg": SceneEntityCfg("contact_forces", body_names=""),
"asset_cfg": SceneEntityCfg("robot"),
"wheel_asset_cfg": SceneEntityCfg("robot", joint_names=""),
"wheel_radius": 0.096,
"wheel_radius": 0.11,
"support_force_threshold": 15.0,
"wheel_obstacle_force_threshold": 35.0,
"max_contact_time": 0.45,
@@ -161,6 +161,16 @@ class DeeproboticsM20RewardsCfg(RewardsCfg):
},
)
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):
@@ -188,6 +198,8 @@ class DeeproboticsM20RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
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
@@ -219,63 +231,6 @@ class DeeproboticsM20RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
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
self.observations.policy.wheel_contact = ObsTerm(
func=mdp.contact_state,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=[self.foot_link_name]),
"threshold": 2.0,
},
clip=(0.0, 1.0),
scale=1.0,
)
self.observations.policy.support_contact = ObsTerm(
func=mdp.contact_state,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=self.support_link_names),
"threshold": 2.0,
},
clip=(0.0, 1.0),
scale=1.0,
)
self.observations.policy.wheel_contact_force = ObsTerm(
func=mdp.contact_force_norm,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=[self.foot_link_name]),
"scale": 100.0,
"clip": 2.0,
},
clip=(0.0, 2.0),
scale=1.0,
)
self.observations.policy.support_contact_force = ObsTerm(
func=mdp.contact_force_norm,
params={
"sensor_cfg": SceneEntityCfg("contact_forces", body_names=self.support_link_names),
"scale": 100.0,
"clip": 2.0,
},
clip=(0.0, 2.0),
scale=1.0,
)
self.observations.policy.wheel_slip = ObsTerm(
func=mdp.wheel_slip_proxy,
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=self.wheel_joint_names, preserve_order=True),
"wheel_radius": 0.096,
},
clip=(0.0, 3.0),
scale=0.5,
)
self.observations.policy.applied_torque = ObsTerm(
func=mdp.applied_torque,
params={
"asset_cfg": SceneEntityCfg("robot", joint_names=self.joint_names, preserve_order=True),
"scale": 80.0,
"clip": 2.0,
},
clip=(-2.0, 2.0),
scale=1.0,
)
# ------------------------------Actions------------------------------
# reduce action scale
@@ -328,8 +283,9 @@ class DeeproboticsM20RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
# Root penalties
self.rewards.lin_vel_z_l2.weight = -2.0
self.rewards.ang_vel_xy_l2.weight = -0.02
self.rewards.flat_orientation_l2.weight = -0.5
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]
@@ -345,7 +301,7 @@ class DeeproboticsM20RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
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 = -2e-7
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
@@ -361,17 +317,28 @@ class DeeproboticsM20RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
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.03
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.01
self.rewards.action_rate_l2.weight = -0.015
# Contact sensor
self.rewards.undesired_contacts.weight = -1.0
@@ -424,7 +391,10 @@ class DeeproboticsM20RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
# ------------------------------Terminations------------------------------
# self.terminations.illegal_contact.params["sensor_cfg"].body_names = [self.base_link_name]
self.terminations.illegal_contact = None
self.terminations.bad_orientation_2 = 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)
@@ -469,7 +439,7 @@ class DeeproboticsM20SupportRoughEnvCfg(DeeproboticsM20RoughEnvCfg):
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 = 1.0
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
@@ -481,7 +451,8 @@ class DeeproboticsM20SupportRoughEnvCfg(DeeproboticsM20RoughEnvCfg):
# 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.45
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
@@ -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()
@@ -19,3 +19,4 @@ 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
@@ -330,6 +330,33 @@ def joint_pos_penalty(
return reward
def knee_inward_fold_penalty(
env: ManagerBasedRLEnv,
front_asset_cfg: SceneEntityCfg,
hind_asset_cfg: SceneEntityCfg,
max_inward_tilt: float = 0.0,
) -> torch.Tensor:
"""Penalize front/hind knee joints that fold inward past ``max_inward_tilt``.
The knee link points straight down when its joint angle is zero, so the signed joint
position is the leg's tilt from vertical (all four knee axes are ``-Y`` in the URDF).
Front legs fold inward when the angle decreases, hind legs fold inward when it
increases. ``max_inward_tilt`` is the largest inward tilt allowed from the vertical
(straight-down) pose: 0.0 stops the leg at vertical, ``pi / 2`` would allow it to
reach horizontal. Only the inward direction is limited; outward folding is left to
the regular joint position limits. The penalty is quadratic in the excess angle and
is zero while the joint stays inside the limit.
"""
front_asset: Articulation = env.scene[front_asset_cfg.name]
hind_asset: Articulation = env.scene[hind_asset_cfg.name]
front_pos = front_asset.data.joint_pos[:, front_asset_cfg.joint_ids]
hind_pos = hind_asset.data.joint_pos[:, hind_asset_cfg.joint_ids]
# front: q >= -max_inward_tilt, hind: q <= max_inward_tilt
front_excess = torch.clamp(-max_inward_tilt - front_pos, min=0.0)
hind_excess = torch.clamp(hind_pos - max_inward_tilt, min=0.0)
return torch.sum(front_excess.square() + hind_excess.square(), dim=1)
def wheel_vel_penalty(
env: ManagerBasedRLEnv,
sensor_cfg: SceneEntityCfg,
@@ -944,6 +971,21 @@ def flat_orientation_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = Scen
# reward *= torch.clamp(-env.scene["robot"].data.projected_gravity_b[:, 2], 0, 0.7) / 0.7
return reward
def roll_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
"""Penalize body roll angle in radians (squared)."""
asset: RigidObject = env.scene[asset_cfg.name]
roll, _, _ = math_utils.euler_xyz_from_quat(asset.data.root_quat_w)
return torch.square(roll)
def pitch_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor:
"""Penalize body pitch angle in radians (squared)."""
asset: RigidObject = env.scene[asset_cfg.name]
_, pitch, _ = math_utils.euler_xyz_from_quat(asset.data.root_quat_w)
return torch.square(pitch)
def feet_air_time_including_ang_z(
env: ManagerBasedRLEnv, command_name: str, sensor_cfg: SceneEntityCfg, threshold: float
) -> torch.Tensor:
@@ -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."""
@@ -386,6 +386,8 @@ class RewardsCfg:
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,