diff --git a/docs/source/_static/css/environment-browser.js b/docs/source/_static/css/environment-browser.js index 02dd2940055e..e2600acb9271 100644 --- a/docs/source/_static/css/environment-browser.js +++ b/docs/source/_static/css/environment-browser.js @@ -34,11 +34,14 @@ ["Isaac-Reach-Franka", "rl_games,rsl_rl,skrl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", "diffik,diffik_abs,joint_pos,newton_ik"], ["Isaac-Reach-Franka-OSC", "rsl_rl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], ["Isaac-Reach-UR10", "rl_games,rsl_rl,skrl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], - ["Isaac-Reorient-Cube-Allegro", "rl_games,rsl_rl,skrl", "", "", ""], + ["Isaac-Reorient-Cube-Allegro", "rl_games,rsl_rl,skrl", "isaacsim_physx,newton_mjwarp,ovphysx", "", ""], ["Isaac-Reorient-Cube-Allegro-Direct", "rl_games,rsl_rl,skrl", "isaacsim_physx,newton_mjwarp,ovphysx", "", ""], + ["Isaac-Reorient-Cube-Shadow", "rl_games,rsl_rl,skrl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], ["Isaac-Reorient-Cube-Shadow-Camera-Direct", "rl_games,rsl_rl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "isaacsim_rtx,newton_renderer,ovrtx", "albedo,depth,full,rgb,semantic_segmentation,simple_shading_constant_diffuse,simple_shading_diffuse_mdl,simple_shading_full_mdl"], ["Isaac-Reorient-Cube-Shadow-Direct", "rl_games,rsl_rl,skrl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], + ["Isaac-Reorient-Cube-Shadow-OpenAI-FF", "rl_games,rsl_rl,skrl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], ["Isaac-Reorient-Cube-Shadow-OpenAI-FF-Direct", "rl_games,rsl_rl,skrl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], + ["Isaac-Reorient-Cube-Shadow-OpenAI-LSTM", "rl_games,rsl_rl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], ["Isaac-Reorient-Cube-Shadow-OpenAI-LSTM-Direct", "rl_games,rsl_rl", "isaacsim_physx,newton_kamino,newton_mjwarp,ovphysx", "", ""], ["Isaac-Reorient-Franka", "rsl_rl", "isaacsim_physx,newton_mjwarp", "", "cube,shapes"], ["Isaac-Reorient-KukaAllegro", "rsl_rl", "isaacsim_physx,newton_mjwarp,ovphysx", "", "cube,shapes"], diff --git a/docs/source/overview/environments.rst b/docs/source/overview/environments.rst index b8d38a83dccc..30d0c9aede98 100644 --- a/docs/source/overview/environments.rst +++ b/docs/source/overview/environments.rst @@ -1152,11 +1152,15 @@ including disabling runtime perturbations used for training. * - Isaac-Reorient-Cube-Allegro - Manager Based - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - - + - **physics=** ``isaacsim_physx``, ``newton_mjwarp``, ``ovphysx`` * - Isaac-Reorient-Cube-Allegro-Direct - Direct - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - **physics=** ``isaacsim_physx``, ``newton_mjwarp``, ``ovphysx`` + * - Isaac-Reorient-Cube-Shadow + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + - **physics=** ``isaacsim_physx``, ``newton_kamino``, ``newton_mjwarp``, ``ovphysx`` * - Isaac-Reorient-Cube-Shadow-Camera-Direct - Direct - **rl_games** (PPO), **rsl_rl** (PPO) @@ -1167,10 +1171,18 @@ including disabling runtime perturbations used for training. - Direct - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - **physics=** ``isaacsim_physx``, ``newton_kamino``, ``newton_mjwarp``, ``ovphysx`` + * - Isaac-Reorient-Cube-Shadow-OpenAI-FF + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + - **physics=** ``isaacsim_physx``, ``newton_kamino``, ``newton_mjwarp``, ``ovphysx`` * - Isaac-Reorient-Cube-Shadow-OpenAI-FF-Direct - Direct - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) - **physics=** ``isaacsim_physx``, ``newton_kamino``, ``newton_mjwarp``, ``ovphysx`` + * - Isaac-Reorient-Cube-Shadow-OpenAI-LSTM + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO) + - **physics=** ``isaacsim_physx``, ``newton_kamino``, ``newton_mjwarp``, ``ovphysx`` * - Isaac-Reorient-Cube-Shadow-OpenAI-LSTM-Direct - Direct - **rl_games** (PPO), **rsl_rl** (PPO) diff --git a/docs/source/setup/quickstart_details.rst b/docs/source/setup/quickstart_details.rst index cfc7f59156df..0c5d51662ab2 100644 --- a/docs/source/setup/quickstart_details.rst +++ b/docs/source/setup/quickstart_details.rst @@ -62,7 +62,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_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..0e0b29c7aee7 --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part08.major.rst @@ -0,0 +1,61 @@ +Added +^^^^^ + +* Added manager-based counterparts for the Shadow cube reorientation task and its + OpenAI FF/LSTM 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 Allegro and Shadow manager + environments. +* Added Newton and OvPhysX presets to the manager-based reorientation environments, + selectable with ``physics=``. +* Added a Direct-versus-manager value-parity test covering timing, success tolerance, + fall distance, and the consecutive-success cap. + +Changed +^^^^^^^ + +* **Breaking:** Changed ``Metrics/success_rate`` on the manager-based reorientation + tasks to a per-episode success bit drawn at + ``ReorientCommandCfg.success_count_threshold``, matching 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 and on for the OpenAI variants, matching each task's Direct + counterpart. Set ``enable_domain_randomization`` to restore it. +* **Breaking:** Changed the manager-based Allegro environment to match the Direct + observation, action, reset, and termination contracts, and to use the same agent + configurations. The observation space changes size, so existing manager checkpoints + must be retrained. ``rl_games_manager_ppo_cfg.yaml``, ``skrl_manager_ppo_cfg.yaml`` + and ``AllegroCubePPORunnerCfg`` are gone; use ``rl_games_ppo_cfg.yaml``, + ``skrl_ppo_cfg.yaml`` and ``AllegroHandPPORunnerCfg``. +* **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 one. +* **Breaking:** Changed the in-hand cube to a rigid body on the Newton backend, where + it was declared as an articulation with no joints or actuators. Code resolving the + object through :class:`~isaaclab.assets.Articulation` must use + :class:`~isaaclab.assets.RigidObject`. +* 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 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 per-robot fields on the Direct + configurations. +* Removed the handover ``EventCfg``, which was never wired into + ``HandoverEnvCfg.events``. +* Removed the ``clone_in_fabric`` settings from the reorientation scenes. The flag no + longer reaches the replicator, so the value had no effect. + +Fixed +^^^^^ + +* Fixed the manager-based reorientation tasks not reporting ``Metrics/success_rate``. 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/handover_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py index 53aa2d740159..aa7a47ff838d 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], 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..9b3e1d36e1c7 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,20 +48,7 @@ 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( - 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="", - ) - ovphysx = RigidObjectCfg( + 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", @@ -67,17 +56,14 @@ class ObjectCfg(PresetCfg): kinematic_enabled=False, disable_gravity=False, enable_gyroscopic_forces=True, - solver_position_iteration_count=8, - solver_velocity_iteration_count=0, - sleep_threshold=0.005, - stabilization_threshold=0.0025, - max_depenetration_velocity=1000.0, ), + collision_props=sim_utils.CollisionPropertiesCfg(), mass_props=sim_utils.MassPropertiesCfg(density=400.0), scale=(1.2, 1.2, 1.2), ), - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.17, 0.56), rot=(0.0, 0.0, 0.0, 1.0)), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.17, 0.565), rot=(0.0, 0.0, 0.0, 1.0)), ) + ovphysx = physx # OvPhysX is PhysX-based; it shares the PhysX cube verbatim. isaacsim_physx = physx default = newton_mjwarp @@ -103,9 +89,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..b8e68408ccb2 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, @@ -30,11 +53,11 @@ ) gym.register( - id="Isaac-Reorient-Cube-Shadow-OpenAI-FF-Direct", - entry_point=reorient_direct_entry, + 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_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_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", @@ -42,38 +65,35 @@ ) gym.register( - id="Isaac-Reorient-Cube-Shadow-OpenAI-LSTM-Direct", + id="Isaac-Reorient-Cube-Shadow-OpenAI-FF-Direct", entry_point=reorient_direct_entry, disable_env_checker=True, kwargs={ "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", + "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", }, ) -# ------- -# 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", + entry_point="isaaclab.envs:ManagerBasedRLEnv", 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_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", }, ) 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-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: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", + "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", }, ) 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..849995571f9a 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,21 +183,23 @@ 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", + rigid_props=sim_utils.RigidBodyPropertiesCfg( + kinematic_enabled=False, + disable_gravity=False, + enable_gyroscopic_forces=True, + ), + collision_props=sim_utils.CollisionPropertiesCfg(), mass_props=sim_utils.MassPropertiesCfg(density=400.0), 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 +229,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 025a5c692b3b..2afe96d33629 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 @@ -18,7 +18,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 +41,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) 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 bbc5a11c3f51..a852caf72975 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 @@ -93,9 +93,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,13 +122,9 @@ 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.isaacsim_physx @@ -162,7 +158,7 @@ def validate_config(self): "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), " + "(e.g. use IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct), " "or choose a data type that includes colour, e.g. presets=rgb." ) @@ -175,22 +171,3 @@ def play_mode(self): # inference for 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..13881dfbaea5 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,19 @@ # 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", "goal_quat_diff", "success_bonus", "track_orientation_inv_l2", @@ -14,17 +25,32 @@ __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 ( + 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..869bff657db3 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,19 @@ 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 JointWrenchSensor from .commands import ReorientCommand @@ -25,6 +28,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 +101,139 @@ 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) + - # obtain the orientations - goal_quat_w = command_term.command[:, 3:7] - asset_quat_w = asset.data.root_quat_w.torch +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) - # 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 + +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. + + 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) + + 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) 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 c2c47e66491b..0ec95e42898d 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..f6947d8ac420 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/utils.py @@ -123,15 +123,24 @@ 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 -def randomize_rotation(rand0, rand1, x_unit_tensor, y_unit_tensor): - """Compose ``[-pi, pi]``-scaled random X- and Y-axis rotations into ``(x, y, z, w)`` quaternions.""" +def randomize_rotation( + rand0: torch.Tensor, rand1: torch.Tensor, x_unit_tensor: torch.Tensor, y_unit_tensor: torch.Tensor +) -> torch.Tensor: + """Compose ``[-pi, pi]``-scaled random X- and Y-axis rotations into ``(x, y, z, w)`` quaternions. + + Args: + rand0: Rotation amounts about the X axis in ``[-1, 1]``, scaled to ``[-pi, pi]`` [rad]. + rand1: Rotation amounts about the Y axis in ``[-1, 1]``, scaled to ``[-pi, pi]`` [rad]. + x_unit_tensor: Per-sample X axis, shape ``(count, 3)``. + y_unit_tensor: Per-sample Y axis, shape ``(count, 3)``. + + Returns: + Composed ``(x, y, z, w)`` unit quaternions, shape ``(count, 4)``. + """ return quat_mul( quat_from_angle_axis(rand0 * np.pi, x_unit_tensor), quat_from_angle_axis(rand1 * np.pi, y_unit_tensor) ) 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..43bba092f781 --- /dev/null +++ b/source/isaaclab_tasks/test/core/test_reorient_value_parity.py @@ -0,0 +1,52 @@ +# 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 diff --git a/source/isaaclab_visualizers/changelog.d/task-cleanup-dex-part08.skip b/source/isaaclab_visualizers/changelog.d/task-cleanup-dex-part08.skip new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/source/isaaclab_visualizers/test/visualizer_integration_utils.py b/source/isaaclab_visualizers/test/visualizer_integration_utils.py index 125141920e72..800ae6ccae01 100644 --- a/source/isaaclab_visualizers/test/visualizer_integration_utils.py +++ b/source/isaaclab_visualizers/test/visualizer_integration_utils.py @@ -50,6 +50,7 @@ from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_env_cfg import ShadowHandEnvCfg from isaaclab_tasks.core.reorient.reorient_direct_env import ReorientDirectEnv from isaaclab_tasks.core.velocity.config.anymal_d.flat_env_cfg import AnymalDFlatEnvCfg +from isaaclab_tasks.utils.hydra import PresetCfg # Debugging mode configs. @@ -1524,12 +1525,17 @@ def _make_shadow_hand_env( """ env_cfg = copy.deepcopy(ShadowHandEnvCfg()) preset_key = "newton_mjwarp" if backend_kind == "newton" else "physx" - env_cfg.sim.physics = getattr(env_cfg.sim.physics, preset_key) - env_cfg.scene = getattr(env_cfg.scene, preset_key) - env_cfg.robot_cfg = getattr(env_cfg.robot_cfg, preset_key) - env_cfg.object_cfg = getattr(env_cfg.object_cfg, preset_key) + + def resolve(cfg): + """Pick the backend variant, leaving fields that do not vary by backend alone.""" + return getattr(cfg, preset_key) if isinstance(cfg, PresetCfg) else cfg + + env_cfg.sim.physics = resolve(env_cfg.sim.physics) + env_cfg.scene = resolve(env_cfg.scene) + env_cfg.robot_cfg = resolve(env_cfg.robot_cfg) + env_cfg.object_cfg = resolve(env_cfg.object_cfg) if env_cfg.events is not None: - env_cfg.events = getattr(env_cfg.events, preset_key) + env_cfg.events = resolve(env_cfg.events) env_cfg.scene.num_envs = ( _SHADOW_HAND_TILED_CAMERA_INTEGRATION_NUM_ENVS if tiled_camera else _SHADOW_HAND_INTEGRATION_NUM_ENVS )