diff --git a/docs/source/setup/quickstart_details.rst b/docs/source/setup/quickstart_details.rst index ad4cc9663d3f..3176c31782f2 100644 --- a/docs/source/setup/quickstart_details.rst +++ b/docs/source/setup/quickstart_details.rst @@ -57,7 +57,7 @@ options (observation modes, camera configs, etc.). They fold into Hydra override # OVRTX rendering (kit-less, no Kit visualizer) uv run isaaclab train --rl_library rsl_rl \ - --task=Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct \ + --task=IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct \ --num_envs=16 --max_iterations=10 \ physics=newton_mjwarp renderer=ovrtx presets=simple_shading_diffuse_mdl diff --git a/source/isaaclab/changelog.d/task-cleanup-dex-part08.rst b/source/isaaclab/changelog.d/task-cleanup-dex-part08.rst new file mode 100644 index 000000000000..20f9533f8564 --- /dev/null +++ b/source/isaaclab/changelog.d/task-cleanup-dex-part08.rst @@ -0,0 +1,6 @@ +Fixed +^^^^^ + +* Fixed :attr:`~isaaclab.envs.ManagerBasedRLEnv.reset_buf` not existing until the first + call to :meth:`~isaaclab.envs.ManagerBasedRLEnv.step`, so manager terms that run during + the initial reset could not read it. diff --git a/source/isaaclab/isaaclab/envs/manager_based_rl_env.py b/source/isaaclab/isaaclab/envs/manager_based_rl_env.py index b17fe38bee0c..cb02f4b746cd 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_rl_env.py +++ b/source/isaaclab/isaaclab/envs/manager_based_rl_env.py @@ -73,8 +73,11 @@ def __init__(self, cfg: ManagerBasedRLEnvCfg, render_mode: str | None = None, ** # -- counter for curriculum self.common_step_counter = 0 - # initialize the episode length buffer BEFORE loading the managers to use it in mdp functions. + # initialize the episode length and reset buffers BEFORE loading the managers to use them in + # mdp functions. The reset buffer is only assigned its computed value in :meth:`step`, so terms + # that run during the initial reset would otherwise not find it. self.episode_length_buf = torch.zeros(cfg.scene.num_envs, device=cfg.sim.device, dtype=torch.long) + self.reset_buf = torch.zeros(cfg.scene.num_envs, device=cfg.sim.device, dtype=torch.bool) # Forward render_mode and viewer camera to VideoRecorderCfg before super().__init__() # creates the VideoRecorder, so fallback cameras are only spawned when --video is active diff --git a/source/isaaclab_assets/changelog.d/task-cleanup-dex-part11.minor.rst b/source/isaaclab_assets/changelog.d/task-cleanup-dex-part11.minor.rst new file mode 100644 index 000000000000..93635087e69d --- /dev/null +++ b/source/isaaclab_assets/changelog.d/task-cleanup-dex-part11.minor.rst @@ -0,0 +1,7 @@ +Added +^^^^^ + +* Added :obj:`~isaaclab_assets.robots.shadow_hand.SHADOW_ACTUATED_JOINT_NAMES_NEWTON`. + The Newton Shadow Hand asset numbers its finger joints one higher than the + PhysX one, so terms that address joints by name need the Newton ordering + rather than :obj:`~isaaclab_assets.robots.shadow_hand.SHADOW_ACTUATED_JOINT_NAMES`. diff --git a/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py b/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py index 3d3c96876c2f..6dc214aebb5e 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py @@ -169,9 +169,10 @@ ) """Configuration of the Shadow Hand robot on the Newton (MJWarp) asset. -The Newton USD renumbers the finger joints (+1) relative to :obj:`SHADOW_HAND_CFG`, but the -names in :obj:`SHADOW_ACTUATED_JOINT_NAMES` resolve on both assets, so the two backends share -one actuated-joint list. Gains default to the PhysX values; tasks override them as needed. +The Newton USD renumbers the finger joints (+1) relative to :obj:`SHADOW_HAND_CFG`. Actuator +regexes resolve on both assets, so the two backends share one actuator configuration, but terms +that need the joints in order use :obj:`SHADOW_ACTUATED_JOINT_NAMES_NEWTON` on this asset. +Gains default to the PhysX values; tasks override them as needed. """ @@ -210,3 +211,31 @@ These names resolve on both the PhysX and Newton assets, so every backend shares this list. """ + +SHADOW_ACTUATED_JOINT_NAMES_NEWTON: list[str] = [ + "robot0_WRJ1", + "robot0_WRJ0", + "robot0_FFJ4", + "robot0_FFJ3", + "robot0_FFJ2", + "robot0_MFJ4", + "robot0_MFJ3", + "robot0_MFJ2", + "robot0_RFJ4", + "robot0_RFJ3", + "robot0_RFJ2", + "robot0_LFJ5", + "robot0_LFJ4", + "robot0_LFJ3", + "robot0_LFJ2", + "robot0_THJ4", + "robot0_THJ3", + "robot0_THJ2", + "robot0_THJ1", + "robot0_THJ0", +] +"""Shadow Hand actuated joint names on the Newton asset, in the same order. + +The Newton USD numbers the finger joints one higher than :obj:`SHADOW_ACTUATED_JOINT_NAMES`, +so action terms that address joints by name need this list rather than the PhysX one. +""" diff --git a/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part08.major.rst b/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part08.major.rst new file mode 100644 index 000000000000..ab39ef560494 --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part08.major.rst @@ -0,0 +1,74 @@ +Added +^^^^^ + +* Added manager-based counterparts for the Shadow cube reorientation task and + its OpenAI FF/LSTM observation variants, alongside the existing Allegro + manager task. +* Added :class:`~isaaclab_tasks.core.reorient.mdp.reorient_timeout`, which + restarts the episode timer on every goal reach so OpenAI-variant episodes + extend across success streaks. +* Added ``enable_domain_randomization`` to the manager-based Allegro and Shadow + environment for turning off its startup randomization terms. +* Added Newton and OvPhysx physics presets to the manager-based reorientation + environments, selectable with ``physics=``. +* Added a Direct-versus-manager value-parity check covering timing, success + tolerance, fall distance, and the consecutive-success cap. + +Changed +^^^^^^^ + +* **Breaking:** Changed the manager-based reorientation tasks to report + ``Metrics/success_rate`` as a per-episode success bit, drawn at + ``ReorientCommandCfg.success_count_threshold`` like the Direct environments, + instead of a per-attempt ratio. Curves from earlier runs are not comparable. +* **Breaking:** Changed domain randomization to default off on the Allegro and + Shadow manager tasks, so they match their Direct counterparts, which randomize + nothing beyond the reset distributions. Set ``enable_domain_randomization`` on + the configuration to restore it. The OpenAI variants keep it enabled, matching + their Direct counterparts. +* **Breaking:** Changed the manager-based Allegro task to use the same RL agent + configurations as its Direct counterpart, so the two are comparable. The + ``rl_games_manager_ppo_cfg.yaml``, ``skrl_manager_ppo_cfg.yaml`` and + ``AllegroCubePPORunnerCfg`` entries are removed; use ``rl_games_ppo_cfg.yaml``, + ``skrl_ppo_cfg.yaml`` and ``AllegroHandPPORunnerCfg``. + +* **Breaking:** Changed the manager-based Allegro reorientation environment to + match the Direct observation, action, reset, and termination contracts. The + observation space changes size, so existing manager checkpoints cannot be + loaded and must be retrained. +* **Breaking:** Changed the Shadow Hand reorientation tasks to apply the same + randomization on every physics backend. PhysX now also randomizes joint gains, + object mass, and gravity, and Newton now also randomizes contact materials. + Policies trained before this change must be retrained. +* **Breaking:** Moved the Shadow Hand camera benchmark task to the contributed + tasks as ``IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct``. The + released ``Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct`` identifier no + longer resolves; use the contributed identifier instead. +* **Breaking:** Changed the in-hand cube to a rigid body on the Newton backend, + where it was previously declared as an articulation with no joints or + actuators. Code that resolved the object through + :class:`~isaaclab.assets.Articulation` must use + :class:`~isaaclab.assets.RigidObject`. +* Removed the ``clone_in_fabric`` settings from the reorientation scenes. The + flag no longer reaches the replicator, so the value had no effect. +* Renamed the per-robot scene constants to name what they hold: ``ROBOT_CFG`` + becomes ``SHADOW_HAND_ROBOT_CFG`` or ``ALLEGRO_HAND_ROBOT_CFG``, + ``OBJECT_CFG`` becomes ``CUBE_CFG``, and ``ObjectCfg`` becomes ``CubeCfg``. + +Removed +^^^^^^^ + +* Removed ``ReorientObjectEnvCfg`` and the shared reorientation observation, + action, and command configurations. Each manager task now declares its own; + derive from :class:`~isaaclab.envs.ManagerBasedRLEnvCfg` directly. +* Removed ``reorient_common``. Its constants are declared by the tasks that use + them, and the in-hand offset and goal-marker position are now per-robot fields + on the Direct configurations. + +Fixed +^^^^^ + +* Fixed the manager-based reorientation tasks not reporting + ``Metrics/success_rate``. +* Fixed manager ``Metrics/success_rate`` counting goal attempts rather than the + per-episode success bit the Direct tasks report. 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..e8397c2895ff --- /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 PhysX by default. The + RTX render modalities require Fabric cloning, which Newton does not support. + 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/contrib/reorient/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/__init__.py new file mode 100644 index 000000000000..26defb14af38 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/__init__.py @@ -0,0 +1,6 @@ +# 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 + +"""Contributed variants of the in-hand reorientation tasks.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/__init__.py new file mode 100644 index 000000000000..cf7a6f752621 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/__init__.py @@ -0,0 +1,6 @@ +# 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 + +"""Configurations for the contributed reorientation environments.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/shadow_hand/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/shadow_hand/__init__.py new file mode 100644 index 000000000000..ea3dc061aa17 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/shadow_hand/__init__.py @@ -0,0 +1,21 @@ +# 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 + +"""Shadow Hand rendering-throughput benchmark task.""" + +import gymnasium as gym + +from isaaclab_tasks.core.reorient.config.shadow_hand import agents + +gym.register( + id="IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct", + entry_point="isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_camera_env:ShadowHandCameraEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.shadow_hand_camera_benchmark_env_cfg:ShadowHandCameraBenchmarkEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandCameraFFPPORunnerCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_camera_cfg.yaml", + }, +) diff --git a/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/shadow_hand/shadow_hand_camera_benchmark_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/shadow_hand/shadow_hand_camera_benchmark_env_cfg.py new file mode 100644 index 000000000000..4b879f6d52cd --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/shadow_hand/shadow_hand_camera_benchmark_env_cfg.py @@ -0,0 +1,30 @@ +# 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 + +"""Rendering-throughput benchmark variant of the Shadow Hand camera task.""" + +from isaaclab.utils.configclass import configclass + +from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractorCfg +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_camera_env_cfg import ShadowHandCameraEnvCfg + + +@configclass +class ShadowHandCameraBenchmarkEnvCfg(ShadowHandCameraEnvCfg): + """Benchmark configuration with the feature extractor CNN disabled. + + The tiled camera renders frames each step as normal, but the CNN forward pass is + bypassed — zero embeddings are returned instead. This isolates rendering throughput + from CNN inference overhead when profiling. + + The renderer backend and camera data types can still be selected via ``presets``:: + + presets = newton_renderer # benchmark with Newton renderer + presets = ovrtx # benchmark with OVRTX renderer + presets = rgb # benchmark RGB rendering only + presets = depth, newton_renderer # benchmark depth rendering with Newton + """ + + feature_extractor: FeatureExtractorCfg = FeatureExtractorCfg(enabled=False) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py index 410f83ced7a3..444aae7a0aa3 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_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_common.py index a5bec600e21c..9e73e5a2039d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_common.py @@ -15,15 +15,21 @@ import isaaclab.sim as sim_utils from isaaclab.markers import VisualizationMarkersCfg +from isaaclab_tasks.utils.hydra import preset + from isaaclab_assets.robots.shadow_hand import ( SHADOW_ACTUATED_JOINT_NAMES as ACTUATED_JOINT_NAMES, ) +from isaaclab_assets.robots.shadow_hand import ( + SHADOW_ACTUATED_JOINT_NAMES_NEWTON, +) from isaaclab_assets.robots.shadow_hand import ( SHADOW_FINGERTIP_BODY_NAMES as FINGERTIP_BODY_NAMES, ) __all__ = [ "ACTUATED_JOINT_NAMES", + "ACTUATED_JOINT_NAMES_PRESET", "FINGERTIP_BODY_NAMES", "GOAL_MARKER_CFG", "GOAL_POSITION_OFFSET", @@ -31,6 +37,15 @@ ] +ACTUATED_JOINT_NAMES_PRESET = preset( + physx=ACTUATED_JOINT_NAMES, + newton_mjwarp=SHADOW_ACTUATED_JOINT_NAMES_NEWTON, + ovphysx=ACTUATED_JOINT_NAMES, + default=ACTUATED_JOINT_NAMES, +) +"""Per-backend actuated joint names, resolved by the physics preset key.""" + + OBJECT_RADIUS: float = 0.0335 """Hand-over object sphere radius [m], also used for the goal marker.""" 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 53aa2d740159..df622dd51440 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_ovphysx.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,105 +30,6 @@ 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], @@ -295,7 +193,7 @@ class HandoverEnvCfg(DirectMARLEnvCfg): observation_spaces = {"right_hand": 157, "left_hand": 157} state_space = 290 - # simulation — values mirrored by the manager cfg (guarded by the value-parity test) + # simulation — values mirrored by the manager cfg sim: SimulationCfg = SimulationCfg( dt=1 / 120, render_interval=decimation, 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..0cfc43c0ae76 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py @@ -0,0 +1,194 @@ +# 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 +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 +from isaaclab_tasks.core.handover.handover_common import ( + ACTUATED_JOINT_NAMES_PRESET, + FINGERTIP_BODY_NAMES, +) +from isaaclab_tasks.core.handover.handover_env_cfg import ( + LEFT_HAND_CFG, + RIGHT_HAND_CFG, + ObjectCfg, + 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: ObjectCfg = ObjectCfg() + 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_PRESET, + alpha=1.0, + rescale_to_limits=True, + ) + left_hand = mdp.EMAJointPositionToLimitsActionCfg( + asset_name="left_hand", + joint_names=ACTUATED_JOINT_NAMES_PRESET, + alpha=1.0, + rescale_to_limits=True, + ) + + +def _hand_entity(name: str) -> SceneEntityCfg: + return SceneEntityCfg(name, joint_names=".*") + + +def _fingertip_entity(name: str) -> SceneEntityCfg: + return SceneEntityCfg(name, body_names=FINGERTIP_BODY_NAMES) + + +@configclass +class ObservationsCfg: + """Single-agent observations matching the Direct MARL adapter.""" + + @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": _hand_entity("right_hand")}) + right_joint_vel = ObsTerm(func=mdp.joint_vel, scale=0.2, params={"asset_cfg": _hand_entity("right_hand")}) + right_fingertip_pos = ObsTerm(func=mdp.fingertip_pos, params={"asset_cfg": _fingertip_entity("right_hand")}) + right_fingertip_quat = ObsTerm(func=mdp.fingertip_quat, params={"asset_cfg": _fingertip_entity("right_hand")}) + right_fingertip_vel = ObsTerm(func=mdp.fingertip_vel, params={"asset_cfg": _fingertip_entity("right_hand")}) + right_action = ObsTerm(func=mdp.hand_action, params={"action_name": "right_hand"}) + right_object_goal = ObsTerm( + func=mdp.object_goal, + params={"command_name": "object_pose", "object_cfg": SceneEntityCfg("object"), "vel_obs_scale": 0.2}, + ) + + # 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": _hand_entity("left_hand")}) + left_joint_vel = ObsTerm(func=mdp.joint_vel, scale=0.2, params={"asset_cfg": _hand_entity("left_hand")}) + left_fingertip_pos = ObsTerm(func=mdp.fingertip_pos, params={"asset_cfg": _fingertip_entity("left_hand")}) + left_fingertip_quat = ObsTerm(func=mdp.fingertip_quat, params={"asset_cfg": _fingertip_entity("left_hand")}) + left_fingertip_vel = ObsTerm(func=mdp.fingertip_vel, params={"asset_cfg": _fingertip_entity("left_hand")}) + left_action = ObsTerm(func=mdp.hand_action, params={"action_name": "left_hand"}) + left_object_goal = ObsTerm( + func=mdp.object_goal, + params={"command_name": "object_pose", "object_cfg": SceneEntityCfg("object"), "vel_obs_scale": 0.2}, + ) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = True + + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventCfg: + """Reset distributions matching the Direct handover environment.""" + + reset_handover = EventTerm( + func=mdp.reset_handover_state, + mode="reset", + params={ + "position_noise": 0.01, + "joint_position_noise": 0.2, + "joint_velocity_noise": 0.0, + "action_names": ("right_hand", "left_hand"), + }, + ) + + +@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: EventCfg = EventCfg() + + 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..d7f37e3f573b 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,27 @@ # SPDX-License-Identifier: BSD-3-Clause __all__ = [ + "HandoverCommand", + "HandoverCommandCfg", + "reset_handover_state", + "fingertip_pos", + "fingertip_quat", + "fingertip_vel", + "hand_action", + "object_goal", + "handover_goal_distance_reward", "handover_reward", "evaluate_handover_success", ] -from .rewards import evaluate_handover_success, handover_reward +from .commands import HandoverCommand, HandoverCommandCfg +from .events import reset_handover_state +from .observations import ( + fingertip_pos, + fingertip_quat, + fingertip_vel, + hand_action, + object_goal, +) +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..dab1ac10c893 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py @@ -0,0 +1,107 @@ +# 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._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) + extras = super().reset(env_ids) + log = self._env.extras.setdefault("log", {}) + log["Metrics/success_rate"] = self._succeeded[env_ids].float().mean().item() + self._succeeded[env_ids] = False + 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/events.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/events.py new file mode 100644 index 000000000000..e3cc54a125a9 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/events.py @@ -0,0 +1,76 @@ +# 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 + +"""Reset events for the manager-based handover task.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.managers import SceneEntityCfg + +from isaaclab_tasks.core.reorient.mdp.events import random_xy_rotation, sample_joint_positions_within_limits + +if TYPE_CHECKING: + from isaaclab.assets import Articulation, RigidObject + from isaaclab.envs import ManagerBasedRLEnv + + +def reset_handover_state( + env: ManagerBasedRLEnv, + env_ids: Sequence[int], + position_noise: float, + joint_position_noise: float, + joint_velocity_noise: float, + action_names: tuple[str, ...], + right_hand_cfg: SceneEntityCfg = SceneEntityCfg("right_hand"), + left_hand_cfg: SceneEntityCfg = SceneEntityCfg("left_hand"), + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), +) -> None: + """Reset the object and both hands with the Direct task's distributions. + + Args: + env: Environment containing both hands and the object. + env_ids: Environment indices to reset. + position_noise: Object-position noise half-width [m]. + joint_position_noise: Scale applied to sampled joint-position deltas. + joint_velocity_noise: Joint-velocity noise half-width [rad/s]. + action_names: Action terms whose pre-reset raw actions are retained in reset observations. + right_hand_cfg: Right-hand scene entity. + left_hand_cfg: Left-hand scene entity. + object_cfg: Object scene entity. + """ + if not hasattr(env, "_handover_reset_actions"): + env._handover_reset_actions = {} + for action_name in action_names: + raw_action = env.action_manager.get_term(action_name).raw_actions + if action_name not in env._handover_reset_actions: + env._handover_reset_actions[action_name] = torch.zeros_like(raw_action) + env._handover_reset_actions[action_name][env_ids] = raw_action[env_ids] + + object_asset: RigidObject = env.scene[object_cfg.name] + object_pose = object_asset.data.default_root_pose.torch[env_ids].clone() + object_velocity = torch.zeros_like(object_asset.data.default_root_vel.torch[env_ids]) + position_delta = math_utils.sample_uniform(-1.0, 1.0, (len(env_ids), 3), device=env.device) + object_pose[:, :3] += position_noise * position_delta + env.scene.env_origins[env_ids] + object_pose[:, 3:7] = random_xy_rotation(len(env_ids), env.device) + object_asset.write_root_pose_to_sim_index(root_pose=object_pose, env_ids=env_ids) + object_asset.write_root_velocity_to_sim_index(root_velocity=object_velocity, env_ids=env_ids) + + for hand_cfg in (right_hand_cfg, left_hand_cfg): + hand: Articulation = env.scene[hand_cfg.name] + default_position = hand.data.default_joint_pos.torch[env_ids] + limits = hand.data.joint_limits.torch[env_ids] + joint_position = sample_joint_positions_within_limits(default_position, limits, joint_position_noise) + velocity_sample = math_utils.sample_uniform(-1.0, 1.0, (len(env_ids), hand.num_joints), device=env.device) + joint_velocity = hand.data.default_joint_vel.torch[env_ids] + joint_velocity_noise * velocity_sample + + hand.set_joint_position_target_index(target=joint_position, env_ids=env_ids) + hand.write_joint_position_to_sim_index(position=joint_position, env_ids=env_ids) + hand.write_joint_velocity_to_sim_index(velocity=joint_velocity, env_ids=env_ids) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/observations.py new file mode 100644 index 000000000000..5c1395e9df3b --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/observations.py @@ -0,0 +1,81 @@ +# 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 + +"""Observation terms for the manager-based handover task.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.managers import SceneEntityCfg + +# Handover reuses the reorientation fingertip observation terms verbatim. +from isaaclab_tasks.core.reorient.mdp.observations import ( # noqa: F401 + fingertip_pos, + fingertip_quat, + fingertip_vel, +) + +if TYPE_CHECKING: + from isaaclab.assets import RigidObject + from isaaclab.envs import ManagerBasedRLEnv + + +def hand_action(env: ManagerBasedRLEnv, action_name: str) -> torch.Tensor: + """Return one hand's Direct-compatible raw action across resets. + + Args: + env: Environment containing the action term and episode-length buffer. + action_name: Action term whose raw action is observed. + + Returns: + Current raw actions, retaining pre-reset actions while episode length is zero. + """ + raw_action = env.action_manager.get_term(action_name).raw_actions + reset_actions = getattr(env, "_handover_reset_actions", None) + episode_length_buf = getattr(env, "episode_length_buf", None) + if reset_actions is None or action_name not in reset_actions or episode_length_buf is None: + return raw_action + return torch.where((episode_length_buf == 0).unsqueeze(-1), reset_actions[action_name], raw_action) + + +def object_goal( + env: ManagerBasedRLEnv, command_name: str, object_cfg: SceneEntityCfg, vel_obs_scale: float +) -> torch.Tensor: + """Return the 24-dimensional object and handover-goal observation block. + + Position components use [m], linear velocities [m/s], angular velocities + [rad/s], and quaternion components are unitless. The angular-velocity + scale arrives as the ``vel_obs_scale`` term param, wired at declaration. + + Args: + env: Environment containing the object and goal command. + command_name: Goal command term name. + object_cfg: Object scene entity. + vel_obs_scale: Angular-velocity observation scale. + + Returns: + Object pose, spatial velocity, goal pose, and quaternion error, shape ``(num_envs, 24)``. + """ + object_asset: RigidObject = env.scene[object_cfg.name] + command_term = env.command_manager.get_term(command_name) + object_pos_e = object_asset.data.root_pos_w.torch - env.scene.env_origins + object_quat = object_asset.data.root_quat_w.torch + quat_error = math_utils.quat_mul(object_quat, math_utils.quat_conjugate(command_term.quat_command_w)) + return torch.cat( + ( + object_pos_e, + object_quat, + object_asset.data.root_lin_vel_w.torch, + vel_obs_scale * object_asset.data.root_ang_vel_w.torch, + command_term.pos_command_e, + command_term.quat_command_w, + quat_error, + ), + dim=-1, + ) 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/isaaclab_tasks/core/reorient/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/__init__.py index 81f1425e1bf8..b12e5252b837 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/__init__.py @@ -8,10 +8,9 @@ This package consolidates the direct-workflow and manager-based-workflow in-hand manipulation tasks, where a dexterous hand reorients an object to match a goal orientation. The shared direct base environment lives in -:mod:`~isaaclab_tasks.core.reorient.reorient_direct_env` and the shared manager-based -base configuration in :mod:`~isaaclab_tasks.core.reorient.reorient_manager_env_cfg`. -Robot-specific tasks are organized under the ``config`` subpackage -(``config/allegro_hand`` and ``config/shadow_hand``). +:mod:`~isaaclab_tasks.core.reorient.reorient_direct_env`; the manager-based +configurations live with their robot-specific tasks under the ``config`` +subpackage (``config/allegro_hand`` and ``config/shadow_hand``). These environments are based on the `dexterous cube manipulation`_ environments provided in IsaacGymEnvs repository from NVIDIA. However, they contain certain diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/__init__.py index 90fb41f30541..52c3862ae4b0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/__init__.py @@ -19,9 +19,9 @@ disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.allegro_hand_manager_env_cfg:AllegroCubeEnvCfg", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AllegroCubePPORunnerCfg", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_manager_ppo_cfg.yaml", - "skrl_cfg_entry_point": f"{agents.__name__}:skrl_manager_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AllegroHandPPORunnerCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", }, ) @@ -35,8 +35,8 @@ disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.allegro_hand_direct_env_cfg:AllegroHandEnvCfg", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_direct_ppo_cfg.yaml", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:AllegroHandPPORunnerCfg", - "skrl_cfg_entry_point": f"{agents.__name__}:skrl_direct_ppo_cfg.yaml", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", }, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rl_games_manager_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rl_games_manager_ppo_cfg.yaml deleted file mode 100644 index 2fa70902ab61..000000000000 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rl_games_manager_ppo_cfg.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# 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 - -params: - seed: 42 - - # environment wrapper clipping - env: - clip_observations: 5.0 - clip_actions: 1.0 - - algo: - name: a2c_continuous - - model: - name: continuous_a2c_logstd - - network: - name: actor_critic - separate: False - - space: - continuous: - mu_activation: None - sigma_activation: None - mu_init: - name: default - sigma_init: - name: const_initializer - val: 0 - fixed_sigma: True - - mlp: - units: [512, 256, 128] - activation: elu - d2rl: False - - initializer: - name: default - regularizer: - name: None - - load_checkpoint: False - load_path: '' - - config: - name: allegro_cube - env_name: rlgpu - device: 'cuda:0' - device_name: 'cuda:0' - multi_gpu: False - ppo: True - mixed_precision: False - normalize_input: True - normalize_value: True - value_bootstrap: True - num_actors: -1 # configured from the script (based on num_envs) - reward_shaper: - scale_value: 0.1 - normalize_advantage: True - gamma: 0.998 - tau: 0.95 - learning_rate: 5e-4 - lr_schedule: adaptive - schedule_type: standard - kl_threshold: 0.016 - score_to_win: 100000 - max_epochs: 5000 - save_best_after: 500 - save_frequency: 200 - print_stats: True - grad_norm: 1.0 - entropy_coef: 0.002 - truncate_grads: True - e_clip: 0.2 - horizon_length: 24 - minibatch_size: 16384 # 32768 - mini_epochs: 5 - critic_coef: 4 - clip_value: True - seq_length: 4 - bounds_loss_coef: 0.0005 - - player: - #render: True - deterministic: True - games_num: 100000 - print_stats: True diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rl_games_direct_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rl_games_ppo_cfg.yaml similarity index 100% rename from source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rl_games_direct_ppo_cfg.yaml rename to source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rl_games_ppo_cfg.yaml diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rsl_rl_ppo_cfg.py index bec125790b57..784a18239f6e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rsl_rl_ppo_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rsl_rl_ppo_cfg.py @@ -8,39 +8,6 @@ from isaaclab_rl.rsl_rl import RslRlMLPModelCfg, RslRlOnPolicyRunnerCfg, RslRlPpoAlgorithmCfg -@configclass -class AllegroCubePPORunnerCfg(RslRlOnPolicyRunnerCfg): - num_steps_per_env = 24 - max_iterations = 5000 - save_interval = 50 - experiment_name = "allegro_cube" - actor = RslRlMLPModelCfg( - hidden_dims=[512, 256, 128], - activation="elu", - obs_normalization=True, - distribution_cfg=RslRlMLPModelCfg.GaussianDistributionCfg(init_std=1.0), - ) - critic = RslRlMLPModelCfg( - hidden_dims=[512, 256, 128], - activation="elu", - obs_normalization=True, - ) - algorithm = RslRlPpoAlgorithmCfg( - value_loss_coef=1.0, - use_clipped_value_loss=True, - clip_param=0.2, - entropy_coef=0.002, - num_learning_epochs=5, - num_mini_batches=4, - learning_rate=0.001, - schedule="adaptive", - gamma=0.998, - lam=0.95, - desired_kl=0.01, - max_grad_norm=1.0, - ) - - @configclass class AllegroHandPPORunnerCfg(RslRlOnPolicyRunnerCfg): num_steps_per_env = 16 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/skrl_manager_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/skrl_manager_ppo_cfg.yaml deleted file mode 100644 index f61e7f50132d..000000000000 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/skrl_manager_ppo_cfg.yaml +++ /dev/null @@ -1,85 +0,0 @@ -# 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 - -seed: 42 - - -# Models are instantiated using skrl's model instantiator utility -# https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html -models: - separate: False - policy: # see gaussian_model parameters - class: GaussianMixin - clip_actions: False - clip_log_std: True - min_log_std: -20.0 - max_log_std: 2.0 - initial_log_std: 0.0 - network: - - name: net - input: OBSERVATIONS - layers: [512, 256, 128] - activations: elu - output: ACTIONS - value: # see deterministic_model parameters - class: DeterministicMixin - clip_actions: False - network: - - name: net - input: OBSERVATIONS - layers: [512, 256, 128] - activations: elu - output: ONE - - -# Rollout memory -# https://skrl.readthedocs.io/en/latest/api/memories/random.html -memory: - class: RandomMemory - memory_size: -1 # automatically determined (same as agent:rollouts) - - -# PPO agent configuration (field names are from PPO_DEFAULT_CONFIG) -# https://skrl.readthedocs.io/en/latest/api/agents/ppo.html -agent: - class: PPO - rollouts: 24 - learning_epochs: 5 - mini_batches: 12 - discount_factor: 0.998 - lambda: 0.95 - learning_rate: 5.0e-04 - learning_rate_scheduler: KLAdaptiveLR - learning_rate_scheduler_kwargs: - kl_threshold: 0.016 - state_preprocessor: RunningStandardScaler - state_preprocessor_kwargs: null - value_preprocessor: RunningStandardScaler - value_preprocessor_kwargs: null - random_timesteps: 0 - learning_starts: 0 - grad_norm_clip: 1.0 - ratio_clip: 0.2 - value_clip: 0.2 - clip_predicted_values: True - entropy_loss_scale: 0.002 - value_loss_scale: 2.0 - kl_threshold: 0.0 - rewards_shaper_scale: 0.1 - time_limit_bootstrap: False - # logging and checkpoint - experiment: - directory: "allegro_cube" - experiment_name: "" - write_interval: auto - checkpoint_interval: auto - - -# Sequential trainer -# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html -trainer: - class: SequentialTrainer - timesteps: 120000 - environment_info: log diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/skrl_direct_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/skrl_ppo_cfg.yaml similarity index 100% rename from source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/skrl_direct_ppo_cfg.yaml rename to source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/skrl_ppo_cfg.yaml diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py index 2605d401c76e..ef6c443d9828 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py @@ -14,7 +14,7 @@ from isaaclab_physx.physics import PhysxCfg import isaaclab.sim as sim_utils -from isaaclab.assets import ArticulationCfg, RigidObjectCfg +from isaaclab.assets import RigidObjectCfg from isaaclab.markers import VisualizationMarkersCfg from isaaclab.physics import PhysxAutoCfg from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR @@ -24,9 +24,11 @@ from isaaclab_assets.robots.allegro import ALLEGRO_HAND_CFG +ALLEGRO_HAND_ROBOT_CFG = ALLEGRO_HAND_CFG.replace(prim_path="/World/envs/env_.*/Robot") + @configclass -class ObjectCfg(PresetCfg): +class CubeCfg(PresetCfg): physx = RigidObjectCfg( prim_path="/World/envs/env_.*/object", spawn=sim_utils.UsdFileCfg( @@ -46,18 +48,14 @@ class ObjectCfg(PresetCfg): ), init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.17, 0.56), rot=(0.0, 0.0, 0.0, 1.0)), ) - newton_mjwarp = ArticulationCfg( + newton_mjwarp = RigidObjectCfg( prim_path="/World/envs/env_.*/object", spawn=sim_utils.UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", mass_props=sim_utils.MassPropertiesCfg(density=400.0), scale=(1.2, 1.2, 1.2), ), - init_state=ArticulationCfg.InitialStateCfg( - pos=(0.0, -0.17, 0.565), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} - ), - actuators={}, - articulation_root_prim_path="", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.17, 0.565), rot=(0.0, 0.0, 0.0, 1.0)), ) ovphysx = RigidObjectCfg( prim_path="/World/envs/env_.*/object", @@ -103,9 +101,6 @@ class PhysicsCfg(PresetCfg): default = newton_mjwarp -# Scene pieces shared verbatim by the manager-based variant. -ROBOT_CFG = ALLEGRO_HAND_CFG.replace(prim_path="/World/envs/env_.*/Robot") -OBJECT_CFG = ObjectCfg() GOAL_OBJECT_CFG = VisualizationMarkersCfg( prim_path="/Visuals/goal_marker", markers={ diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py index 66c9ab696fb8..3562e6f55e1d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_direct_env_cfg.py @@ -12,10 +12,9 @@ from isaaclab.utils.configclass import configclass from isaaclab_tasks.core.reorient.config.allegro_hand.allegro_hand_common import ( + ALLEGRO_HAND_ROBOT_CFG, GOAL_OBJECT_CFG, - OBJECT_CFG, - ROBOT_CFG, - ObjectCfg, + CubeCfg, PhysicsCfg, ) @@ -36,18 +35,18 @@ class AllegroHandEnvCfg(DirectRLEnvCfg): # simulation — values mirrored by the manager cfg (guarded by the value-parity test) sim: SimulationCfg = SimulationCfg( dt=1 / 120, - render_interval=4, + render_interval=decimation, physics_material=RigidBodyMaterialBaseCfg(static_friction=1.0, dynamic_friction=1.0), physics=PhysicsCfg(), ) # robot - robot_cfg: ArticulationCfg = ROBOT_CFG + robot_cfg: ArticulationCfg = ALLEGRO_HAND_ROBOT_CFG actuated_joint_names = ALLEGRO_ACTUATED_JOINT_NAMES fingertip_body_names = ALLEGRO_FINGERTIP_BODY_NAMES # in-hand object - object_cfg: ObjectCfg = OBJECT_CFG + object_cfg: CubeCfg = CubeCfg() # goal object goal_object_cfg: VisualizationMarkersCfg = GOAL_OBJECT_CFG # scene @@ -55,7 +54,6 @@ class AllegroHandEnvCfg(DirectRLEnvCfg): num_envs=8192, env_spacing=0.75, replicate_physics=True, - clone_in_fabric=True, ) # reset reset_position_noise = 0.01 # range of position at reset @@ -74,6 +72,10 @@ class AllegroHandEnvCfg(DirectRLEnvCfg): max_consecutive_success = 0 success_count_threshold: int = 1 """Minimum number of goals reached in an episode to count it as a successful episode.""" + in_hand_pos_offset: tuple[float, float, float] = (0.0, 0.0, -0.04) + """In-hand goal anchor, relative to the object's default position [m].""" + goal_marker_position: tuple[float, float, float] = (-0.2, -0.45, 0.68) + """Fixed goal-marker display position [m], environment frame.""" av_factor = 0.1 act_moving_average = 1.0 force_torque_obs_scale = 10.0 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py index 9615cd32d6a1..c03c2c0cbf9f 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py @@ -3,30 +3,285 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Manager-based counterpart of the Allegro Hand Direct reorientation task.""" + +import isaaclab.sim as sim_utils +from isaaclab.assets import ArticulationCfg, AssetBaseCfg +from isaaclab.envs import ManagerBasedRLEnvCfg, ViewerCfg +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.simulation_cfg import SimulationCfg +from isaaclab.sim.spawners.materials import RigidBodyMaterialBaseCfg from isaaclab.utils.configclass import configclass -from isaaclab_tasks.core.reorient.reorient_manager_env_cfg import ReorientObjectEnvCfg +import isaaclab_tasks.core.reorient.mdp as mdp +from isaaclab_tasks.core.reorient.config.allegro_hand.allegro_hand_common import ( + ALLEGRO_HAND_ROBOT_CFG, + GOAL_OBJECT_CFG, + CubeCfg, + PhysicsCfg, +) -## -# Pre-defined configs -## -from isaaclab_assets import ALLEGRO_HAND_CFG # isort: skip +from isaaclab_assets.robots.allegro import ALLEGRO_ACTUATED_JOINT_NAMES, ALLEGRO_FINGERTIP_BODY_NAMES @configclass -class AllegroCubeEnvCfg(ReorientObjectEnvCfg): - def __post_init__(self): - # post init of parent - super().__post_init__() +class AllegroCubeSceneCfg(InteractiveSceneCfg): + """Shared reorientation scene with the Allegro hand and a ground plane.""" + + num_envs = 8192 + env_spacing = 0.75 + + robot: ArticulationCfg = ALLEGRO_HAND_ROBOT_CFG + object: CubeCfg = CubeCfg() + ground = AssetBaseCfg(prim_path="/World/ground", spawn=sim_utils.GroundPlaneCfg()) + light = AssetBaseCfg( + prim_path="/World/Light", + spawn=sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)), + ) + dome_light = None + + +@configclass +class CommandsCfg: + """Object pose goal matching the Direct in-hand target.""" + + object_pose = mdp.ReorientCommandCfg( + asset_name="object", + init_pos_offset=(0.0, 0.0, -0.04), + update_goal_on_success=True, + orientation_success_threshold=0.2, + make_quat_unique=False, + fixed_marker_pos=(-0.2, -0.45, 0.68), + goal_pose_visualizer_cfg=GOAL_OBJECT_CFG, + debug_vis=True, + ) + + +@configclass +class ActionsCfg: + """Sixteen actuated Allegro Hand joints in Direct order.""" + + joint_pos = mdp.EMAJointPositionToLimitsActionCfg( + asset_name="robot", + joint_names=ALLEGRO_ACTUATED_JOINT_NAMES, + alpha=1.0, + rescale_to_limits=True, + ) + + +@configclass +class ObservationsCfg: + """Full 124-dimensional state observation in Direct order.""" - # switch robot to allegro hand - self.scene.robot = ALLEGRO_HAND_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") - # enable clone in fabric - self.scene.clone_in_fabric = True + @configclass + class PolicyCfg(ObsGroup): + # -- robot + joint_pos = ObsTerm( + func=mdp.joint_pos_limit_normalized, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=False)}, + ) + joint_vel = ObsTerm( + func=mdp.joint_vel, + scale=0.2, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=False)}, + ) + # -- object + 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"), "make_quat_unique": False}, + ) + 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")}, + ) + # -- command + goal_pose = ObsTerm(func=mdp.generated_commands, params={"command_name": "object_pose"}) + goal_quat_diff = ObsTerm( + func=mdp.goal_quat_diff, + params={"asset_cfg": SceneEntityCfg("object"), "command_name": "object_pose", "make_quat_unique": False}, + ) + # -- robot fingertips + fingertip_pos = ObsTerm( + func=mdp.fingertip_pos, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=ALLEGRO_FINGERTIP_BODY_NAMES, preserve_order=False) + }, + ) + fingertip_quat = ObsTerm( + func=mdp.fingertip_quat, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=ALLEGRO_FINGERTIP_BODY_NAMES, preserve_order=False) + }, + ) + fingertip_vel = ObsTerm( + func=mdp.fingertip_vel, + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=ALLEGRO_FINGERTIP_BODY_NAMES, preserve_order=False) + }, + ) + last_action = ObsTerm(func=mdp.reorient_last_action, params={"action_name": "joint_pos"}) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = True + + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventCfg: + """Shared randomization terms with the Direct task's reset distribution. - def play_mode(self): - # play-mode overrides of parent - super().play_mode() + Gated by :attr:`AllegroCubeEnvCfg.enable_domain_randomization`. + """ - # remove termination due to timeouts - self.terminations.time_out = None + robot_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*"), + "static_friction_range": (0.7, 1.3), + "dynamic_friction_range": (0.7, 1.3), + "restitution_range": (0.0, 0.0), + "num_buckets": 250, + }, + ) + robot_scale_mass = EventTerm( + func=mdp.randomize_rigid_body_mass, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*"), + "mass_distribution_params": (0.95, 1.05), + "operation": "scale", + }, + ) + robot_joint_stiffness_and_damping = EventTerm( + func=mdp.randomize_actuator_gains, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*"), + "stiffness_distribution_params": (0.3, 3.0), # default: 3.0 + "damping_distribution_params": (0.75, 1.5), # default: 0.1 + "operation": "scale", + "distribution": "log_uniform", + }, + ) + + # -- object + object_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("object", body_names=".*"), + "static_friction_range": (0.7, 1.3), + "dynamic_friction_range": (0.7, 1.3), + "restitution_range": (0.0, 0.0), + "num_buckets": 250, + }, + ) + object_scale_mass = EventTerm( + func=mdp.randomize_rigid_body_mass, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("object"), + "mass_distribution_params": (0.4, 1.6), + "operation": "scale", + }, + ) + + reset_state = EventTerm( + func=mdp.reset_reorient_state, + mode="reset", + params={ + "position_noise": 0.01, + "joint_position_noise": 0.2, + "joint_velocity_noise": 0.0, + "action_name": "joint_pos", + }, + ) + + +@configclass +class RewardsCfg: + """Shared reward terms tuned to the Direct task's scales.""" + + track_orientation_inv_l2 = RewTerm( + func=mdp.track_orientation_inv_l2, + weight=1.0, + params={"object_cfg": SceneEntityCfg("object"), "rot_eps": 0.1, "command_name": "object_pose"}, + ) + success_bonus = RewTerm( + func=mdp.success_bonus, + weight=250.0, + params={"object_cfg": SceneEntityCfg("object"), "command_name": "object_pose"}, + ) + track_pos_l2 = RewTerm( + func=mdp.track_pos_l2, + weight=-10.0, + params={"command_name": "object_pose", "object_cfg": SceneEntityCfg("object")}, + ) + action_l2 = RewTerm(func=mdp.action_l2, weight=-0.0002) + + +@configclass +class TerminationsCfg: + """Shared terminations reduced to the Direct task's fall condition.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + object_out_of_reach = DoneTerm( + func=mdp.object_away_from_goal, + params={ + "threshold": 0.24, + "command_name": "object_pose", + "object_cfg": SceneEntityCfg("object"), + }, + ) + + +@configclass +class AllegroCubeEnvCfg(ManagerBasedRLEnvCfg): + """Manager-based Allegro Hand task with Direct-compatible semantics.""" + + scene: AllegroCubeSceneCfg = AllegroCubeSceneCfg() + decimation = 4 + episode_length_s = 10.0 + sim: SimulationCfg = SimulationCfg( + dt=1 / 120, + render_interval=decimation, + physics_material=RigidBodyMaterialBaseCfg(static_friction=1.0, dynamic_friction=1.0), + physics=PhysicsCfg(), + ) + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() + + enable_domain_randomization: bool = False + """Apply the domain-randomization event terms. + + Off by default so the task matches its Direct counterpart, which randomizes nothing beyond + the reset distributions. ``__post_init__`` reads it while building the configuration, before + Hydra applies command-line overrides, so ``env.enable_domain_randomization=true`` has no + effect -- set it on the configuration. Changing it requires retraining. + """ + + viewer: ViewerCfg = ViewerCfg(eye=(2.0, 2.0, 2.0)) + + def __post_init__(self): + if not self.enable_domain_randomization: + self.events.robot_physics_material = None + self.events.robot_scale_mass = None + self.events.robot_joint_stiffness_and_damping = None + self.events.object_physics_material = None + self.events.object_scale_mass = None diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/__init__.py index 95a6cc7e599d..1d4d101b53fc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/__init__.py @@ -17,6 +17,29 @@ reorient_direct_entry = "isaaclab_tasks.core.reorient.reorient_direct_env:ReorientDirectEnv" +gym.register( + id="Isaac-Reorient-Cube-Shadow", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.shadow_hand_manager_env_cfg:ShadowHandManagerEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandPPORunnerCfg", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Reorient-Cube-Shadow-Camera-Direct", + entry_point=f"{__name__}.shadow_hand_direct_camera_env:ShadowHandCameraEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.shadow_hand_direct_camera_env_cfg:ShadowHandCameraEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandCameraFFPPORunnerCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_camera_cfg.yaml", + }, +) + gym.register( id="Isaac-Reorient-Cube-Shadow-Direct", entry_point=reorient_direct_entry, @@ -29,6 +52,18 @@ }, ) +gym.register( + id="Isaac-Reorient-Cube-Shadow-OpenAI-FF", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.shadow_hand_openai_manager_env_cfg:ShadowHandOpenAIManagerEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_ff_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandAsymFFPPORunnerCfg", + "skrl_cfg_entry_point": f"{agents.__name__}:skrl_ff_ppo_cfg.yaml", + }, +) + gym.register( id="Isaac-Reorient-Cube-Shadow-OpenAI-FF-Direct", entry_point=reorient_direct_entry, @@ -42,37 +77,33 @@ ) gym.register( - id="Isaac-Reorient-Cube-Shadow-OpenAI-LSTM-Direct", - entry_point=reorient_direct_entry, + id="Isaac-Reorient-Cube-Shadow-OpenAI-LSTM", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.shadow_hand_direct_env_cfg:ShadowHandOpenAIEnvCfg", + "env_cfg_entry_point": f"{__name__}.shadow_hand_openai_manager_env_cfg:ShadowHandOpenAIManagerEnvCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_lstm_cfg.yaml", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandAsymLSTMPPORunnerCfg", }, ) -# ------- -# Vision -# ------- - gym.register( - id="Isaac-Reorient-Cube-Shadow-Camera-Direct", - entry_point=f"{__name__}.shadow_hand_direct_camera_env:ShadowHandCameraEnv", + id="Isaac-Reorient-Cube-Shadow-OpenAI-LSTM-Direct", + entry_point=reorient_direct_entry, disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.shadow_hand_direct_camera_env_cfg:ShadowHandCameraEnvCfg", - "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandCameraFFPPORunnerCfg", - "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_camera_cfg.yaml", + "env_cfg_entry_point": f"{__name__}.shadow_hand_direct_env_cfg:ShadowHandOpenAIEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_lstm_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandAsymLSTMPPORunnerCfg", }, ) gym.register( - id="Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct", - entry_point=f"{__name__}.shadow_hand_direct_camera_env:ShadowHandCameraEnv", + id="Isaac-Reorient-Cube-Shadow-Camera", + entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={ - "env_cfg_entry_point": f"{__name__}.shadow_hand_direct_camera_env_cfg:ShadowHandCameraBenchmarkEnvCfg", + "env_cfg_entry_point": f"{__name__}.shadow_hand_camera_manager_env_cfg:ShadowHandCameraManagerEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandCameraFFPPORunnerCfg", "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_camera_cfg.yaml", }, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py index 56c159a1446e..7c14cfe93984 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py @@ -3,8 +3,11 @@ # # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + import glob import os +from typing import TYPE_CHECKING import torch import torch.nn as nn @@ -13,6 +16,11 @@ from isaaclab.sensors import save_images_to_file from isaaclab.utils.configclass import configclass +# re-exported for backward compatibility; the shared implementation lives in the family math root + +if TYPE_CHECKING: + pass + # Number of output channels for each supported camera data type. _DATA_TYPE_CHANNELS: dict[str, int] = { "rgb": 3, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py new file mode 100644 index 000000000000..c418c0cf407d --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py @@ -0,0 +1,121 @@ +# 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 camera reorientation task.""" + +from isaaclab.managers import ObservationTermCfg as ObsTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.sensors import JointWrenchSensorCfg +from isaaclab.utils.configclass import configclass + +import isaaclab_tasks.core.reorient.mdp as mdp +from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractorCfg +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_camera_env_cfg import ( + ShadowHandTiledCameraCfg, + validate_shadow_hand_camera_settings, +) +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_manager_env_cfg import ( + FullStateObsCfg, + ShadowHandManagerEnvCfg, + ShadowHandManagerSceneCfg, +) + +from isaaclab_assets.robots.shadow_hand import SHADOW_FINGERTIP_BODY_NAMES + + +@configclass +class ShadowHandCameraManagerSceneCfg(ShadowHandManagerSceneCfg): + """State Manager scene augmented with camera and fingertip-wrench sensors.""" + + num_envs = 1225 + env_spacing = 2.0 + + ground = None + tiled_camera: ShadowHandTiledCameraCfg = ShadowHandTiledCameraCfg() + joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot") + + +@configclass +class CameraPolicyCfg(FullStateObsCfg): + """Direct-compatible 191-dimensional camera actor observation.""" + + last_action = ObsTerm(func=mdp.reorient_last_action, params={"action_name": "joint_pos"}) + camera_features = ObsTerm( + func=mdp.ShadowHandCameraFeatures, + params={ + "feature_extractor_cfg": FeatureExtractorCfg(), + "sensor_cfg": SceneEntityCfg("tiled_camera"), + "object_cfg": SceneEntityCfg("object"), + }, + ) + goal_keypoints = ObsTerm(func=mdp.shadow_hand_goal_keypoints, params={"command_name": "object_pose"}) + + def __post_init__(self): + super().__post_init__() + # Camera actor observations infer object state from pixels. These five + # privileged state terms are present only in the critic. + self.object_pos = None + self.object_quat = None + self.object_lin_vel = None + self.object_ang_vel = None + self.goal_quat_diff = None + + +@configclass +class CameraCriticCfg(FullStateObsCfg): + """Direct-compatible 214-dimensional asymmetric camera critic state.""" + + fingertip_wrench = ObsTerm( + func=mdp.fingertip_wrench, + scale=10.0, + params={ + "sensor_cfg": SceneEntityCfg("joint_wrench", body_names=SHADOW_FINGERTIP_BODY_NAMES, preserve_order=False) + }, + ) + last_action = ObsTerm(func=mdp.reorient_last_action, params={"action_name": "joint_pos"}) + camera_features = ObsTerm(func=mdp.shadow_hand_camera_cached_features) + + +@configclass +class CameraObservationsCfg: + """Camera actor and asymmetric critic observation groups.""" + + policy: CameraPolicyCfg = CameraPolicyCfg() + critic: CameraCriticCfg = CameraCriticCfg() + + +@configclass +class ShadowHandCameraManagerEnvCfg(ShadowHandManagerEnvCfg): + """Manager-based camera task with exact Direct dynamics and observations.""" + + # only the fields that differ from ShadowHandManagerEnvCfg are overridden + scene: ShadowHandCameraManagerSceneCfg = ShadowHandCameraManagerSceneCfg() + observations: CameraObservationsCfg = CameraObservationsCfg() + feature_extractor: FeatureExtractorCfg = FeatureExtractorCfg() + + def __post_init__(self): + super().__post_init__() + # camera tasks display the goal inside the tiled camera's frustum + # goal cube must sit inside the tiled camera's frustum + self.commands.object_pose.fixed_marker_pos = (-0.2, 0.1, 0.6) + self.observations.policy.camera_features.params["feature_extractor_cfg"] = self.feature_extractor + # The RTX modalities need Fabric cloning, which Newton does not support, so the + # camera task renders out of the box on PhysX; Newton stays selectable through + # ``physics=newton_mjwarp`` for the state-only observation groups. + for backend_cfg in (self.sim.physics, self.scene.robot, self.scene.object): + backend_cfg.default = backend_cfg.physx + + def validate_config(self): + """Check the camera pipeline against the feature extractor it feeds.""" + validate_shadow_hand_camera_settings(self.scene.tiled_camera, self.feature_extractor) + + def play_mode(self): + super().play_mode() + # the tiled camera needs more environments than the shared play default + self.scene.num_envs = 64 + # mutate rather than replace: subclasses may have disabled the CNN + self.feature_extractor.train = False + self.feature_extractor.load_checkpoint = True + self.observations.policy.camera_features.params["feature_extractor_cfg"] = self.feature_extractor diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py index fa0d6c3fbe02..619ebbe230c3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py @@ -25,6 +25,7 @@ from isaaclab.utils.configclass import configclass from isaaclab.utils.noise import GaussianNoiseCfg, NoiseModelWithAdditiveBiasCfg +import isaaclab_tasks.core.reorient.mdp as reorient_mdp from isaaclab_tasks.utils import PresetCfg from isaaclab_assets.robots.shadow_hand import ( @@ -34,13 +35,8 @@ @configclass -class NewtonEventCfg: - """Event randomization config for the Newton physics backend. - - Includes joint-parameter, mass, and gravity randomization. - Material and tendon randomization are omitted: Newton does not expose - per-body friction-material buckets or fixed-tendon APIs. - """ +class ShadowHandEventCfg: + """Randomization of the hand and the object, applied on every physics backend.""" robot_joint_stiffness_and_damping = EventTerm( func=mdp.randomize_actuator_gains, @@ -93,10 +89,6 @@ class NewtonEventCfg: }, ) - -@configclass -class PhysxEventCfg: - # -- robot robot_physics_material = EventTerm( func=mdp.randomize_rigid_body_material, mode="reset", @@ -109,20 +101,7 @@ class PhysxEventCfg: "num_buckets": 250, }, ) - robot_tendon_properties = EventTerm( - func=mdp.randomize_fixed_tendon_parameters, - min_step_count_between_reset=720, - mode="reset", - params={ - "asset_cfg": SceneEntityCfg("robot", 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, @@ -138,13 +117,19 @@ class PhysxEventCfg: @configclass -class ShadowHandEventCfg(PresetCfg): - physx = PhysxEventCfg() - isaacsim_physx = physx - newton_mjwarp = NewtonEventCfg() - ovphysx = physx # OvPhysX is PhysX-based; reuse the PhysX randomization terms - default = newton_mjwarp - newton_kamino = newton_mjwarp +class ShadowHandManagerEventCfg(ShadowHandEventCfg): + """Randomization plus the state reset the manager tasks apply on every episode.""" + + reset_state = EventTerm( + func=reorient_mdp.reset_reorient_state, + mode="reset", + params={ + "position_noise": 0.01, # [m] + "joint_position_noise": 0.2, # [rad] + "joint_velocity_noise": 0.0, # [rad/s] + "action_name": "joint_pos", + }, + ) @configclass @@ -176,7 +161,7 @@ class ShadowHandRobotCfg(PresetCfg): @configclass -class ObjectCfg(PresetCfg): +class CubeCfg(PresetCfg): physx = RigidObjectCfg( prim_path="/World/envs/env_.*/object", spawn=sim_utils.UsdFileCfg( @@ -198,7 +183,7 @@ class ObjectCfg(PresetCfg): ) isaacsim_physx = physx - newton_mjwarp = ArticulationCfg( + newton_mjwarp = RigidObjectCfg( prim_path="/World/envs/env_.*/object", spawn=sim_utils.UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", @@ -206,13 +191,9 @@ class ObjectCfg(PresetCfg): semantic_tags=[("class", "cube")], scale=(0.9, 0.9, 0.9), ), - init_state=ArticulationCfg.InitialStateCfg( - pos=(0.0, -0.36, 0.535), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} - ), - actuators={}, - articulation_root_prim_path="", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.36, 0.535), rot=(0.0, 0.0, 0.0, 1.0)), ) - ovphysx = physx # OvPhysX is PhysX-based; use the rigid-body cube, not Newton's articulation + ovphysx = physx # OvPhysX is PhysX-based; it keeps the PhysX cube's density and pose. default = newton_mjwarp newton_kamino = newton_mjwarp @@ -242,8 +223,8 @@ class PhysicsCfg(PresetCfg): # Scene pieces shared verbatim by the manager-based variants. -ROBOT_CFG = ShadowHandRobotCfg() -OBJECT_CFG = ObjectCfg() +SHADOW_HAND_ROBOT_CFG = ShadowHandRobotCfg() +CUBE_CFG = CubeCfg() GOAL_OBJECT_CFG = VisualizationMarkersCfg( prim_path="/Visuals/goal_marker", diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py index d424b46400e5..7329009fef86 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py @@ -6,6 +6,7 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING import torch @@ -18,7 +19,6 @@ from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractor from isaaclab_tasks.core.reorient.mdp.observations import compute_cube_keypoints -from isaaclab_tasks.core.reorient.reorient_common import CAMERA_GOAL_MARKER_POSITION from isaaclab_tasks.core.reorient.reorient_direct_env import ReorientDirectEnv if TYPE_CHECKING: @@ -42,7 +42,7 @@ def __init__(self, cfg: ShadowHandCameraEnvCfg, render_mode: str | None = None, width=self.cfg.tiled_camera.width, ) # hide goal cubes - self.goal_pos[:, :] = torch.tensor(CAMERA_GOAL_MARKER_POSITION, device=self.device) + self.goal_pos[:, :] = torch.tensor((-0.2, 0.1, 0.6), device=self.device) # inside the tiled camera frustum # keypoints buffer self.gt_keypoints = torch.ones(self.num_envs, 8, 3, dtype=torch.float32, device=self.device) self.goal_keypoints = torch.ones(self.num_envs, 8, 3, dtype=torch.float32, device=self.device) @@ -138,3 +138,31 @@ def _get_observations(self) -> dict: observations = {"policy": obs, "critic": state} return observations + + +def compute_keypoints( + pose: torch.Tensor, + num_keypoints: int = 8, + size: tuple[float, float, float] = (2 * 0.03, 2 * 0.03, 2 * 0.03), + out: torch.Tensor | None = None, +) -> torch.Tensor: + """Compute cube keypoints using the shared implementation. + + .. deprecated:: 9.0.0 + Use :func:`compute_cube_keypoints` instead. + + Args: + pose: Cube center poses ``(x, y, z, qx, qy, qz, qw)`` [m, unit quaternion]. + num_keypoints: Number of binary-sign corners to compute. + size: Cube side lengths along each axis [m]. + out: Optional output buffer [m], shape ``(num_envs, num_keypoints, 3)``. + + Returns: + Cube-corner positions [m], shape ``(num_envs, num_keypoints, 3)``. + """ + warnings.warn( + "compute_keypoints() is deprecated; use compute_cube_keypoints() instead.", + DeprecationWarning, + stacklevel=2, + ) + return compute_cube_keypoints(pose, num_keypoints=num_keypoints, size=size, out=out) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py index 19292a97429f..4dcbf53189f9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py @@ -16,12 +16,55 @@ from isaaclab_tasks.utils.presets import MultiBackendRendererCfg +def validate_shadow_hand_camera_settings( + tiled_camera: CameraCfg | ShadowHandTiledCameraCfg, + feature_extractor: FeatureExtractorCfg, +) -> None: + """Validate one resolved or defaulted Shadow Hand camera pipeline.""" + while isinstance(tiled_camera, PresetCfg): + tiled_camera = tiled_camera.default + renderer_cfg = tiled_camera.renderer_cfg + while isinstance(renderer_cfg, PresetCfg): + renderer_cfg = renderer_cfg.default + + renderer_type = getattr(renderer_cfg, "renderer_type", None) + warp_supported = { + "rgb", + "depth", + "distance_to_camera", + "distance_to_image_plane", + "normals", + "semantic_segmentation", + "instance_segmentation", + } + if renderer_type == "newton_warp": + unsupported = set(tiled_camera.data_types) - warp_supported + if unsupported: + raise ValueError( + f"Warp renderer only supports data types {sorted(warp_supported)}, " + f"but the camera is configured with unsupported types: {sorted(unsupported)}. " + "Choose a compatible preset, e.g. presets=newton_renderer,rgb." + ) + + non_depth_data_types = set(tiled_camera.data_types).difference( + {"depth", "distance_to_image_plane", "distance_to_camera"} + ) + if tiled_camera.data_types and not non_depth_data_types and feature_extractor.enabled: + raise ValueError( + "Depth-only camera data type is intended for benchmarking only. " + "The keypoint-regression CNN cannot be meaningfully trained from depth alone. " + "Disable the feature extractor with 'feature_extractor.enabled=False' " + "(e.g. use IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct), " + "or choose a data type that includes colour, e.g. presets=rgb." + ) + + @configclass class _ShadowHandBaseTiledCameraCfg(CameraCfg): """Base camera configuration for the shadow hand vision environment. - This is an internal config used by :class:`ShadowHandTiledCameraCfg` presets and - by derived env configs that hard-code a specific data type. It embeds + This is a module-level config used by :class:`ShadowHandTiledCameraCfg` presets, by + derived env configs that hard-code a specific data type, and by the rendering tests. It embeds :class:`~isaaclab_tasks.utils.MultiBackendRendererCfg` so the renderer backend can still be selected via the ``presets`` CLI argument. """ @@ -49,6 +92,7 @@ class ShadowHandTiledCameraCfg(PresetCfg): Select a data-type preset via the ``presets`` CLI argument, e.g.:: presets = rgb # RGB only (3 channels) + presets = rgb_depth # RGB + depth (4 channels) presets = albedo # albedo (3 channels) presets = simple_shading_constant_diffuse # simple shading, constant diffuse (3 channels) @@ -70,6 +114,9 @@ class ShadowHandTiledCameraCfg(PresetCfg): rgb: _ShadowHandBaseTiledCameraCfg = _ShadowHandBaseTiledCameraCfg(data_types=["rgb"]) """RGB only (3 CNN input channels).""" + rgb_depth: _ShadowHandBaseTiledCameraCfg = _ShadowHandBaseTiledCameraCfg(data_types=["rgb", "depth"]) + """RGB and depth (4 CNN input channels).""" + albedo: _ShadowHandBaseTiledCameraCfg = _ShadowHandBaseTiledCameraCfg(data_types=["albedo"]) """Albedo (3 CNN input channels).""" @@ -93,9 +140,9 @@ class ShadowHandTiledCameraCfg(PresetCfg): .. warning:: This preset is intended for **benchmarking only**. The keypoint-regression CNN - cannot be meaningfully trained from depth alone. Use it with - :class:`ShadowHandCameraBenchmarkEnvCfg` (``feature_extractor.enabled=False``) - to measure pure depth-rendering throughput, e.g.:: + cannot be meaningfully trained from depth alone. Use it with the contributed + benchmark task, which disables the feature extractor, to measure pure + depth-rendering throughput, e.g.:: presets=depth # depth rendering, default renderer presets=depth,newton_renderer # depth rendering with Newton renderer @@ -122,75 +169,21 @@ class ShadowHandCameraEnvCfg(ShadowHandEnvCfg): state_space = 187 + 27 # asymmetric states + vision CNN embedding def __post_init__(self): - # The vision env renders through the Isaac RTX tiled camera, whose render - # products require the Fabric cloning path. The Newton backend disables Fabric - # cloning (see the base env's ``clone_in_fabric`` scene preset), so under Newton - # the ``rgb`` annotator has no render products for ``num_envs > 1`` and the - # default RGB/depth/semantic render fails. Default the vision env to PhysX so it - # renders out of the box; Newton stays selectable via ``physics=newton_mjwarp`` - # for the depth-only Newton-warp-renderer benchmark path (``presets=newton_renderer``). + # Only the Isaac RTX tiled camera renders the default RGB/depth/semantic set, so + # the vision env defaults to PhysX. Newton stays selectable via + # ``physics=newton_mjwarp`` for the depth-only benchmark path. super().__post_init__() for backend_cfg in (self.sim.physics, self.robot_cfg, self.object_cfg): backend_cfg.default = backend_cfg.physx def validate_config(self): """Check renderer/data-type and feature-extractor compatibility.""" - renderer_type = getattr(self.tiled_camera.renderer_cfg, "renderer_type", None) - warp_supported = { - "rgb", - "depth", - "distance_to_camera", - "distance_to_image_plane", - "normals", - "semantic_segmentation", - "instance_segmentation", - } - if renderer_type == "newton_warp": - unsupported = set(self.tiled_camera.data_types) - warp_supported - if unsupported: - raise ValueError( - f"Warp renderer only supports data types {sorted(warp_supported)}, " - f"but the camera is configured with unsupported types: {sorted(unsupported)}. " - "Choose a compatible preset, e.g. presets=newton_renderer,rgb." - ) - - non_depth_data_types = set(self.tiled_camera.data_types).difference( - {"depth", "distance_to_image_plane", "distance_to_camera"} - ) - if self.tiled_camera.data_types and not non_depth_data_types and self.feature_extractor.enabled: - raise ValueError( - "Depth-only camera data type is intended for benchmarking only. " - "The keypoint-regression CNN cannot be meaningfully trained from depth alone. " - "Disable the feature extractor with 'feature_extractor.enabled=False' " - "(e.g. use Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct), " - "or choose a data type that includes colour, e.g. presets=rgb." - ) + validate_shadow_hand_camera_settings(self.tiled_camera, self.feature_extractor) def play_mode(self): - # play-mode overrides of parent super().play_mode() - - # scene + # the tiled camera needs more environments than the shared play default self.scene.num_envs = 64 - # inference for CNN + # mutate rather than replace: subclasses may have disabled the CNN self.feature_extractor.train = False self.feature_extractor.load_checkpoint = True - - -@configclass -class ShadowHandCameraBenchmarkEnvCfg(ShadowHandCameraEnvCfg): - """Benchmark configuration with the feature extractor CNN disabled. - - The tiled camera renders frames each step as normal, but the CNN forward pass is - bypassed — zero embeddings are returned instead. This isolates rendering throughput - from CNN inference overhead when profiling. - - The renderer backend and camera data types can still be selected via ``presets``:: - - presets = newton_renderer # benchmark with Newton renderer - presets = ovrtx # benchmark with OVRTX renderer - presets = rgb # benchmark RGB rendering only - presets = depth, newton_renderer # benchmark depth rendering with Newton - """ - - feature_extractor: FeatureExtractorCfg = FeatureExtractorCfg(enabled=False) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py index a7a8c4830be7..7fa4d4511fad 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_env_cfg.py @@ -12,29 +12,23 @@ from isaaclab.utils.noise import NoiseModelWithAdditiveBiasCfg from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_common import ( + CUBE_CFG, GOAL_OBJECT_CFG, - OBJECT_CFG, OPENAI_ACTION_NOISE_CFG, OPENAI_OBSERVATION_NOISE_CFG, - ROBOT_CFG, - ObjectCfg, + SHADOW_HAND_ROBOT_CFG, + CubeCfg, PhysicsCfg, ShadowHandEventCfg, ShadowHandRobotCfg, ) -from isaaclab_tasks.utils import preset from isaaclab_assets.robots.shadow_hand import SHADOW_ACTUATED_JOINT_NAMES, SHADOW_FINGERTIP_BODY_NAMES @configclass class ShadowHandSceneCfg(InteractiveSceneCfg): - """Shadow Direct scene defaults. - - ``clone_in_fabric`` is the only backend-varying field: PhysX/OvPhysX use Fabric - cloning for speed; Newton does not support it. The per-backend value is selected - inline at the scene field via :func:`~isaaclab_tasks.utils.preset`. - """ + """Shadow Direct scene defaults.""" num_envs = 8192 env_spacing = 0.75 @@ -55,29 +49,22 @@ class ShadowHandEnvCfg(DirectRLEnvCfg): # simulation — values mirrored by the manager cfg (guarded by the value-parity test) sim: SimulationCfg = SimulationCfg( dt=1 / 120, - render_interval=2, + render_interval=decimation, physics_material=RigidBodyMaterialBaseCfg(static_friction=1.0, dynamic_friction=1.0), physics=PhysicsCfg(), ) # robot - robot_cfg: ShadowHandRobotCfg = ROBOT_CFG + robot_cfg: ShadowHandRobotCfg = SHADOW_HAND_ROBOT_CFG actuated_joint_names = SHADOW_ACTUATED_JOINT_NAMES fingertip_body_names = SHADOW_FINGERTIP_BODY_NAMES # in-hand object - object_cfg: ObjectCfg = OBJECT_CFG + object_cfg: CubeCfg = CUBE_CFG # goal object goal_object_cfg: VisualizationMarkersCfg = GOAL_OBJECT_CFG - # scene — clone_in_fabric is the only backend-varying field (Newton cannot use Fabric cloning) - scene: InteractiveSceneCfg = preset( - default=ShadowHandSceneCfg(clone_in_fabric=False), - physx=ShadowHandSceneCfg(clone_in_fabric=True), - isaacsim_physx=ShadowHandSceneCfg(clone_in_fabric=True), - ovphysx=ShadowHandSceneCfg(clone_in_fabric=True), - newton_mjwarp=ShadowHandSceneCfg(clone_in_fabric=False), - newton_kamino=ShadowHandSceneCfg(clone_in_fabric=False), - ) + # scene + scene: InteractiveSceneCfg = ShadowHandSceneCfg() # reset reset_position_noise = 0.01 # range of position at reset @@ -96,6 +83,10 @@ class ShadowHandEnvCfg(DirectRLEnvCfg): max_consecutive_success = 0 success_count_threshold: int = 1 """Minimum number of goals reached in an episode to count it as a successful episode.""" + in_hand_pos_offset: tuple[float, float, float] = (0.0, 0.0, -0.04) + """In-hand goal anchor, relative to the object's default position [m].""" + goal_marker_position: tuple[float, float, float] = (-0.2, -0.45, 0.68) + """Fixed goal-marker display position [m], environment frame.""" av_factor = 0.1 act_moving_average = 1.0 force_torque_obs_scale = 10.0 @@ -115,7 +106,7 @@ class ShadowHandOpenAIEnvCfg(ShadowHandEnvCfg): # simulation — values mirrored by the manager cfg (guarded by the value-parity test) sim: SimulationCfg = SimulationCfg( dt=1 / 60, - render_interval=3, + render_interval=decimation, physics_material=RigidBodyMaterialBaseCfg(static_friction=1.0, dynamic_friction=1.0), physics=PhysicsCfg(), ) @@ -138,7 +129,5 @@ class ShadowHandOpenAIEnvCfg(ShadowHandEnvCfg): force_torque_obs_scale = 10.0 # domain randomization config events: ShadowHandEventCfg = ShadowHandEventCfg() - # at every time-step add gaussian noise + bias. The bias is a gaussian sampled at reset action_noise_model: NoiseModelWithAdditiveBiasCfg = OPENAI_ACTION_NOISE_CFG - # at every time-step add gaussian noise + bias. The bias is a gaussian sampled at reset observation_noise_model: NoiseModelWithAdditiveBiasCfg = OPENAI_OBSERVATION_NOISE_CFG diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py new file mode 100644 index 000000000000..5bab033564cd --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py @@ -0,0 +1,219 @@ +# 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 state-based Shadow Hand reorientation task.""" + +import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg +from isaaclab.envs import ManagerBasedRLEnvCfg, ViewerCfg +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.simulation_cfg import SimulationCfg +from isaaclab.sim.spawners.materials import RigidBodyMaterialBaseCfg +from isaaclab.utils.configclass import configclass + +import isaaclab_tasks.core.reorient.mdp as mdp +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_common import ( + CUBE_CFG, + GOAL_OBJECT_CFG, + SHADOW_HAND_ROBOT_CFG, + CubeCfg, + PhysicsCfg, + ShadowHandManagerEventCfg, +) +from isaaclab_tasks.utils import PresetCfg + +from isaaclab_assets.robots.shadow_hand import SHADOW_ACTUATED_JOINT_NAMES, SHADOW_FINGERTIP_BODY_NAMES + +# ---------------------------------- state task ---------------------------------- + + +@configclass +class ShadowHandManagerSceneCfg(InteractiveSceneCfg): + """Shared reorientation scene with the Shadow hand and a ground plane.""" + + num_envs = 8192 + env_spacing = 0.75 + + robot: PresetCfg = SHADOW_HAND_ROBOT_CFG + object: CubeCfg = CUBE_CFG + ground = AssetBaseCfg(prim_path="/World/ground", spawn=sim_utils.GroundPlaneCfg()) + light = AssetBaseCfg( + prim_path="/World/Light", + spawn=sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)), + ) + dome_light = None + + +@configclass +class CommandsCfg: + """Object pose goal matching the Direct in-hand target.""" + + object_pose = mdp.ReorientCommandCfg( + asset_name="object", + init_pos_offset=(0.0, 0.0, -0.04), + update_goal_on_success=True, + orientation_success_threshold=0.1, + make_quat_unique=False, + fixed_marker_pos=(-0.2, -0.45, 0.68), + goal_pose_visualizer_cfg=GOAL_OBJECT_CFG, + debug_vis=True, + ) + + +@configclass +class ActionsCfg: + """Twenty actuated Shadow Hand joints.""" + + joint_pos = mdp.EMAJointPositionToLimitsActionCfg( + asset_name="robot", + joint_names=SHADOW_ACTUATED_JOINT_NAMES, + alpha=1.0, + rescale_to_limits=True, + ) + + +@configclass +class FullStateObsCfg(ObsGroup): + """Shared first 137 dimensions of the full Shadow state, before the action terms.""" + + # -- robot + joint_pos = ObsTerm( + func=mdp.joint_pos_limit_normalized, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=False)}, + ) + joint_vel = ObsTerm( + func=mdp.joint_vel, + scale=0.2, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*", preserve_order=False)}, + ) + # -- object + 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"), "make_quat_unique": False}, + ) + 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")}, + ) + # -- command + goal_pose = ObsTerm(func=mdp.generated_commands, params={"command_name": "object_pose"}) + goal_quat_diff = ObsTerm( + func=mdp.goal_quat_diff, + params={"asset_cfg": SceneEntityCfg("object"), "command_name": "object_pose", "make_quat_unique": False}, + ) + # -- robot fingertips + fingertip_pos = ObsTerm( + func=mdp.fingertip_pos, + params={"asset_cfg": SceneEntityCfg("robot", body_names=SHADOW_FINGERTIP_BODY_NAMES, preserve_order=False)}, + ) + fingertip_quat = ObsTerm( + func=mdp.fingertip_quat, + params={"asset_cfg": SceneEntityCfg("robot", body_names=SHADOW_FINGERTIP_BODY_NAMES, preserve_order=False)}, + ) + fingertip_vel = ObsTerm( + func=mdp.fingertip_vel, + params={"asset_cfg": SceneEntityCfg("robot", body_names=SHADOW_FINGERTIP_BODY_NAMES, preserve_order=False)}, + ) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = True + + +@configclass +class ObservationsCfg: + """Full 157-dimensional state observation in Direct order.""" + + @configclass + class PolicyCfg(FullStateObsCfg): + last_action = ObsTerm(func=mdp.reorient_last_action, params={"action_name": "joint_pos"}) + + policy: PolicyCfg = PolicyCfg() + + +@configclass +class RewardsCfg: + """Shared reward terms tuned to the Direct task's scales.""" + + track_orientation_inv_l2 = RewTerm( + func=mdp.track_orientation_inv_l2, + weight=1.0, + params={"object_cfg": SceneEntityCfg("object"), "rot_eps": 0.1, "command_name": "object_pose"}, + ) + success_bonus = RewTerm( + func=mdp.success_bonus, + weight=250.0, + params={"object_cfg": SceneEntityCfg("object"), "command_name": "object_pose"}, + ) + track_pos_l2 = RewTerm( + func=mdp.track_pos_l2, + weight=-10.0, + params={"command_name": "object_pose", "object_cfg": SceneEntityCfg("object")}, + ) + action_l2 = RewTerm(func=mdp.action_l2, weight=-0.0002) + + +@configclass +class TerminationsCfg: + """Shared terminations reduced to the Direct task's fall condition.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + object_out_of_reach = DoneTerm( + func=mdp.object_away_from_goal, + params={ + "threshold": 0.24, + "command_name": "object_pose", + "object_cfg": SceneEntityCfg("object"), + }, + ) + + +@configclass +class ShadowHandManagerEnvCfg(ManagerBasedRLEnvCfg): + """Manager-based state Shadow Hand task with Direct-compatible semantics.""" + + scene: ShadowHandManagerSceneCfg = ShadowHandManagerSceneCfg() + decimation = 2 + episode_length_s = 10.0 + sim: SimulationCfg = SimulationCfg( + dt=1 / 120, + render_interval=decimation, + physics_material=RigidBodyMaterialBaseCfg(static_friction=1.0, dynamic_friction=1.0), + physics=PhysicsCfg(), + ) + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: ShadowHandManagerEventCfg = ShadowHandManagerEventCfg() + + enable_domain_randomization: bool = False + """Apply the domain-randomization event terms. + + Off by default so the task matches its Direct counterpart, which randomizes nothing beyond + the reset distributions. ``__post_init__`` reads it while building the configuration, before + Hydra applies command-line overrides, so ``env.enable_domain_randomization=true`` has no + effect -- set it on the configuration. Changing it requires retraining. + """ + + viewer: ViewerCfg = ViewerCfg(eye=(2.0, 2.0, 2.0)) + + def __post_init__(self): + if not self.enable_domain_randomization: + self.events.robot_joint_stiffness_and_damping = None + self.events.object_scale_mass = None + self.events.reset_gravity = None + self.events.robot_tendon_properties = None + self.events.robot_physics_material = None + self.events.object_physics_material = None diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_openai_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_openai_manager_env_cfg.py new file mode 100644 index 000000000000..f81e618df43d --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_openai_manager_env_cfg.py @@ -0,0 +1,216 @@ +# 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 OpenAI Shadow Hand reorientation variants (FF and LSTM). + +The observation, action-noise, and episode conventions follow OpenAI et al., "Learning +Dexterous In-Hand Manipulation" (https://arxiv.org/abs/1808.00177). +""" + +from isaaclab.envs import ManagerBasedRLEnvCfg, ViewerCfg +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.sensors import JointWrenchSensorCfg +from isaaclab.sim.simulation_cfg import SimulationCfg +from isaaclab.sim.spawners.materials import RigidBodyMaterialBaseCfg +from isaaclab.utils.configclass import configclass + +import isaaclab_tasks.core.reorient.mdp as mdp +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_common import ( + GOAL_OBJECT_CFG, + OPENAI_ACTION_NOISE_CFG, + OPENAI_OBSERVATION_NOISE_CFG, + PhysicsCfg, + ShadowHandManagerEventCfg, +) +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_manager_env_cfg import ( + FullStateObsCfg, + ShadowHandManagerSceneCfg, +) + +from isaaclab_assets.robots.shadow_hand import SHADOW_ACTUATED_JOINT_NAMES, SHADOW_FINGERTIP_BODY_NAMES + + +@configclass +class CommandsCfg: + """OpenAI goal command with its wider success tolerance.""" + + object_pose = mdp.ReorientCommandCfg( + asset_name="object", + init_pos_offset=(0.0, 0.0, -0.04), + update_goal_on_success=True, + orientation_success_threshold=0.4, + make_quat_unique=False, + fixed_marker_pos=(-0.2, -0.45, 0.68), + goal_pose_visualizer_cfg=GOAL_OBJECT_CFG, + debug_vis=True, + ) + + +@configclass +class ActionsCfg: + """OpenAI actions with Direct-compatible EMA and stateful noise.""" + + joint_pos = mdp.NoisyEMAJointPositionToLimitsActionCfg( + asset_name="robot", + joint_names=SHADOW_ACTUATED_JOINT_NAMES, + alpha=0.3, + rescale_to_limits=True, + noise_model=OPENAI_ACTION_NOISE_CFG, + ) + + +@configclass +class ObservationsCfg: + """OpenAI 42-dimensional actor and 187-dimensional critic observations.""" + + @configclass + class PolicyCfg(ObsGroup): + openai = ObsTerm( + func=mdp.openai_policy_observation, + params={ + "command_name": "object_pose", + "action_name": "joint_pos", + "noise_model": OPENAI_OBSERVATION_NOISE_CFG, + "robot_cfg": SceneEntityCfg("robot", body_names=SHADOW_FINGERTIP_BODY_NAMES, preserve_order=False), + "object_cfg": SceneEntityCfg("object"), + }, + ) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = True + + @configclass + class CriticCfg(FullStateObsCfg): + fingertip_wrench = ObsTerm( + func=mdp.fingertip_wrench, + scale=10.0, + params={ + "sensor_cfg": SceneEntityCfg( + "joint_wrench", body_names=SHADOW_FINGERTIP_BODY_NAMES, preserve_order=False + ) + }, + ) + last_action = ObsTerm(func=mdp.reorient_last_action, params={"action_name": "joint_pos"}) + + policy: PolicyCfg = PolicyCfg() + critic: CriticCfg = CriticCfg() + + +@configclass +class ShadowHandOpenAIManagerSceneCfg(ShadowHandManagerSceneCfg): + """Shadow Hand scene with fingertip joint-wrench sensing.""" + + joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot") + + +@configclass +class RewardsCfg: + """Shared reward terms tuned to the Direct OpenAI variant's scales.""" + + track_orientation_inv_l2 = RewTerm( + func=mdp.track_orientation_inv_l2, + weight=1.0, + params={"object_cfg": SceneEntityCfg("object"), "rot_eps": 0.1, "command_name": "object_pose"}, + ) + success_bonus = RewTerm( + func=mdp.success_bonus, + weight=250.0, + params={"object_cfg": SceneEntityCfg("object"), "command_name": "object_pose"}, + ) + track_pos_l2 = RewTerm( + func=mdp.track_pos_l2, + weight=-10.0, + params={"command_name": "object_pose", "object_cfg": SceneEntityCfg("object")}, + ) + action_l2 = RewTerm(func=mdp.action_l2, weight=-0.0002) + object_away_penalty = RewTerm( + func=mdp.is_terminated_term, + weight=-50.0, + params={"term_keys": "object_out_of_reach"}, + ) + + +@configclass +class TerminationsCfg: + """Shared terminations with the OpenAI streak cap and success-extended timer. + + The Direct variant reports both the streak cap and the elapsed-time limit as + truncations, so both carry ``time_out=True``. + """ + + object_out_of_reach = DoneTerm( + func=mdp.object_away_from_goal, + params={ + "threshold": 0.24, + "command_name": "object_pose", + "object_cfg": SceneEntityCfg("object"), + }, + ) + max_consecutive_success = DoneTerm( + func=mdp.max_consecutive_success, + time_out=True, + params={"num_success": 50, "command_name": "object_pose"}, + ) + time_out = DoneTerm( + func=mdp.reorient_timeout, + time_out=True, + params={ + "command_name": "object_pose", + "success_tolerance": 0.4, + "object_cfg": SceneEntityCfg("object"), + }, + ) + + +@configclass +class ShadowHandOpenAIManagerEnvCfg(ManagerBasedRLEnvCfg): + """Manager counterpart shared by the OpenAI FF and LSTM variants. + + Standalone rather than a subclass of :class:`ShadowHandManagerEnvCfg`: + every section differs from the state task, so this block is the complete + recipe. + """ + + scene: ShadowHandOpenAIManagerSceneCfg = ShadowHandOpenAIManagerSceneCfg() + decimation = 3 + episode_length_s = 8.0 + sim: SimulationCfg = SimulationCfg( + dt=1 / 60, + render_interval=decimation, + physics_material=RigidBodyMaterialBaseCfg(static_friction=1.0, dynamic_friction=1.0), + physics=PhysicsCfg(), + ) + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: ShadowHandManagerEventCfg = ShadowHandManagerEventCfg() + + enable_domain_randomization: bool = True + """Apply the domain-randomization event terms. + + On by default: unlike the other reorientation tasks, the OpenAI Direct environment + randomizes as well, so the two workflows only match with these enabled. ``__post_init__`` + reads it while building the configuration, before Hydra applies command-line overrides, so + ``env.enable_domain_randomization=false`` has no effect -- set it on the configuration. + Changing it requires retraining. + """ + + viewer: ViewerCfg = ViewerCfg(eye=(2.0, 2.0, 2.0)) + + def __post_init__(self): + if not self.enable_domain_randomization: + self.events.robot_joint_stiffness_and_damping = None + self.events.object_scale_mass = None + self.events.reset_gravity = None + self.events.robot_tendon_properties = None + self.events.robot_physics_material = None + self.events.object_physics_material = None diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi index 3221965e78a6..9bd01bd20972 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi @@ -4,8 +4,22 @@ # SPDX-License-Identifier: BSD-3-Clause __all__ = [ + "NoisyEMAJointPositionToLimitsAction", + "NoisyEMAJointPositionToLimitsActionCfg", "ReorientCommand", "ReorientCommandCfg", + "ReorientCommand", + "ReorientCommandCfg", + "reset_reorient_state", + "fingertip_pos", + "fingertip_quat", + "fingertip_vel", + "fingertip_wrench", + "reorient_last_action", + "openai_policy_observation", + "ShadowHandCameraFeatures", + "shadow_hand_camera_cached_features", + "shadow_hand_goal_keypoints", "goal_quat_diff", "success_bonus", "track_orientation_inv_l2", @@ -14,17 +28,35 @@ __all__ = [ "reorient_reward", "max_consecutive_success", "object_away_from_goal", - "object_away_from_robot", + "reorient_timeout", ] from .commands import ReorientCommand, ReorientCommandCfg -from .observations import goal_quat_diff +from .events import reset_reorient_state +from .noisy_actions import NoisyEMAJointPositionToLimitsAction +from .actions import NoisyEMAJointPositionToLimitsActionCfg +from .observations import ( + ShadowHandCameraFeatures, + shadow_hand_camera_cached_features, + shadow_hand_goal_keypoints, + fingertip_pos, + fingertip_quat, + fingertip_vel, + fingertip_wrench, + goal_quat_diff, + openai_policy_observation, + reorient_last_action, +) from .rewards import ( - reorient_reward, evaluate_reorient_success, + reorient_reward, success_bonus, track_orientation_inv_l2, track_pos_l2, ) -from .terminations import max_consecutive_success, object_away_from_goal, object_away_from_robot +from .terminations import ( + max_consecutive_success, + object_away_from_goal, + reorient_timeout, +) from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/actions.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/actions.py new file mode 100644 index 000000000000..718c4cdfe9e5 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/actions.py @@ -0,0 +1,30 @@ +# 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 + +"""Action configurations for the reorientation task family.""" + +from __future__ import annotations + +from dataclasses import MISSING +from typing import TYPE_CHECKING + +from isaaclab.envs.mdp import EMAJointPositionToLimitsActionCfg +from isaaclab.utils.configclass import configclass +from isaaclab.utils.noise import NoiseModelCfg + +if TYPE_CHECKING: + from .noisy_actions import NoisyEMAJointPositionToLimitsAction + + +@configclass +class NoisyEMAJointPositionToLimitsActionCfg(EMAJointPositionToLimitsActionCfg): + """EMA joint action configuration with Direct-compatible stateful noise.""" + + class_type: type[NoisyEMAJointPositionToLimitsAction] | str = ( + "{DIR}.noisy_actions:NoisyEMAJointPositionToLimitsAction" + ) + + noise_model: NoiseModelCfg = MISSING + """Stateful noise applied to incoming normalized actions.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py index 77040f635282..04c30305ec03 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py @@ -77,9 +77,12 @@ def __init__(self, cfg: ReorientCommandCfg, env: ManagerBasedRLEnv): self.metrics["position_error"] = torch.zeros(self.num_envs, device=self.device) self.metrics["consecutive_success"] = torch.zeros(self.num_envs, device=self.device) self.metrics["success_rate"] = torch.zeros(self.num_envs, device=self.device) - # -- per-attempt success accounting: each success-driven resample completes one attempt; - # the trailing attempt at episode end counts as one unsuccessful attempt. + # -- goals reached this episode; each success-driven resample completes one. self._completed_attempts = torch.zeros(self.num_envs, device=self.device) + # An auto-reset lands immediately before CommandManager.compute(); suppress success + # handling for those environments until one new physics step has run. + self._skip_success_update = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + self._fixed_marker_pos_w: torch.Tensor | None = None # adds (optional) cmd kind and element names for leapp export # during export, semantic data about this command will be used to annotate the command input @@ -119,12 +122,13 @@ def _update_metrics(self): self.metrics["consecutive_success"] += successes.float() def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: - # Snapshot per-attempt success rate BEFORE the base class logs and zeros metrics. - # success_rate = completed_attempts / (completed_attempts + 1 trailing in-progress). + # Snapshot the episode-success bit BEFORE the base class logs and zeros metrics. The + # Direct environment draws it the same way: goals reached this episode against + # ``success_count_threshold``. if env_ids is None: env_ids = slice(None) completed = self._completed_attempts[env_ids] - self.metrics["success_rate"][env_ids] = completed / (completed + 1.0) + self.metrics["success_rate"][env_ids] = (completed >= self.cfg.success_count_threshold).float() extras = super().reset(env_ids) # super().reset() invoked _resample_command for the new initial goal, which # incremented _completed_attempts; zero it back out so the new episode starts clean. @@ -133,6 +137,7 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: # card across tasks); pop it from the returned dict so CommandManager does not # additionally log it under ``Metrics//success_rate``. self._env.extras.setdefault("log", {})["Metrics/success_rate"] = extras.pop("success_rate") + self._skip_success_update[env_ids] = self._env.reset_buf[env_ids] return extras def _resample_command(self, env_ids: Sequence[int]): @@ -150,13 +155,12 @@ def _resample_command(self, env_ids: Sequence[int]): self.quat_command_w[env_ids] = math_utils.quat_unique(quat) if self.cfg.make_quat_unique else quat def _update_command(self): - # update the command if goal is reached if self.cfg.update_goal_on_success: - # compute the goal resets - goal_resets = self.metrics["orientation_error"] < self.cfg.orientation_success_threshold - goal_reset_ids = goal_resets.nonzero(as_tuple=False).squeeze(-1) - # resample the goals - self._resample(goal_reset_ids) + goal_resets = ( + self.metrics["orientation_error"] < self.cfg.orientation_success_threshold + ) & ~self._skip_success_update + self._resample(goal_resets.nonzero(as_tuple=False).squeeze(-1)) + self._skip_success_update[:] = False def _set_debug_vis_impl(self, debug_vis: bool): # set visibility of markers @@ -172,11 +176,17 @@ def _set_debug_vis_impl(self, debug_vis: bool): self.goal_pose_visualizer.set_visibility(False) def _debug_vis_callback(self, event): - # add an offset to the marker position to visualize the goal - marker_pos = self.pos_command_w + torch.tensor(self.cfg.marker_pos_offset, device=self.device) - marker_quat = self.quat_command_w - # visualize the goal marker - self.goal_pose_visualizer.visualize(translations=marker_pos, orientations=marker_quat) + if self.cfg.fixed_marker_pos is None: + marker_pos = self.pos_command_w + torch.tensor(self.cfg.marker_pos_offset, device=self.device) + else: + if self._fixed_marker_pos_w is None: + # constant per run; cached to avoid a host-to-device allocation every render frame + self._fixed_marker_pos_w = ( + torch.tensor(self.cfg.fixed_marker_pos, device=self.device).repeat(self.num_envs, 1) + + self._env.scene.env_origins + ) + marker_pos = self._fixed_marker_pos_w + self.goal_pose_visualizer.visualize(translations=marker_pos, orientations=self.quat_command_w) @configclass @@ -208,12 +218,22 @@ class ReorientCommandCfg(CommandTermCfg): If True, the quaternion is made unique by ensuring the real part is positive. """ + fixed_marker_pos: tuple[float, float, float] | None = None + """Fixed goal-marker position [m] in each environment, or ``None`` to follow the goal.""" + orientation_success_threshold: float = MISSING """Threshold for the orientation error to consider the goal orientation to be reached.""" update_goal_on_success: bool = MISSING """Whether to update the goal orientation when the goal orientation is reached.""" + success_count_threshold: int = 1 + """Goals an episode must reach to count as successful in ``Metrics/success_rate``. + + Mirrors the Direct environment's configuration field of the same name, so both workflows + draw the episode-success bit at the same goal count. + """ + marker_pos_offset: tuple[float, float, float] = (0.0, 0.0, 0.0) """Position offset of the marker from the object's desired position. diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py new file mode 100644 index 000000000000..7f032faa3c2c --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py @@ -0,0 +1,71 @@ +# 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 + +"""Reset events for state-based in-hand reorientation tasks.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.managers import SceneEntityCfg + +from isaaclab_tasks.core.utils import random_xy_rotation, sample_joint_positions_within_limits + +if TYPE_CHECKING: + from isaaclab.assets import Articulation, RigidObject + from isaaclab.envs import ManagerBasedRLEnv + + +def reset_reorient_state( + env: ManagerBasedRLEnv, + env_ids: Sequence[int], + position_noise: float, + joint_position_noise: float, + joint_velocity_noise: float, + action_name: str, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), +) -> None: + """Reset the object and hand with the Direct task's distributions. + + Args: + env: Environment containing the robot and object. + env_ids: Environment indices to reset. + position_noise: Object-position noise half-width [m]. + joint_position_noise: Scale applied to sampled joint-position deltas. + joint_velocity_noise: Joint-velocity noise half-width [rad/s]. + action_name: Action term whose terminal raw action is retained in the reset observation. + robot_cfg: Robot scene entity. + object_cfg: Object scene entity. + """ + raw_action = env.action_manager.get_term(action_name).raw_actions + if not hasattr(env, "_reorient_reset_action"): + env._reorient_reset_action = torch.zeros_like(raw_action) + env._reorient_reset_step = torch.full((env.num_envs,), -1, dtype=torch.long, device=raw_action.device) + env._reorient_reset_action[env_ids] = raw_action[env_ids] + env._reorient_reset_step[env_ids] = env.common_step_counter + + object_asset: Articulation | RigidObject = env.scene[object_cfg.name] + object_pose = object_asset.data.default_root_pose.torch[env_ids].clone() + object_velocity = torch.zeros_like(object_asset.data.default_root_vel.torch[env_ids]) + position_delta = math_utils.sample_uniform(-1.0, 1.0, (len(env_ids), 3), device=env.device) + object_pose[:, :3] += position_noise * position_delta + env.scene.env_origins[env_ids] + object_pose[:, 3:7] = random_xy_rotation(len(env_ids), env.device) + object_asset.write_root_pose_to_sim_index(root_pose=object_pose, env_ids=env_ids) + object_asset.write_root_velocity_to_sim_index(root_velocity=object_velocity, env_ids=env_ids) + + robot: Articulation = env.scene[robot_cfg.name] + default_position = robot.data.default_joint_pos.torch[env_ids] + limits = robot.data.joint_limits.torch[env_ids] + joint_position = sample_joint_positions_within_limits(default_position, limits, joint_position_noise) + velocity_sample = math_utils.sample_uniform(-1.0, 1.0, (len(env_ids), robot.num_joints), device=env.device) + joint_velocity = robot.data.default_joint_vel.torch[env_ids] + joint_velocity_noise * velocity_sample + robot.set_joint_position_target_index(target=joint_position, env_ids=env_ids) + robot.write_joint_position_to_sim_index(position=joint_position, env_ids=env_ids) + robot.write_joint_velocity_to_sim_index(velocity=joint_velocity, env_ids=env_ids) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/noisy_actions.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/noisy_actions.py new file mode 100644 index 000000000000..8016e506f053 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/noisy_actions.py @@ -0,0 +1,56 @@ +# 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 + +"""Action term implementations for the reorientation task family. + +Kept apart from :mod:`~isaaclab_tasks.core.reorient.mdp.actions` because importing the +base action class pulls in the USD stage bindings, which configuration loading must not +require. The configuration there names this module through ``class_type`` instead. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import torch + +from isaaclab.envs.mdp.actions import EMAJointPositionToLimitsAction + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedEnv + + from .actions import NoisyEMAJointPositionToLimitsActionCfg + + +class NoisyEMAJointPositionToLimitsAction(EMAJointPositionToLimitsAction): + """Apply a stateful noise model before EMA joint-position processing.""" + + def __init__(self, cfg: NoisyEMAJointPositionToLimitsActionCfg, env: ManagerBasedEnv): + """Initialize the noisy action term. + + Args: + cfg: Action configuration including the stateful noise model. + env: Manager-based environment containing the hand. + """ + super().__init__(cfg, env) + self._noise_model = cfg.noise_model.class_type(cfg.noise_model, num_envs=self.num_envs, device=self.device) + + def process_actions(self, actions: torch.Tensor) -> None: + """Apply noise to normalized actions before scaling and EMA filtering. + + Args: + actions: Normalized joint actions, shape ``(num_envs, num_actions)``. + """ + super().process_actions(self._noise_model(actions)) + + def reset(self, env_ids: Sequence[int] | None = None) -> None: + """Reset the noise state and standard EMA action buffers. + + Args: + env_ids: Environment indices to reset, or ``None`` for every environment. + """ + self._noise_model.reset(env_ids) + super().reset(env_ids) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py index 8bff42fb7b41..dbc3300d09a2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py @@ -7,16 +7,21 @@ from __future__ import annotations +from collections.abc import Sequence from typing import TYPE_CHECKING import torch import isaaclab.utils.math as math_utils -from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import ManagerTermBase, ObservationTermCfg, SceneEntityCfg +from isaaclab.utils.noise import NoiseModelCfg if TYPE_CHECKING: from isaaclab.assets import RigidObject from isaaclab.envs import ManagerBasedRLEnv + from isaaclab.sensors import Camera, JointWrenchSensor + + from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractorCfg from .commands import ReorientCommand @@ -25,6 +30,7 @@ """Half side lengths [m] of the reorientation cube.""" +# -- cube keypoint helpers, shared by the camera and state observation terms def _cube_corner_offsets( size: tuple[float, float, float], num_keypoints: int, device: torch.device | str ) -> torch.Tensor: @@ -97,22 +103,256 @@ def cube_keypoints_from_quat( return rotated.reshape(num_envs, num_keypoints * 3) +# -- command terms def goal_quat_diff( env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg, command_name: str, make_quat_unique: bool ) -> torch.Tensor: """Goal orientation relative to the asset's root frame. - The quaternion is represented as (w, x, y, z). The real part is always positive. + The real part is always positive when ``make_quat_unique`` is set. + + Args: + env: The environment object. + asset_cfg: The scene entity whose root orientation is compared. + command_name: The command term to be used for extracting the goal. + make_quat_unique: Whether to keep the quaternion real part non-negative. + + Returns: + Per-environment quaternion error ``asset * conjugate(goal)`` in ``(x, y, z, w)`` order. """ - # extract useful elements asset: RigidObject = env.scene[asset_cfg.name] command_term: ReorientCommand = env.command_manager.get_term(command_name) + quat_error = math_utils.quat_mul( + asset.data.root_quat_w.torch, math_utils.quat_conjugate(command_term.quat_command_w) + ) + return math_utils.quat_unique(quat_error) if make_quat_unique else quat_error + + +# -- fingertip terms +def fingertip_pos(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: + """Flattened fingertip positions in the environment frame [m], shape ``(num_envs, num_fingertips * 3)``.""" + asset = env.scene[asset_cfg.name] + positions = asset.data.body_pos_w.torch[:, asset_cfg.body_ids] - env.scene.env_origins.unsqueeze(1) + return positions.reshape(env.num_envs, -1) + + +def fingertip_quat(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: + """Flattened fingertip ``(x, y, z, w)`` orientations, shape ``(num_envs, num_fingertips * 4)``.""" + asset = env.scene[asset_cfg.name] + return asset.data.body_quat_w.torch[:, asset_cfg.body_ids].reshape(env.num_envs, -1) + + +def fingertip_vel(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: + """Flattened fingertip spatial velocities [m/s, rad/s], shape ``(num_envs, num_fingertips * 6)``.""" + asset = env.scene[asset_cfg.name] + return asset.data.body_vel_w.torch[:, asset_cfg.body_ids].reshape(env.num_envs, -1) + + +class fingertip_wrench(ManagerTermBase): + """Fingertip reaction wrenches [N, N·m] with Direct-compatible zero fallback.""" + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + body_ids = cfg.params["sensor_cfg"].body_ids + # Direct-compatible fallback: report zero wrenches until the sensor produces data + self._zeros = torch.zeros(env.num_envs, len(body_ids) * 6, dtype=torch.float32, device=env.device) + + def __call__(self, env: ManagerBasedRLEnv, sensor_cfg: SceneEntityCfg) -> torch.Tensor: + """Return the flattened wrench block, shape ``(num_envs, num_fingertips * 6)``.""" + sensor: JointWrenchSensor = env.scene.sensors[sensor_cfg.name] + force_data = sensor.data.force + torque_data = sensor.data.torque + if force_data is None or torque_data is None: + return self._zeros + force = force_data.torch[:, sensor_cfg.body_ids] + torque = torque_data.torch[:, sensor_cfg.body_ids] + return torch.cat((force, torque), dim=-1).reshape(env.num_envs, -1) + + +# -- action terms +def reorient_last_action(env: ManagerBasedRLEnv, action_name: str) -> torch.Tensor: + """Return the Direct-compatible last action across same-step autoreset. + + Args: + env: Environment containing the action term and reset buffers. + action_name: Action term whose raw action is observed. + + Returns: + Raw actions, retaining each terminal action in its same-step reset observation. + """ + raw_action = env.action_manager.get_term(action_name).raw_actions + reset_action = getattr(env, "_reorient_reset_action", None) + reset_step = getattr(env, "_reorient_reset_step", None) + common_step_counter = getattr(env, "common_step_counter", None) + if reset_action is None or reset_step is None or common_step_counter is None: + return raw_action + return torch.where((reset_step == common_step_counter).unsqueeze(-1), reset_action, raw_action) + + +# -- composed observation groups +class openai_policy_observation(ManagerTermBase): + """Apply one stateful noise model to the concatenated OpenAI actor observation.""" + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + noise_model: NoiseModelCfg = cfg.params["noise_model"] + self._noise_model = noise_model.class_type(noise_model, num_envs=self.num_envs, device=self.device) + # ObservationManager probes callable terms once for their shape and then + # calls reset. Keep that probe side-effect free so initialization matches + # DirectRLEnv's first noise-model reset and application. + self._shape_probe_pending = True + + def reset(self, env_ids: Sequence[int] | None = None) -> None: + """Reset the actor observation bias for selected environments. - # obtain the orientations - goal_quat_w = command_term.command[:, 3:7] - asset_quat_w = asset.data.root_quat_w.torch + Args: + env_ids: Environment indices to reset, or ``None`` for every environment. + """ + if self._shape_probe_pending: + self._shape_probe_pending = False + return + self._noise_model.reset(env_ids) - # compute quaternion difference - quat = math_utils.quat_mul(asset_quat_w, math_utils.quat_conjugate(goal_quat_w)) - # make sure the quaternion real-part is always positive - return math_utils.quat_unique(quat) if make_quat_unique else quat + def __call__( + self, + env: ManagerBasedRLEnv, + command_name: str, + action_name: str, + noise_model: NoiseModelCfg, + robot_cfg: SceneEntityCfg, + object_cfg: SceneEntityCfg, + ) -> torch.Tensor: + """Return the corrupted 42-dimensional actor observation.""" + del noise_model + object_asset: RigidObject = env.scene[object_cfg.name] + object_pos = object_asset.data.root_pos_w.torch - env.scene.env_origins + command_term: ReorientCommand = env.command_manager.get_term(command_name) + quat_error = math_utils.quat_mul( + object_asset.data.root_quat_w.torch, math_utils.quat_conjugate(command_term.quat_command_w) + ) + fingertips = fingertip_pos(env, robot_cfg) + # Direct actor-observation order: fingertips, object position, goal quat error, last action + observation = torch.cat( + (fingertips, object_pos, quat_error, reorient_last_action(env, action_name)), + dim=-1, + ) + if self._shape_probe_pending: + return observation + return self._noise_model(observation) + + +# --------------------------------------------------------------------------- +# Shadow Hand camera observation terms. +# +# These terms wrap the CNN feature pipeline defined in the shadow-hand config +# package. The config layer imports the mdp layer, so the FeatureExtractor +# machinery is imported lazily at term construction time. +# --------------------------------------------------------------------------- + + +class ShadowHandCameraFeatures(ManagerTermBase): + """Run the Direct camera feature pipeline as one Manager observation term.""" + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + sensor_cfg: SceneEntityCfg = cfg.params["sensor_cfg"] + camera: Camera = env.scene.sensors[sensor_cfg.name] + # Runtime-only import: the mdp layer must not import the task-config layer + # at module load (config modules import mdp; see the layering note above). + from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractor + + feature_extractor_cfg: FeatureExtractorCfg = cfg.params["feature_extractor_cfg"] + self._feature_extractor = FeatureExtractor( + feature_extractor_cfg, + env.device, + camera.cfg.data_types, + env.cfg.log_dir, + height=camera.cfg.height, + width=camera.cfg.width, + ) + # ObservationManager calls terms once to infer their shape. Do not train + # or save a CNN checkpoint during that initialization probe. + self._shape_probe_pending = True + self._keypoints_buf = torch.empty(env.num_envs, 8, 3, dtype=torch.float32, device=env.device) + + def reset(self, env_ids: Sequence[int] | None = None) -> None: + """Finish the shape-probe phase on the first Manager reset. + + Args: + env_ids: Environment indices being reset. The feature extractor + has no per-environment state, so the indices are unused. + """ + del env_ids + if self._shape_probe_pending: + self._shape_probe_pending = False + + def __call__( + self, + env: ManagerBasedRLEnv, + feature_extractor_cfg: FeatureExtractorCfg, + sensor_cfg: SceneEntityCfg, + object_cfg: SceneEntityCfg, + ) -> torch.Tensor: + """Return the detached 27-dimensional cube-pose embedding. + + Args: + env: Environment containing the object and tiled camera. + feature_extractor_cfg: Feature-extractor configuration. + sensor_cfg: Tiled-camera scene entity. + object_cfg: Reoriented-object scene entity. + + Returns: + Predicted object position and cube keypoints [m], shape + ``(num_envs, 27)``. + """ + del feature_extractor_cfg # consumed in __init__ + if self._shape_probe_pending: + embeddings = torch.zeros(env.num_envs, 27, dtype=torch.float32, device=env.device) + env._shadow_hand_camera_embeddings = embeddings + return embeddings + + camera: Camera = env.scene.sensors[sensor_cfg.name] + object_asset: RigidObject = env.scene[object_cfg.name] + object_pos = object_asset.data.root_pos_w.torch - env.scene.env_origins + object_pose = torch.cat((object_pos, object_asset.data.root_quat_w.torch), dim=-1) + keypoints = compute_cube_keypoints(object_pose, out=self._keypoints_buf) + target = torch.cat((object_pos, keypoints.flatten(start_dim=1)), dim=-1) + camera_output = { + data_type: value if isinstance(value, torch.Tensor) else value.torch + for data_type, value in camera.data.output.items() + } + pose_loss, embeddings = self._feature_extractor.step(camera_output, target) + embeddings = embeddings.clone().detach() + env._shadow_hand_camera_embeddings = embeddings + if pose_loss is not None: + env.extras.setdefault("log", {})["pose_loss"] = pose_loss + return embeddings + + +def shadow_hand_camera_cached_features(env: ManagerBasedRLEnv) -> torch.Tensor: + """Return camera features computed by the preceding policy observation group. + + Args: + env: Environment whose policy group cached the current camera embedding. + + Returns: + Detached camera embeddings, shape ``(num_envs, 27)``. + """ + embeddings = getattr(env, "_shadow_hand_camera_embeddings", None) + if embeddings is None: + raise RuntimeError("Shadow Hand camera policy features must be computed before critic observations.") + return embeddings + + +def shadow_hand_goal_keypoints(env: ManagerBasedRLEnv, command_name: str) -> torch.Tensor: + """Flattened zero-origin cube keypoints [m] for the current goal orientation. + + Args: + env: Environment containing the goal command term. + command_name: Goal command term name. + + Returns: + Flattened zero-origin cube keypoints [m], shape ``(num_envs, 24)``. + """ + command_term = env.command_manager.get_term(command_name) + return cube_keypoints_from_quat(command_term.quat_command_w) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/terminations.py index 32d6df6aa18a..a99a93a0bc4d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/terminations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/terminations.py @@ -7,11 +7,13 @@ from __future__ import annotations +from collections.abc import Sequence from typing import TYPE_CHECKING import torch -from isaaclab.managers import SceneEntityCfg +import isaaclab.utils.math as math_utils +from isaaclab.managers import ManagerTermBase, SceneEntityCfg, TerminationTermCfg if TYPE_CHECKING: from isaaclab.envs import ManagerBasedRLEnv @@ -60,28 +62,55 @@ def object_away_from_goal( return torch.linalg.norm(asset_pos_e - goal_pos_e, ord=2, dim=1) > threshold -def object_away_from_robot( - env: ManagerBasedRLEnv, - threshold: float, - asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), - object_cfg: SceneEntityCfg = SceneEntityCfg("object"), -) -> torch.Tensor: - """Check if object has gone far from the robot. +class reorient_timeout(ManagerTermBase): + """Time out an episode that has run its full length without reaching a goal. - The object is considered to be out-of-reach if the distance between the robot and the object is greater - than the threshold. + The timer restarts on every goal reach, so episodes extend across success streaks. + This matches the OpenAI Direct variant, which is the only configuration that enables + the behavior. Pair it with :func:`max_consecutive_success` to also stop on the streak + cap, and declare both with ``time_out=True``. Args: - env: The environment object. - threshold: The threshold for the distance between the robot and the object. - asset_cfg: The configuration for the robot entity. Default is "robot". - object_cfg: The configuration for the object entity. Default is "object". + cfg: Configuration object specifying term parameters. + env: The manager-based RL environment. """ - # extract useful elements - robot = env.scene[asset_cfg.name] - object = env.scene[object_cfg.name] - - # compute distance - dist = torch.linalg.norm(robot.data.root_pos_w.torch - object.data.root_pos_w.torch, dim=1) - return dist > threshold + def __init__(self, cfg: TerminationTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + self._steps_since_success = torch.zeros(self.num_envs, dtype=torch.long, device=self.device) + # resolved on first call: the command term does not exist yet during manager construction + self._command_term: ReorientCommand | None = None + + def reset(self, env_ids: Sequence[int] | None = None) -> None: + if env_ids is None: + env_ids = slice(None) + self._steps_since_success[env_ids] = 0 + + def __call__( + self, + env: ManagerBasedRLEnv, + command_name: str, + success_tolerance: float, + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), + ) -> torch.Tensor: + """Return per-environment timeout flags. + + Args: + env: The environment object. + command_name: The command term to be used for extracting the goal. + success_tolerance: Maximum successful orientation error [rad]. + object_cfg: The configuration for the scene entity. Default is "object". + """ + asset = env.scene[object_cfg.name] + if self._command_term is None: + self._command_term = env.command_manager.get_term(command_name) + # Terminations run before the command manager, so the command's metrics still + # describe the previous step. Evaluate success here instead, matching the Direct + # environment, which refreshes the object pose inside its dones computation. + dtheta = math_utils.quat_error_magnitude(asset.data.root_quat_w.torch, self._command_term.quat_command_w) + goal_reached = dtheta <= success_tolerance + self._steps_since_success += 1 + # masked_fill_ rather than boolean indexing: the latter forces a host synchronization + self._steps_since_success.masked_fill_(goal_reached, 0) + + return self._steps_since_success >= env.max_episode_length - 1 diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_common.py deleted file mode 100644 index 946f1d2e2f08..000000000000 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_common.py +++ /dev/null @@ -1,32 +0,0 @@ -# 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 - -"""Task-family identity shared by the Direct and manager-based reorientation tasks. - -Geometry constants only — task tunables live inline in the workflow -configuration files, and robot identity lives in the per-robot ``*_common`` -modules. -""" - -GOAL_MARKER_POSITION: tuple[float, float, float] = (-0.2, -0.45, 0.68) -"""Fixed goal-marker display position [m], environment frame (state-based tasks).""" - -CAMERA_GOAL_MARKER_POSITION: tuple[float, float, float] = (-0.2, 0.1, 0.6) -"""Goal-marker display position [m] for the camera tasks. - -Deviates from :data:`GOAL_MARKER_POSITION` so the goal cube sits inside the -tiled camera's frustum. -""" - -IN_HAND_POS_OFFSET: tuple[float, float, float] = (0.0, 0.0, -0.04) -"""Offset from the object's default position to the in-hand goal anchor [m]. - -Defines the Direct/manager goal-position parity: the Direct environments and -the manager command terms derive the same in-hand target point from the -object's default root position plus this offset. -""" - -CAMERA_PLAY_NUM_ENVS: int = 64 -"""Camera-task environment count for checkpoint playback.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py index a2ada7e457a1..266f9786b5e0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py @@ -21,7 +21,6 @@ from isaaclab.utils.math import quat_conjugate, quat_mul, sample_uniform, saturate, scale_transform, unscale_transform from isaaclab_tasks.core.reorient.mdp.rewards import evaluate_reorient_success, reorient_reward -from isaaclab_tasks.core.reorient.reorient_common import GOAL_MARKER_POSITION, IN_HAND_POS_OFFSET from isaaclab_tasks.core.utils import EpisodeErrorRecorder, randomize_rotation, sample_joint_positions_within_limits if TYPE_CHECKING: @@ -64,11 +63,11 @@ def __init__(self, cfg: AllegroHandEnvCfg | ShadowHandEnvCfg, render_mode: str | # -- goal and success state -- # in-hand target = object default position + shared offset (mirrors ReorientCommand) self.in_hand_pos = self.object.data.default_root_pose.torch[:, 0:3].clone() - self.in_hand_pos += torch.tensor(IN_HAND_POS_OFFSET, dtype=torch.float, device=self.device) + self.in_hand_pos += torch.tensor(self.cfg.in_hand_pos_offset, dtype=torch.float, device=self.device) self.goal_rot = torch.zeros((self.num_envs, 4), dtype=torch.float, device=self.device) self.goal_rot[:, 3] = 1.0 # identity quaternion in (x, y, z, w) layout self.goal_pos = torch.zeros((self.num_envs, 3), dtype=torch.float, device=self.device) - self.goal_pos[:, :] = torch.tensor(GOAL_MARKER_POSITION, device=self.device) + self.goal_pos[:, :] = torch.tensor(self.cfg.goal_marker_position, device=self.device) self.reset_goal_buf = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) self.successes = torch.zeros(self.num_envs, dtype=torch.float, device=self.device) self.consecutive_successes = torch.zeros(1, dtype=torch.float, device=self.device) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py deleted file mode 100644 index ef1639a61e32..000000000000 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py +++ /dev/null @@ -1,348 +0,0 @@ -# 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 - -from __future__ import annotations - -from dataclasses import MISSING - -from isaaclab_physx.physics import PhysxCfg - -import isaaclab.sim as sim_utils -from isaaclab.assets import ArticulationCfg, 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.simulation_cfg import SimulationCfg -from isaaclab.sim.spawners.materials import RigidBodyMaterialCfg -from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR -from isaaclab.utils.configclass import configclass -from isaaclab.utils.noise import GaussianNoiseCfg as Gnoise - -import isaaclab_tasks.core.reorient.mdp as mdp - -## -# Scene definition -## - - -@configclass -class ReorientObjectSceneCfg(InteractiveSceneCfg): - """Configuration for a scene with an object and a dexterous hand.""" - - # robots - robot: ArticulationCfg = MISSING - - # objects - object: RigidObjectCfg = RigidObjectCfg( - prim_path="{ENV_REGEX_NS}/object", - spawn=sim_utils.UsdFileCfg( - usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", - 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, - ), - mass_props=sim_utils.MassPropertiesCfg(density=400.0), - ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.19, 0.56), rot=(0.0, 0.0, 0.0, 1.0)), - ) - - # lights - light = AssetBaseCfg( - prim_path="/World/light", - spawn=sim_utils.DistantLightCfg(color=(0.95, 0.95, 0.95), intensity=1000.0), - ) - - dome_light = AssetBaseCfg( - prim_path="/World/domeLight", - spawn=sim_utils.DomeLightCfg(color=(0.02, 0.02, 0.02), intensity=1000.0), - ) - - -## -# MDP settings -## - - -@configclass -class CommandsCfg: - """Command specifications for the MDP.""" - - object_pose = mdp.ReorientCommandCfg( - asset_name="object", - init_pos_offset=(0.0, 0.0, -0.04), - update_goal_on_success=True, - orientation_success_threshold=0.1, - make_quat_unique=False, - marker_pos_offset=(-0.2, -0.06, 0.08), - debug_vis=True, - ) - - -@configclass -class ActionsCfg: - """Action specifications for the MDP.""" - - joint_pos = mdp.EMAJointPositionToLimitsActionCfg( - asset_name="robot", - joint_names=[".*"], - alpha=0.95, - rescale_to_limits=True, - ) - - -@configclass -class ObservationsCfg: - """Observation specifications for the MDP.""" - - @configclass - class KinematicObsGroupCfg(ObsGroup): - """Observations with full-kinematic state information. - - This does not include acceleration or force information. - """ - - # observation terms (order preserved) - # -- robot terms - joint_pos = ObsTerm(func=mdp.joint_pos_limit_normalized, noise=Gnoise(std=0.005)) - joint_vel = ObsTerm(func=mdp.joint_vel_rel, scale=0.2, noise=Gnoise(std=0.01)) - - # -- object terms - object_pos = ObsTerm( - func=mdp.root_pos_w, noise=Gnoise(std=0.002), params={"asset_cfg": SceneEntityCfg("object")} - ) - object_quat = ObsTerm( - func=mdp.root_quat_w, params={"asset_cfg": SceneEntityCfg("object"), "make_quat_unique": False} - ) - object_lin_vel = ObsTerm( - func=mdp.root_lin_vel_w, noise=Gnoise(std=0.002), params={"asset_cfg": SceneEntityCfg("object")} - ) - object_ang_vel = ObsTerm( - func=mdp.root_ang_vel_w, - scale=0.2, - noise=Gnoise(std=0.002), - params={"asset_cfg": SceneEntityCfg("object")}, - ) - - # -- command terms - goal_pose = ObsTerm(func=mdp.generated_commands, params={"command_name": "object_pose"}) - goal_quat_diff = ObsTerm( - func=mdp.goal_quat_diff, - params={"asset_cfg": SceneEntityCfg("object"), "command_name": "object_pose", "make_quat_unique": False}, - ) - - # -- action terms - last_action = ObsTerm(func=mdp.last_action) - - def __post_init__(self): - self.enable_corruption = True - self.concatenate_terms = True - - @configclass - class NoVelocityKinematicObsGroupCfg(KinematicObsGroupCfg): - """Observations with partial kinematic state information. - - In contrast to the full-kinematic state group, this group does not include velocity information - about the robot joints and the object root frame. This is useful for tasks where velocity information - is not available or has a lot of noise. - """ - - def __post_init__(self): - # call parent post init - super().__post_init__() - # set unused terms to None - self.joint_vel = None - self.object_lin_vel = None - self.object_ang_vel = None - - # observation groups - policy: KinematicObsGroupCfg = KinematicObsGroupCfg() - - -@configclass -class EventCfg: - """Configuration for randomization.""" - - # startup - # -- robot - robot_physics_material = EventTerm( - func=mdp.randomize_rigid_body_material, - mode="startup", - params={ - "asset_cfg": SceneEntityCfg("robot", body_names=".*"), - "static_friction_range": (0.7, 1.3), - "dynamic_friction_range": (0.7, 1.3), - "restitution_range": (0.0, 0.0), - "num_buckets": 250, - }, - ) - robot_scale_mass = EventTerm( - func=mdp.randomize_rigid_body_mass, - mode="startup", - params={ - "asset_cfg": SceneEntityCfg("robot", body_names=".*"), - "mass_distribution_params": (0.95, 1.05), - "operation": "scale", - }, - ) - robot_joint_stiffness_and_damping = EventTerm( - func=mdp.randomize_actuator_gains, - mode="startup", - params={ - "asset_cfg": SceneEntityCfg("robot", joint_names=".*"), - "stiffness_distribution_params": (0.3, 3.0), # default: 3.0 - "damping_distribution_params": (0.75, 1.5), # default: 0.1 - "operation": "scale", - "distribution": "log_uniform", - }, - ) - - # -- object - object_physics_material = EventTerm( - func=mdp.randomize_rigid_body_material, - mode="startup", - params={ - "asset_cfg": SceneEntityCfg("object", body_names=".*"), - "static_friction_range": (0.7, 1.3), - "dynamic_friction_range": (0.7, 1.3), - "restitution_range": (0.0, 0.0), - "num_buckets": 250, - }, - ) - object_scale_mass = EventTerm( - func=mdp.randomize_rigid_body_mass, - mode="startup", - params={ - "asset_cfg": SceneEntityCfg("object"), - "mass_distribution_params": (0.4, 1.6), - "operation": "scale", - }, - ) - - # reset - reset_object = EventTerm( - func=mdp.reset_root_state_uniform, - mode="reset", - params={ - "pose_range": {"x": [-0.01, 0.01], "y": [-0.01, 0.01], "z": [-0.01, 0.01]}, - "velocity_range": {}, - "asset_cfg": SceneEntityCfg("object", body_names=".*"), - }, - ) - reset_robot_joints = EventTerm( - func=mdp.reset_joints_within_limits_range, - mode="reset", - params={ - "position_range": {".*": [0.2, 0.2]}, - "velocity_range": {".*": [0.0, 0.0]}, - "use_default_offset": True, - "operation": "scale", - }, - ) - - -@configclass -class RewardsCfg: - """Reward terms for the MDP.""" - - # -- task - # track_pos_l2 = RewTerm( - # func=mdp.track_pos_l2, - # weight=-10.0, - # params={"object_cfg": SceneEntityCfg("object"), "command_name": "object_pose"}, - # ) - track_orientation_inv_l2 = RewTerm( - func=mdp.track_orientation_inv_l2, - weight=1.0, - params={"object_cfg": SceneEntityCfg("object"), "rot_eps": 0.1, "command_name": "object_pose"}, - ) - success_bonus = RewTerm( - func=mdp.success_bonus, - weight=250.0, - params={"object_cfg": SceneEntityCfg("object"), "command_name": "object_pose"}, - ) - - # -- penalties - joint_vel_l2 = RewTerm(func=mdp.joint_vel_l2, weight=-2.5e-5) - action_l2 = RewTerm(func=mdp.action_l2, weight=-0.0001) - action_rate_l2 = RewTerm(func=mdp.action_rate_l2, weight=-0.01) - - # -- optional penalties (these are disabled by default) - # object_away_penalty = RewTerm( - # func=mdp.is_terminated_term, - # weight=-0.0, - # params={"term_keys": "object_out_of_reach"}, - # ) - - -@configclass -class TerminationsCfg: - """Termination terms for the MDP.""" - - time_out = DoneTerm(func=mdp.time_out, time_out=True) - - max_consecutive_success = DoneTerm( - func=mdp.max_consecutive_success, params={"num_success": 50, "command_name": "object_pose"} - ) - - object_out_of_reach = DoneTerm(func=mdp.object_away_from_robot, params={"threshold": 0.3}) - - # object_out_of_reach = DoneTerm( - # func=mdp.object_away_from_goal, params={"threshold": 0.24, "command_name": "object_pose"} - # ) - - -## -# Environment configuration -## - - -@configclass -class ReorientObjectEnvCfg(ManagerBasedRLEnvCfg): - """Configuration for the in hand reorientation environment.""" - - # Scene settings - scene: ReorientObjectSceneCfg = ReorientObjectSceneCfg(num_envs=8192, env_spacing=0.6) - # Simulation settings - sim: SimulationCfg = SimulationCfg( - physics_material=RigidBodyMaterialCfg( - static_friction=1.0, - dynamic_friction=1.0, - ), - physics=PhysxCfg( - bounce_threshold_velocity=0.2, - gpu_max_rigid_contact_count=2**20, - gpu_max_rigid_patch_count=2**23, - ), - ) - # Basic settings - observations: ObservationsCfg = ObservationsCfg() - actions: ActionsCfg = ActionsCfg() - commands: CommandsCfg = CommandsCfg() - # MDP settings - rewards: RewardsCfg = RewardsCfg() - terminations: TerminationsCfg = TerminationsCfg() - events: EventCfg = EventCfg() - - def __post_init__(self): - """Post initialization.""" - # general settings - self.decimation = 4 - self.episode_length_s = 20.0 - # simulation settings - self.sim.dt = 1.0 / 120.0 - self.sim.render_interval = self.decimation - # change viewer settings - self.viewer.eye = (2.0, 2.0, 2.0) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/utils.py b/source/isaaclab_tasks/isaaclab_tasks/core/utils.py index 3a3790615ed9..14fa1c490553 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/utils.py @@ -123,10 +123,7 @@ def random_xy_rotation(count: int, device: str | torch.device) -> torch.Tensor: random_values = math_utils.sample_uniform(-1.0, 1.0, (count, 2), device=device) x_unit = torch.tensor([1.0, 0.0, 0.0], device=device).repeat(count, 1) y_unit = torch.tensor([0.0, 1.0, 0.0], device=device).repeat(count, 1) - return math_utils.quat_mul( - math_utils.quat_from_angle_axis(random_values[:, 0] * torch.pi, x_unit), - math_utils.quat_from_angle_axis(random_values[:, 1] * torch.pi, y_unit), - ) + return randomize_rotation(random_values[:, 0], random_values[:, 1], x_unit, y_unit) @torch.jit.script diff --git a/source/isaaclab_tasks/test/core/test_reorient_value_parity.py b/source/isaaclab_tasks/test/core/test_reorient_value_parity.py new file mode 100644 index 000000000000..5f064f20a868 --- /dev/null +++ b/source/isaaclab_tasks/test/core/test_reorient_value_parity.py @@ -0,0 +1,71 @@ +# 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 + +"""Value parity between the Direct and manager-based reorientation configurations. + +Covers the values that define the task rather than how it is solved: timing, the +success tolerance, and the termination thresholds. Reward weights are deliberately +excluded, since each workflow tunes them against its own RL agent configuration. + +This module is the check that the configuration comments refer to, so drift on +either side fails here rather than silently changing what a manager task trains on. +""" + +import pytest + +from isaaclab_tasks.core.reorient.config.allegro_hand.allegro_hand_direct_env_cfg import AllegroHandEnvCfg +from isaaclab_tasks.core.reorient.config.allegro_hand.allegro_hand_manager_env_cfg import AllegroCubeEnvCfg +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_env_cfg import ( + ShadowHandEnvCfg, + ShadowHandOpenAIEnvCfg, +) +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_manager_env_cfg import ShadowHandManagerEnvCfg +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_openai_manager_env_cfg import ( + ShadowHandOpenAIManagerEnvCfg, +) + + +@pytest.mark.parametrize( + "direct_cls, manager_cls", + [ + pytest.param(AllegroHandEnvCfg, AllegroCubeEnvCfg, id="allegro"), + pytest.param(ShadowHandEnvCfg, ShadowHandManagerEnvCfg, id="shadow"), + pytest.param(ShadowHandOpenAIEnvCfg, ShadowHandOpenAIManagerEnvCfg, id="shadow-openai"), + ], +) +def test_manager_config_matches_direct_values(direct_cls, manager_cls): + direct, manager = direct_cls(), manager_cls() + + assert (manager.decimation, manager.episode_length_s, manager.sim.dt) == ( + direct.decimation, + direct.episode_length_s, + direct.sim.dt, + ) + assert manager.commands.object_pose.orientation_success_threshold == pytest.approx(direct.success_tolerance) + assert manager.terminations.object_out_of_reach.params["threshold"] == pytest.approx(direct.fall_dist) + assert manager.commands.object_pose.success_count_threshold == direct.success_count_threshold + # The Direct tasks fold the streak cap into their time-out signal. + streak_cap = getattr(manager.terminations, "max_consecutive_success", None) + assert (0 if streak_cap is None else streak_cap.params["num_success"]) == direct.max_consecutive_success + assert streak_cap is None or streak_cap.time_out + + +def test_handover_manager_config_matches_direct_values(): + """The handover manager task mirrors its Direct counterpart's task-defining values.""" + from isaaclab_tasks.core.handover.handover_env_cfg import HandoverEnvCfg + from isaaclab_tasks.core.handover.handover_manager_env_cfg import HandoverManagerEnvCfg + + direct, manager = HandoverEnvCfg(), HandoverManagerEnvCfg() + + assert (manager.decimation, manager.episode_length_s, manager.sim.dt) == ( + direct.decimation, + direct.episode_length_s, + direct.sim.dt, + ) + assert manager.sim.render_interval == direct.sim.render_interval + assert manager.commands.object_pose.success_distance_threshold == pytest.approx(direct.success_distance_threshold) + assert manager.rewards.goal_distance.params["distance_scale"] == pytest.approx(direct.dist_reward_scale) + for action_term in (manager.actions.right_hand, manager.actions.left_hand): + assert action_term.alpha == pytest.approx(direct.act_moving_average) 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