diff --git a/docs/source/_static/css/environment-browser.js b/docs/source/_static/css/environment-browser.js index 4698a7d3f080..3081e431d4dd 100644 --- a/docs/source/_static/css/environment-browser.js +++ b/docs/source/_static/css/environment-browser.js @@ -47,6 +47,7 @@ ["Isaac-Reorient-KukaAllegro", "rsl_rl", "isaacsim_physx,newton_mjwarp,ovphysx", "", "cube,shapes"], ["Isaac-Reorient-KukaAllegro-Camera", "rsl_rl", "isaacsim_physx,newton_mjwarp,ovphysx", "isaacsim_rtx,newton_renderer,ovrtx", "albedo128,albedo256,albedo64,cube,depth128,depth256,depth64,duo_camera,raycaster_depth128,raycaster_depth256,raycaster_depth64,rgb128,rgb256,rgb64,semantic_segmentation128,semantic_segmentation256,semantic_segmentation64,shapes,simple_shading_constant_diffuse128,simple_shading_constant_diffuse256,simple_shading_constant_diffuse64,simple_shading_diffuse_mdl128,simple_shading_diffuse_mdl256,simple_shading_diffuse_mdl64,simple_shading_full_mdl128,simple_shading_full_mdl256,simple_shading_full_mdl64,single_camera"], ["Isaac-Shadow-Handover-Direct", "rl_games,rsl_rl,skrl", "isaacsim_physx,newton_mjwarp,ovphysx", "", ""], + ["Isaac-Shadow-Handover", "rsl_rl", "isaacsim_physx,newton_mjwarp,ovphysx", "", "randomized"], ["Isaac-Velocity-Flat-AnymalD", "rsl_rl,skrl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], ["Isaac-Velocity-Flat-Cassie", "rsl_rl,skrl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], ["Isaac-Velocity-Flat-G1", "rsl_rl,skrl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], diff --git a/docs/source/overview/environments.rst b/docs/source/overview/environments.rst index f09368c9081c..60093539357d 100644 --- a/docs/source/overview/environments.rst +++ b/docs/source/overview/environments.rst @@ -1260,6 +1260,11 @@ including disabling runtime perturbations used for training. - Direct - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO, IPPO, MAPPO) - **physics=** ``isaacsim_physx``, ``newton_mjwarp``, ``ovphysx`` + * - Isaac-Shadow-Handover + - Manager Based + - **rsl_rl** (PPO) + - | **physics=** ``isaacsim_physx``, ``newton_mjwarp``, ``ovphysx`` + | **presets=** ``randomized`` * - Isaac-Velocity-Flat-AnymalD - Manager Based - **rsl_rl** (PPO, DISTILLATION, DISTILLATION_RECURRENT, RECURRENT), **skrl** (PPO) diff --git a/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part11.minor.rst b/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part11.minor.rst new file mode 100644 index 000000000000..fde903331a13 --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part11.minor.rst @@ -0,0 +1,36 @@ +Added +^^^^^ + +* Added manager-based counterparts for the Shadow handover and Shadow camera + reorientation tasks, completing the manager coverage of the dexterous task + families. +* Added a Direct-versus-manager value-parity check for the handover task, + alongside the reorientation one. + +Changed +^^^^^^^ + +* Changed the manager-based Shadow camera task to run on the Kit PhysX backend by + default, since only the Isaac RTX tiled camera renders the default modalities. + Select Newton with ``physics=newton_mjwarp`` for the state-only observation + groups. +* Changed the handover reward to a plain reward term, moving success and + goal-distance bookkeeping to + :class:`~isaaclab_tasks.core.handover.mdp.commands.HandoverCommand`. +* Changed the reorientation action configuration to name its term through a + module path, so loading a task configuration no longer imports the USD + bindings. + +Removed +^^^^^^^ + +* Removed the ``Isaac-Reorient-Cube-Shadow-Camera-Play`` and + ``Isaac-Reorient-Cube-Shadow-Camera-Direct-Play`` tasks. Use the training task + with ``--play`` instead; playback settings now live in + :meth:`~isaaclab.envs.ManagerBasedRLEnvCfg.play_mode`. + +Fixed +^^^^^ + +* Fixed the Shadow camera feature-extractor observation term ignoring its + declared ``feature_extractor_cfg`` parameter. diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py index a4f401545fee..5e6e83b42a57 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py @@ -15,6 +15,16 @@ # Register Gym environments. ## +gym.register( + id="Isaac-Shadow-Handover", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.handover_manager_env_cfg:HandoverManagerEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:HandoverPPORunnerCfg", + }, +) + gym.register( id="Isaac-Shadow-Handover-Direct", entry_point=f"{__name__}.handover_env:HandoverEnv", diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py index 5ca49da11404..8fd173345048 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py @@ -8,13 +8,10 @@ from isaaclab_ov.physics import OvPhysxCfg from isaaclab_physx.physics import PhysxCfg -import isaaclab.envs.mdp as mdp import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils from isaaclab.assets import ArticulationCfg, RigidObjectCfg from isaaclab.envs import DirectMARLEnvCfg -from isaaclab.managers import EventTermCfg as EventTerm -from isaaclab.managers import SceneEntityCfg from isaaclab.markers import VisualizationMarkersCfg from isaaclab.physics import PhysxAutoCfg from isaaclab.scene import InteractiveSceneCfg @@ -33,120 +30,21 @@ from isaaclab_assets.robots.shadow_hand import SHADOW_HAND_CFG, SHADOW_HAND_NEWTON_CFG -@configclass -class EventCfg: - """Configuration for randomization (PhysX path). - - Note: this config is currently not wired into ``HandoverEnvCfg.events`` - - it is kept as a reference for future event-randomization work. The event - terms here use PhysX-only APIs (rigid-body materials, fixed tendons), so - they would need a Newton variant before being enabled in the env. - """ - - # -- robot - robot_physics_material = EventTerm( - func=mdp.randomize_rigid_body_material, - mode="reset", - min_step_count_between_reset=720, - params={ - "asset_cfg": SceneEntityCfg("right_hand"), - "static_friction_range": (0.7, 1.3), - "dynamic_friction_range": (1.0, 1.0), - "restitution_range": (1.0, 1.0), - "num_buckets": 250, - }, - ) - robot_joint_stiffness_and_damping = EventTerm( - func=mdp.randomize_actuator_gains, - min_step_count_between_reset=720, - mode="reset", - params={ - "asset_cfg": SceneEntityCfg("right_hand", joint_names=".*"), - "stiffness_distribution_params": (0.75, 1.5), - "damping_distribution_params": (0.3, 3.0), - "operation": "scale", - "distribution": "log_uniform", - }, - ) - robot_joint_pos_limits = EventTerm( - func=mdp.randomize_joint_parameters, - min_step_count_between_reset=720, - mode="reset", - params={ - "asset_cfg": SceneEntityCfg("right_hand", joint_names=".*"), - "lower_limit_distribution_params": (0.00, 0.01), - "upper_limit_distribution_params": (0.00, 0.01), - "operation": "add", - "distribution": "gaussian", - }, - ) - robot_tendon_properties = EventTerm( - func=mdp.randomize_fixed_tendon_parameters, - min_step_count_between_reset=720, - mode="reset", - params={ - "asset_cfg": SceneEntityCfg("right_hand", fixed_tendon_names=".*"), - "stiffness_distribution_params": (0.75, 1.5), - "damping_distribution_params": (0.3, 3.0), - "operation": "scale", - "distribution": "log_uniform", - }, - ) - - # -- object - object_physics_material = EventTerm( - func=mdp.randomize_rigid_body_material, - min_step_count_between_reset=720, - mode="reset", - params={ - "asset_cfg": SceneEntityCfg("object"), - "static_friction_range": (0.7, 1.3), - "dynamic_friction_range": (1.0, 1.0), - "restitution_range": (1.0, 1.0), - "num_buckets": 250, - }, - ) - object_scale_mass = EventTerm( - func=mdp.randomize_rigid_body_mass, - min_step_count_between_reset=720, - mode="reset", - params={ - "asset_cfg": SceneEntityCfg("object"), - "mass_distribution_params": (0.5, 1.5), - "operation": "scale", - "distribution": "uniform", - }, - ) - - # -- scene - reset_gravity = EventTerm( - func=mdp.randomize_physics_scene_gravity, - mode="interval", - is_global_time=True, - interval_range_s=(36.0, 36.0), # time_s = num_steps * (decimation * dt) - params={ - "gravity_distribution_params": ([0.0, 0.0, 0.0], [0.0, 0.0, 0.4]), - "operation": "add", - "distribution": "gaussian", - }, - ) - - def _shadow_hand_cfg( prim_path: str, init_pos: tuple[float, float, float], init_rot: tuple[float, float, float, float], ) -> PresetCfg: - """Per-hand Shadow Hand preset (PhysX, Newton MJWarp, and OVPhysX variants). + """Build the per-hand Shadow Hand preset for each supported backend. - All variants are placed at *prim_path* with the same init pose; per-hand - differences (right vs left) come from the caller's *prim_path* / *init_pos* / - *init_rot* — the gain tuning is identical on both hands. + Args: + prim_path: Scene path the hand spawns at. + init_pos: Spawn position [m]. + init_rot: Spawn orientation as ``(w, x, y, z)``. - The Newton variant reuses the shared actuator defined on ``SHADOW_HAND_NEWTON_CFG``, - raising its gains to ``20.0`` / ``2.0`` -- the catch needs more joint authority than - reorientation. The scalar replaces the per-joint gain mapping, so it applies to the whole - actuator group. + Returns: + A preset carrying the PhysX, Newton MJWarp and OvPhysX variants, each at + *prim_path* with the given pose. The two hands differ only in these arguments. """ physx_cfg = SHADOW_HAND_CFG.replace(prim_path=prim_path).replace( init_state=ArticulationCfg.InitialStateCfg(pos=init_pos, rot=init_rot, joint_pos={".*": 0.0}) @@ -196,67 +94,37 @@ def _shadow_hand_cfg( ) -@configclass -class ObjectCfg(PresetCfg): - """Hand-over object preset. - - Both backends spawn the same procedural sphere as a free rigid body: - Newton's :class:`~isaaclab_newton.assets.RigidObject` resolves the - asset via the ``UsdPhysics.RigidBodyAPI`` that - :class:`~isaaclab.sim.RigidBodyPropertiesCfg` applies. The Newton - variant drops PhysX-only knobs (per-shape solver iterations, sleep - thresholds, max depenetration velocity, custom physics material). - """ - - physx = RigidObjectCfg( - prim_path="{ENV_REGEX_NS}/object", - spawn=sim_utils.SphereCfg( - radius=OBJECT_RADIUS, - visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.8, 1.0, 0.0)), - physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=0.7), - rigid_props=sim_utils.RigidBodyPropertiesCfg( - kinematic_enabled=False, - disable_gravity=False, - enable_gyroscopic_forces=True, - solver_position_iteration_count=8, - solver_velocity_iteration_count=0, - sleep_threshold=0.005, - stabilization_threshold=0.0025, - max_depenetration_velocity=1000.0, - ), - collision_props=sim_utils.CollisionPropertiesCfg(), - mass_props=sim_utils.MassPropertiesCfg(density=500.0), - ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.39, 0.54), rot=(0.0, 0.0, 0.0, 1.0)), - ) - newton_mjwarp = RigidObjectCfg( - prim_path="{ENV_REGEX_NS}/object", - spawn=sim_utils.SphereCfg( - radius=OBJECT_RADIUS, - visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.8, 1.0, 0.0)), - rigid_props=sim_utils.RigidBodyPropertiesCfg( - kinematic_enabled=False, - disable_gravity=False, - enable_gyroscopic_forces=True, - ), - collision_props=sim_utils.CollisionPropertiesCfg(), - mass_props=sim_utils.MassPropertiesCfg(density=500.0), +BALL_CFG = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/object", + spawn=sim_utils.SphereCfg( + radius=OBJECT_RADIUS, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.8, 1.0, 0.0)), + physics_material=sim_utils.RigidBodyMaterialCfg(static_friction=0.7), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + kinematic_enabled=False, + disable_gravity=False, + enable_gyroscopic_forces=True, + solver_position_iteration_count=8, + solver_velocity_iteration_count=0, + sleep_threshold=0.005, + stabilization_threshold=0.0025, + max_depenetration_velocity=1000.0, ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.39, 0.54), rot=(0.0, 0.0, 0.0, 1.0)), - ) - ovphysx = physx - isaacsim_physx = physx - default = newton_mjwarp + collision_props=sim_utils.CollisionPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(density=500.0), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.39, 0.54), rot=(0.0, 0.0, 0.0, 1.0)), +) +"""Hand-over ball, thrown from one Shadow hand to the other.""" @configclass class PhysicsCfg(PresetCfg): """Physics-backend preset (PhysX vs Newton/MJWarp). - Newton settings mirror the single-agent ShadowHand Newton port: elliptic - cone, ``impratio=10`` (favors normal contacts over friction), 100 solver - iterations, 2 substeps. Empirically converges on the single-agent ShadowHand - tasks; tuning may be needed for handover-specific contact dynamics. + Newton mirrors the single-agent Shadow Hand Newton port: an elliptic friction + cone with ``impratio=10``, which weights normal contacts over friction, 100 + solver iterations and 2 substeps. """ isaacsim_physx = PhysxCfg( @@ -310,7 +178,7 @@ class HandoverEnvCfg(DirectMARLEnvCfg): fingertip_body_names = FINGERTIP_BODY_NAMES # in-hand object - object_cfg: ObjectCfg = ObjectCfg() + object_cfg: RigidObjectCfg = BALL_CFG # goal object goal_object_cfg: VisualizationMarkersCfg = GOAL_MARKER_CFG # scene diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py new file mode 100644 index 000000000000..27d0373dcbe5 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py @@ -0,0 +1,291 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Manager-based counterpart of the Shadow Hand handover task.""" + +import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg +from isaaclab.envs import ManagerBasedRLEnvCfg +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.sim.spawners.materials import RigidBodyMaterialBaseCfg +from isaaclab.utils.configclass import configclass + +import isaaclab_tasks.core.handover.mdp as mdp +import isaaclab_tasks.core.reorient.mdp as reorient_mdp +from isaaclab_tasks.core.handover.handover_common import ( + ACTUATED_JOINT_NAMES, + FINGERTIP_BODY_NAMES, +) +from isaaclab_tasks.core.handover.handover_env_cfg import ( + BALL_CFG, + LEFT_HAND_CFG, + RIGHT_HAND_CFG, + PhysicsCfg, +) +from isaaclab_tasks.utils import PresetCfg + + +@configclass +class HandoverManagerSceneCfg(InteractiveSceneCfg): + """Two Shadow hands facing each other over a ground plane.""" + + num_envs = 2048 + env_spacing = 1.5 + replicate_physics = True + + ground = AssetBaseCfg( + prim_path="/World/ground", + spawn=sim_utils.GroundPlaneCfg(), + ) + right_hand: PresetCfg = RIGHT_HAND_CFG + left_hand: PresetCfg = LEFT_HAND_CFG + object: RigidObjectCfg = BALL_CFG + light = AssetBaseCfg( + prim_path="/World/Light", + spawn=sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)), + ) + + +@configclass +class CommandsCfg: + """Handover goal command.""" + + object_pose = mdp.HandoverCommandCfg(asset_name="object", success_distance_threshold=0.1, debug_vis=True) + + +@configclass +class ActionsCfg: + """Two-hand action terms, ordered right then left like the Direct adapter.""" + + right_hand = mdp.EMAJointPositionToLimitsActionCfg( + asset_name="right_hand", + joint_names=ACTUATED_JOINT_NAMES, + alpha=1.0, + rescale_to_limits=True, + ) + left_hand = mdp.EMAJointPositionToLimitsActionCfg( + asset_name="left_hand", + joint_names=ACTUATED_JOINT_NAMES, + alpha=1.0, + rescale_to_limits=True, + ) + + +@configclass +class PolicyCfg(ObsGroup): + # Right agent: 133 hand dimensions followed by 24 object/goal dimensions. + # soft limits equal the hard limits here: soft_joint_pos_limits_factor defaults to 1.0 + right_joint_pos = ObsTerm( + func=mdp.joint_pos_limit_normalized, params={"asset_cfg": SceneEntityCfg("right_hand", joint_names=".*")} + ) + right_joint_vel = ObsTerm( + func=mdp.joint_vel, scale=0.2, params={"asset_cfg": SceneEntityCfg("right_hand", joint_names=".*")} + ) + right_fingertip_pose = ObsTerm( + func=mdp.body_pose_w, params={"asset_cfg": SceneEntityCfg("right_hand", body_names=FINGERTIP_BODY_NAMES)} + ) + right_fingertip_vel = ObsTerm( + func=reorient_mdp.fingertip_vel, + params={"asset_cfg": SceneEntityCfg("right_hand", body_names=FINGERTIP_BODY_NAMES)}, + ) + right_action = ObsTerm(func=mdp.last_action, params={"action_name": "right_hand"}) + object_pos = ObsTerm(func=mdp.root_pos_w, params={"asset_cfg": SceneEntityCfg("object")}) + object_quat = ObsTerm(func=mdp.root_quat_w, params={"asset_cfg": SceneEntityCfg("object")}) + object_lin_vel = ObsTerm(func=mdp.root_lin_vel_w, params={"asset_cfg": SceneEntityCfg("object")}) + object_ang_vel = ObsTerm(func=mdp.root_ang_vel_w, scale=0.2, params={"asset_cfg": SceneEntityCfg("object")}) + goal_pose = ObsTerm(func=mdp.generated_commands, params={"command_name": "object_pose"}) + goal_quat_diff = ObsTerm( + func=reorient_mdp.goal_quat_diff, + params={"asset_cfg": SceneEntityCfg("object"), "command_name": "object_pose", "make_quat_unique": False}, + ) + + # Left agent: the same 157-dimensional layout. + # soft limits equal the hard limits here: soft_joint_pos_limits_factor defaults to 1.0 + left_joint_pos = ObsTerm( + func=mdp.joint_pos_limit_normalized, params={"asset_cfg": SceneEntityCfg("left_hand", joint_names=".*")} + ) + left_joint_vel = ObsTerm( + func=mdp.joint_vel, scale=0.2, params={"asset_cfg": SceneEntityCfg("left_hand", joint_names=".*")} + ) + left_fingertip_pose = ObsTerm( + func=mdp.body_pose_w, params={"asset_cfg": SceneEntityCfg("left_hand", body_names=FINGERTIP_BODY_NAMES)} + ) + left_fingertip_vel = ObsTerm( + func=reorient_mdp.fingertip_vel, + params={"asset_cfg": SceneEntityCfg("left_hand", body_names=FINGERTIP_BODY_NAMES)}, + ) + left_action = ObsTerm(func=mdp.last_action, params={"action_name": "left_hand"}) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = True + + +@configclass +class ObservationsCfg: + """Single-agent observations matching the Direct MARL adapter.""" + + policy: PolicyCfg = PolicyCfg() + + +@configclass +class RandomizationEventCfg: + """Randomization of both hands and the object, applied on every physics backend.""" + + right_hand_joint_stiffness_and_damping = EventTerm( + func=mdp.randomize_actuator_gains, + min_step_count_between_reset=720, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("right_hand"), + "stiffness_distribution_params": (0.75, 1.5), + "damping_distribution_params": (0.3, 3.0), + "operation": "scale", + "distribution": "log_uniform", + }, + ) + left_hand_joint_stiffness_and_damping = EventTerm( + func=mdp.randomize_actuator_gains, + min_step_count_between_reset=720, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("left_hand"), + "stiffness_distribution_params": (0.75, 1.5), + "damping_distribution_params": (0.3, 3.0), + "operation": "scale", + "distribution": "log_uniform", + }, + ) + object_scale_mass = EventTerm( + func=mdp.randomize_rigid_body_mass, + min_step_count_between_reset=720, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("object"), + "mass_distribution_params": (0.5, 1.5), + "operation": "scale", + "distribution": "uniform", + "recompute_inertia": False, + }, + ) + + # -- scene + reset_gravity = EventTerm( + func=mdp.randomize_physics_scene_gravity, + mode="interval", + is_global_time=True, + interval_range_s=(36.0, 36.0), # time_s = num_steps * (decimation * dt) + params={ + "gravity_distribution_params": ([0.0, 0.0, 0.0], [0.0, 0.0, 0.4]), + "operation": "add", + "distribution": "gaussian", + }, + ) + + +@configclass +class ResetEventCfg: + """Reset distributions matching the Direct handover environment.""" + + reset_object = EventTerm( + func=mdp.reset_root_state_with_random_orientation, + mode="reset", + params={ + # the Direct task jitters the drop position and samples a random orientation + "pose_range": {"x": (-0.01, 0.01), "y": (-0.01, 0.01), "z": (-0.01, 0.01)}, # [m] + "velocity_range": {}, + "asset_cfg": SceneEntityCfg("object"), + }, + ) + reset_right_hand = EventTerm( + func=reorient_mdp.reset_reorient_hand, + mode="reset", + params={ + "joint_position_noise": 0.2, # [rad] + "joint_velocity_noise": 0.0, # [rad/s] + "robot_cfg": SceneEntityCfg("right_hand"), + }, + ) + reset_left_hand = EventTerm( + func=reorient_mdp.reset_reorient_hand, + mode="reset", + params={ + "joint_position_noise": 0.2, # [rad] + "joint_velocity_noise": 0.0, # [rad/s] + "robot_cfg": SceneEntityCfg("left_hand"), + }, + ) + + +@configclass +class HandoverEventCfg(RandomizationEventCfg, ResetEventCfg): + """Randomization plus the state reset the manager task applies on every episode.""" + + +@configclass +class HandoverEventPresetCfg(PresetCfg): + """``presets=randomized`` adds the domain-randomization terms to the reset.""" + + randomized = HandoverEventCfg() + default = ResetEventCfg() + + +@configclass +class RewardsCfg: + """Summed two-agent reward exposed by the Direct single-agent adapter.""" + + goal_distance = RewTerm( + func=mdp.handover_goal_distance_reward, + weight=1.0, + params={ + "command_name": "object_pose", + "distance_scale": 20.0, + "object_cfg": SceneEntityCfg("object"), + }, + ) + + +@configclass +class TerminationsCfg: + """Termination conditions for the handover task. + + The generic ``time_out`` term ends an episode one control step later than the Direct + environment, which stops at ``max_episode_length - 1``. + """ + + object_out_of_reach = DoneTerm( + func=mdp.root_height_below_minimum, + params={"minimum_height": 0.24, "asset_cfg": SceneEntityCfg("object")}, + ) + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + +@configclass +class HandoverManagerEnvCfg(ManagerBasedRLEnvCfg): + """Manager-based handover environment matching the Direct RSL-RL view.""" + + scene: HandoverManagerSceneCfg = HandoverManagerSceneCfg() + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: HandoverEventPresetCfg = HandoverEventPresetCfg() + + def __post_init__(self): + self.decimation = 2 + self.episode_length_s = 7.5 + # simulation — mirrors the Direct cfg + self.sim.dt = 1 / 120 + self.sim.render_interval = self.decimation + self.sim.physics_material = RigidBodyMaterialBaseCfg(static_friction=1.0, dynamic_friction=1.0) + self.sim.physics = PhysicsCfg() + self.viewer.eye = (2.0, 2.0, 2.0) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.pyi index 8100050074e1..95ad82e44ab6 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.pyi @@ -4,9 +4,13 @@ # SPDX-License-Identifier: BSD-3-Clause __all__ = [ + "HandoverCommand", + "HandoverCommandCfg", + "handover_goal_distance_reward", "handover_reward", "evaluate_handover_success", ] -from .rewards import evaluate_handover_success, handover_reward +from .commands import HandoverCommand, HandoverCommandCfg +from .rewards import evaluate_handover_success, handover_goal_distance_reward, handover_reward from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py new file mode 100644 index 000000000000..68fc8b93b53f --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py @@ -0,0 +1,114 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Goal-pose command for the manager-based handover task.""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import MISSING +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.managers import CommandTerm, CommandTermCfg +from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg +from isaaclab.utils.configclass import configclass + +from isaaclab_tasks.core.handover.handover_common import GOAL_MARKER_CFG, GOAL_POSITION_OFFSET +from isaaclab_tasks.core.utils import EpisodeErrorRecorder + +if TYPE_CHECKING: + from isaaclab.assets import RigidObject + from isaaclab.envs import ManagerBasedRLEnv + + +class HandoverCommand(CommandTerm): + """Sample the fixed-position, random-orientation handover goal pose.""" + + cfg: HandoverCommandCfg + + def __init__(self, cfg: HandoverCommandCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + self._object: RigidObject = env.scene[cfg.asset_name] + offset = torch.tensor(cfg.position_offset, dtype=torch.float, device=self.device) + self.pos_command_e = self._object.data.default_root_pose.torch[:, :3] + offset + self.quat_command_w = torch.zeros(self.num_envs, 4, device=self.device) + self.quat_command_w[:, 3] = 1.0 # identity quaternion in (x, y, z, w) layout + self._x_unit = torch.tensor([1.0, 0.0, 0.0], device=self.device).repeat(self.num_envs, 1) + self._y_unit = torch.tensor([0.0, 1.0, 0.0], device=self.device).repeat(self.num_envs, 1) + self.metrics["goal_distance"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["success_rate"] = torch.zeros(self.num_envs, device=self.device) + self._minimum_goal_distance = EpisodeErrorRecorder(self.num_envs, self.device) + # Whether each environment has brought the object within the success distance this episode. + self._succeeded = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + + @property + def command(self) -> torch.Tensor: + """Goal pose in the environment frame [m, unit quaternion]. Shape is (num_envs, 7).""" + return torch.cat((self.pos_command_e, self.quat_command_w), dim=-1) + + def _update_metrics(self) -> None: + object_pos = self._object.data.root_pos_w.torch - self._env.scene.env_origins + goal_distance = torch.linalg.norm(object_pos - self.pos_command_e, ord=2, dim=-1) + self.metrics["goal_distance"][:] = goal_distance + self._minimum_goal_distance.update(goal_distance) + self._succeeded |= goal_distance < self.cfg.success_distance_threshold + + def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: + if env_ids is None: + env_ids = slice(None) + # the base class means the metric over ``env_ids``, converts it to a float and + # zeroes it, so the episode's success bit is written before delegating + self.metrics["success_rate"][env_ids] = self._succeeded[env_ids].float() + extras = super().reset(env_ids) + self._succeeded[env_ids] = False + log = self._env.extras.setdefault("log", {}) + # Route success_rate to the unified ``Metrics/success_rate`` path (shared TensorBoard + # card across tasks); pop it from the returned dict so CommandManager does not + # additionally log it under ``Metrics//success_rate``. + log["Metrics/success_rate"] = extras.pop("success_rate") + for statistic, value in self._minimum_goal_distance.reset(env_ids).items(): + log[f"Diagnostics/episode_min_goal_distance_{statistic}"] = value + return extras + + def _resample_command(self, env_ids: Sequence[int]) -> None: + random_values = 2.0 * torch.rand((len(env_ids), 2), device=self.device) - 1.0 + self.quat_command_w[env_ids] = math_utils.quat_mul( + math_utils.quat_from_angle_axis(random_values[:, 0] * torch.pi, self._x_unit[env_ids]), + math_utils.quat_from_angle_axis(random_values[:, 1] * torch.pi, self._y_unit[env_ids]), + ) + + def _update_command(self) -> None: + pass + + def _set_debug_vis_impl(self, debug_vis: bool) -> None: + if debug_vis: + if not hasattr(self, "_goal_visualizer"): + self._goal_visualizer = VisualizationMarkers(self.cfg.goal_visualizer_cfg) + self._goal_visualizer.set_visibility(True) + elif hasattr(self, "_goal_visualizer"): + self._goal_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event) -> None: + self._goal_visualizer.visualize( + translations=self.pos_command_e + self._env.scene.env_origins, + orientations=self.quat_command_w, + ) + + +@configclass +class HandoverCommandCfg(CommandTermCfg): + """Configuration for :class:`HandoverCommand`.""" + + class_type: type[HandoverCommand] = HandoverCommand + resampling_time_range: tuple[float, float] = (1.0e6, 1.0e6) + asset_name: str = MISSING + position_offset: tuple[float, float, float] = GOAL_POSITION_OFFSET + """Goal-position offset from the object's default position [m].""" + success_distance_threshold: float = 0.1 + """Object-to-goal distance below which an episode counts as successful [m].""" + goal_visualizer_cfg: VisualizationMarkersCfg = GOAL_MARKER_CFG.replace(prim_path="/Visuals/Command/goal_marker") diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/rewards.py index c31065214a65..225aec0d92dd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/rewards.py @@ -7,8 +7,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import torch +from isaaclab.managers import SceneEntityCfg + +if TYPE_CHECKING: + from isaaclab.assets import RigidObject + from isaaclab.envs import ManagerBasedRLEnv + def handover_reward(goal_distance: torch.Tensor, distance_scale: float) -> torch.Tensor: """Return one hand's Direct reward for the current object-goal distance.""" @@ -31,3 +39,28 @@ def evaluate_handover_success( """ goal_distance = torch.linalg.norm(object_position - target_position, ord=2, dim=-1) return goal_distance < success_distance_threshold, goal_distance + + +def handover_goal_distance_reward( + env: ManagerBasedRLEnv, + command_name: str, + distance_scale: float, + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), +) -> torch.Tensor: + """Reward both hands for holding the object near its goal. + + The Direct environment sums one identical reward per hand, so this returns twice the + single-hand value. The command term owns the episode success bookkeeping behind + ``Metrics/success_rate``. + + Args: + env: The environment object. + command_name: The command term to be used for extracting the goal. + distance_scale: Exponential decay rate of the distance reward [1/m]. + object_cfg: The configuration for the scene entity. Default is "object". + """ + object_asset: RigidObject = env.scene[object_cfg.name] + object_pos = object_asset.data.root_pos_w.torch - env.scene.env_origins + goal_pos = env.command_manager.get_command(command_name)[:, :3] + goal_distance = torch.linalg.norm(object_pos - goal_pos, ord=2, dim=-1) + return 2.0 * handover_reward(goal_distance, distance_scale) diff --git a/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-depth.png b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-depth.png new file mode 100644 index 000000000000..c229b583dfb7 --- /dev/null +++ b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-depth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a37f6bca30bb2d093eb68186c601551d52aafe8ed19c6c090de149b3210d81a5 +size 3665 diff --git a/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgb.png b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgb.png new file mode 100644 index 000000000000..eace991f49eb --- /dev/null +++ b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b60263834743f0508a437281d36bd2f46296789b28ad930627fb72e406ab8700 +size 19962 diff --git a/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgba.png b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgba.png new file mode 100644 index 000000000000..7d6fa735693d --- /dev/null +++ b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgba.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8da49db87ab2afe4f2a32c9354a72fd1b5a32aaf76484a21f787b01c0a47197a +size 22127 diff --git a/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-semantic_segmentation.png b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-semantic_segmentation.png new file mode 100644 index 000000000000..4bad29d72ce6 --- /dev/null +++ b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-semantic_segmentation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75576f31118081f96b0cec2151ada7016794a933a035ea35665a752e8552b503 +size 1474