From a54cfbb0b2f662975c96e320cf6b0cffd4b05d64 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Mon, 3 Aug 2026 03:28:39 -0700 Subject: [PATCH 1/9] Initialize the manager reset buffer before the managers load Manager terms that run during the initial reset had no reset_buf to read: the attribute was only assigned in step(), which has not run yet at that point. Allocate it alongside episode_length_buf, before the managers load. --- source/isaaclab/changelog.d/task-cleanup-dex-part08.rst | 6 ++++++ source/isaaclab/isaaclab/envs/manager_based_rl_env.py | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 source/isaaclab/changelog.d/task-cleanup-dex-part08.rst 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 From 40096f485a163c89269d89b9148bc0f34bbaa407 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Mon, 3 Aug 2026 03:28:49 -0700 Subject: [PATCH 2/9] Add the reorientation manager MDP terms Provide the observation, action, event, command and termination terms the manager-based reorientation tasks need, alongside a parity test that pins their values against the Direct environment. Terminations evaluate success directly rather than reading the command's metrics, which the command manager only refreshes after the termination phase of step() has already run. --- .../core/reorient/mdp/__init__.pyi | 40 ++++- .../core/reorient/mdp/actions.py | 30 ++++ .../core/reorient/mdp/commands.py | 35 +++-- .../core/reorient/mdp/events.py | 71 +++++++++ .../core/reorient/mdp/noisy_actions.py | 56 +++++++ .../core/reorient/mdp/observations.py | 141 ++++++++++++++++-- .../core/reorient/mdp/terminations.py | 71 ++++++--- .../core/reorient/reorient_direct_env.py | 5 +- .../isaaclab_tasks/core/utils.py | 5 +- .../test/core/test_reorient_value_parity.py | 54 +++++++ 10 files changed, 455 insertions(+), 53 deletions(-) create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/actions.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/events.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/noisy_actions.py create mode 100644 source/isaaclab_tasks/test/core/test_reorient_value_parity.py diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi index 3221965e78a6..9bd01bd20972 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi @@ -4,8 +4,22 @@ # SPDX-License-Identifier: BSD-3-Clause __all__ = [ + "NoisyEMAJointPositionToLimitsAction", + "NoisyEMAJointPositionToLimitsActionCfg", "ReorientCommand", "ReorientCommandCfg", + "ReorientCommand", + "ReorientCommandCfg", + "reset_reorient_state", + "fingertip_pos", + "fingertip_quat", + "fingertip_vel", + "fingertip_wrench", + "reorient_last_action", + "openai_policy_observation", + "ShadowHandCameraFeatures", + "shadow_hand_camera_cached_features", + "shadow_hand_goal_keypoints", "goal_quat_diff", "success_bonus", "track_orientation_inv_l2", @@ -14,17 +28,35 @@ __all__ = [ "reorient_reward", "max_consecutive_success", "object_away_from_goal", - "object_away_from_robot", + "reorient_timeout", ] from .commands import ReorientCommand, ReorientCommandCfg -from .observations import goal_quat_diff +from .events import reset_reorient_state +from .noisy_actions import NoisyEMAJointPositionToLimitsAction +from .actions import NoisyEMAJointPositionToLimitsActionCfg +from .observations import ( + ShadowHandCameraFeatures, + shadow_hand_camera_cached_features, + shadow_hand_goal_keypoints, + fingertip_pos, + fingertip_quat, + fingertip_vel, + fingertip_wrench, + goal_quat_diff, + openai_policy_observation, + reorient_last_action, +) from .rewards import ( - reorient_reward, evaluate_reorient_success, + reorient_reward, success_bonus, track_orientation_inv_l2, track_pos_l2, ) -from .terminations import max_consecutive_success, object_away_from_goal, object_away_from_robot +from .terminations import ( + max_consecutive_success, + object_away_from_goal, + reorient_timeout, +) from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/actions.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/actions.py new file mode 100644 index 000000000000..718c4cdfe9e5 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/actions.py @@ -0,0 +1,30 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Action configurations for the reorientation task family.""" + +from __future__ import annotations + +from dataclasses import MISSING +from typing import TYPE_CHECKING + +from isaaclab.envs.mdp import EMAJointPositionToLimitsActionCfg +from isaaclab.utils.configclass import configclass +from isaaclab.utils.noise import NoiseModelCfg + +if TYPE_CHECKING: + from .noisy_actions import NoisyEMAJointPositionToLimitsAction + + +@configclass +class NoisyEMAJointPositionToLimitsActionCfg(EMAJointPositionToLimitsActionCfg): + """EMA joint action configuration with Direct-compatible stateful noise.""" + + class_type: type[NoisyEMAJointPositionToLimitsAction] | str = ( + "{DIR}.noisy_actions:NoisyEMAJointPositionToLimitsAction" + ) + + noise_model: NoiseModelCfg = MISSING + """Stateful noise applied to incoming normalized actions.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py index 77040f635282..bcd205832991 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py @@ -80,6 +80,10 @@ def __init__(self, cfg: ReorientCommandCfg, env: ManagerBasedRLEnv): # -- per-attempt success accounting: each success-driven resample completes one attempt; # the trailing attempt at episode end counts as one unsuccessful attempt. 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 @@ -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,6 +218,9 @@ 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.""" 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_direct_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py index a2ada7e457a1..266f9786b5e0 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_direct_env.py @@ -21,7 +21,6 @@ from isaaclab.utils.math import quat_conjugate, quat_mul, sample_uniform, saturate, scale_transform, unscale_transform from isaaclab_tasks.core.reorient.mdp.rewards import evaluate_reorient_success, reorient_reward -from isaaclab_tasks.core.reorient.reorient_common import GOAL_MARKER_POSITION, IN_HAND_POS_OFFSET from isaaclab_tasks.core.utils import EpisodeErrorRecorder, randomize_rotation, sample_joint_positions_within_limits if TYPE_CHECKING: @@ -64,11 +63,11 @@ def __init__(self, cfg: AllegroHandEnvCfg | ShadowHandEnvCfg, render_mode: str | # -- goal and success state -- # in-hand target = object default position + shared offset (mirrors ReorientCommand) self.in_hand_pos = self.object.data.default_root_pose.torch[:, 0:3].clone() - self.in_hand_pos += torch.tensor(IN_HAND_POS_OFFSET, dtype=torch.float, device=self.device) + self.in_hand_pos += torch.tensor(self.cfg.in_hand_pos_offset, dtype=torch.float, device=self.device) self.goal_rot = torch.zeros((self.num_envs, 4), dtype=torch.float, device=self.device) self.goal_rot[:, 3] = 1.0 # identity quaternion in (x, y, z, w) layout self.goal_pos = torch.zeros((self.num_envs, 3), dtype=torch.float, device=self.device) - self.goal_pos[:, :] = torch.tensor(GOAL_MARKER_POSITION, device=self.device) + self.goal_pos[:, :] = torch.tensor(self.cfg.goal_marker_position, device=self.device) self.reset_goal_buf = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) self.successes = torch.zeros(self.num_envs, dtype=torch.float, device=self.device) self.consecutive_successes = torch.zeros(1, dtype=torch.float, device=self.device) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/utils.py b/source/isaaclab_tasks/isaaclab_tasks/core/utils.py index 3a3790615ed9..14fa1c490553 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/utils.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/utils.py @@ -123,10 +123,7 @@ def random_xy_rotation(count: int, device: str | torch.device) -> torch.Tensor: random_values = math_utils.sample_uniform(-1.0, 1.0, (count, 2), device=device) x_unit = torch.tensor([1.0, 0.0, 0.0], device=device).repeat(count, 1) y_unit = torch.tensor([0.0, 1.0, 0.0], device=device).repeat(count, 1) - return math_utils.quat_mul( - math_utils.quat_from_angle_axis(random_values[:, 0] * torch.pi, x_unit), - math_utils.quat_from_angle_axis(random_values[:, 1] * torch.pi, y_unit), - ) + return randomize_rotation(random_values[:, 0], random_values[:, 1], x_unit, y_unit) @torch.jit.script diff --git a/source/isaaclab_tasks/test/core/test_reorient_value_parity.py b/source/isaaclab_tasks/test/core/test_reorient_value_parity.py new file mode 100644 index 000000000000..edcafc083ce9 --- /dev/null +++ b/source/isaaclab_tasks/test/core/test_reorient_value_parity.py @@ -0,0 +1,54 @@ +# 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) + # Both workflows publish ``Metrics/success_rate``; the curves only compare if the + # episode-success bit is drawn at the same goal count. + + # 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 From 6dc299d87abd375f2bfc758432ebc482fb4915b7 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Mon, 3 Aug 2026 03:28:58 -0700 Subject: [PATCH 3/9] Add the manager-based reorientation task configurations Register manager counterparts for the Allegro, Shadow and OpenAI Shadow reorientation tasks, each configured in its own per-robot module. Replace the single shared reorient_manager_env_cfg with those per-robot configurations: the shared base could only describe the Allegro task, so every Shadow variant overrode most of what it inherited. --- .../task-cleanup-dex-part08.major.rst | 52 +++ .../isaaclab_tasks/core/reorient/__init__.py | 7 +- .../allegro_hand/allegro_hand_common.py | 7 +- .../allegro_hand_direct_env_cfg.py | 15 +- .../allegro_hand_manager_env_cfg.py | 294 ++++++++++++++- .../reorient/config/shadow_hand/__init__.py | 60 ++- .../config/shadow_hand/shadow_hand_common.py | 53 +-- .../shadow_hand_direct_camera_env.py | 3 +- .../shadow_hand_direct_camera_env_cfg.py | 27 +- .../shadow_hand/shadow_hand_direct_env_cfg.py | 20 +- .../shadow_hand_manager_env_cfg.py | 205 +++++++++++ .../shadow_hand_openai_manager_env_cfg.py | 197 ++++++++++ .../core/reorient/reorient_common.py | 32 -- .../core/reorient/reorient_manager_env_cfg.py | 348 ------------------ 14 files changed, 825 insertions(+), 495 deletions(-) create mode 100644 source/isaaclab_tasks/changelog.d/task-cleanup-dex-part08.major.rst create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_openai_manager_env_cfg.py delete mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_common.py delete mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/reorient/reorient_manager_env_cfg.py 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..0554edb23b8d --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part08.major.rst @@ -0,0 +1,52 @@ +Added +^^^^^ + +* Added manager-based counterparts for the Shadow cube reorientation task and + its OpenAI FF/LSTM observation variants, alongside the existing Allegro + manager task. +* Added :class:`~isaaclab_tasks.core.reorient.mdp.reorient_timeout`, which + restarts the episode timer on every goal reach so OpenAI-variant episodes + extend across success streaks. +* Added ``enable_domain_randomization`` to the manager-based Allegro + environment for turning off its startup randomization terms. +* Added Newton and OvPhysx physics presets to the manager-based reorientation + environments, selectable with ``physics=``. +* Added a Direct-versus-manager value-parity check covering timing, success + tolerance, fall distance, and the consecutive-success cap. + +Changed +^^^^^^^ + +* **Breaking:** Changed the manager-based Allegro reorientation environment to + match the Direct observation, action, reset, and termination contracts. The + observation space changes size, so existing manager checkpoints cannot be + loaded and must be retrained. +* **Breaking:** Changed the Shadow Hand reorientation tasks to apply the same + randomization on every physics backend. PhysX now also randomizes joint gains, + object mass, and gravity, and Newton now also randomizes contact materials. + Policies trained before this change must be retrained. +* **Breaking:** Moved the Shadow Hand camera benchmark task to the contributed + tasks as ``IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct``. The + released ``Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct`` identifier + still resolves and warns; switch to the new identifier. +* Renamed the per-robot scene constants to name what they hold: ``ROBOT_CFG`` + becomes ``SHADOW_HAND_ROBOT_CFG`` or ``ALLEGRO_HAND_ROBOT_CFG``, + ``OBJECT_CFG`` becomes ``CUBE_CFG``, and ``ObjectCfg`` becomes ``CubeCfg``. + +Removed +^^^^^^^ + +* Removed ``ReorientObjectEnvCfg`` and the shared reorientation observation, + action, and command configurations. Each manager task now declares its own; + derive from :class:`~isaaclab.envs.ManagerBasedRLEnvCfg` directly. +* Removed ``reorient_common``. Its constants are declared by the tasks that use + them, and the in-hand offset and goal-marker position are now per-robot fields + on the Direct configurations. + +Fixed +^^^^^ + +* Fixed the manager-based reorientation tasks not reporting + ``Metrics/success_rate``. +* Fixed manager ``Metrics/success_rate`` counting goal attempts rather than the + per-episode success bit the Direct tasks report. diff --git a/source/isaaclab_tasks/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/allegro_hand_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py index 2605d401c76e..3023993391ff 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 @@ -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( @@ -103,9 +105,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..f5ef10d68fb1 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 @@ -74,6 +73,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..a12b9272c04c 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,26 +3,294 @@ # # 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, +) +from isaaclab_tasks.utils import preset -## -# 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.""" + + # ``clone_in_fabric`` is the only backend-varying field: PhysX/OvPhysX use Fabric + # cloning for speed, Newton does not support it. + clone_in_fabric = preset(default=False, physx=True, ovphysx=True, newton_mjwarp=False) + + 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.""" + + @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. + + Gated by :attr:`AllegroCubeEnvCfg.enable_domain_randomization`. + """ + + 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_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", + }, + ) + - # 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 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 = True + """Apply the startup domain-randomization terms. + + Set it on the configuration to disable them: ``__post_init__`` reads it while building + the configuration, before Hydra applies command-line overrides, so + ``env.enable_domain_randomization=false`` has no effect. Individual terms can still be + disabled from the command line, for example ``env.events.robot_scale_mass=null``. + 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 def play_mode(self): # play-mode overrides of parent 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..874f9db6a555 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( @@ -242,8 +227,8 @@ class PhysicsCfg(PresetCfg): # Scene pieces shared verbatim by the manager-based variants. -ROBOT_CFG = ShadowHandRobotCfg() -OBJECT_CFG = ObjectCfg() +SHADOW_HAND_ROBOT_CFG = ShadowHandRobotCfg() +CUBE_CFG = CubeCfg() GOAL_OBJECT_CFG = VisualizationMarkersCfg( prim_path="/Visuals/goal_marker", diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py index d424b46400e5..b56774c03f09 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 19292a97429f..f7d7a9e35e34 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 @@ -162,7 +162,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 +175,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..c0353c3e30eb 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,12 +12,12 @@ 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, @@ -55,18 +55,18 @@ 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) @@ -96,6 +96,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 +119,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 +142,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..8ba024b6a6c9 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_manager_env_cfg.py @@ -0,0 +1,205 @@ +# 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, preset + +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.""" + + # ``clone_in_fabric`` is the only backend-varying field: PhysX/OvPhysX use Fabric + # cloning for speed, Newton does not support it. + clone_in_fabric = preset(default=False, physx=True, ovphysx=True, newton_mjwarp=False) + + 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() + + viewer: ViewerCfg = ViewerCfg(eye=(2.0, 2.0, 2.0)) 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..f50e4da9a6b6 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_openai_manager_env_cfg.py @@ -0,0 +1,197 @@ +# 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() + + viewer: ViewerCfg = ViewerCfg(eye=(2.0, 2.0, 2.0)) 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_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) From 445ad89c7f9f9257ff62a9ef30eeb9bebd446529 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Mon, 3 Aug 2026 03:29:10 -0700 Subject: [PATCH 4/9] Move the Shadow camera benchmark task to the contributed tasks The benchmark variant exists to measure rendering throughput rather than to train a policy, so it belongs alongside the other contributed tasks. Keep the released task ID working as a deprecated alias. --- docs/source/setup/quickstart_details.rst | 2 +- .../contrib/reorient/__init__.py | 6 +++ .../contrib/reorient/config/__init__.py | 6 +++ .../reorient/config/shadow_hand/__init__.py | 41 +++++++++++++++++++ .../shadow_hand_camera_benchmark_env_cfg.py | 30 ++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/shadow_hand/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/shadow_hand/shadow_hand_camera_benchmark_env_cfg.py diff --git a/docs/source/setup/quickstart_details.rst b/docs/source/setup/quickstart_details.rst index ad4cc9663d3f..3176c31782f2 100644 --- a/docs/source/setup/quickstart_details.rst +++ b/docs/source/setup/quickstart_details.rst @@ -57,7 +57,7 @@ options (observation modes, camera configs, etc.). They fold into Hydra override # OVRTX rendering (kit-less, no Kit visualizer) uv run isaaclab train --rl_library rsl_rl \ - --task=Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct \ + --task=IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct \ --num_envs=16 --max_iterations=10 \ physics=newton_mjwarp renderer=ovrtx presets=simple_shading_diffuse_mdl diff --git a/source/isaaclab_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..949475cab7e5 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/contrib/reorient/config/shadow_hand/__init__.py @@ -0,0 +1,41 @@ +# 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 gymnasium.envs.registration import EnvSpec, registry + +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", + }, +) + +# Retain the released ID as a deprecated alias by inserting its spec directly. +registry.update( + { + "Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct": EnvSpec( + id="Isaac-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", + "deprecated": {"alias": "--task IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct"}, + }, + ), + } +) 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) From 56e855879b06fca3a4bd8fe24d4c729191e08b56 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Mon, 3 Aug 2026 16:12:14 -0700 Subject: [PATCH 5/9] Align manager reorient tasks with Direct counterparts The manager-based reorientation tasks were not comparable to the Direct environments they mirror: they reported success differently, randomized physics the Direct tasks leave fixed, and trained under different PPO hyper-parameters. Benchmarking one workflow against the other therefore measured the configuration gap rather than the workflow. Report Metrics/success_rate as a per-episode bit drawn at success_count_threshold, matching how the Direct environments define it and how the other 18 success_rate producers in the tree behave; the per-attempt ratio was the sole outlier. Gate domain randomization behind enable_domain_randomization on the Shadow tasks as the Allegro task already did, and default it to the value its Direct counterpart uses: off for Allegro and Shadow, on for the OpenAI variants. Move the Allegro randomization terms from startup to reset so they follow the same schedule as Shadow's. Point the Allegro manager task at the Direct agent configurations instead of its own copies, so the two run identical hyper-parameters. Drop the unreferenced EventCfg from the handover configuration; its own docstring recorded that it was never wired into HandoverEnvCfg.events. Remove the play_mode override that cleared the time-out termination, the only such line in the tree and residue from an earlier refactor. --- .../task-cleanup-dex-part08.major.rst | 17 ++- .../core/handover/handover_env_cfg.py | 102 ------------------ .../reorient/config/allegro_hand/__init__.py | 10 +- .../agents/rl_games_manager_ppo_cfg.yaml | 90 ---------------- ...ect_ppo_cfg.yaml => rl_games_ppo_cfg.yaml} | 0 .../allegro_hand/agents/rsl_rl_ppo_cfg.py | 33 ------ .../agents/skrl_manager_ppo_cfg.yaml | 85 --------------- ..._direct_ppo_cfg.yaml => skrl_ppo_cfg.yaml} | 0 .../allegro_hand_manager_env_cfg.py | 30 ++---- .../shadow_hand_manager_env_cfg.py | 18 ++++ .../shadow_hand_openai_manager_env_cfg.py | 19 ++++ .../core/reorient/mdp/commands.py | 17 ++- .../test/core/test_reorient_value_parity.py | 4 +- 13 files changed, 82 insertions(+), 343 deletions(-) delete mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/rl_games_manager_ppo_cfg.yaml rename source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/{rl_games_direct_ppo_cfg.yaml => rl_games_ppo_cfg.yaml} (100%) delete mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/skrl_manager_ppo_cfg.yaml rename source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/agents/{skrl_direct_ppo_cfg.yaml => skrl_ppo_cfg.yaml} (100%) 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 index 0554edb23b8d..63dc5472877a 100644 --- a/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part08.major.rst +++ b/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part08.major.rst @@ -7,7 +7,7 @@ Added * Added :class:`~isaaclab_tasks.core.reorient.mdp.reorient_timeout`, which restarts the episode timer on every goal reach so OpenAI-variant episodes extend across success streaks. -* Added ``enable_domain_randomization`` to the manager-based Allegro +* Added ``enable_domain_randomization`` to the manager-based Allegro and Shadow environment for turning off its startup randomization terms. * Added Newton and OvPhysx physics presets to the manager-based reorientation environments, selectable with ``physics=``. @@ -17,6 +17,21 @@ Added Changed ^^^^^^^ +* **Breaking:** Changed the manager-based reorientation tasks to report + ``Metrics/success_rate`` as a per-episode success bit, drawn at + ``ReorientCommandCfg.success_count_threshold`` like the Direct environments, + instead of a per-attempt ratio. Curves from earlier runs are not comparable. +* **Breaking:** Changed domain randomization to default off on the Allegro and + Shadow manager tasks, so they match their Direct counterparts, which randomize + nothing beyond the reset distributions. Set ``enable_domain_randomization`` on + the configuration to restore it. The OpenAI variants keep it enabled, matching + their Direct counterparts. +* **Breaking:** Changed the manager-based Allegro task to use the same RL agent + configurations as its Direct counterpart, so the two are comparable. The + ``rl_games_manager_ppo_cfg.yaml``, ``skrl_manager_ppo_cfg.yaml`` and + ``AllegroCubePPORunnerCfg`` entries are removed; use ``rl_games_ppo_cfg.yaml``, + ``skrl_ppo_cfg.yaml`` and ``AllegroHandPPORunnerCfg``. + * **Breaking:** Changed the manager-based Allegro reorientation environment to match the Direct observation, action, reset, and termination contracts. The observation space changes size, so existing manager checkpoints cannot be 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/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_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_manager_env_cfg.py index a12b9272c04c..2974c40c5f9e 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 @@ -151,7 +151,7 @@ class EventCfg: robot_physics_material = EventTerm( func=mdp.randomize_rigid_body_material, - mode="startup", + mode="reset", params={ "asset_cfg": SceneEntityCfg("robot", body_names=".*"), "static_friction_range": (0.7, 1.3), @@ -162,7 +162,7 @@ class EventCfg: ) robot_scale_mass = EventTerm( func=mdp.randomize_rigid_body_mass, - mode="startup", + mode="reset", params={ "asset_cfg": SceneEntityCfg("robot", body_names=".*"), "mass_distribution_params": (0.95, 1.05), @@ -171,7 +171,7 @@ class EventCfg: ) robot_joint_stiffness_and_damping = EventTerm( func=mdp.randomize_actuator_gains, - mode="startup", + mode="reset", params={ "asset_cfg": SceneEntityCfg("robot", joint_names=".*"), "stiffness_distribution_params": (0.3, 3.0), # default: 3.0 @@ -184,7 +184,7 @@ class EventCfg: # -- object object_physics_material = EventTerm( func=mdp.randomize_rigid_body_material, - mode="startup", + mode="reset", params={ "asset_cfg": SceneEntityCfg("object", body_names=".*"), "static_friction_range": (0.7, 1.3), @@ -195,7 +195,7 @@ class EventCfg: ) object_scale_mass = EventTerm( func=mdp.randomize_rigid_body_mass, - mode="startup", + mode="reset", params={ "asset_cfg": SceneEntityCfg("object"), "mass_distribution_params": (0.4, 1.6), @@ -272,14 +272,13 @@ class AllegroCubeEnvCfg(ManagerBasedRLEnvCfg): terminations: TerminationsCfg = TerminationsCfg() events: EventCfg = EventCfg() - enable_domain_randomization: bool = True - """Apply the startup domain-randomization terms. + enable_domain_randomization: bool = False + """Apply the domain-randomization event terms. - Set it on the configuration to disable them: ``__post_init__`` reads it while building - the configuration, before Hydra applies command-line overrides, so - ``env.enable_domain_randomization=false`` has no effect. Individual terms can still be - disabled from the command line, for example ``env.events.robot_scale_mass=null``. - Changing it requires retraining. + 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)) @@ -291,10 +290,3 @@ def __post_init__(self): self.events.robot_joint_stiffness_and_damping = None self.events.object_physics_material = None self.events.object_scale_mass = None - - def play_mode(self): - # play-mode overrides of parent - super().play_mode() - - # remove termination due to timeouts - self.terminations.time_out = None 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 index 8ba024b6a6c9..b5728b5aaad9 100644 --- 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 @@ -202,4 +202,22 @@ class ShadowHandManagerEnvCfg(ManagerBasedRLEnvCfg): 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 index f50e4da9a6b6..f81e618df43d 100644 --- 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 @@ -194,4 +194,23 @@ class ShadowHandOpenAIManagerEnvCfg(ManagerBasedRLEnvCfg): 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/commands.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/commands.py index bcd205832991..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,8 +77,7 @@ 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. @@ -123,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. @@ -227,6 +227,13 @@ class ReorientCommandCfg(CommandTermCfg): 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/test/core/test_reorient_value_parity.py b/source/isaaclab_tasks/test/core/test_reorient_value_parity.py index edcafc083ce9..43bba092f781 100644 --- a/source/isaaclab_tasks/test/core/test_reorient_value_parity.py +++ b/source/isaaclab_tasks/test/core/test_reorient_value_parity.py @@ -45,9 +45,7 @@ def test_manager_config_matches_direct_values(direct_cls, manager_cls): ) 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) - # Both workflows publish ``Metrics/success_rate``; the curves only compare if the - # episode-success bit is drawn at the same goal count. - + 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 From 8ebd90fe68d3fcd71f94d3ab4c9e82e968e7bf58 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Mon, 3 Aug 2026 18:41:45 -0700 Subject: [PATCH 6/9] Drop dead scene flags and the fake articulation cube Review of the reorientation configurations surfaced three settings that describe the simulator inaccurately. ``clone_in_fabric`` no longer reaches the PhysX replicator: the scene configuration documents it as a deprecated legacy flag and nothing reads it, so ``useFabricForReplication`` is always false. The per-backend presets built around it therefore selected between identical scenes. Removing it leaves the Shadow Direct scene with no backend-varying field, so its six-way preset collapses to a single scene configuration. The in-hand cube was declared as an articulation on the Newton backend with no actuators, no joints and an empty articulation root path. It is a rigid body, nothing addresses it through the articulation API, and the other backends already declare it as one. The camera benchmark task keeps only its contributed identifier. The released alias is removed rather than deprecated, since the task is a rendering-throughput probe rather than a trainable environment. Also drop three names from the reorientation MDP stub that no module defines. ``lazy_export`` builds the package namespace from the stub, so accessing any of them raised ``AttributeError`` and a star import failed. --- .../task-cleanup-dex-part08.major.rst | 11 ++++++++-- .../reorient/config/shadow_hand/__init__.py | 20 ------------------- .../allegro_hand/allegro_hand_common.py | 10 +++------- .../allegro_hand_direct_env_cfg.py | 1 - .../allegro_hand_manager_env_cfg.py | 5 ----- .../config/shadow_hand/shadow_hand_common.py | 10 +++------- .../shadow_hand_direct_camera_env_cfg.py | 10 +++------- .../shadow_hand/shadow_hand_direct_env_cfg.py | 19 +++--------------- .../shadow_hand_manager_env_cfg.py | 6 +----- .../core/reorient/mdp/__init__.pyi | 6 ------ 10 files changed, 22 insertions(+), 76 deletions(-) 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 index 63dc5472877a..ab39ef560494 100644 --- a/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part08.major.rst +++ b/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part08.major.rst @@ -42,8 +42,15 @@ Changed Policies trained before this change must be retrained. * **Breaking:** Moved the Shadow Hand camera benchmark task to the contributed tasks as ``IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct``. The - released ``Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct`` identifier - still resolves and warns; switch to the new identifier. + released ``Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct`` identifier no + longer resolves; use the contributed identifier instead. +* **Breaking:** Changed the in-hand cube to a rigid body on the Newton backend, + where it was previously declared as an articulation with no joints or + actuators. Code that resolved the object through + :class:`~isaaclab.assets.Articulation` must use + :class:`~isaaclab.assets.RigidObject`. +* Removed the ``clone_in_fabric`` settings from the reorientation scenes. The + flag no longer reaches the replicator, so the value had no effect. * Renamed the per-robot scene constants to name what they hold: ``ROBOT_CFG`` becomes ``SHADOW_HAND_ROBOT_CFG`` or ``ALLEGRO_HAND_ROBOT_CFG``, ``OBJECT_CFG`` becomes ``CUBE_CFG``, and ``ObjectCfg`` becomes ``CubeCfg``. 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 index 949475cab7e5..ea3dc061aa17 100644 --- 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 @@ -6,7 +6,6 @@ """Shadow Hand rendering-throughput benchmark task.""" import gymnasium as gym -from gymnasium.envs.registration import EnvSpec, registry from isaaclab_tasks.core.reorient.config.shadow_hand import agents @@ -20,22 +19,3 @@ "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_camera_cfg.yaml", }, ) - -# Retain the released ID as a deprecated alias by inserting its spec directly. -registry.update( - { - "Isaac-Reorient-Cube-Shadow-Camera-Benchmark-Direct": EnvSpec( - id="Isaac-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", - "deprecated": {"alias": "--task IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct"}, - }, - ), - } -) 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 3023993391ff..ef6c443d9828 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/allegro_hand/allegro_hand_common.py @@ -14,7 +14,7 @@ from isaaclab_physx.physics import PhysxCfg import isaaclab.sim as sim_utils -from isaaclab.assets import ArticulationCfg, RigidObjectCfg +from isaaclab.assets import RigidObjectCfg from isaaclab.markers import VisualizationMarkersCfg from isaaclab.physics import PhysxAutoCfg from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR @@ -48,18 +48,14 @@ class CubeCfg(PresetCfg): ), init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.17, 0.56), rot=(0.0, 0.0, 0.0, 1.0)), ) - newton_mjwarp = ArticulationCfg( + newton_mjwarp = RigidObjectCfg( prim_path="/World/envs/env_.*/object", spawn=sim_utils.UsdFileCfg( usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd", mass_props=sim_utils.MassPropertiesCfg(density=400.0), scale=(1.2, 1.2, 1.2), ), - init_state=ArticulationCfg.InitialStateCfg( - pos=(0.0, -0.17, 0.565), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} - ), - actuators={}, - articulation_root_prim_path="", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.17, 0.565), rot=(0.0, 0.0, 0.0, 1.0)), ) ovphysx = RigidObjectCfg( prim_path="/World/envs/env_.*/object", 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 f5ef10d68fb1..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 @@ -54,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 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 2974c40c5f9e..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 @@ -26,7 +26,6 @@ CubeCfg, PhysicsCfg, ) -from isaaclab_tasks.utils import preset from isaaclab_assets.robots.allegro import ALLEGRO_ACTUATED_JOINT_NAMES, ALLEGRO_FINGERTIP_BODY_NAMES @@ -35,10 +34,6 @@ class AllegroCubeSceneCfg(InteractiveSceneCfg): """Shared reorientation scene with the Allegro hand and a ground plane.""" - # ``clone_in_fabric`` is the only backend-varying field: PhysX/OvPhysX use Fabric - # cloning for speed, Newton does not support it. - clone_in_fabric = preset(default=False, physx=True, ovphysx=True, newton_mjwarp=False) - num_envs = 8192 env_spacing = 0.75 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 874f9db6a555..619ebbe230c3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_common.py @@ -183,7 +183,7 @@ class CubeCfg(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", @@ -191,13 +191,9 @@ class CubeCfg(PresetCfg): semantic_tags=[("class", "cube")], scale=(0.9, 0.9, 0.9), ), - init_state=ArticulationCfg.InitialStateCfg( - pos=(0.0, -0.36, 0.535), rot=(0.0, 0.0, 0.0, 1.0), joint_pos={}, joint_vel={} - ), - actuators={}, - articulation_root_prim_path="", + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, -0.36, 0.535), rot=(0.0, 0.0, 0.0, 1.0)), ) - ovphysx = physx # OvPhysX is PhysX-based; use the rigid-body cube, not Newton's articulation + ovphysx = physx # OvPhysX is PhysX-based; it keeps the PhysX cube's density and pose. default = newton_mjwarp newton_kamino = newton_mjwarp 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 f7d7a9e35e34..39fbe3fc578f 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 @@ -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.physx 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 c0353c3e30eb..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 @@ -22,19 +22,13 @@ 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 @@ -69,15 +63,8 @@ class ShadowHandEnvCfg(DirectRLEnvCfg): 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 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 index b5728b5aaad9..5bab033564cd 100644 --- 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 @@ -27,7 +27,7 @@ PhysicsCfg, ShadowHandManagerEventCfg, ) -from isaaclab_tasks.utils import PresetCfg, preset +from isaaclab_tasks.utils import PresetCfg from isaaclab_assets.robots.shadow_hand import SHADOW_ACTUATED_JOINT_NAMES, SHADOW_FINGERTIP_BODY_NAMES @@ -38,10 +38,6 @@ class ShadowHandManagerSceneCfg(InteractiveSceneCfg): """Shared reorientation scene with the Shadow hand and a ground plane.""" - # ``clone_in_fabric`` is the only backend-varying field: PhysX/OvPhysX use Fabric - # cloning for speed, Newton does not support it. - clone_in_fabric = preset(default=False, physx=True, ovphysx=True, newton_mjwarp=False) - num_envs = 8192 env_spacing = 0.75 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 9bd01bd20972..13881dfbaea5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi @@ -17,9 +17,6 @@ __all__ = [ "fingertip_wrench", "reorient_last_action", "openai_policy_observation", - "ShadowHandCameraFeatures", - "shadow_hand_camera_cached_features", - "shadow_hand_goal_keypoints", "goal_quat_diff", "success_bonus", "track_orientation_inv_l2", @@ -36,9 +33,6 @@ from .events import reset_reorient_state from .noisy_actions import NoisyEMAJointPositionToLimitsAction from .actions import NoisyEMAJointPositionToLimitsActionCfg from .observations import ( - ShadowHandCameraFeatures, - shadow_hand_camera_cached_features, - shadow_hand_goal_keypoints, fingertip_pos, fingertip_quat, fingertip_vel, From 4ec270f2175de9ea76a1ba64323fe2a6c2f3160b Mon Sep 17 00:00:00 2001 From: jichuanh Date: Mon, 3 Aug 2026 03:48:57 -0700 Subject: [PATCH 7/9] Move the Newton actuated-joint names to the Shadow Hand asset The handover configuration listed the Newton actuated joints itself, which duplicated knowledge that belongs to the robot asset and would drift the moment the asset gained or renamed a joint. --- .../task-cleanup-dex-part11.minor.rst | 7 ++++ .../isaaclab_assets/robots/shadow_hand.py | 35 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 source/isaaclab_assets/changelog.d/task-cleanup-dex-part11.minor.rst diff --git a/source/isaaclab_assets/changelog.d/task-cleanup-dex-part11.minor.rst b/source/isaaclab_assets/changelog.d/task-cleanup-dex-part11.minor.rst new file mode 100644 index 000000000000..93635087e69d --- /dev/null +++ b/source/isaaclab_assets/changelog.d/task-cleanup-dex-part11.minor.rst @@ -0,0 +1,7 @@ +Added +^^^^^ + +* Added :obj:`~isaaclab_assets.robots.shadow_hand.SHADOW_ACTUATED_JOINT_NAMES_NEWTON`. + The Newton Shadow Hand asset numbers its finger joints one higher than the + PhysX one, so terms that address joints by name need the Newton ordering + rather than :obj:`~isaaclab_assets.robots.shadow_hand.SHADOW_ACTUATED_JOINT_NAMES`. diff --git a/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py b/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py index 3d3c96876c2f..6dc214aebb5e 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/shadow_hand.py @@ -169,9 +169,10 @@ ) """Configuration of the Shadow Hand robot on the Newton (MJWarp) asset. -The Newton USD renumbers the finger joints (+1) relative to :obj:`SHADOW_HAND_CFG`, but the -names in :obj:`SHADOW_ACTUATED_JOINT_NAMES` resolve on both assets, so the two backends share -one actuated-joint list. Gains default to the PhysX values; tasks override them as needed. +The Newton USD renumbers the finger joints (+1) relative to :obj:`SHADOW_HAND_CFG`. Actuator +regexes resolve on both assets, so the two backends share one actuator configuration, but terms +that need the joints in order use :obj:`SHADOW_ACTUATED_JOINT_NAMES_NEWTON` on this asset. +Gains default to the PhysX values; tasks override them as needed. """ @@ -210,3 +211,31 @@ These names resolve on both the PhysX and Newton assets, so every backend shares this list. """ + +SHADOW_ACTUATED_JOINT_NAMES_NEWTON: list[str] = [ + "robot0_WRJ1", + "robot0_WRJ0", + "robot0_FFJ4", + "robot0_FFJ3", + "robot0_FFJ2", + "robot0_MFJ4", + "robot0_MFJ3", + "robot0_MFJ2", + "robot0_RFJ4", + "robot0_RFJ3", + "robot0_RFJ2", + "robot0_LFJ5", + "robot0_LFJ4", + "robot0_LFJ3", + "robot0_LFJ2", + "robot0_THJ4", + "robot0_THJ3", + "robot0_THJ2", + "robot0_THJ1", + "robot0_THJ0", +] +"""Shadow Hand actuated joint names on the Newton asset, in the same order. + +The Newton USD numbers the finger joints one higher than :obj:`SHADOW_ACTUATED_JOINT_NAMES`, +so action terms that address joints by name need this list rather than the PhysX one. +""" From bc2f4bdee1273d8bb1eb8fdeec5ea39fac8ab0b2 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Mon, 3 Aug 2026 03:49:06 -0700 Subject: [PATCH 8/9] Add the handover manager counterpart Register Isaac-Handover-Shadow as the manager counterpart of the Direct handover task. The fused Direct reward becomes a plain reward term, and the command term that owns the goal also owns the success and goal-distance bookkeeping, reporting the same per-episode success bit the Direct task does. A value-parity test pins the shared task values against the Direct configuration so drift on either side fails CI. --- .../task-cleanup-dex-part11.minor.rst | 36 ++++ .../isaaclab_tasks/core/handover/__init__.py | 10 + .../core/handover/handover_common.py | 15 ++ .../core/handover/handover_env_cfg.py | 2 +- .../core/handover/handover_manager_env_cfg.py | 194 ++++++++++++++++++ .../core/handover/mdp/__init__.pyi | 20 +- .../core/handover/mdp/commands.py | 107 ++++++++++ .../core/handover/mdp/events.py | 76 +++++++ .../core/handover/mdp/observations.py | 81 ++++++++ .../core/handover/mdp/rewards.py | 33 +++ .../test/core/test_reorient_value_parity.py | 19 ++ 11 files changed, 591 insertions(+), 2 deletions(-) create mode 100644 source/isaaclab_tasks/changelog.d/task-cleanup-dex-part11.minor.rst create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/events.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/observations.py diff --git a/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part11.minor.rst b/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part11.minor.rst new file mode 100644 index 000000000000..e8397c2895ff --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/task-cleanup-dex-part11.minor.rst @@ -0,0 +1,36 @@ +Added +^^^^^ + +* Added manager-based counterparts for the Shadow handover and Shadow camera + reorientation tasks, completing the manager coverage of the dexterous task + families. +* Added a Direct-versus-manager value-parity check for the handover task, + alongside the reorientation one. + +Changed +^^^^^^^ + +* Changed the manager-based Shadow camera task to run on PhysX by default. The + RTX render modalities require Fabric cloning, which Newton does not support. + Select Newton with ``physics=newton_mjwarp`` for the state-only observation + groups. +* Changed the handover reward to a plain reward term, moving success and + goal-distance bookkeeping to + :class:`~isaaclab_tasks.core.handover.mdp.commands.HandoverCommand`. +* Changed the reorientation action configuration to name its term through a + module path, so loading a task configuration no longer imports the USD + bindings. + +Removed +^^^^^^^ + +* Removed the ``Isaac-Reorient-Cube-Shadow-Camera-Play`` and + ``Isaac-Reorient-Cube-Shadow-Camera-Direct-Play`` tasks. Use the training task + with ``--play`` instead; playback settings now live in + :meth:`~isaaclab.envs.ManagerBasedRLEnvCfg.play_mode`. + +Fixed +^^^^^ + +* Fixed the Shadow camera feature-extractor observation term ignoring its + declared ``feature_extractor_cfg`` parameter. diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py index 410f83ced7a3..444aae7a0aa3 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/__init__.py @@ -15,6 +15,16 @@ # Register Gym environments. ## +gym.register( + id="Isaac-Shadow-Handover", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.handover_manager_env_cfg:HandoverManagerEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:HandoverPPORunnerCfg", + }, +) + gym.register( id="Isaac-Shadow-Handover-Direct", entry_point=f"{__name__}.handover_env:HandoverEnv", diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_common.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_common.py index a5bec600e21c..9e73e5a2039d 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_common.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_common.py @@ -15,15 +15,21 @@ import isaaclab.sim as sim_utils from isaaclab.markers import VisualizationMarkersCfg +from isaaclab_tasks.utils.hydra import preset + from isaaclab_assets.robots.shadow_hand import ( SHADOW_ACTUATED_JOINT_NAMES as ACTUATED_JOINT_NAMES, ) +from isaaclab_assets.robots.shadow_hand import ( + SHADOW_ACTUATED_JOINT_NAMES_NEWTON, +) from isaaclab_assets.robots.shadow_hand import ( SHADOW_FINGERTIP_BODY_NAMES as FINGERTIP_BODY_NAMES, ) __all__ = [ "ACTUATED_JOINT_NAMES", + "ACTUATED_JOINT_NAMES_PRESET", "FINGERTIP_BODY_NAMES", "GOAL_MARKER_CFG", "GOAL_POSITION_OFFSET", @@ -31,6 +37,15 @@ ] +ACTUATED_JOINT_NAMES_PRESET = preset( + physx=ACTUATED_JOINT_NAMES, + newton_mjwarp=SHADOW_ACTUATED_JOINT_NAMES_NEWTON, + ovphysx=ACTUATED_JOINT_NAMES, + default=ACTUATED_JOINT_NAMES, +) +"""Per-backend actuated joint names, resolved by the physics preset key.""" + + OBJECT_RADIUS: float = 0.0335 """Hand-over object sphere radius [m], also used for the goal marker.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py index aa7a47ff838d..df622dd51440 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_env_cfg.py @@ -193,7 +193,7 @@ class HandoverEnvCfg(DirectMARLEnvCfg): observation_spaces = {"right_hand": 157, "left_hand": 157} state_space = 290 - # simulation — values mirrored by the manager cfg (guarded by the value-parity test) + # simulation — values mirrored by the manager cfg sim: SimulationCfg = SimulationCfg( dt=1 / 120, render_interval=decimation, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py new file mode 100644 index 000000000000..0cfc43c0ae76 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/handover_manager_env_cfg.py @@ -0,0 +1,194 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Manager-based counterpart of the Shadow Hand handover task.""" + +import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg +from isaaclab.envs import ManagerBasedRLEnvCfg +from isaaclab.managers import EventTermCfg as EventTerm +from isaaclab.managers import ObservationGroupCfg as ObsGroup +from isaaclab.managers import ObservationTermCfg as ObsTerm +from isaaclab.managers import RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.sim.spawners.materials import RigidBodyMaterialBaseCfg +from isaaclab.utils.configclass import configclass + +import isaaclab_tasks.core.handover.mdp as mdp +from isaaclab_tasks.core.handover.handover_common import ( + ACTUATED_JOINT_NAMES_PRESET, + FINGERTIP_BODY_NAMES, +) +from isaaclab_tasks.core.handover.handover_env_cfg import ( + LEFT_HAND_CFG, + RIGHT_HAND_CFG, + ObjectCfg, + PhysicsCfg, +) +from isaaclab_tasks.utils import PresetCfg + + +@configclass +class HandoverManagerSceneCfg(InteractiveSceneCfg): + """Two Shadow hands facing each other over a ground plane.""" + + num_envs = 2048 + env_spacing = 1.5 + replicate_physics = True + + ground = AssetBaseCfg( + prim_path="/World/ground", + spawn=sim_utils.GroundPlaneCfg(), + ) + right_hand: PresetCfg = RIGHT_HAND_CFG + left_hand: PresetCfg = LEFT_HAND_CFG + object: ObjectCfg = ObjectCfg() + light = AssetBaseCfg( + prim_path="/World/Light", + spawn=sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)), + ) + + +@configclass +class CommandsCfg: + """Handover goal command.""" + + object_pose = mdp.HandoverCommandCfg(asset_name="object", success_distance_threshold=0.1, debug_vis=True) + + +@configclass +class ActionsCfg: + """Two-hand action terms, ordered right then left like the Direct adapter.""" + + right_hand = mdp.EMAJointPositionToLimitsActionCfg( + asset_name="right_hand", + joint_names=ACTUATED_JOINT_NAMES_PRESET, + alpha=1.0, + rescale_to_limits=True, + ) + left_hand = mdp.EMAJointPositionToLimitsActionCfg( + asset_name="left_hand", + joint_names=ACTUATED_JOINT_NAMES_PRESET, + alpha=1.0, + rescale_to_limits=True, + ) + + +def _hand_entity(name: str) -> SceneEntityCfg: + return SceneEntityCfg(name, joint_names=".*") + + +def _fingertip_entity(name: str) -> SceneEntityCfg: + return SceneEntityCfg(name, body_names=FINGERTIP_BODY_NAMES) + + +@configclass +class ObservationsCfg: + """Single-agent observations matching the Direct MARL adapter.""" + + @configclass + class PolicyCfg(ObsGroup): + # Right agent: 133 hand dimensions followed by 24 object/goal dimensions. + # soft limits equal the hard limits here: soft_joint_pos_limits_factor defaults to 1.0 + right_joint_pos = ObsTerm(func=mdp.joint_pos_limit_normalized, params={"asset_cfg": _hand_entity("right_hand")}) + right_joint_vel = ObsTerm(func=mdp.joint_vel, scale=0.2, params={"asset_cfg": _hand_entity("right_hand")}) + right_fingertip_pos = ObsTerm(func=mdp.fingertip_pos, params={"asset_cfg": _fingertip_entity("right_hand")}) + right_fingertip_quat = ObsTerm(func=mdp.fingertip_quat, params={"asset_cfg": _fingertip_entity("right_hand")}) + right_fingertip_vel = ObsTerm(func=mdp.fingertip_vel, params={"asset_cfg": _fingertip_entity("right_hand")}) + right_action = ObsTerm(func=mdp.hand_action, params={"action_name": "right_hand"}) + right_object_goal = ObsTerm( + func=mdp.object_goal, + params={"command_name": "object_pose", "object_cfg": SceneEntityCfg("object"), "vel_obs_scale": 0.2}, + ) + + # Left agent: the same 157-dimensional layout. + # soft limits equal the hard limits here: soft_joint_pos_limits_factor defaults to 1.0 + left_joint_pos = ObsTerm(func=mdp.joint_pos_limit_normalized, params={"asset_cfg": _hand_entity("left_hand")}) + left_joint_vel = ObsTerm(func=mdp.joint_vel, scale=0.2, params={"asset_cfg": _hand_entity("left_hand")}) + left_fingertip_pos = ObsTerm(func=mdp.fingertip_pos, params={"asset_cfg": _fingertip_entity("left_hand")}) + left_fingertip_quat = ObsTerm(func=mdp.fingertip_quat, params={"asset_cfg": _fingertip_entity("left_hand")}) + left_fingertip_vel = ObsTerm(func=mdp.fingertip_vel, params={"asset_cfg": _fingertip_entity("left_hand")}) + left_action = ObsTerm(func=mdp.hand_action, params={"action_name": "left_hand"}) + left_object_goal = ObsTerm( + func=mdp.object_goal, + params={"command_name": "object_pose", "object_cfg": SceneEntityCfg("object"), "vel_obs_scale": 0.2}, + ) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = True + + policy: PolicyCfg = PolicyCfg() + + +@configclass +class EventCfg: + """Reset distributions matching the Direct handover environment.""" + + reset_handover = EventTerm( + func=mdp.reset_handover_state, + mode="reset", + params={ + "position_noise": 0.01, + "joint_position_noise": 0.2, + "joint_velocity_noise": 0.0, + "action_names": ("right_hand", "left_hand"), + }, + ) + + +@configclass +class RewardsCfg: + """Summed two-agent reward exposed by the Direct single-agent adapter.""" + + goal_distance = RewTerm( + func=mdp.handover_goal_distance_reward, + weight=1.0, + params={ + "command_name": "object_pose", + "distance_scale": 20.0, + "object_cfg": SceneEntityCfg("object"), + }, + ) + + +@configclass +class TerminationsCfg: + """Termination conditions for the handover task. + + The generic ``time_out`` term ends an episode one control step later than the Direct + environment, which stops at ``max_episode_length - 1``. + """ + + object_out_of_reach = DoneTerm( + func=mdp.root_height_below_minimum, + params={"minimum_height": 0.24, "asset_cfg": SceneEntityCfg("object")}, + ) + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + +@configclass +class HandoverManagerEnvCfg(ManagerBasedRLEnvCfg): + """Manager-based handover environment matching the Direct RSL-RL view.""" + + scene: HandoverManagerSceneCfg = HandoverManagerSceneCfg() + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() + + def __post_init__(self): + self.decimation = 2 + self.episode_length_s = 7.5 + # simulation — mirrors the Direct cfg + self.sim.dt = 1 / 120 + self.sim.render_interval = self.decimation + self.sim.physics_material = RigidBodyMaterialBaseCfg(static_friction=1.0, dynamic_friction=1.0) + self.sim.physics = PhysicsCfg() + self.viewer.eye = (2.0, 2.0, 2.0) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.pyi b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.pyi index 8100050074e1..d7f37e3f573b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/__init__.pyi @@ -4,9 +4,27 @@ # SPDX-License-Identifier: BSD-3-Clause __all__ = [ + "HandoverCommand", + "HandoverCommandCfg", + "reset_handover_state", + "fingertip_pos", + "fingertip_quat", + "fingertip_vel", + "hand_action", + "object_goal", + "handover_goal_distance_reward", "handover_reward", "evaluate_handover_success", ] -from .rewards import evaluate_handover_success, handover_reward +from .commands import HandoverCommand, HandoverCommandCfg +from .events import reset_handover_state +from .observations import ( + fingertip_pos, + fingertip_quat, + fingertip_vel, + hand_action, + object_goal, +) +from .rewards import evaluate_handover_success, handover_goal_distance_reward, handover_reward from isaaclab.envs.mdp import * diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py new file mode 100644 index 000000000000..dab1ac10c893 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/commands.py @@ -0,0 +1,107 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Goal-pose command for the manager-based handover task.""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import MISSING +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.managers import CommandTerm, CommandTermCfg +from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg +from isaaclab.utils.configclass import configclass + +from isaaclab_tasks.core.handover.handover_common import GOAL_MARKER_CFG, GOAL_POSITION_OFFSET +from isaaclab_tasks.core.utils import EpisodeErrorRecorder + +if TYPE_CHECKING: + from isaaclab.assets import RigidObject + from isaaclab.envs import ManagerBasedRLEnv + + +class HandoverCommand(CommandTerm): + """Sample the fixed-position, random-orientation handover goal pose.""" + + cfg: HandoverCommandCfg + + def __init__(self, cfg: HandoverCommandCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + self._object: RigidObject = env.scene[cfg.asset_name] + offset = torch.tensor(cfg.position_offset, dtype=torch.float, device=self.device) + self.pos_command_e = self._object.data.default_root_pose.torch[:, :3] + offset + self.quat_command_w = torch.zeros(self.num_envs, 4, device=self.device) + self.quat_command_w[:, 3] = 1.0 # identity quaternion in (x, y, z, w) layout + self._x_unit = torch.tensor([1.0, 0.0, 0.0], device=self.device).repeat(self.num_envs, 1) + self._y_unit = torch.tensor([0.0, 1.0, 0.0], device=self.device).repeat(self.num_envs, 1) + self.metrics["goal_distance"] = torch.zeros(self.num_envs, device=self.device) + self._minimum_goal_distance = EpisodeErrorRecorder(self.num_envs, self.device) + # Whether each environment has brought the object within the success distance this episode. + self._succeeded = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + + @property + def command(self) -> torch.Tensor: + """Goal pose in the environment frame [m, unit quaternion]. Shape is (num_envs, 7).""" + return torch.cat((self.pos_command_e, self.quat_command_w), dim=-1) + + def _update_metrics(self) -> None: + object_pos = self._object.data.root_pos_w.torch - self._env.scene.env_origins + goal_distance = torch.linalg.norm(object_pos - self.pos_command_e, ord=2, dim=-1) + self.metrics["goal_distance"][:] = goal_distance + self._minimum_goal_distance.update(goal_distance) + self._succeeded |= goal_distance < self.cfg.success_distance_threshold + + def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: + if env_ids is None: + env_ids = slice(None) + extras = super().reset(env_ids) + log = self._env.extras.setdefault("log", {}) + log["Metrics/success_rate"] = self._succeeded[env_ids].float().mean().item() + self._succeeded[env_ids] = False + for statistic, value in self._minimum_goal_distance.reset(env_ids).items(): + log[f"Diagnostics/episode_min_goal_distance_{statistic}"] = value + return extras + + def _resample_command(self, env_ids: Sequence[int]) -> None: + random_values = 2.0 * torch.rand((len(env_ids), 2), device=self.device) - 1.0 + self.quat_command_w[env_ids] = math_utils.quat_mul( + math_utils.quat_from_angle_axis(random_values[:, 0] * torch.pi, self._x_unit[env_ids]), + math_utils.quat_from_angle_axis(random_values[:, 1] * torch.pi, self._y_unit[env_ids]), + ) + + def _update_command(self) -> None: + pass + + def _set_debug_vis_impl(self, debug_vis: bool) -> None: + if debug_vis: + if not hasattr(self, "_goal_visualizer"): + self._goal_visualizer = VisualizationMarkers(self.cfg.goal_visualizer_cfg) + self._goal_visualizer.set_visibility(True) + elif hasattr(self, "_goal_visualizer"): + self._goal_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event) -> None: + self._goal_visualizer.visualize( + translations=self.pos_command_e + self._env.scene.env_origins, + orientations=self.quat_command_w, + ) + + +@configclass +class HandoverCommandCfg(CommandTermCfg): + """Configuration for :class:`HandoverCommand`.""" + + class_type: type[HandoverCommand] = HandoverCommand + resampling_time_range: tuple[float, float] = (1.0e6, 1.0e6) + asset_name: str = MISSING + position_offset: tuple[float, float, float] = GOAL_POSITION_OFFSET + """Goal-position offset from the object's default position [m].""" + success_distance_threshold: float = 0.1 + """Object-to-goal distance below which an episode counts as successful [m].""" + goal_visualizer_cfg: VisualizationMarkersCfg = GOAL_MARKER_CFG.replace(prim_path="/Visuals/Command/goal_marker") diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/events.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/events.py new file mode 100644 index 000000000000..e3cc54a125a9 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/events.py @@ -0,0 +1,76 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Reset events for the manager-based handover task.""" + +from __future__ import annotations + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.managers import SceneEntityCfg + +from isaaclab_tasks.core.reorient.mdp.events import random_xy_rotation, sample_joint_positions_within_limits + +if TYPE_CHECKING: + from isaaclab.assets import Articulation, RigidObject + from isaaclab.envs import ManagerBasedRLEnv + + +def reset_handover_state( + env: ManagerBasedRLEnv, + env_ids: Sequence[int], + position_noise: float, + joint_position_noise: float, + joint_velocity_noise: float, + action_names: tuple[str, ...], + right_hand_cfg: SceneEntityCfg = SceneEntityCfg("right_hand"), + left_hand_cfg: SceneEntityCfg = SceneEntityCfg("left_hand"), + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), +) -> None: + """Reset the object and both hands with the Direct task's distributions. + + Args: + env: Environment containing both hands and the object. + env_ids: Environment indices to reset. + position_noise: Object-position noise half-width [m]. + joint_position_noise: Scale applied to sampled joint-position deltas. + joint_velocity_noise: Joint-velocity noise half-width [rad/s]. + action_names: Action terms whose pre-reset raw actions are retained in reset observations. + right_hand_cfg: Right-hand scene entity. + left_hand_cfg: Left-hand scene entity. + object_cfg: Object scene entity. + """ + if not hasattr(env, "_handover_reset_actions"): + env._handover_reset_actions = {} + for action_name in action_names: + raw_action = env.action_manager.get_term(action_name).raw_actions + if action_name not in env._handover_reset_actions: + env._handover_reset_actions[action_name] = torch.zeros_like(raw_action) + env._handover_reset_actions[action_name][env_ids] = raw_action[env_ids] + + object_asset: RigidObject = env.scene[object_cfg.name] + object_pose = object_asset.data.default_root_pose.torch[env_ids].clone() + object_velocity = torch.zeros_like(object_asset.data.default_root_vel.torch[env_ids]) + position_delta = math_utils.sample_uniform(-1.0, 1.0, (len(env_ids), 3), device=env.device) + object_pose[:, :3] += position_noise * position_delta + env.scene.env_origins[env_ids] + object_pose[:, 3:7] = random_xy_rotation(len(env_ids), env.device) + object_asset.write_root_pose_to_sim_index(root_pose=object_pose, env_ids=env_ids) + object_asset.write_root_velocity_to_sim_index(root_velocity=object_velocity, env_ids=env_ids) + + for hand_cfg in (right_hand_cfg, left_hand_cfg): + hand: Articulation = env.scene[hand_cfg.name] + default_position = hand.data.default_joint_pos.torch[env_ids] + limits = hand.data.joint_limits.torch[env_ids] + joint_position = sample_joint_positions_within_limits(default_position, limits, joint_position_noise) + velocity_sample = math_utils.sample_uniform(-1.0, 1.0, (len(env_ids), hand.num_joints), device=env.device) + joint_velocity = hand.data.default_joint_vel.torch[env_ids] + joint_velocity_noise * velocity_sample + + hand.set_joint_position_target_index(target=joint_position, env_ids=env_ids) + hand.write_joint_position_to_sim_index(position=joint_position, env_ids=env_ids) + hand.write_joint_velocity_to_sim_index(velocity=joint_velocity, env_ids=env_ids) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/observations.py new file mode 100644 index 000000000000..5c1395e9df3b --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/observations.py @@ -0,0 +1,81 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Observation terms for the manager-based handover task.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +import isaaclab.utils.math as math_utils +from isaaclab.managers import SceneEntityCfg + +# Handover reuses the reorientation fingertip observation terms verbatim. +from isaaclab_tasks.core.reorient.mdp.observations import ( # noqa: F401 + fingertip_pos, + fingertip_quat, + fingertip_vel, +) + +if TYPE_CHECKING: + from isaaclab.assets import RigidObject + from isaaclab.envs import ManagerBasedRLEnv + + +def hand_action(env: ManagerBasedRLEnv, action_name: str) -> torch.Tensor: + """Return one hand's Direct-compatible raw action across resets. + + Args: + env: Environment containing the action term and episode-length buffer. + action_name: Action term whose raw action is observed. + + Returns: + Current raw actions, retaining pre-reset actions while episode length is zero. + """ + raw_action = env.action_manager.get_term(action_name).raw_actions + reset_actions = getattr(env, "_handover_reset_actions", None) + episode_length_buf = getattr(env, "episode_length_buf", None) + if reset_actions is None or action_name not in reset_actions or episode_length_buf is None: + return raw_action + return torch.where((episode_length_buf == 0).unsqueeze(-1), reset_actions[action_name], raw_action) + + +def object_goal( + env: ManagerBasedRLEnv, command_name: str, object_cfg: SceneEntityCfg, vel_obs_scale: float +) -> torch.Tensor: + """Return the 24-dimensional object and handover-goal observation block. + + Position components use [m], linear velocities [m/s], angular velocities + [rad/s], and quaternion components are unitless. The angular-velocity + scale arrives as the ``vel_obs_scale`` term param, wired at declaration. + + Args: + env: Environment containing the object and goal command. + command_name: Goal command term name. + object_cfg: Object scene entity. + vel_obs_scale: Angular-velocity observation scale. + + Returns: + Object pose, spatial velocity, goal pose, and quaternion error, shape ``(num_envs, 24)``. + """ + object_asset: RigidObject = env.scene[object_cfg.name] + command_term = env.command_manager.get_term(command_name) + object_pos_e = object_asset.data.root_pos_w.torch - env.scene.env_origins + object_quat = object_asset.data.root_quat_w.torch + quat_error = math_utils.quat_mul(object_quat, math_utils.quat_conjugate(command_term.quat_command_w)) + return torch.cat( + ( + object_pos_e, + object_quat, + object_asset.data.root_lin_vel_w.torch, + vel_obs_scale * object_asset.data.root_ang_vel_w.torch, + command_term.pos_command_e, + command_term.quat_command_w, + quat_error, + ), + dim=-1, + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/rewards.py index c31065214a65..225aec0d92dd 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/rewards.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/handover/mdp/rewards.py @@ -7,8 +7,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import torch +from isaaclab.managers import SceneEntityCfg + +if TYPE_CHECKING: + from isaaclab.assets import RigidObject + from isaaclab.envs import ManagerBasedRLEnv + def handover_reward(goal_distance: torch.Tensor, distance_scale: float) -> torch.Tensor: """Return one hand's Direct reward for the current object-goal distance.""" @@ -31,3 +39,28 @@ def evaluate_handover_success( """ goal_distance = torch.linalg.norm(object_position - target_position, ord=2, dim=-1) return goal_distance < success_distance_threshold, goal_distance + + +def handover_goal_distance_reward( + env: ManagerBasedRLEnv, + command_name: str, + distance_scale: float, + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), +) -> torch.Tensor: + """Reward both hands for holding the object near its goal. + + The Direct environment sums one identical reward per hand, so this returns twice the + single-hand value. The command term owns the episode success bookkeeping behind + ``Metrics/success_rate``. + + Args: + env: The environment object. + command_name: The command term to be used for extracting the goal. + distance_scale: Exponential decay rate of the distance reward [1/m]. + object_cfg: The configuration for the scene entity. Default is "object". + """ + object_asset: RigidObject = env.scene[object_cfg.name] + object_pos = object_asset.data.root_pos_w.torch - env.scene.env_origins + goal_pos = env.command_manager.get_command(command_name)[:, :3] + goal_distance = torch.linalg.norm(object_pos - goal_pos, ord=2, dim=-1) + return 2.0 * handover_reward(goal_distance, distance_scale) diff --git a/source/isaaclab_tasks/test/core/test_reorient_value_parity.py b/source/isaaclab_tasks/test/core/test_reorient_value_parity.py index 43bba092f781..5f064f20a868 100644 --- a/source/isaaclab_tasks/test/core/test_reorient_value_parity.py +++ b/source/isaaclab_tasks/test/core/test_reorient_value_parity.py @@ -50,3 +50,22 @@ def test_manager_config_matches_direct_values(direct_cls, manager_cls): streak_cap = getattr(manager.terminations, "max_consecutive_success", None) assert (0 if streak_cap is None else streak_cap.params["num_success"]) == direct.max_consecutive_success assert streak_cap is None or streak_cap.time_out + + +def test_handover_manager_config_matches_direct_values(): + """The handover manager task mirrors its Direct counterpart's task-defining values.""" + from isaaclab_tasks.core.handover.handover_env_cfg import HandoverEnvCfg + from isaaclab_tasks.core.handover.handover_manager_env_cfg import HandoverManagerEnvCfg + + direct, manager = HandoverEnvCfg(), HandoverManagerEnvCfg() + + assert (manager.decimation, manager.episode_length_s, manager.sim.dt) == ( + direct.decimation, + direct.episode_length_s, + direct.sim.dt, + ) + assert manager.sim.render_interval == direct.sim.render_interval + assert manager.commands.object_pose.success_distance_threshold == pytest.approx(direct.success_distance_threshold) + assert manager.rewards.goal_distance.params["distance_scale"] == pytest.approx(direct.dist_reward_scale) + for action_term in (manager.actions.right_hand, manager.actions.left_hand): + assert action_term.alpha == pytest.approx(direct.act_moving_average) From d45cb0acdf2c1363eb92a7d7084d41802217efe5 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Mon, 3 Aug 2026 03:49:19 -0700 Subject: [PATCH 9/9] Add the Shadow camera manager counterpart Register Isaac-Reorient-Cube-Shadow-Camera as the manager counterpart of the Direct camera task. It defaults to PhysX: the RTX render modalities need Fabric cloning, which Newton does not support, so the inherited default could not render. Newton stays selectable for the state-only observation groups. Move the camera playback settings into play_mode, removing the last two -Play registrations in isaaclab_tasks. The override mutates the feature extractor rather than replacing it, since replacing resets every field the caller does not name -- which had silently re-enabled the CNN in the benchmark configuration whose purpose is to disable it. --- .../reorient/config/shadow_hand/__init__.py | 11 ++ .../config/shadow_hand/feature_extractor.py | 8 ++ .../shadow_hand_camera_manager_env_cfg.py | 121 ++++++++++++++++++ .../shadow_hand_direct_camera_env.py | 29 +++++ .../shadow_hand_direct_camera_env_cfg.py | 88 +++++++------ .../core/reorient/mdp/__init__.pyi | 6 + .../core/reorient/mdp/observations.py | 121 +++++++++++++++++- ...default_physics-default_renderer-depth.png | 3 + .../default_physics-default_renderer-rgb.png | 3 + .../default_physics-default_renderer-rgba.png | 3 + ...default_renderer-semantic_segmentation.png | 3 + 11 files changed, 359 insertions(+), 37 deletions(-) create mode 100644 source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py create mode 100644 source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-depth.png create mode 100644 source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgb.png create mode 100644 source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgba.png create mode 100644 source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-semantic_segmentation.png 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 b8e68408ccb2..1d4d101b53fc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/__init__.py @@ -97,3 +97,14 @@ "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandAsymLSTMPPORunnerCfg", }, ) + +gym.register( + id="Isaac-Reorient-Cube-Shadow-Camera", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.shadow_hand_camera_manager_env_cfg:ShadowHandCameraManagerEnvCfg", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:ShadowHandCameraFFPPORunnerCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_camera_cfg.yaml", + }, +) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py index 56c159a1446e..7c14cfe93984 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/feature_extractor.py @@ -3,8 +3,11 @@ # # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + import glob import os +from typing import TYPE_CHECKING import torch import torch.nn as nn @@ -13,6 +16,11 @@ from isaaclab.sensors import save_images_to_file from isaaclab.utils.configclass import configclass +# re-exported for backward compatibility; the shared implementation lives in the family math root + +if TYPE_CHECKING: + pass + # Number of output channels for each supported camera data type. _DATA_TYPE_CHANNELS: dict[str, int] = { "rgb": 3, diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py new file mode 100644 index 000000000000..c418c0cf407d --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_camera_manager_env_cfg.py @@ -0,0 +1,121 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Manager-based counterpart of the Shadow Hand camera reorientation task.""" + +from isaaclab.managers import ObservationTermCfg as ObsTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.sensors import JointWrenchSensorCfg +from isaaclab.utils.configclass import configclass + +import isaaclab_tasks.core.reorient.mdp as mdp +from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractorCfg +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_direct_camera_env_cfg import ( + ShadowHandTiledCameraCfg, + validate_shadow_hand_camera_settings, +) +from isaaclab_tasks.core.reorient.config.shadow_hand.shadow_hand_manager_env_cfg import ( + FullStateObsCfg, + ShadowHandManagerEnvCfg, + ShadowHandManagerSceneCfg, +) + +from isaaclab_assets.robots.shadow_hand import SHADOW_FINGERTIP_BODY_NAMES + + +@configclass +class ShadowHandCameraManagerSceneCfg(ShadowHandManagerSceneCfg): + """State Manager scene augmented with camera and fingertip-wrench sensors.""" + + num_envs = 1225 + env_spacing = 2.0 + + ground = None + tiled_camera: ShadowHandTiledCameraCfg = ShadowHandTiledCameraCfg() + joint_wrench = JointWrenchSensorCfg(prim_path="{ENV_REGEX_NS}/Robot") + + +@configclass +class CameraPolicyCfg(FullStateObsCfg): + """Direct-compatible 191-dimensional camera actor observation.""" + + last_action = ObsTerm(func=mdp.reorient_last_action, params={"action_name": "joint_pos"}) + camera_features = ObsTerm( + func=mdp.ShadowHandCameraFeatures, + params={ + "feature_extractor_cfg": FeatureExtractorCfg(), + "sensor_cfg": SceneEntityCfg("tiled_camera"), + "object_cfg": SceneEntityCfg("object"), + }, + ) + goal_keypoints = ObsTerm(func=mdp.shadow_hand_goal_keypoints, params={"command_name": "object_pose"}) + + def __post_init__(self): + super().__post_init__() + # Camera actor observations infer object state from pixels. These five + # privileged state terms are present only in the critic. + self.object_pos = None + self.object_quat = None + self.object_lin_vel = None + self.object_ang_vel = None + self.goal_quat_diff = None + + +@configclass +class CameraCriticCfg(FullStateObsCfg): + """Direct-compatible 214-dimensional asymmetric camera critic state.""" + + fingertip_wrench = ObsTerm( + func=mdp.fingertip_wrench, + scale=10.0, + params={ + "sensor_cfg": SceneEntityCfg("joint_wrench", body_names=SHADOW_FINGERTIP_BODY_NAMES, preserve_order=False) + }, + ) + last_action = ObsTerm(func=mdp.reorient_last_action, params={"action_name": "joint_pos"}) + camera_features = ObsTerm(func=mdp.shadow_hand_camera_cached_features) + + +@configclass +class CameraObservationsCfg: + """Camera actor and asymmetric critic observation groups.""" + + policy: CameraPolicyCfg = CameraPolicyCfg() + critic: CameraCriticCfg = CameraCriticCfg() + + +@configclass +class ShadowHandCameraManagerEnvCfg(ShadowHandManagerEnvCfg): + """Manager-based camera task with exact Direct dynamics and observations.""" + + # only the fields that differ from ShadowHandManagerEnvCfg are overridden + scene: ShadowHandCameraManagerSceneCfg = ShadowHandCameraManagerSceneCfg() + observations: CameraObservationsCfg = CameraObservationsCfg() + feature_extractor: FeatureExtractorCfg = FeatureExtractorCfg() + + def __post_init__(self): + super().__post_init__() + # camera tasks display the goal inside the tiled camera's frustum + # goal cube must sit inside the tiled camera's frustum + self.commands.object_pose.fixed_marker_pos = (-0.2, 0.1, 0.6) + self.observations.policy.camera_features.params["feature_extractor_cfg"] = self.feature_extractor + # The RTX modalities need Fabric cloning, which Newton does not support, so the + # camera task renders out of the box on PhysX; Newton stays selectable through + # ``physics=newton_mjwarp`` for the state-only observation groups. + for backend_cfg in (self.sim.physics, self.scene.robot, self.scene.object): + backend_cfg.default = backend_cfg.physx + + def validate_config(self): + """Check the camera pipeline against the feature extractor it feeds.""" + validate_shadow_hand_camera_settings(self.scene.tiled_camera, self.feature_extractor) + + def play_mode(self): + super().play_mode() + # the tiled camera needs more environments than the shared play default + self.scene.num_envs = 64 + # mutate rather than replace: subclasses may have disabled the CNN + self.feature_extractor.train = False + self.feature_extractor.load_checkpoint = True + self.observations.policy.camera_features.params["feature_extractor_cfg"] = self.feature_extractor diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py index b56774c03f09..7329009fef86 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env.py @@ -6,6 +6,7 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING import torch @@ -137,3 +138,31 @@ def _get_observations(self) -> dict: observations = {"policy": obs, "critic": state} return observations + + +def compute_keypoints( + pose: torch.Tensor, + num_keypoints: int = 8, + size: tuple[float, float, float] = (2 * 0.03, 2 * 0.03, 2 * 0.03), + out: torch.Tensor | None = None, +) -> torch.Tensor: + """Compute cube keypoints using the shared implementation. + + .. deprecated:: 9.0.0 + Use :func:`compute_cube_keypoints` instead. + + Args: + pose: Cube center poses ``(x, y, z, qx, qy, qz, qw)`` [m, unit quaternion]. + num_keypoints: Number of binary-sign corners to compute. + size: Cube side lengths along each axis [m]. + out: Optional output buffer [m], shape ``(num_envs, num_keypoints, 3)``. + + Returns: + Cube-corner positions [m], shape ``(num_envs, num_keypoints, 3)``. + """ + warnings.warn( + "compute_keypoints() is deprecated; use compute_cube_keypoints() instead.", + DeprecationWarning, + stacklevel=2, + ) + return compute_cube_keypoints(pose, num_keypoints=num_keypoints, size=size, out=out) diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py index 39fbe3fc578f..4dcbf53189f9 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/config/shadow_hand/shadow_hand_direct_camera_env_cfg.py @@ -16,12 +16,55 @@ from isaaclab_tasks.utils.presets import MultiBackendRendererCfg +def validate_shadow_hand_camera_settings( + tiled_camera: CameraCfg | ShadowHandTiledCameraCfg, + feature_extractor: FeatureExtractorCfg, +) -> None: + """Validate one resolved or defaulted Shadow Hand camera pipeline.""" + while isinstance(tiled_camera, PresetCfg): + tiled_camera = tiled_camera.default + renderer_cfg = tiled_camera.renderer_cfg + while isinstance(renderer_cfg, PresetCfg): + renderer_cfg = renderer_cfg.default + + renderer_type = getattr(renderer_cfg, "renderer_type", None) + warp_supported = { + "rgb", + "depth", + "distance_to_camera", + "distance_to_image_plane", + "normals", + "semantic_segmentation", + "instance_segmentation", + } + if renderer_type == "newton_warp": + unsupported = set(tiled_camera.data_types) - warp_supported + if unsupported: + raise ValueError( + f"Warp renderer only supports data types {sorted(warp_supported)}, " + f"but the camera is configured with unsupported types: {sorted(unsupported)}. " + "Choose a compatible preset, e.g. presets=newton_renderer,rgb." + ) + + non_depth_data_types = set(tiled_camera.data_types).difference( + {"depth", "distance_to_image_plane", "distance_to_camera"} + ) + if tiled_camera.data_types and not non_depth_data_types and feature_extractor.enabled: + raise ValueError( + "Depth-only camera data type is intended for benchmarking only. " + "The keypoint-regression CNN cannot be meaningfully trained from depth alone. " + "Disable the feature extractor with 'feature_extractor.enabled=False' " + "(e.g. use IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct), " + "or choose a data type that includes colour, e.g. presets=rgb." + ) + + @configclass class _ShadowHandBaseTiledCameraCfg(CameraCfg): """Base camera configuration for the shadow hand vision environment. - This is an internal config used by :class:`ShadowHandTiledCameraCfg` presets and - by derived env configs that hard-code a specific data type. It embeds + This is a module-level config used by :class:`ShadowHandTiledCameraCfg` presets, by + derived env configs that hard-code a specific data type, and by the rendering tests. It embeds :class:`~isaaclab_tasks.utils.MultiBackendRendererCfg` so the renderer backend can still be selected via the ``presets`` CLI argument. """ @@ -49,6 +92,7 @@ class ShadowHandTiledCameraCfg(PresetCfg): Select a data-type preset via the ``presets`` CLI argument, e.g.:: presets = rgb # RGB only (3 channels) + presets = rgb_depth # RGB + depth (4 channels) presets = albedo # albedo (3 channels) presets = simple_shading_constant_diffuse # simple shading, constant diffuse (3 channels) @@ -70,6 +114,9 @@ class ShadowHandTiledCameraCfg(PresetCfg): rgb: _ShadowHandBaseTiledCameraCfg = _ShadowHandBaseTiledCameraCfg(data_types=["rgb"]) """RGB only (3 CNN input channels).""" + rgb_depth: _ShadowHandBaseTiledCameraCfg = _ShadowHandBaseTiledCameraCfg(data_types=["rgb", "depth"]) + """RGB and depth (4 CNN input channels).""" + albedo: _ShadowHandBaseTiledCameraCfg = _ShadowHandBaseTiledCameraCfg(data_types=["albedo"]) """Albedo (3 CNN input channels).""" @@ -131,43 +178,12 @@ def __post_init__(self): def validate_config(self): """Check renderer/data-type and feature-extractor compatibility.""" - renderer_type = getattr(self.tiled_camera.renderer_cfg, "renderer_type", None) - warp_supported = { - "rgb", - "depth", - "distance_to_camera", - "distance_to_image_plane", - "normals", - "semantic_segmentation", - "instance_segmentation", - } - if renderer_type == "newton_warp": - unsupported = set(self.tiled_camera.data_types) - warp_supported - if unsupported: - raise ValueError( - f"Warp renderer only supports data types {sorted(warp_supported)}, " - f"but the camera is configured with unsupported types: {sorted(unsupported)}. " - "Choose a compatible preset, e.g. presets=newton_renderer,rgb." - ) - - non_depth_data_types = set(self.tiled_camera.data_types).difference( - {"depth", "distance_to_image_plane", "distance_to_camera"} - ) - if self.tiled_camera.data_types and not non_depth_data_types and self.feature_extractor.enabled: - raise ValueError( - "Depth-only camera data type is intended for benchmarking only. " - "The keypoint-regression CNN cannot be meaningfully trained from depth alone. " - "Disable the feature extractor with 'feature_extractor.enabled=False' " - "(e.g. use IsaacContrib-Reorient-Cube-Shadow-Camera-Benchmark-Direct), " - "or choose a data type that includes colour, e.g. presets=rgb." - ) + validate_shadow_hand_camera_settings(self.tiled_camera, self.feature_extractor) def play_mode(self): - # play-mode overrides of parent super().play_mode() - - # scene + # the tiled camera needs more environments than the shared play default self.scene.num_envs = 64 - # inference for CNN + # mutate rather than replace: subclasses may have disabled the CNN self.feature_extractor.train = False self.feature_extractor.load_checkpoint = True 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 13881dfbaea5..9bd01bd20972 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/__init__.pyi @@ -17,6 +17,9 @@ __all__ = [ "fingertip_wrench", "reorient_last_action", "openai_policy_observation", + "ShadowHandCameraFeatures", + "shadow_hand_camera_cached_features", + "shadow_hand_goal_keypoints", "goal_quat_diff", "success_bonus", "track_orientation_inv_l2", @@ -33,6 +36,9 @@ from .events import reset_reorient_state from .noisy_actions import NoisyEMAJointPositionToLimitsAction from .actions import NoisyEMAJointPositionToLimitsActionCfg from .observations import ( + ShadowHandCameraFeatures, + shadow_hand_camera_cached_features, + shadow_hand_goal_keypoints, fingertip_pos, fingertip_quat, fingertip_vel, 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 869bff657db3..dbc3300d09a2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/reorient/mdp/observations.py @@ -19,7 +19,9 @@ if TYPE_CHECKING: from isaaclab.assets import RigidObject from isaaclab.envs import ManagerBasedRLEnv - from isaaclab.sensors import JointWrenchSensor + from isaaclab.sensors import Camera, JointWrenchSensor + + from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractorCfg from .commands import ReorientCommand @@ -237,3 +239,120 @@ def __call__( if self._shape_probe_pending: return observation return self._noise_model(observation) + + +# --------------------------------------------------------------------------- +# Shadow Hand camera observation terms. +# +# These terms wrap the CNN feature pipeline defined in the shadow-hand config +# package. The config layer imports the mdp layer, so the FeatureExtractor +# machinery is imported lazily at term construction time. +# --------------------------------------------------------------------------- + + +class ShadowHandCameraFeatures(ManagerTermBase): + """Run the Direct camera feature pipeline as one Manager observation term.""" + + def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + sensor_cfg: SceneEntityCfg = cfg.params["sensor_cfg"] + camera: Camera = env.scene.sensors[sensor_cfg.name] + # Runtime-only import: the mdp layer must not import the task-config layer + # at module load (config modules import mdp; see the layering note above). + from isaaclab_tasks.core.reorient.config.shadow_hand.feature_extractor import FeatureExtractor + + feature_extractor_cfg: FeatureExtractorCfg = cfg.params["feature_extractor_cfg"] + self._feature_extractor = FeatureExtractor( + feature_extractor_cfg, + env.device, + camera.cfg.data_types, + env.cfg.log_dir, + height=camera.cfg.height, + width=camera.cfg.width, + ) + # ObservationManager calls terms once to infer their shape. Do not train + # or save a CNN checkpoint during that initialization probe. + self._shape_probe_pending = True + self._keypoints_buf = torch.empty(env.num_envs, 8, 3, dtype=torch.float32, device=env.device) + + def reset(self, env_ids: Sequence[int] | None = None) -> None: + """Finish the shape-probe phase on the first Manager reset. + + Args: + env_ids: Environment indices being reset. The feature extractor + has no per-environment state, so the indices are unused. + """ + del env_ids + if self._shape_probe_pending: + self._shape_probe_pending = False + + def __call__( + self, + env: ManagerBasedRLEnv, + feature_extractor_cfg: FeatureExtractorCfg, + sensor_cfg: SceneEntityCfg, + object_cfg: SceneEntityCfg, + ) -> torch.Tensor: + """Return the detached 27-dimensional cube-pose embedding. + + Args: + env: Environment containing the object and tiled camera. + feature_extractor_cfg: Feature-extractor configuration. + sensor_cfg: Tiled-camera scene entity. + object_cfg: Reoriented-object scene entity. + + Returns: + Predicted object position and cube keypoints [m], shape + ``(num_envs, 27)``. + """ + del feature_extractor_cfg # consumed in __init__ + if self._shape_probe_pending: + embeddings = torch.zeros(env.num_envs, 27, dtype=torch.float32, device=env.device) + env._shadow_hand_camera_embeddings = embeddings + return embeddings + + camera: Camera = env.scene.sensors[sensor_cfg.name] + object_asset: RigidObject = env.scene[object_cfg.name] + object_pos = object_asset.data.root_pos_w.torch - env.scene.env_origins + object_pose = torch.cat((object_pos, object_asset.data.root_quat_w.torch), dim=-1) + keypoints = compute_cube_keypoints(object_pose, out=self._keypoints_buf) + target = torch.cat((object_pos, keypoints.flatten(start_dim=1)), dim=-1) + camera_output = { + data_type: value if isinstance(value, torch.Tensor) else value.torch + for data_type, value in camera.data.output.items() + } + pose_loss, embeddings = self._feature_extractor.step(camera_output, target) + embeddings = embeddings.clone().detach() + env._shadow_hand_camera_embeddings = embeddings + if pose_loss is not None: + env.extras.setdefault("log", {})["pose_loss"] = pose_loss + return embeddings + + +def shadow_hand_camera_cached_features(env: ManagerBasedRLEnv) -> torch.Tensor: + """Return camera features computed by the preceding policy observation group. + + Args: + env: Environment whose policy group cached the current camera embedding. + + Returns: + Detached camera embeddings, shape ``(num_envs, 27)``. + """ + embeddings = getattr(env, "_shadow_hand_camera_embeddings", None) + if embeddings is None: + raise RuntimeError("Shadow Hand camera policy features must be computed before critic observations.") + return embeddings + + +def shadow_hand_goal_keypoints(env: ManagerBasedRLEnv, command_name: str) -> torch.Tensor: + """Flattened zero-origin cube keypoints [m] for the current goal orientation. + + Args: + env: Environment containing the goal command term. + command_name: Goal command term name. + + Returns: + Flattened zero-origin cube keypoints [m], shape ``(num_envs, 24)``. + """ + command_term = env.command_manager.get_term(command_name) + return cube_keypoints_from_quat(command_term.quat_command_w) diff --git a/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-depth.png b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-depth.png new file mode 100644 index 000000000000..c229b583dfb7 --- /dev/null +++ b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-depth.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a37f6bca30bb2d093eb68186c601551d52aafe8ed19c6c090de149b3210d81a5 +size 3665 diff --git a/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgb.png b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgb.png new file mode 100644 index 000000000000..eace991f49eb --- /dev/null +++ b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b60263834743f0508a437281d36bd2f46296789b28ad930627fb72e406ab8700 +size 19962 diff --git a/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgba.png b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgba.png new file mode 100644 index 000000000000..7d6fa735693d --- /dev/null +++ b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-rgba.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8da49db87ab2afe4f2a32c9354a72fd1b5a32aaf76484a21f787b01c0a47197a +size 22127 diff --git a/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-semantic_segmentation.png b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-semantic_segmentation.png new file mode 100644 index 000000000000..4bad29d72ce6 --- /dev/null +++ b/source/isaaclab_tasks/test/golden_images/registered_tasks/Isaac-Reorient-Cube-Shadow-Camera/default_physics-default_renderer-semantic_segmentation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75576f31118081f96b0cec2151ada7016794a933a035ea35665a752e8552b503 +size 1474