Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions isaaclab_arena/assets/asset_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions isaaclab_arena/assets/dexsuite_assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# 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"]
# ``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,
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)
1 change: 1 addition & 0 deletions isaaclab_arena/embodiments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
from .g1.g1 import *
from .galbot.galbot import *
from .gr1t2.gr1t2 import *
from .kuka_allegro.kuka_allegro import *
6 changes: 6 additions & 0 deletions isaaclab_arena/embodiments/kuka_allegro/__init__.py
Original file line number Diff line number Diff line change
@@ -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
229 changes: 229 additions & 0 deletions isaaclab_arena/embodiments/kuka_allegro/kuka_allegro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# 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 = 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

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 ""
5 changes: 3 additions & 2 deletions isaaclab_arena/evaluation/policy_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions isaaclab_arena/metrics/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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"])

Expand Down
Loading
Loading