Source code for wbc_mjlab.env.mdp.rewards

"""Motion-tracking and regularization rewards for WBC tasks.

All ``motion_*`` terms share optional kernel params:

- ``kappa`` — exponential stiffness (default ``1.0``; Zest uses ``0.25``)
- ``std`` / ``sigma_per_joint`` / ``sigma_per_keybody`` — error scale
- ``per_joint`` / ``per_keybody`` — mean of per-DoF/per-body exponentials
- ``body_error_aggregate`` — ``mean`` (default) or ``sum`` over keybodies
- ``body_names`` — subset of tracked bodies (default: all command bodies)
"""

from __future__ import annotations

import math
from typing import TYPE_CHECKING, cast

import torch

from mjlab.entity import Entity
from mjlab.managers.scene_entity_config import SceneEntityCfg
from mjlab.sensor import ContactSensor
from mjlab.utils.lab_api.math import quat_apply_inverse, quat_error_magnitude

from wbc_mjlab.env.mdp.torque_envelope import torque_speed_limits

from .commands import MotionCommand

if TYPE_CHECKING:
  from mjlab.envs import ManagerBasedRlEnv

_DEFAULT_ASSET_CFG = SceneEntityCfg("robot")


[docs] def tracking_std_from_sigma(sigma: float, *, dim: int = 1) -> float: """Map per-DoF σ to aggregate ``std`` with ``std = 2σ√dim`` (Table S4 convention).""" return sigma * math.sqrt(dim)
def dim_scaled_std(per_dim: float, *, dim: int) -> float: """``std`` when σ scales as ``per_dim · √dim`` over joints or keybodies.""" return tracking_std_from_sigma(per_dim, dim=dim) def _resolve_tracking_std( std: float | None, *, sigma_per: float | None, dim: int, ) -> float: if sigma_per is not None: return dim_scaled_std(sigma_per, dim=dim) if std is not None: return std raise ValueError("Either ``std`` or ``sigma_per`` must be set.") def _tracking_exp( error: torch.Tensor, *, std: float, kappa: float = 1.0, ) -> torch.Tensor: std = max(std, 1.0e-6) return torch.exp(-kappa * error / std**2) def _body_sq_error_per_keybody( command: MotionCommand, body_indexes: list[int], *, relative_pos: bool, relative_ori: bool, lin_vel: bool, ang_vel: bool, ) -> torch.Tensor: if relative_pos: diff = ( command.body_pos_relative_w[:, body_indexes] - command.robot_body_pos_w[:, body_indexes] ) return torch.sum(torch.square(diff), dim=-1) if relative_ori: error = quat_error_magnitude( command.body_quat_relative_w[:, body_indexes], command.robot_body_quat_w[:, body_indexes], ) return error**2 if lin_vel: diff = ( command.body_lin_vel_w[:, body_indexes] - command.robot_body_lin_vel_w[:, body_indexes] ) return torch.sum(torch.square(diff), dim=-1) assert ang_vel diff = ( command.body_ang_vel_w[:, body_indexes] - command.robot_body_ang_vel_w[:, body_indexes] ) return torch.sum(torch.square(diff), dim=-1) def _motion_keybody_tracking_exp( sq_error_per_body: torch.Tensor, *, std: float | None, sigma_per_keybody: float | None, per_keybody: bool, body_error_aggregate: str, kappa: float, num_bodies: int, ) -> torch.Tensor: if per_keybody: if sigma_per_keybody is None: raise ValueError("``sigma_per_keybody`` is required when ``per_keybody`` is True.") std_b = tracking_std_from_sigma(sigma_per_keybody, dim=1) return _tracking_exp(sq_error_per_body, std=std_b, kappa=kappa).mean(dim=-1) std_eff = _resolve_tracking_std( std, sigma_per=sigma_per_keybody, dim=num_bodies ) if body_error_aggregate == "sum": error = sq_error_per_body.sum(dim=-1) elif body_error_aggregate == "mean": error = sq_error_per_body.mean(dim=-1) else: raise ValueError( f"Unsupported body_error_aggregate {body_error_aggregate!r}; use 'mean' or 'sum'." ) return _tracking_exp(error, std=std_eff, kappa=kappa)
[docs] def action_rate_l1(env: ManagerBasedRlEnv) -> torch.Tensor: """Penalize action changes with L1 (sum of absolute deltas).""" return torch.sum( torch.abs(env.action_manager.action - env.action_manager.prev_action), dim=1, )
def joint_acc_l1( env: ManagerBasedRlEnv, asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG ) -> torch.Tensor: """Penalize joint accelerations with L1 (sum of absolute values).""" asset: Entity = env.scene[asset_cfg.name] return torch.sum( torch.abs(asset.data.joint_acc[:, asset_cfg.joint_ids]), dim=1, ) def _get_body_indexes( command: MotionCommand, body_names: tuple[str, ...] | None ) -> list[int]: return [ i for i, name in enumerate(command.cfg.body_names) if (body_names is None) or (name in body_names) ] def anti_shake_ang_vel_l2( env: ManagerBasedRlEnv, command_name: str, threshold: float, body_names: tuple[str, ...] | None = None, ) -> torch.Tensor: """Penalize high-frequency wrist spin above a deadzone (SONIC-style anti-shake).""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) body_indexes = _get_body_indexes(command, body_names) ang_vel = command.robot_body_ang_vel_w[:, body_indexes] speed = torch.linalg.norm(ang_vel, dim=-1) excess = torch.relu(speed - threshold) return (excess * excess).mean(dim=-1)
[docs] def motion_global_anchor_position_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float, *, kappa: float = 1.0 ) -> torch.Tensor: """Exponential reward for anchor position tracking in world frame.""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) error = torch.sum( torch.square(command.anchor_pos_w - command.robot_anchor_pos_w), dim=-1 ) return _tracking_exp(error, std=std, kappa=kappa)
[docs] def motion_global_anchor_orientation_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float, *, kappa: float = 1.0 ) -> torch.Tensor: """Exponential reward for anchor orientation tracking in world frame.""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) error = quat_error_magnitude(command.anchor_quat_w, command.robot_anchor_quat_w) ** 2 return _tracking_exp(error, std=std, kappa=kappa)
[docs] def motion_anchor_linear_velocity_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float, *, kappa: float = 1.0 ) -> torch.Tensor: """Exponential reward for anchor linear velocity tracking (world frame).""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) error = torch.sum( torch.square(command.anchor_lin_vel_w - command.robot_anchor_lin_vel_w), dim=-1, ) return _tracking_exp(error, std=std, kappa=kappa)
[docs] def motion_anchor_angular_velocity_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float, *, kappa: float = 1.0 ) -> torch.Tensor: """Exponential reward for anchor angular velocity tracking (world frame).""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) error = torch.sum( torch.square(command.anchor_ang_vel_w - command.robot_anchor_ang_vel_w), dim=-1, ) return _tracking_exp(error, std=std, kappa=kappa)
def motion_anchor_linear_velocity_body_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float, *, kappa: float = 1.0 ) -> torch.Tensor: """Root linear velocity tracking in the robot anchor (base) frame.""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) anchor_quat = command.robot_anchor_quat_w ref_lin_b = quat_apply_inverse(anchor_quat, command.anchor_lin_vel_w) robot_lin_b = quat_apply_inverse(anchor_quat, command.robot_anchor_lin_vel_w) error = torch.sum(torch.square(ref_lin_b - robot_lin_b), dim=-1) return _tracking_exp(error, std=std, kappa=kappa) def motion_anchor_angular_velocity_body_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float, *, kappa: float = 1.0 ) -> torch.Tensor: """Root angular velocity tracking in the robot anchor (base) frame.""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) anchor_quat = command.robot_anchor_quat_w ref_ang_b = quat_apply_inverse(anchor_quat, command.anchor_ang_vel_w) robot_ang_b = quat_apply_inverse(anchor_quat, command.robot_anchor_ang_vel_w) error = torch.sum(torch.square(ref_ang_b - robot_ang_b), dim=-1) return _tracking_exp(error, std=std, kappa=kappa)
[docs] def motion_joint_position_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float | None = None, sigma_per_joint: float | None = None, per_joint: bool = False, *, kappa: float = 1.0, asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG, ) -> torch.Tensor: """Joint position tracking with optional per-joint exponential kernels. Default: ``exp(-κ Σ e_i² / std²)`` with ``std = 2σ√n`` when ``sigma_per_joint`` is set. ``per_joint=True``: mean over joints of ``exp(-κ e_i² / (2σ)²)``. """ command = cast(MotionCommand, env.command_manager.get_term(command_name)) jnt_ids = asset_cfg.joint_ids ref_joint = command.joint_pos[:, jnt_ids] robot_joint = command.robot_joint_pos[:, jnt_ids] sq_err = torch.square(ref_joint - robot_joint) if per_joint: if sigma_per_joint is None: raise ValueError("``sigma_per_joint`` is required when ``per_joint`` is True.") std_j = tracking_std_from_sigma(sigma_per_joint, dim=1) return _tracking_exp(sq_err, std=std_j, kappa=kappa).mean(dim=-1) std_eff = _resolve_tracking_std( std, sigma_per=sigma_per_joint, dim=ref_joint.shape[-1] ) return _tracking_exp(torch.sum(sq_err, dim=-1), std=std_eff, kappa=kappa)
[docs] def motion_joint_velocity_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float | None = 1.0, sigma_per_joint: float | None = None, per_joint: bool = False, *, kappa: float = 1.0, asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG, ) -> torch.Tensor: """Joint velocity tracking; same kernel options as ``motion_joint_position_error_exp``.""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) jnt_ids = asset_cfg.joint_ids sq_err = torch.square( command.joint_vel[:, jnt_ids] - command.robot_joint_vel[:, jnt_ids] ) if per_joint: if sigma_per_joint is None: raise ValueError("``sigma_per_joint`` is required when ``per_joint`` is True.") std_j = tracking_std_from_sigma(sigma_per_joint, dim=1) return _tracking_exp(sq_err, std=std_j, kappa=kappa).mean(dim=-1) std_eff = _resolve_tracking_std( std, sigma_per=sigma_per_joint, dim=sq_err.shape[-1] ) return _tracking_exp(torch.sum(sq_err, dim=-1), std=std_eff, kappa=kappa)
[docs] def motion_relative_body_position_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float | None = None, body_names: tuple[str, ...] | None = None, sigma_per_keybody: float | None = None, per_keybody: bool = False, body_error_aggregate: str = "mean", *, kappa: float = 1.0, ) -> torch.Tensor: """Exponential reward for keybody position tracking (anchor-relative).""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) body_indexes = _get_body_indexes(command, body_names) sq_err = _body_sq_error_per_keybody( command, body_indexes, relative_pos=True, relative_ori=False, lin_vel=False, ang_vel=False ) return _motion_keybody_tracking_exp( sq_err, std=std, sigma_per_keybody=sigma_per_keybody, per_keybody=per_keybody, body_error_aggregate=body_error_aggregate, kappa=kappa, num_bodies=len(body_indexes), )
[docs] def motion_relative_body_orientation_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float | None = None, body_names: tuple[str, ...] | None = None, sigma_per_keybody: float | None = None, per_keybody: bool = False, body_error_aggregate: str = "mean", *, kappa: float = 1.0, ) -> torch.Tensor: """Exponential reward for keybody orientation tracking (anchor-relative).""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) body_indexes = _get_body_indexes(command, body_names) sq_err = _body_sq_error_per_keybody( command, body_indexes, relative_pos=False, relative_ori=True, lin_vel=False, ang_vel=False ) return _motion_keybody_tracking_exp( sq_err, std=std, sigma_per_keybody=sigma_per_keybody, per_keybody=per_keybody, body_error_aggregate=body_error_aggregate, kappa=kappa, num_bodies=len(body_indexes), )
[docs] def motion_global_body_linear_velocity_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float | None = None, body_names: tuple[str, ...] | None = None, sigma_per_keybody: float | None = None, per_keybody: bool = False, body_error_aggregate: str = "mean", *, kappa: float = 1.0, ) -> torch.Tensor: """Exponential reward for keybody linear velocity tracking (world frame).""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) body_indexes = _get_body_indexes(command, body_names) sq_err = _body_sq_error_per_keybody( command, body_indexes, relative_pos=False, relative_ori=False, lin_vel=True, ang_vel=False ) return _motion_keybody_tracking_exp( sq_err, std=std, sigma_per_keybody=sigma_per_keybody, per_keybody=per_keybody, body_error_aggregate=body_error_aggregate, kappa=kappa, num_bodies=len(body_indexes), )
[docs] def motion_global_body_angular_velocity_error_exp( env: ManagerBasedRlEnv, command_name: str, std: float | None = None, body_names: tuple[str, ...] | None = None, sigma_per_keybody: float | None = None, per_keybody: bool = False, body_error_aggregate: str = "mean", *, kappa: float = 1.0, ) -> torch.Tensor: """Exponential reward for keybody angular velocity tracking (world frame).""" command = cast(MotionCommand, env.command_manager.get_term(command_name)) body_indexes = _get_body_indexes(command, body_names) sq_err = _body_sq_error_per_keybody( command, body_indexes, relative_pos=False, relative_ori=False, lin_vel=False, ang_vel=True ) return _motion_keybody_tracking_exp( sq_err, std=std, sigma_per_keybody=sigma_per_keybody, per_keybody=per_keybody, body_error_aggregate=body_error_aggregate, kappa=kappa, num_bodies=len(body_indexes), )
[docs] def actuator_torque_soft_limit( env: ManagerBasedRlEnv, soft_ratio: float = 0.95, asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG, ) -> torch.Tensor: """Penalize actuator forces approaching MuJoCo torque limits (normalized soft margin).""" asset: Entity = env.scene[asset_cfg.name] forces = asset.data.actuator_force ctrl_ids = asset.indexing.ctrl_ids tau_max = env.sim.model.actuator_forcerange[:, ctrl_ids, 1] ratio = torch.abs(forces) / tau_max.clamp(min=1.0e-6) violation = torch.clamp(ratio - soft_ratio, min=0.0) return torch.sum(violation, dim=-1)
[docs] def angular_momentum_penalty( env: ManagerBasedRlEnv, sensor_name: str, *, axes: str = "xy", ) -> torch.Tensor: """Penalize whole-body angular momentum (roll/pitch by default).""" angmom = env.scene[sensor_name].data if axes == "xy": return torch.sum(torch.square(angmom[..., :2]), dim=-1) if axes == "xyz": return torch.sum(torch.square(angmom), dim=-1) raise ValueError(f"Unsupported axes {axes!r}; use 'xy' or 'xyz'.")
def self_collision_cost( env: ManagerBasedRlEnv, sensor_name: str, force_threshold: float = 10.0, ) -> torch.Tensor: sensor: ContactSensor = env.scene[sensor_name] data = sensor.data if data.force_history is not None: force_mag = torch.norm(data.force_history, dim=-1) hit = (force_mag > force_threshold).any(dim=1) return hit.sum(dim=-1).float() assert data.found is not None return data.found.squeeze(-1) def _joint_torque_and_vel( env: ManagerBasedRlEnv, asset_cfg: SceneEntityCfg ) -> tuple[torch.Tensor, torch.Tensor]: asset: Entity = env.scene[asset_cfg.name] ids = asset_cfg.joint_ids return asset.data.qfrc_actuator[:, ids], asset.data.joint_vel[:, ids] def mechanical_power_l1( env: ManagerBasedRlEnv, asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG, ) -> torch.Tensor: """Sum of positive mechanical power P = τ·ω (motoring only).""" tau, qd = _joint_torque_and_vel(env, asset_cfg) power = tau * qd return torch.sum(torch.clamp(power, min=0.0), dim=-1) def negative_mechanical_power_l2( env: ManagerBasedRlEnv, asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG, *, power_deadband: float = 0.0, penalty_scale: float = 1.0, joint_names: tuple[str, ...] | None = None, ) -> torch.Tensor: """Penalize excessive regenerative braking (OmniXtreme power-safety term).""" cfg = ( SceneEntityCfg(asset_cfg.name, joint_names=joint_names) if joint_names is not None else asset_cfg ) tau, qd = _joint_torque_and_vel(env, cfg) power = tau * qd excess = torch.clamp(-power - power_deadband, min=0.0) / max(penalty_scale, 1.0e-6) return torch.sum(torch.square(excess), dim=-1) def torque_envelope_violation_l2( env: ManagerBasedRlEnv, asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG, ) -> torch.Tensor: """Penalty when applied torque exceeds G1 velocity-dependent envelope.""" from wbc_mjlab.robots.g1.envelope import g1_joint_envelope_tensors tau, qd = _joint_torque_and_vel(env, asset_cfg) envl = g1_joint_envelope_tensors(env, asset_cfg) tau_low, tau_high = torque_speed_limits(qd, envl) over_high = torch.clamp(tau - tau_high, min=0.0) over_low = torch.clamp(tau_low - tau, min=0.0) return torch.sum(torch.square(over_high) + torch.square(over_low), dim=-1)
[docs] def feet_slip( env: ManagerBasedRlEnv, sensor_name: str, asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG, ) -> torch.Tensor: """Penalize foot planar velocity while in contact (slip).""" asset: Entity = env.scene[asset_cfg.name] contact_sensor: ContactSensor = env.scene[sensor_name] assert contact_sensor.data.found is not None in_contact = (contact_sensor.data.found > 0).float() foot_vel_xy = asset.data.site_lin_vel_w[:, asset_cfg.site_ids, :2] vel_xy_norm = torch.norm(foot_vel_xy, dim=-1) vel_xy_norm_sq = torch.square(vel_xy_norm) cost = torch.sum(vel_xy_norm_sq * in_contact, dim=1) num_in_contact = torch.sum(in_contact) mean_slip_vel = torch.sum(vel_xy_norm * in_contact) / torch.clamp(num_in_contact, min=1) env.extras["log"]["Metrics/slip_velocity_mean"] = mean_slip_vel return cost