From eb44c5e13455c514cdc335367ab5c18b409b07df Mon Sep 17 00:00:00 2001 From: Qian Lin Date: Mon, 23 Mar 2026 02:11:49 -0700 Subject: [PATCH 1/6] WIP port newton example --- isaaclab_arena/assets/asset_registry.py | 1 + isaaclab_arena/assets/dexsuite_assets.py | 112 +++++++++ isaaclab_arena/embodiments/__init__.py | 1 + .../embodiments/kuka_allegro/__init__.py | 6 + .../embodiments/kuka_allegro/kuka_allegro.py | 227 ++++++++++++++++++ .../tasks/dexsuite_kuka_allegro_lift_task.py | 133 ++++++++++ isaaclab_arena/tests/test_configclass.py | 21 ++ .../tests/test_dexsuite_kuka_lift_example.py | 36 +++ .../tests/test_kuka_allegro_embodiment.py | 103 ++++++++ isaaclab_arena/utils/configclass.py | 5 +- isaaclab_arena_environments/cli.py | 2 + .../kuka_allegro_dexsuite_lift_environment.py | 87 +++++++ 12 files changed, 733 insertions(+), 1 deletion(-) create mode 100644 isaaclab_arena/assets/dexsuite_assets.py create mode 100644 isaaclab_arena/embodiments/kuka_allegro/__init__.py create mode 100644 isaaclab_arena/embodiments/kuka_allegro/kuka_allegro.py create mode 100644 isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py create mode 100644 isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py create mode 100644 isaaclab_arena/tests/test_kuka_allegro_embodiment.py create mode 100644 isaaclab_arena_environments/kuka_allegro_dexsuite_lift_environment.py diff --git a/isaaclab_arena/assets/asset_registry.py b/isaaclab_arena/assets/asset_registry.py index 33bb69acc..dfc0aa39a 100644 --- a/isaaclab_arena/assets/asset_registry.py +++ b/isaaclab_arena/assets/asset_registry.py @@ -215,6 +215,7 @@ def ensure_assets_registered(): if not _assets_registered: # Import modules to trigger asset registration via decorators import isaaclab_arena.assets.background_library # noqa: F401 + import isaaclab_arena.assets.dexsuite_assets # noqa: F401 import isaaclab_arena.assets.device_library # noqa: F401 import isaaclab_arena.assets.hdr_image_library # noqa: F401 import isaaclab_arena.assets.object_library # noqa: F401 diff --git a/isaaclab_arena/assets/dexsuite_assets.py b/isaaclab_arena/assets/dexsuite_assets.py new file mode 100644 index 000000000..67a562a79 --- /dev/null +++ b/isaaclab_arena/assets/dexsuite_assets.py @@ -0,0 +1,112 @@ +# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Dexsuite-aligned procedural assets (table + lift object) for Newton / Arena environments.""" + +import isaaclab.sim as sim_utils +from isaaclab.assets import RigidObjectCfg +from isaaclab.sim import CuboidCfg, RigidBodyMaterialCfg + +from isaaclab_arena.assets.object import Object +from isaaclab_arena.assets.object_base import ObjectType +from isaaclab_arena.assets.register import register_asset +from isaaclab_arena.utils.pose import Pose + + +# Kinematic table from Isaac Lab Dexsuite (``dexsuite_env_cfg.TABLE_SPAWN_CFG``). +_DEXSUITE_TABLE_SPAWN_CFG = sim_utils.CuboidCfg( + size=(0.8, 1.5, 0.04), + rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), + collision_props=sim_utils.CollisionPropertiesCfg(), + visible=False, +) + +# Single cuboid preset used with Newton (no multi-asset spawner); matches ``ObjectCfg.cube`` in Dexsuite. +_DEXSUITE_LIFT_CUBE_SPAWN_CFG = CuboidCfg( + size=(0.05, 0.1, 0.1), + physics_material=RigidBodyMaterialCfg(static_friction=0.5), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=16, + solver_velocity_iteration_count=0, + disable_gravity=False, + ), + collision_props=sim_utils.CollisionPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=0.2), +) + + +@register_asset +class DexsuiteManipTable(Object): + """Kinematic cuboid table matching Isaac Lab Dexsuite (asset name ``table`` for command visuals).""" + + name = "dexsuite_manip_table" + tags = ["background", "dexsuite"] + + def __init__( + self, + instance_name: str | None = None, + prim_path: str | None = None, + initial_pose: Pose | None = None, + ): + resolved_name = instance_name if instance_name is not None else "table" + resolved_prim = prim_path if prim_path is not None else "{ENV_REGEX_NS}/table" + pose = initial_pose or Pose( + position_xyz=(-0.55, 0.0, 0.235), + rotation_xyzw=(0.0, 0.0, 0.0, 1.0), + ) + # ``usd_path`` is required by ``Object`` for RIGID; unused when spawn is procedural (``object_type`` is fixed). + super().__init__( + name=resolved_name, + prim_path=resolved_prim, + object_type=ObjectType.RIGID, + usd_path="", + initial_pose=pose, + ) + self.disable_reset_pose() + + def _generate_rigid_cfg(self) -> RigidObjectCfg: + cfg = RigidObjectCfg( + prim_path=self.prim_path, + spawn=_DEXSUITE_TABLE_SPAWN_CFG, + **self.asset_cfg_addon, + ) + return self._add_initial_pose_to_cfg(cfg) + + +@register_asset +class DexsuiteLiftManipObject(Object): + """Lift-task manipuland: procedural cuboid matching Dexsuite ``ObjectCfg.cube`` (Newton-safe, single geometry).""" + + name = "dexsuite_lift_object" + tags = ["object", "dexsuite"] + + def __init__( + self, + instance_name: str | None = None, + prim_path: str | None = None, + initial_pose: Pose | None = None, + ): + resolved_name = instance_name if instance_name is not None else "object" + resolved_prim = prim_path if prim_path is not None else "{ENV_REGEX_NS}/Object" + pose = initial_pose or Pose( + position_xyz=(-0.55, 0.1, 0.35), + rotation_xyzw=(0.0, 0.0, 0.0, 1.0), + ) + super().__init__( + name=resolved_name, + prim_path=resolved_prim, + object_type=ObjectType.RIGID, + usd_path="", + initial_pose=pose, + ) + self.disable_reset_pose() + + def _generate_rigid_cfg(self) -> RigidObjectCfg: + cfg = RigidObjectCfg( + prim_path=self.prim_path, + spawn=_DEXSUITE_LIFT_CUBE_SPAWN_CFG, + **self.asset_cfg_addon, + ) + return self._add_initial_pose_to_cfg(cfg) diff --git a/isaaclab_arena/embodiments/__init__.py b/isaaclab_arena/embodiments/__init__.py index 5f3052fd2..d63721b26 100644 --- a/isaaclab_arena/embodiments/__init__.py +++ b/isaaclab_arena/embodiments/__init__.py @@ -9,3 +9,4 @@ from .g1.g1 import * from .galbot.galbot import * from .gr1t2.gr1t2 import * +from .kuka_allegro.kuka_allegro import * diff --git a/isaaclab_arena/embodiments/kuka_allegro/__init__.py b/isaaclab_arena/embodiments/kuka_allegro/__init__.py new file mode 100644 index 000000000..7465b02fe --- /dev/null +++ b/isaaclab_arena/embodiments/kuka_allegro/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +from .kuka_allegro import * # noqa: F403 diff --git a/isaaclab_arena/embodiments/kuka_allegro/kuka_allegro.py b/isaaclab_arena/embodiments/kuka_allegro/kuka_allegro.py new file mode 100644 index 000000000..f8fcc3ac6 --- /dev/null +++ b/isaaclab_arena/embodiments/kuka_allegro/kuka_allegro.py @@ -0,0 +1,227 @@ +# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Kuka + Allegro embodiment for Dexsuite-style manipulation. + +Aligned with +:class:`isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro.dexsuite_kuka_allegro_env_cfg.KukaAllegroMixinCfg`: +robot, fingertip contact sensors, relative joint actions, state or tiled-camera observations, +PhysX vs Newton event presets, and Dexsuite simulation rates. + +Scene geometry (object, table, ground, lights) is supplied by the Arena :class:`~isaaclab_arena.scene.scene.Scene`; +this embodiment only adds the robot and sensors (and optional cameras). +""" + +from __future__ import annotations + +from typing import Any, Literal + +from isaaclab.assets import ArticulationCfg +from isaaclab.managers import ObservationGroupCfg as ObsGroup +from isaaclab.managers import ObservationTermCfg as ObsTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.sensors import ContactSensorCfg, TiledCameraCfg +from isaaclab.utils import configclass +from isaaclab.utils.noise import UniformNoiseCfg as Unoise +from isaaclab_assets.robots import KUKA_ALLEGRO_CFG + +from isaaclab_tasks.manager_based.manipulation.dexsuite import dexsuite_env_cfg as dexsuite +from isaaclab_tasks.manager_based.manipulation.dexsuite import mdp as dexsuite_mdp +from isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro import ( + dexsuite_kuka_allegro_env_cfg as kuka_dexsuite_cfg, +) +from isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro.camera_cfg import ( + BaseTiledCameraCfg, + WristTiledCameraCfg, +) + +from isaaclab_arena.assets.register import register_asset +from isaaclab_arena.embodiments.common.arm_mode import ArmMode +from isaaclab_arena.embodiments.embodiment_base import EmbodimentBase +from isaaclab_arena.environments.isaaclab_arena_manager_based_env import IsaacLabArenaManagerBasedRLEnvCfg +from isaaclab_arena.utils.pose import Pose + +FINGERTIP_LIST = kuka_dexsuite_cfg.FINGERTIP_LIST + + +# --------------------------------------------------------------------------- +# Observation cfgs (Arena-local): Isaac Lab's ``StateObservationCfg`` / camera obs use ``super()`` in +# ``__post_init__``. Arena's :func:`~isaaclab_arena.utils.configclass.combine_configclass_instances` +# calls those ``__post_init__`` hooks with ``self`` equal to the *merged* config type, which is not a +# subclass of ``StateObservationCfg``, so ``super()`` raises. We duplicate upstream layout with explicit +# ``Parent.__post_init__(self)`` calls instead (same as ``camera_cfg.StateObservationCfg`` et al.). +# --------------------------------------------------------------------------- + + +@configclass +class ArenaDexsuiteKukaStateObservationCfg(dexsuite.ObservationsCfg): + """State observations matching ``camera_cfg.StateObservationCfg``; merge-safe ``__post_init__``.""" + + def __post_init__(self) -> None: + dexsuite.ObservationsCfg.__post_init__(self) + self.proprio.contact = ObsTerm( + func=dexsuite_mdp.fingers_contact_force_b, + params={"contact_sensor_names": [f"{link}_object_s" for link in FINGERTIP_LIST]}, + clip=(-20.0, 20.0), + ) + self.proprio.hand_tips_state_b.params["body_asset_cfg"].body_names = ["palm_link", ".*_tip"] + + +@configclass +class ArenaDexsuiteKukaSingleCameraObservationsCfg(ArenaDexsuiteKukaStateObservationCfg): + """Single-camera observations matching ``camera_cfg.SingleCameraObservationsCfg``.""" + + @configclass + class BaseImageObsCfg(ObsGroup): + object_observation_b = ObsTerm( + func=dexsuite_mdp.vision_camera, + noise=Unoise(n_min=-0.0, n_max=0.0), + clip=(-1.0, 1.0), + params={"sensor_cfg": SceneEntityCfg("base_camera")}, + ) + + base_image: BaseImageObsCfg = BaseImageObsCfg() + + def __post_init__(self) -> None: + ArenaDexsuiteKukaStateObservationCfg.__post_init__(self) + for group in self.__dataclass_fields__.values(): + obs_group = getattr(self, group.name) + obs_group.history_length = None + + +@configclass +class ArenaDexsuiteKukaDuoCameraObservationsCfg(ArenaDexsuiteKukaSingleCameraObservationsCfg): + """Duo-camera observations matching ``camera_cfg.DuoCameraObservationsCfg``.""" + + @configclass + class WristImageObsCfg(ObsGroup): + wrist_observation = ObsTerm( + func=dexsuite_mdp.vision_camera, + noise=Unoise(n_min=-0.0, n_max=0.0), + clip=(-1.0, 1.0), + params={"sensor_cfg": SceneEntityCfg("wrist_camera")}, + ) + + wrist_image: WristImageObsCfg = WristImageObsCfg() + + +@configclass +class DexsuiteKukaAllegroEmbodimentSceneCfg: + """Robot and fingertip contact sensors (Dexsuite naming). + + .. note:: + Do not set :attr:`replicate_physics` here. It belongs on :class:`~isaaclab.scene.InteractiveSceneCfg`; + merging both into one Arena scene class triggers a type clash across Isaac Lab versions. + :meth:`KukaAllegroDexsuiteEmbodiment.modify_env_cfg` sets ``env_cfg.scene.replicate_physics = True`` + to match Dexsuite. + """ + + robot: ArticulationCfg = KUKA_ALLEGRO_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + index_link_3_object_s: ContactSensorCfg = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/ee_link/index_link_3", + filter_prim_paths_expr=["{ENV_REGEX_NS}/Object"], + ) + middle_link_3_object_s: ContactSensorCfg = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/ee_link/middle_link_3", + filter_prim_paths_expr=["{ENV_REGEX_NS}/Object"], + ) + ring_link_3_object_s: ContactSensorCfg = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/ee_link/ring_link_3", + filter_prim_paths_expr=["{ENV_REGEX_NS}/Object"], + ) + thumb_link_3_object_s: ContactSensorCfg = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/ee_link/thumb_link_3", + filter_prim_paths_expr=["{ENV_REGEX_NS}/Object"], + ) + + +@configclass +class KukaAllegroDexsuiteCameraSceneCfg: + """Tiled base camera and optional wrist camera (Dexsuite layouts).""" + + base_camera: TiledCameraCfg = BaseTiledCameraCfg() + wrist_camera: TiledCameraCfg | None = None + + +@register_asset +class KukaAllegroDexsuiteEmbodiment(EmbodimentBase): + """Kuka Allegro for Dexsuite tasks: joint-space actions, contact-rich proprioception, optional RGB cameras.""" + + name = "kuka_allegro_dexsuite" + default_arm_mode = ArmMode.SINGLE_ARM + + def __init__( + self, + physics_preset: Literal["physx", "newton"] = "physx", + enable_cameras: bool = False, + duo_cameras: bool = False, + initial_pose: Pose | None = None, + concatenate_observation_terms: bool = False, + arm_mode: ArmMode | None = None, + ): + super().__init__(enable_cameras, initial_pose, concatenate_observation_terms, arm_mode) + self.physics_preset = physics_preset + self.duo_cameras = duo_cameras + + self.scene_config = DexsuiteKukaAllegroEmbodimentSceneCfg() + self.action_config = kuka_dexsuite_cfg.KukaAllegroRelJointPosActionCfg() + self.observation_config = self._make_observation_cfg() + self._apply_concatenate_observation_terms(self.observation_config) + + # ``PresetCfg`` subclasses (e.g. ``KukaAllegroEventCfg``) store presets as *instance* fields, not + # class attributes — ``KukaAllegroEventCfg.newton`` raises ``AttributeError``. + _event_presets = kuka_dexsuite_cfg.KukaAllegroEventCfg() + if physics_preset == "newton": + self.event_config = getattr(_event_presets, "newton", None) + if self.event_config is None: + from isaaclab_tasks.manager_based.manipulation.dexsuite import dexsuite_env_cfg as _dexsuite + + self.event_config = _dexsuite.EventCfg() + else: + self.event_config = _event_presets.default + + self.reward_config = None + self.command_config = None + self.termination_cfg = None + self.curriculum_config = None + self.mimic_env = None + + if enable_cameras: + self.camera_config = KukaAllegroDexsuiteCameraSceneCfg() + if duo_cameras: + self.camera_config.wrist_camera = WristTiledCameraCfg() + else: + self.camera_config = None + + def _make_observation_cfg(self) -> Any: + if self.enable_cameras: + if self.duo_cameras: + return ArenaDexsuiteKukaDuoCameraObservationsCfg() + return ArenaDexsuiteKukaSingleCameraObservationsCfg() + return ArenaDexsuiteKukaStateObservationCfg() + + def _apply_concatenate_observation_terms(self, obs_cfg: Any) -> None: + for field_name in ("policy", "proprio", "perception", "base_image", "wrist_image"): + if hasattr(obs_cfg, field_name): + grp = getattr(obs_cfg, field_name) + if hasattr(grp, "concatenate_terms"): + grp.concatenate_terms = self.concatenate_observation_terms + + def modify_env_cfg(self, env_cfg: IsaacLabArenaManagerBasedRLEnvCfg) -> IsaacLabArenaManagerBasedRLEnvCfg: + physics_cfg = kuka_dexsuite_cfg.KukaAllegroPhysicsCfg() + env_cfg.sim.physics = getattr(physics_cfg, self.physics_preset, physics_cfg.default) + env_cfg.sim.dt = 1 / 120 + env_cfg.decimation = 2 + # Dexsuite uses replicated physics; Arena's builder defaults InteractiveSceneCfg to False. + if hasattr(env_cfg, "scene") and env_cfg.scene is not None: + env_cfg.scene.replicate_physics = True + return env_cfg + + def get_ee_frame_name(self, arm_mode: ArmMode) -> str: + return "palm_link" + + def get_command_body_name(self) -> str: + # Dexsuite object_pose command uses asset frame on the robot, not a named body. + return "" diff --git a/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py b/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py new file mode 100644 index 000000000..911a08357 --- /dev/null +++ b/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py @@ -0,0 +1,133 @@ +# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Dexsuite Kuka Allegro **lift** MDP: commands, rewards, success signal, terminations, curriculum. + +Mirrors :class:`isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro.dexsuite_kuka_allegro_env_cfg.DexsuiteKukaAllegroLiftEnvCfg` +(Kuka-specific reward params + lift: no orientation reward / position-only goal / ``success`` without orientation). +""" + +from __future__ import annotations + +from typing import Any + +from isaaclab.envs.common import ViewerCfg +from isaaclab.managers import RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.utils import configclass + +from isaaclab_tasks.manager_based.manipulation.dexsuite import dexsuite_env_cfg as dexsuite +from isaaclab_tasks.manager_based.manipulation.dexsuite import mdp as dexsuite_mdp +from isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro.dexsuite_kuka_allegro_env_cfg import ( + FINGER_SENSORS, + THUMB_SENSOR, +) + +from isaaclab_arena.embodiments.common.arm_mode import ArmMode +from isaaclab_arena.environments.isaaclab_arena_manager_based_env import IsaacLabArenaManagerBasedRLEnvCfg +from isaaclab_arena.metrics.metric_base import MetricBase +from isaaclab_arena.tasks.task_base import TaskBase + + +@configclass +class ArenaDexsuiteKukaReorientRewardCfg(dexsuite.RewardsCfg): + """Same as ``KukaAllegroReorientRewardCfg`` but merge-safe: no ``super()`` in ``__post_init__``.""" + + good_finger_contact = RewTerm( + func=dexsuite_mdp.contacts, + weight=0.5, + params={"threshold": 0.1, "thumb_name": THUMB_SENSOR, "finger_names": FINGER_SENSORS}, + ) + + contact_count = RewTerm( + func=dexsuite_mdp.contact_count, + weight=1.0, + params={ + "threshold": 0.01, + "sensor_names": FINGER_SENSORS + [THUMB_SENSOR], + }, + ) + + def __post_init__(self) -> None: + dexsuite.RewardsCfg.__post_init__(self) + self.fingers_to_object.params["asset_cfg"] = SceneEntityCfg("robot", body_names=["palm_link", ".*_tip"]) + self.fingers_to_object.params["thumb_name"] = THUMB_SENSOR + self.fingers_to_object.params["finger_names"] = FINGER_SENSORS + self.position_tracking.params["thumb_name"] = THUMB_SENSOR + self.position_tracking.params["finger_names"] = FINGER_SENSORS + if self.orientation_tracking: + self.orientation_tracking.params["thumb_name"] = THUMB_SENSOR + self.orientation_tracking.params["finger_names"] = FINGER_SENSORS + self.success.params["thumb_name"] = THUMB_SENSOR + self.success.params["finger_names"] = FINGER_SENSORS + + +def _build_dexsuite_kuka_lift_cfgs() -> tuple[Any, Any, Any, Any]: + from isaaclab_tasks.manager_based.manipulation.dexsuite.adr_curriculum import CurriculumCfg + from isaaclab_tasks.manager_based.manipulation.dexsuite.dexsuite_env_cfg import CommandsCfg, TerminationsCfg + + commands = CommandsCfg() + rewards = ArenaDexsuiteKukaReorientRewardCfg() + terminations = TerminationsCfg() + curriculum = CurriculumCfg() + + # ``DexsuiteLiftEnvCfg.__post_init__`` + rewards.orientation_tracking = None + commands.object_pose.position_only = True + rewards.success.params["rot_std"] = None + + return commands, rewards, terminations, curriculum + + +class DexsuiteKukaAllegroLiftTask(TaskBase): + """Lift task matching Isaac Lab ``Isaac-Dexsuite-Kuka-Allegro-Lift-v0`` (state observations).""" + + def __init__(self) -> None: + super().__init__(episode_length_s=6.0, task_description="Dexsuite Kuka Allegro lift (Arena, Newton-ready scene).") + commands, rewards, terminations, curriculum = _build_dexsuite_kuka_lift_cfgs() + self._commands_cfg = commands + self._rewards_cfg = rewards + self._terminations_cfg = terminations + self._curriculum_cfg = curriculum + self._viewer_cfg = ViewerCfg(eye=(-2.25, 0.0, 0.75), lookat=(0.0, 0.0, 0.45), origin_type="env") + + def get_scene_cfg(self) -> Any: + # Scene layout comes from Arena assets; ``replicate_physics`` is applied in the Kuka Dexsuite embodiment's + # :meth:`modify_env_cfg` (not on the embodiment scene cfg, to avoid merging clashes with InteractiveSceneCfg). + return None + + def get_commands_cfg(self) -> Any: + return self._commands_cfg + + def get_rewards_cfg(self) -> Any: + return self._rewards_cfg + + def get_termination_cfg(self) -> Any: + return self._terminations_cfg + + def get_curriculum_cfg(self) -> Any: + return self._curriculum_cfg + + def get_events_cfg(self) -> Any: + return None + + def get_metrics(self) -> list[MetricBase]: + # Dexsuite uses reward-term success, not a ``success`` termination; skip SuccessRate recorder. + return [] + + def get_viewer_cfg(self) -> ViewerCfg: + return self._viewer_cfg + + def get_mimic_env_cfg(self, arm_mode: ArmMode) -> Any: + raise NotImplementedError("Dexsuite Kuka Allegro lift mimic is not configured in Arena yet.") + + def modify_env_cfg(self, env_cfg: IsaacLabArenaManagerBasedRLEnvCfg) -> IsaacLabArenaManagerBasedRLEnvCfg: + # ``DexsuiteReorientEnvCfg.__post_init__`` timing / horizon (sim.dt and physics come from embodiment). + env_cfg.decimation = 2 + env_cfg.commands.object_pose.resampling_time_range = (2.0, 3.0) + env_cfg.commands.object_pose.position_only = True + env_cfg.episode_length_s = 6.0 + env_cfg.is_finite_horizon = False + return env_cfg diff --git a/isaaclab_arena/tests/test_configclass.py b/isaaclab_arena/tests/test_configclass.py index a95dc2b23..f9a29e8bd 100644 --- a/isaaclab_arena/tests/test_configclass.py +++ b/isaaclab_arena/tests/test_configclass.py @@ -96,8 +96,29 @@ def __post_init__(self): assert CombinedCfg().c == 4 +def test_combine_configclass_instances_preserves_default_factory_nested_config(): + """Merged classes must not assign raw ``default_factory`` callables as field defaults (see ``get_field_info``).""" + from isaaclab.utils import configclass + + from isaaclab_arena.utils.configclass import combine_configclass_instances + + @configclass + class Inner: + x: int = 0 + + @configclass + class Outer: + inner: Inner = Inner() + + merged = combine_configclass_instances("MergedOuter", Outer()) + assert not callable(merged.inner) + assert type(merged.inner).__name__ == "Inner" + assert merged.inner.x == 0 + + if __name__ == "__main__": test_combine_configclasses_with_multiple_inheritance() test_combine_configclasses_with_inheritance() test_combine_configclasses_with_post_init() + test_combine_configclass_instances_preserves_default_factory_nested_config() diff --git a/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py b/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py new file mode 100644 index 000000000..ceba18525 --- /dev/null +++ b/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py @@ -0,0 +1,36 @@ +# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Dexsuite Kuka Allegro lift Arena example (no simulation).""" + +import pytest + +pytest.importorskip("isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro.dexsuite_kuka_allegro_env_cfg") + + +def test_kuka_allegro_dexsuite_lift_example_in_cli_registry() -> None: + from isaaclab_arena_environments.cli import ExampleEnvironments + + assert "kuka_allegro_dexsuite_lift" in ExampleEnvironments + assert ExampleEnvironments["kuka_allegro_dexsuite_lift"].name == "kuka_allegro_dexsuite_lift" + + +def test_dexsuite_procedural_assets_registered() -> None: + from isaaclab_arena.assets.asset_registry import AssetRegistry + + reg = AssetRegistry() + assert reg.is_registered("dexsuite_manip_table") + assert reg.is_registered("dexsuite_lift_object") + + +def test_dexsuite_kuka_lift_task_matches_lift_mdp_flags() -> None: + from isaaclab_arena.tasks.dexsuite_kuka_allegro_lift_task import DexsuiteKukaAllegroLiftTask + + task = DexsuiteKukaAllegroLiftTask() + assert task.get_scene_cfg() is None + assert task._rewards_cfg.orientation_tracking is None + assert task._commands_cfg.object_pose.position_only is True + assert task._rewards_cfg.success.params.get("rot_std") is None + assert task.get_metrics() == [] diff --git a/isaaclab_arena/tests/test_kuka_allegro_embodiment.py b/isaaclab_arena/tests/test_kuka_allegro_embodiment.py new file mode 100644 index 000000000..b60df0d73 --- /dev/null +++ b/isaaclab_arena/tests/test_kuka_allegro_embodiment.py @@ -0,0 +1,103 @@ +# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for :class:`KukaAllegroDexsuiteEmbodiment` (no simulation).""" + +import pytest + +pytest.importorskip("isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro.dexsuite_kuka_allegro_env_cfg") + +from isaaclab_arena.assets.asset_registry import AssetRegistry +from isaaclab_arena.embodiments.kuka_allegro.kuka_allegro import ( + DexsuiteKukaAllegroEmbodimentSceneCfg, + KukaAllegroDexsuiteCameraSceneCfg, + KukaAllegroDexsuiteEmbodiment, +) + + +def test_kuka_allegro_dexsuite_registered_in_asset_registry() -> None: + reg = AssetRegistry() + assert reg.is_registered("kuka_allegro_dexsuite") + cls = reg.get_asset_by_name("kuka_allegro_dexsuite") + assert cls is KukaAllegroDexsuiteEmbodiment + + +def test_scene_cfg_has_robot_and_fingertip_contact_sensors() -> None: + cfg = DexsuiteKukaAllegroEmbodimentSceneCfg() + assert cfg.robot is not None + for link in ("index_link_3", "middle_link_3", "ring_link_3", "thumb_link_3"): + attr = f"{link}_object_s" + assert hasattr(cfg, attr), f"missing sensor {attr}" + sensor = getattr(cfg, attr) + assert "{ENV_REGEX_NS}/Object" in sensor.filter_prim_paths_expr + + +def test_embodiment_default_action_and_observation() -> None: + emb = KukaAllegroDexsuiteEmbodiment() + assert emb.physics_preset == "physx" + assert emb.enable_cameras is False + assert emb.duo_cameras is False + assert emb.action_config.action.scale == 0.1 + assert emb.action_config.action.joint_names == [".*"] + assert emb.action_config.action.asset_name == "robot" + # State observation extends dexsuite ObservationsCfg with fingertip contact term + assert hasattr(emb.observation_config, "proprio") + assert hasattr(emb.observation_config.proprio, "contact") + + +def test_embodiment_physx_vs_newton_events() -> None: + physx_emb = KukaAllegroDexsuiteEmbodiment(physics_preset="physx") + newton_emb = KukaAllegroDexsuiteEmbodiment(physics_preset="newton") + assert physx_emb.event_config is not newton_emb.event_config + + +def test_embodiment_enable_cameras_sets_camera_scene_and_observations() -> None: + single = KukaAllegroDexsuiteEmbodiment(enable_cameras=True, duo_cameras=False) + assert single.camera_config is not None + assert isinstance(single.camera_config, KukaAllegroDexsuiteCameraSceneCfg) + assert single.camera_config.base_camera is not None + assert single.camera_config.wrist_camera is None + assert hasattr(single.observation_config, "base_image") + + duo = KukaAllegroDexsuiteEmbodiment(enable_cameras=True, duo_cameras=True) + assert duo.camera_config.wrist_camera is not None + assert hasattr(duo.observation_config, "wrist_image") + + +def test_get_scene_cfg_includes_cameras_when_enabled() -> None: + emb = KukaAllegroDexsuiteEmbodiment(enable_cameras=True, duo_cameras=True) + scene_cfg = emb.get_scene_cfg() + assert hasattr(scene_cfg, "base_camera") + assert scene_cfg.base_camera is not None + assert hasattr(scene_cfg, "wrist_camera") + assert scene_cfg.wrist_camera is not None + + +def test_modify_env_cfg_sets_dexsuite_timestep_and_physics() -> None: + """Duck-typed env cfg: ``sim``, ``decimation``, and optional ``scene.replicate_physics``.""" + from isaaclab_physx.physics import PhysxCfg + + class _Sim: + def __init__(self) -> None: + self.dt = 0.01 + self.physics = None + + class _Scene: + def __init__(self) -> None: + self.replicate_physics = False + + class _EnvCfg: + def __init__(self) -> None: + self.sim = _Sim() + self.decimation = 4 + self.scene = _Scene() + + emb = KukaAllegroDexsuiteEmbodiment(physics_preset="physx") + cfg = _EnvCfg() + out = emb.modify_env_cfg(cfg) # type: ignore[arg-type] + assert out.decimation == 2 + assert out.sim.dt == pytest.approx(1 / 120) + assert isinstance(out.sim.physics, PhysxCfg) + assert out.scene.replicate_physics is True diff --git a/isaaclab_arena/utils/configclass.py b/isaaclab_arena/utils/configclass.py index 28c69835d..9748c1349 100644 --- a/isaaclab_arena/utils/configclass.py +++ b/isaaclab_arena/utils/configclass.py @@ -127,7 +127,10 @@ def get_field_info(field: dataclasses.Field) -> tuple[str, type, Any]: if field.default is not dataclasses.MISSING: field_info += (field.default,) elif field.default_factory is not dataclasses.MISSING: - field_info += (field.default_factory,) + # Isaac Lab ``@configclass`` turns mutable defaults into ``field(default_factory=...)``. + # Passing the raw callable to :func:`make_configclass` sets ``Class.field_name = `` in the + # generated class body, so merged instances get a function instead of a config instance (e.g. ``proprio``). + field_info += (dataclasses.field(default_factory=field.default_factory),) return field_info diff --git a/isaaclab_arena_environments/cli.py b/isaaclab_arena_environments/cli.py index 6df4797a3..22a5c06d2 100644 --- a/isaaclab_arena_environments/cli.py +++ b/isaaclab_arena_environments/cli.py @@ -22,6 +22,7 @@ ) from isaaclab_arena_environments.gr1_turn_stand_mixer_knob_environment import Gr1TurnStandMixerKnobEnvironment from isaaclab_arena_environments.kitchen_pick_and_place_environment import KitchenPickAndPlaceEnvironment +from isaaclab_arena_environments.kuka_allegro_dexsuite_lift_environment import KukaAllegroDexsuiteLiftEnvironment from isaaclab_arena_environments.lift_object_environment import LiftObjectEnvironment from isaaclab_arena_environments.press_button_environment import PressButtonEnvironment from isaaclab_arena_environments.tabletop_place_upright_environment import TableTopPlaceUprightEnvironment @@ -44,6 +45,7 @@ GalileoG1LocomanipPickAndPlaceEnvironment.name: GalileoG1LocomanipPickAndPlaceEnvironment, PressButtonEnvironment.name: PressButtonEnvironment, CubeGoalPoseEnvironment.name: CubeGoalPoseEnvironment, + KukaAllegroDexsuiteLiftEnvironment.name: KukaAllegroDexsuiteLiftEnvironment, LiftObjectEnvironment.name: LiftObjectEnvironment, TableTopPlaceUprightEnvironment.name: TableTopPlaceUprightEnvironment, Gr1TurnStandMixerKnobEnvironment.name: Gr1TurnStandMixerKnobEnvironment, diff --git a/isaaclab_arena_environments/kuka_allegro_dexsuite_lift_environment.py b/isaaclab_arena_environments/kuka_allegro_dexsuite_lift_environment.py new file mode 100644 index 000000000..b0f439d9d --- /dev/null +++ b/isaaclab_arena_environments/kuka_allegro_dexsuite_lift_environment.py @@ -0,0 +1,87 @@ +# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Arena example: Dexsuite Kuka Allegro **lift** with **Newton** physics. + +Matches the MDP of Isaac Lab ``Isaac-Dexsuite-Kuka-Allegro-Lift-v0`` (state observations, joint actions): +procedural lift cuboid + Dexsuite-style kinematic table, :class:`~isaaclab_arena.embodiments.kuka_allegro.kuka_allegro.KukaAllegroDexsuiteEmbodiment` with ``physics_preset="newton"``. + +**Play a checkpoint trained in Isaac Lab** (same PPO runner / obs groups):: + + ./isaaclab.sh -p isaaclab_arena/scripts/reinforcement_learning/play.py \\ + kuka_allegro_dexsuite_lift \\ + --num_envs 1 --env_spacing 3 \\ + --checkpoint /path/to/logs/rsl_rl/dexsuite_kuka_allegro//model_.pt + +Match Dexsuite training layout: use ``--env_spacing 3`` (Arena CLI defaults to a larger spacing). + +Use ``--enable_cameras`` (and optionally ``--duo_cameras``) if the policy was trained with vision presets. + +**Note:** Checkpoints from the stock Isaac Lab task are usually trained with **PhysX**; this example uses **Newton**, +so replay quality may differ unless you train or fine-tune with Newton as well. +""" + +from __future__ import annotations + +import argparse + +from isaaclab_arena_environments.example_environment_base import ExampleEnvironmentBase + +# NOTE: Same pattern as other example envs — avoid heavy imports before AppLauncher. + + +class KukaAllegroDexsuiteLiftEnvironment(ExampleEnvironmentBase): + """Dexsuite Kuka Allegro lift task; Newton backend; RSL-RL config ``DexsuiteKukaAllegroPPORunnerCfg``.""" + + name: str = "kuka_allegro_dexsuite_lift" + + def get_env(self, args_cli: argparse.Namespace): + import isaaclab_tasks.manager_based.manipulation.dexsuite # noqa: F401 + + from isaaclab_arena.environments.isaaclab_arena_environment import IsaacLabArenaEnvironment + from isaaclab_arena.reinforcement_learning.frameworks import RLFramework + from isaaclab_arena.scene.scene import Scene + from isaaclab_arena.tasks.dexsuite_kuka_allegro_lift_task import DexsuiteKukaAllegroLiftTask + + dexsuite_table = self.asset_registry.get_asset_by_name("dexsuite_manip_table")() + manip_object = self.asset_registry.get_asset_by_name("dexsuite_lift_object")() + ground_plane = self.asset_registry.get_asset_by_name("ground_plane")() + light = self.asset_registry.get_asset_by_name("light")() + + enable_cameras = getattr(args_cli, "enable_cameras", False) + duo_cameras = getattr(args_cli, "duo_cameras", False) + + embodiment = self.asset_registry.get_asset_by_name("kuka_allegro_dexsuite")( + physics_preset="newton", + enable_cameras=enable_cameras, + duo_cameras=duo_cameras, + ) + + scene = Scene(assets=[dexsuite_table, manip_object, ground_plane, light]) + task = DexsuiteKukaAllegroLiftTask() + + dexsuite_rl_cfg_entry = ( + "isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro.agents." + "rsl_rl_ppo_cfg:DexsuiteKukaAllegroPPORunnerCfg" + ) + + return IsaacLabArenaEnvironment( + name=self.name, + embodiment=embodiment, + scene=scene, + task=task, + teleop_device=None, + rl_framework=RLFramework.RSL_RL, + rl_policy_cfg=dexsuite_rl_cfg_entry, + ) + + @staticmethod + def add_cli_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--duo_cameras", + action="store_true", + default=False, + help="Use base+wrist cameras (Dexsuite duo layout). Match the checkpoint training preset.", + ) From ad2b7b00eb69f71296beb5a4d748b9d863e6e0b4 Mon Sep 17 00:00:00 2001 From: Qian Lin Date: Mon, 23 Mar 2026 02:52:34 -0700 Subject: [PATCH 2/6] Reuse lift task --- isaaclab_arena/assets/dexsuite_assets.py | 3 + .../tasks/dexsuite_kuka_allegro_lift_task.py | 59 ++++++++++++------- .../tests/test_dexsuite_kuka_lift_example.py | 10 +++- .../kuka_allegro_dexsuite_lift_environment.py | 6 +- 4 files changed, 56 insertions(+), 22 deletions(-) diff --git a/isaaclab_arena/assets/dexsuite_assets.py b/isaaclab_arena/assets/dexsuite_assets.py index 67a562a79..7304eea0c 100644 --- a/isaaclab_arena/assets/dexsuite_assets.py +++ b/isaaclab_arena/assets/dexsuite_assets.py @@ -43,6 +43,9 @@ class DexsuiteManipTable(Object): name = "dexsuite_manip_table" tags = ["background", "dexsuite"] + # ``LiftObjectTask.make_il_termination_cfg`` reads this before Dexsuite tasks replace terminations. + # Matches Dexsuite ``object_out_of_bound`` z lower bound (see ``dexsuite_env_cfg.TerminationsCfg``). + object_min_z: float = 0.0 def __init__( self, diff --git a/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py b/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py index 911a08357..9adc9ca84 100644 --- a/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py +++ b/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py @@ -5,14 +5,23 @@ """Dexsuite Kuka Allegro **lift** MDP: commands, rewards, success signal, terminations, curriculum. -Mirrors :class:`isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro.dexsuite_kuka_allegro_env_cfg.DexsuiteKukaAllegroLiftEnvCfg` -(Kuka-specific reward params + lift: no orientation reward / position-only goal / ``success`` without orientation). +Extends :class:`~isaaclab_arena.tasks.lift_object_task.LiftObjectTask` so the task holds the same +``lift_object`` / ``background_scene`` :class:`~isaaclab_arena.assets.asset.Asset` references as +other lift examples (viewer look-at, future IL/mimic hooks). The **MDP** still mirrors Isaac Lab +:class:`~isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro.dexsuite_kuka_allegro_env_cfg.DexsuiteKukaAllegroLiftEnvCfg` +(Dexsuite commands/rewards/terminations/curriculum), not Arena's generic +:class:`~isaaclab_arena.tasks.lift_object_task.LiftObjectRewardCfg` stack. + +After construction, parent's IL-style :attr:`~isaaclab_arena.tasks.lift_object_task.LiftObjectTask.termination_cfg` +is replaced with Dexsuite :class:`TerminationsCfg`; :meth:`get_commands_cfg` / :meth:`get_rewards_cfg` / +:meth:`get_curriculum_cfg` return Dexsuite configs. """ from __future__ import annotations from typing import Any +import numpy as np from isaaclab.envs.common import ViewerCfg from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg @@ -25,10 +34,11 @@ THUMB_SENSOR, ) +from isaaclab_arena.assets.asset import Asset from isaaclab_arena.embodiments.common.arm_mode import ArmMode from isaaclab_arena.environments.isaaclab_arena_manager_based_env import IsaacLabArenaManagerBasedRLEnvCfg from isaaclab_arena.metrics.metric_base import MetricBase -from isaaclab_arena.tasks.task_base import TaskBase +from isaaclab_arena.tasks.lift_object_task import LiftObjectTask @configclass @@ -81,22 +91,31 @@ def _build_dexsuite_kuka_lift_cfgs() -> tuple[Any, Any, Any, Any]: return commands, rewards, terminations, curriculum -class DexsuiteKukaAllegroLiftTask(TaskBase): - """Lift task matching Isaac Lab ``Isaac-Dexsuite-Kuka-Allegro-Lift-v0`` (state observations).""" +class DexsuiteKukaAllegroLiftTask(LiftObjectTask): + """Lift task matching Isaac Lab ``Isaac-Dexsuite-Kuka-Allegro-Lift-v0`` (state observations). + + Reuses :class:`LiftObjectTask` for ``lift_object`` / ``background_scene`` and the default + look-at-object viewer. MDP pieces come from Dexsuite (not :class:`LiftObjectTaskRL`). + """ + + def __init__(self, lift_object: Asset, background_scene: Asset) -> None: + # Goal fields from LiftObjectTask are unused for Dexsuite terminations but keep parent API consistent. + super().__init__( + lift_object=lift_object, + background_scene=background_scene, + episode_length_s=6.0, + goal_position_delta_xyz=(0.0, 0.0, 0.3), + goal_position_tolerance=0.05, + ) + self.task_description = "Dexsuite Kuka Allegro lift (Arena, Newton-ready scene)." - def __init__(self) -> None: - super().__init__(episode_length_s=6.0, task_description="Dexsuite Kuka Allegro lift (Arena, Newton-ready scene).") commands, rewards, terminations, curriculum = _build_dexsuite_kuka_lift_cfgs() self._commands_cfg = commands self._rewards_cfg = rewards self._terminations_cfg = terminations self._curriculum_cfg = curriculum - self._viewer_cfg = ViewerCfg(eye=(-2.25, 0.0, 0.75), lookat=(0.0, 0.0, 0.45), origin_type="env") - - def get_scene_cfg(self) -> Any: - # Scene layout comes from Arena assets; ``replicate_physics`` is applied in the Kuka Dexsuite embodiment's - # :meth:`modify_env_cfg` (not on the embodiment scene cfg, to avoid merging clashes with InteractiveSceneCfg). - return None + # Replace parent's IL terminations with Dexsuite MDP terminations. + self.termination_cfg = self._terminations_cfg def get_commands_cfg(self) -> Any: return self._commands_cfg @@ -104,21 +123,21 @@ def get_commands_cfg(self) -> Any: def get_rewards_cfg(self) -> Any: return self._rewards_cfg - def get_termination_cfg(self) -> Any: - return self._terminations_cfg - def get_curriculum_cfg(self) -> Any: return self._curriculum_cfg - def get_events_cfg(self) -> Any: - return None - def get_metrics(self) -> list[MetricBase]: # Dexsuite uses reward-term success, not a ``success`` termination; skip SuccessRate recorder. return [] def get_viewer_cfg(self) -> ViewerCfg: - return self._viewer_cfg + # Reuse LiftObjectTask's look-at-object framing (same offset as generic lift examples). + from isaaclab_arena.utils.cameras import get_viewer_cfg_look_at_object + + return get_viewer_cfg_look_at_object( + lookat_object=self.lift_object, + offset=np.array([-1.5, -1.5, 1.5]), + ) def get_mimic_env_cfg(self, arm_mode: ArmMode) -> Any: raise NotImplementedError("Dexsuite Kuka Allegro lift mimic is not configured in Arena yet.") diff --git a/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py b/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py index ceba18525..af729de83 100644 --- a/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py +++ b/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py @@ -26,9 +26,17 @@ def test_dexsuite_procedural_assets_registered() -> None: def test_dexsuite_kuka_lift_task_matches_lift_mdp_flags() -> None: + from isaaclab_arena.assets.asset_registry import AssetRegistry from isaaclab_arena.tasks.dexsuite_kuka_allegro_lift_task import DexsuiteKukaAllegroLiftTask - task = DexsuiteKukaAllegroLiftTask() + from isaaclab_arena.tasks.lift_object_task import LiftObjectTask + + reg = AssetRegistry() + lift = reg.get_asset_by_name("dexsuite_lift_object")() + table = reg.get_asset_by_name("dexsuite_manip_table")() + task = DexsuiteKukaAllegroLiftTask(lift_object=lift, background_scene=table) + assert isinstance(task, LiftObjectTask) + assert task.lift_object is lift assert task.get_scene_cfg() is None assert task._rewards_cfg.orientation_tracking is None assert task._commands_cfg.object_pose.position_only is True diff --git a/isaaclab_arena_environments/kuka_allegro_dexsuite_lift_environment.py b/isaaclab_arena_environments/kuka_allegro_dexsuite_lift_environment.py index b0f439d9d..d75376ca2 100644 --- a/isaaclab_arena_environments/kuka_allegro_dexsuite_lift_environment.py +++ b/isaaclab_arena_environments/kuka_allegro_dexsuite_lift_environment.py @@ -17,6 +17,10 @@ Match Dexsuite training layout: use ``--env_spacing 3`` (Arena CLI defaults to a larger spacing). +The task subclasses :class:`~isaaclab_arena.tasks.lift_object_task.LiftObjectTask` and uses the same +look-at-lift-object viewer helper as other Arena lift examples (Isaac Lab's stock task uses a fixed +viewer pose instead). + Use ``--enable_cameras`` (and optionally ``--duo_cameras``) if the policy was trained with vision presets. **Note:** Checkpoints from the stock Isaac Lab task are usually trained with **PhysX**; this example uses **Newton**, @@ -60,7 +64,7 @@ def get_env(self, args_cli: argparse.Namespace): ) scene = Scene(assets=[dexsuite_table, manip_object, ground_plane, light]) - task = DexsuiteKukaAllegroLiftTask() + task = DexsuiteKukaAllegroLiftTask(lift_object=manip_object, background_scene=dexsuite_table) dexsuite_rl_cfg_entry = ( "isaaclab_tasks.manager_based.manipulation.dexsuite.config.kuka_allegro.agents." From 9dee2a9d07a345bd7a6313cd4951a71308340256 Mon Sep 17 00:00:00 2001 From: Qian Lin Date: Mon, 23 Mar 2026 02:52:50 -0700 Subject: [PATCH 3/6] Fix file write error missing metrics --- isaaclab_arena/evaluation/policy_runner.py | 5 +++-- isaaclab_arena/metrics/metrics.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/isaaclab_arena/evaluation/policy_runner.py b/isaaclab_arena/evaluation/policy_runner.py index 36176feb2..1e94556ea 100644 --- a/isaaclab_arena/evaluation/policy_runner.py +++ b/isaaclab_arena/evaluation/policy_runner.py @@ -123,8 +123,9 @@ def rollout_policy( else: - # Only compute metrics if env has a non-None metrics list (e.g. NoTask leaves metrics as None). - if hasattr(env.cfg, "metrics") and env.cfg.metrics is not None: + # Only compute metrics when at least one metric is registered. An empty list still configures a + # dataset path but policy_runner may never create the HDF5 (no recorders / no completed episodes). + if getattr(env.cfg, "metrics", None): # NOTE(xinjieyao, 2025-10-07): lazy import to prevent app stalling caused by omni.kit from isaaclab_arena.metrics.metrics import compute_metrics metrics = compute_metrics(env) diff --git a/isaaclab_arena/metrics/metrics.py b/isaaclab_arena/metrics/metrics.py index ad927a4c3..0719eddb7 100644 --- a/isaaclab_arena/metrics/metrics.py +++ b/isaaclab_arena/metrics/metrics.py @@ -48,6 +48,8 @@ def get_recorded_metric_data(dataset_path: pathlib.Path, recorder_term_name: str Returns: A list of recorded metric data for each simulated episode. """ + if not dataset_path.is_file(): + return [] recorded_metric_data_per_demo: list[np.ndarray] = [] with h5py.File(dataset_path, "r") as f: demos = f["data"] @@ -65,6 +67,8 @@ def get_num_episodes(dataset_path: pathlib.Path) -> int: Returns: The number of episodes in the dataset. """ + if not dataset_path.is_file(): + return 0 with h5py.File(dataset_path, "r") as f: return len(f["data"]) From b24dfab0f54a69f8c171138329e22b117f009ecd Mon Sep 17 00:00:00 2001 From: Qian Lin Date: Mon, 23 Mar 2026 03:11:49 -0700 Subject: [PATCH 4/6] Dexsuit support ckpt evaluation --- isaaclab_arena/embodiments/kuka_allegro/kuka_allegro.py | 4 +++- isaaclab_arena/tests/test_kuka_allegro_embodiment.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/isaaclab_arena/embodiments/kuka_allegro/kuka_allegro.py b/isaaclab_arena/embodiments/kuka_allegro/kuka_allegro.py index f8fcc3ac6..d39f361b4 100644 --- a/isaaclab_arena/embodiments/kuka_allegro/kuka_allegro.py +++ b/isaaclab_arena/embodiments/kuka_allegro/kuka_allegro.py @@ -158,9 +158,11 @@ def __init__( enable_cameras: bool = False, duo_cameras: bool = False, initial_pose: Pose | None = None, - concatenate_observation_terms: bool = False, + concatenate_observation_terms: bool = True, arm_mode: ArmMode | None = None, ): + # Default ``True``: Dexsuite ``PolicyCfg`` / ``ProprioObsCfg`` use ``concatenate_terms=True`` in Isaac Lab; + # RSL-RL MLPs require flat 1D-per-env vectors. ``False`` yields invalid shapes (e.g. ``torch.Size([1])``). super().__init__(enable_cameras, initial_pose, concatenate_observation_terms, arm_mode) self.physics_preset = physics_preset self.duo_cameras = duo_cameras diff --git a/isaaclab_arena/tests/test_kuka_allegro_embodiment.py b/isaaclab_arena/tests/test_kuka_allegro_embodiment.py index b60df0d73..2fdfd3fe5 100644 --- a/isaaclab_arena/tests/test_kuka_allegro_embodiment.py +++ b/isaaclab_arena/tests/test_kuka_allegro_embodiment.py @@ -39,6 +39,8 @@ def test_embodiment_default_action_and_observation() -> None: assert emb.physics_preset == "physx" assert emb.enable_cameras is False assert emb.duo_cameras is False + assert emb.concatenate_observation_terms is True + assert emb.observation_config.policy.concatenate_terms is True assert emb.action_config.action.scale == 0.1 assert emb.action_config.action.joint_names == [".*"] assert emb.action_config.action.asset_name == "robot" From b1f76839f6a92d902d9387ab1e9c439dc542f0f4 Mon Sep 17 00:00:00 2001 From: Qian Lin Date: Mon, 23 Mar 2026 03:17:58 -0700 Subject: [PATCH 5/6] Add success rate measure --- .../metrics/dexsuite_lift_success_rate.py | 67 +++++++++++++++++++ .../tasks/dexsuite_kuka_allegro_lift_task.py | 6 +- .../tests/test_dexsuite_kuka_lift_example.py | 7 +- 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 isaaclab_arena/metrics/dexsuite_lift_success_rate.py diff --git a/isaaclab_arena/metrics/dexsuite_lift_success_rate.py b/isaaclab_arena/metrics/dexsuite_lift_success_rate.py new file mode 100644 index 000000000..a23e2958e --- /dev/null +++ b/isaaclab_arena/metrics/dexsuite_lift_success_rate.py @@ -0,0 +1,67 @@ +# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Success-rate metric for Dexsuite lift-style MDPs. + +Dexsuite uses :class:`success_reward` (sticky ``succeeded`` on the reward term) rather than a +``success`` *termination*. :class:`~isaaclab_arena.metrics.success_rate.SuccessRecorder` therefore +cannot be used; this module records from ``reward_manager.get_term_cfg("success").func.succeeded``. +""" + +from __future__ import annotations + +from collections.abc import Sequence + +import torch + +from isaaclab.managers.recorder_manager import RecorderTerm, RecorderTermCfg +from isaaclab.utils import configclass + +from isaaclab_arena.metrics.success_rate import SuccessRateMetric + + +class DexsuiteLiftSuccessRecorder(RecorderTerm): + """Records whether the episode achieved Dexsuite success (sticky reward term), pre-reset.""" + + def __init__(self, cfg: RecorderTermCfg, env): + super().__init__(cfg, env) + self.name = cfg.name + self.first_reset = True + + def record_pre_reset(self, env_ids: Sequence[int]): + if self.first_reset: + assert len(env_ids) == self._env.num_envs + self.first_reset = False + return None, None + + if "success" not in self._env.reward_manager.active_terms: + raise RuntimeError( + "DexsuiteLiftSuccessRecorder requires a reward term named 'success' " + "(Dexsuite ``success_reward``)." + ) + term_cfg = self._env.reward_manager.get_term_cfg("success") + func = term_cfg.func + if not hasattr(func, "succeeded"): + raise TypeError( + f"Reward term 'success' must be a stateful term with `.succeeded` (e.g. Dexsuite " + f"`success_reward`), got {type(func)}." + ) + success_results = func.succeeded[env_ids] + return self.name, success_results + + +@configclass +class DexsuiteLiftSuccessRecorderCfg(RecorderTermCfg): + class_type: type[RecorderTerm] = DexsuiteLiftSuccessRecorder + name: str = "dexsuite_lift_success" + + +class DexsuiteLiftSuccessRateMetric(SuccessRateMetric): + """Same aggregation as :class:`SuccessRateMetric`, sourcing labels from Dexsuite success reward.""" + + recorder_term_name: str = "dexsuite_lift_success" + + def get_recorder_term_cfg(self) -> RecorderTermCfg: + return DexsuiteLiftSuccessRecorderCfg(name=self.recorder_term_name) diff --git a/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py b/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py index 9adc9ca84..ff778d2d4 100644 --- a/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py +++ b/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py @@ -37,6 +37,7 @@ from isaaclab_arena.assets.asset import Asset from isaaclab_arena.embodiments.common.arm_mode import ArmMode from isaaclab_arena.environments.isaaclab_arena_manager_based_env import IsaacLabArenaManagerBasedRLEnvCfg +from isaaclab_arena.metrics.dexsuite_lift_success_rate import DexsuiteLiftSuccessRateMetric from isaaclab_arena.metrics.metric_base import MetricBase from isaaclab_arena.tasks.lift_object_task import LiftObjectTask @@ -127,8 +128,9 @@ def get_curriculum_cfg(self) -> Any: return self._curriculum_cfg def get_metrics(self) -> list[MetricBase]: - # Dexsuite uses reward-term success, not a ``success`` termination; skip SuccessRate recorder. - return [] + # Dexsuite success is sticky state on the ``success`` reward term, not a termination; see + # :class:`~isaaclab_arena.metrics.dexsuite_lift_success_rate.DexsuiteLiftSuccessRateMetric`. + return [DexsuiteLiftSuccessRateMetric()] def get_viewer_cfg(self) -> ViewerCfg: # Reuse LiftObjectTask's look-at-object framing (same offset as generic lift examples). diff --git a/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py b/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py index af729de83..e8c1eab20 100644 --- a/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py +++ b/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py @@ -27,8 +27,8 @@ def test_dexsuite_procedural_assets_registered() -> None: def test_dexsuite_kuka_lift_task_matches_lift_mdp_flags() -> None: from isaaclab_arena.assets.asset_registry import AssetRegistry + from isaaclab_arena.metrics.dexsuite_lift_success_rate import DexsuiteLiftSuccessRateMetric from isaaclab_arena.tasks.dexsuite_kuka_allegro_lift_task import DexsuiteKukaAllegroLiftTask - from isaaclab_arena.tasks.lift_object_task import LiftObjectTask reg = AssetRegistry() @@ -41,4 +41,7 @@ def test_dexsuite_kuka_lift_task_matches_lift_mdp_flags() -> None: assert task._rewards_cfg.orientation_tracking is None assert task._commands_cfg.object_pose.position_only is True assert task._rewards_cfg.success.params.get("rot_std") is None - assert task.get_metrics() == [] + metrics = task.get_metrics() + assert len(metrics) == 1 + assert isinstance(metrics[0], DexsuiteLiftSuccessRateMetric) + assert metrics[0].recorder_term_name == "dexsuite_lift_success" From 784224ffddfa807b44d5d4c72820dc5061d91760 Mon Sep 17 00:00:00 2001 From: Qian Lin Date: Mon, 23 Mar 2026 03:43:04 -0700 Subject: [PATCH 6/6] Improve dexsuite success rate measure --- .../metrics/dexsuite_lift_success_rate.py | 67 ------------------- .../tasks/dexsuite_kuka_allegro_lift_task.py | 50 +++++++++++--- .../tests/test_dexsuite_kuka_lift_example.py | 14 ++-- 3 files changed, 49 insertions(+), 82 deletions(-) delete mode 100644 isaaclab_arena/metrics/dexsuite_lift_success_rate.py diff --git a/isaaclab_arena/metrics/dexsuite_lift_success_rate.py b/isaaclab_arena/metrics/dexsuite_lift_success_rate.py deleted file mode 100644 index a23e2958e..000000000 --- a/isaaclab_arena/metrics/dexsuite_lift_success_rate.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -"""Success-rate metric for Dexsuite lift-style MDPs. - -Dexsuite uses :class:`success_reward` (sticky ``succeeded`` on the reward term) rather than a -``success`` *termination*. :class:`~isaaclab_arena.metrics.success_rate.SuccessRecorder` therefore -cannot be used; this module records from ``reward_manager.get_term_cfg("success").func.succeeded``. -""" - -from __future__ import annotations - -from collections.abc import Sequence - -import torch - -from isaaclab.managers.recorder_manager import RecorderTerm, RecorderTermCfg -from isaaclab.utils import configclass - -from isaaclab_arena.metrics.success_rate import SuccessRateMetric - - -class DexsuiteLiftSuccessRecorder(RecorderTerm): - """Records whether the episode achieved Dexsuite success (sticky reward term), pre-reset.""" - - def __init__(self, cfg: RecorderTermCfg, env): - super().__init__(cfg, env) - self.name = cfg.name - self.first_reset = True - - def record_pre_reset(self, env_ids: Sequence[int]): - if self.first_reset: - assert len(env_ids) == self._env.num_envs - self.first_reset = False - return None, None - - if "success" not in self._env.reward_manager.active_terms: - raise RuntimeError( - "DexsuiteLiftSuccessRecorder requires a reward term named 'success' " - "(Dexsuite ``success_reward``)." - ) - term_cfg = self._env.reward_manager.get_term_cfg("success") - func = term_cfg.func - if not hasattr(func, "succeeded"): - raise TypeError( - f"Reward term 'success' must be a stateful term with `.succeeded` (e.g. Dexsuite " - f"`success_reward`), got {type(func)}." - ) - success_results = func.succeeded[env_ids] - return self.name, success_results - - -@configclass -class DexsuiteLiftSuccessRecorderCfg(RecorderTermCfg): - class_type: type[RecorderTerm] = DexsuiteLiftSuccessRecorder - name: str = "dexsuite_lift_success" - - -class DexsuiteLiftSuccessRateMetric(SuccessRateMetric): - """Same aggregation as :class:`SuccessRateMetric`, sourcing labels from Dexsuite success reward.""" - - recorder_term_name: str = "dexsuite_lift_success" - - def get_recorder_term_cfg(self) -> RecorderTermCfg: - return DexsuiteLiftSuccessRecorderCfg(name=self.recorder_term_name) diff --git a/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py b/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py index ff778d2d4..169ff6abe 100644 --- a/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py +++ b/isaaclab_arena/tasks/dexsuite_kuka_allegro_lift_task.py @@ -13,8 +13,11 @@ :class:`~isaaclab_arena.tasks.lift_object_task.LiftObjectRewardCfg` stack. After construction, parent's IL-style :attr:`~isaaclab_arena.tasks.lift_object_task.LiftObjectTask.termination_cfg` -is replaced with Dexsuite :class:`TerminationsCfg`; :meth:`get_commands_cfg` / :meth:`get_rewards_cfg` / -:meth:`get_curriculum_cfg` return Dexsuite configs. +is replaced with Dexsuite terminations **plus** a ``success`` termination tied to +:class:`~isaaclab_tasks.manager_based.manipulation.dexsuite.mdp.rewards.success_reward` (``succeeded``), +so :class:`~isaaclab_arena.metrics.success_rate.SuccessRateMetric` works like other Arena lift tasks. +That success termination **ends the episode when success is first achieved** (vanilla Dexsuite Isaac +envs typically keep rolling until time-out). """ from __future__ import annotations @@ -22,9 +25,12 @@ from typing import Any import numpy as np +import torch +from isaaclab.envs import ManagerBasedRLEnv from isaaclab.envs.common import ViewerCfg from isaaclab.managers import RewardTermCfg as RewTerm from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm from isaaclab.utils import configclass from isaaclab_tasks.manager_based.manipulation.dexsuite import dexsuite_env_cfg as dexsuite @@ -37,11 +43,31 @@ from isaaclab_arena.assets.asset import Asset from isaaclab_arena.embodiments.common.arm_mode import ArmMode from isaaclab_arena.environments.isaaclab_arena_manager_based_env import IsaacLabArenaManagerBasedRLEnvCfg -from isaaclab_arena.metrics.dexsuite_lift_success_rate import DexsuiteLiftSuccessRateMetric -from isaaclab_arena.metrics.metric_base import MetricBase from isaaclab_arena.tasks.lift_object_task import LiftObjectTask +def success_reward_succeeded(env: ManagerBasedRLEnv) -> torch.Tensor: + """Per-env termination from Dexsuite ``success`` reward term's sticky ``succeeded`` flag. + + Stock Isaac Dexsuite lift does not use this as a termination. Arena adds it so + :class:`~isaaclab_arena.metrics.success_rate.SuccessRateMetric` can use the standard ``success`` + termination channel. With ``time_out=False``, the episode **resets when success is first achieved** + (vanilla Dexsuite may continue until time-out). + + Args: + env: Manager-based RL environment (reward manager must expose a ``success`` term). + + Returns: + Boolean tensor of shape ``(num_envs,)``. + """ + if "success" not in env.reward_manager.active_terms: + return torch.zeros(env.num_envs, dtype=torch.bool, device=env.device) + func = env.reward_manager.get_term_cfg("success").func + if not hasattr(func, "succeeded"): + return torch.zeros(env.num_envs, dtype=torch.bool, device=env.device) + return func.succeeded.clone() + + @configclass class ArenaDexsuiteKukaReorientRewardCfg(dexsuite.RewardsCfg): """Same as ``KukaAllegroReorientRewardCfg`` but merge-safe: no ``super()`` in ``__post_init__``.""" @@ -77,11 +103,11 @@ def __post_init__(self) -> None: def _build_dexsuite_kuka_lift_cfgs() -> tuple[Any, Any, Any, Any]: from isaaclab_tasks.manager_based.manipulation.dexsuite.adr_curriculum import CurriculumCfg - from isaaclab_tasks.manager_based.manipulation.dexsuite.dexsuite_env_cfg import CommandsCfg, TerminationsCfg + from isaaclab_tasks.manager_based.manipulation.dexsuite.dexsuite_env_cfg import CommandsCfg commands = CommandsCfg() rewards = ArenaDexsuiteKukaReorientRewardCfg() - terminations = TerminationsCfg() + terminations = ArenaDexsuiteKukaLiftTerminationsCfg() curriculum = CurriculumCfg() # ``DexsuiteLiftEnvCfg.__post_init__`` @@ -92,6 +118,13 @@ def _build_dexsuite_kuka_lift_cfgs() -> tuple[Any, Any, Any, Any]: return commands, rewards, terminations, curriculum +@configclass +class ArenaDexsuiteKukaLiftTerminationsCfg(dexsuite.TerminationsCfg): + """Dexsuite terminations + ``success`` for :class:`~isaaclab_arena.metrics.success_rate.SuccessRateMetric`.""" + + success = DoneTerm(func=success_reward_succeeded, time_out=False) + + class DexsuiteKukaAllegroLiftTask(LiftObjectTask): """Lift task matching Isaac Lab ``Isaac-Dexsuite-Kuka-Allegro-Lift-v0`` (state observations). @@ -127,11 +160,6 @@ def get_rewards_cfg(self) -> Any: def get_curriculum_cfg(self) -> Any: return self._curriculum_cfg - def get_metrics(self) -> list[MetricBase]: - # Dexsuite success is sticky state on the ``success`` reward term, not a termination; see - # :class:`~isaaclab_arena.metrics.dexsuite_lift_success_rate.DexsuiteLiftSuccessRateMetric`. - return [DexsuiteLiftSuccessRateMetric()] - def get_viewer_cfg(self) -> ViewerCfg: # Reuse LiftObjectTask's look-at-object framing (same offset as generic lift examples). from isaaclab_arena.utils.cameras import get_viewer_cfg_look_at_object diff --git a/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py b/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py index e8c1eab20..cff24686e 100644 --- a/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py +++ b/isaaclab_arena/tests/test_dexsuite_kuka_lift_example.py @@ -27,8 +27,11 @@ def test_dexsuite_procedural_assets_registered() -> None: def test_dexsuite_kuka_lift_task_matches_lift_mdp_flags() -> None: from isaaclab_arena.assets.asset_registry import AssetRegistry - from isaaclab_arena.metrics.dexsuite_lift_success_rate import DexsuiteLiftSuccessRateMetric - from isaaclab_arena.tasks.dexsuite_kuka_allegro_lift_task import DexsuiteKukaAllegroLiftTask + from isaaclab_arena.metrics.success_rate import SuccessRateMetric + from isaaclab_arena.tasks.dexsuite_kuka_allegro_lift_task import ( + ArenaDexsuiteKukaLiftTerminationsCfg, + DexsuiteKukaAllegroLiftTask, + ) from isaaclab_arena.tasks.lift_object_task import LiftObjectTask reg = AssetRegistry() @@ -43,5 +46,8 @@ def test_dexsuite_kuka_lift_task_matches_lift_mdp_flags() -> None: assert task._rewards_cfg.success.params.get("rot_std") is None metrics = task.get_metrics() assert len(metrics) == 1 - assert isinstance(metrics[0], DexsuiteLiftSuccessRateMetric) - assert metrics[0].recorder_term_name == "dexsuite_lift_success" + assert isinstance(metrics[0], SuccessRateMetric) + assert metrics[0].recorder_term_name == "success" + term = task._terminations_cfg # noqa: SLF001 + assert isinstance(term, ArenaDexsuiteKukaLiftTerminationsCfg) + assert hasattr(term, "success")