Skip to content
Draft
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
80 changes: 80 additions & 0 deletions isaaclab_arena/assets/object_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@
_DEFORMABLE_CUBE_TET_USD = str(_LOCAL_ASSET_DIR / "procedural_deformable_cube_tet.usda")
_DEFORMABLE_VOLUME_BLOCK_TET_USD = str(_LOCAL_ASSET_DIR / "procedural_deformable_volume_block_tet.usda")
_DEFORMABLE_CABLE_TET_USD = str(_LOCAL_ASSET_DIR / "procedural_deformable_cable_tet.usda")
_FRANKA_SOFT_LIFT_BLOCK_TET_USD = str(_LOCAL_ASSET_DIR / "franka_soft_lift_block_tet.usda")

_FRANKA_SOFT_LIFT_YOUNGS_MODULUS = 8.0e4
_FRANKA_SOFT_LIFT_POISSONS_RATIO = 0.25
_FRANKA_SOFT_LIFT_BLOCK_SIZE = (0.3, 0.05, 0.05)
_FRANKA_SOFT_LIFT_BLOCK_INITIAL_POSE = Pose(position_xyz=(0.5, 0.0, 0.05))
_FRANKA_SOFT_LIFT_TABLE_INITIAL_POSE = Pose(
position_xyz=(0.5, 0.0, 0.0),
rotation_xyzw=(0.0, 0.0, 0.707, 0.707),
)


class LibraryObject(Object):
Expand Down Expand Up @@ -346,6 +356,31 @@ def __init__(
)


@register_asset
class FrankaSoftLiftTable(LibraryObject):
"""Seattle lab table used by Isaac-Lift-Soft-Franka."""

name = "franka_soft_lift_table"
tags = ["object", "table", "franka_soft_lift"]
usd_path = None
table_usd_path = f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd"
object_type = ObjectType.BASE
default_prim_path = "{ENV_REGEX_NS}/Table"

def __init__(
self,
instance_name: str | None = None,
prim_path: str | None = default_prim_path,
initial_pose: Pose | None = None,
):
super().__init__(
instance_name=instance_name,
prim_path=prim_path,
initial_pose=initial_pose if initial_pose is not None else _FRANKA_SOFT_LIFT_TABLE_INITIAL_POSE,
spawner_cfg=sim_utils.UsdFileCfg(usd_path=self.table_usd_path),
)


@register_asset
class Sphere(LibraryObject):
"""
Expand Down Expand Up @@ -512,6 +547,51 @@ def __init__(
)


@register_asset
class FrankaSoftLiftBlock(DeformableObject):
"""Volume-deformable cuboid used by Isaac-Lift-Soft-Franka."""

name = "franka_soft_lift_block"
tags = ["object", "procedural", "deformable", "volume", "franka_soft_lift"]
default_prim_path = "{ENV_REGEX_NS}/Deformable"

def __init__(
self,
instance_name: str | None = None,
prim_path: str | None = None,
initial_pose: Pose | None = None,
):
from isaaclab_arena.variations.deformable_initial_pose_variation import DeformableInitialPoseVariation

half_extents = tuple(size * 0.5 for size in _FRANKA_SOFT_LIFT_BLOCK_SIZE)
super().__init__(
name=instance_name if instance_name is not None else self.name,
tags=self.tags,
prim_path=prim_path if prim_path is not None else self.default_prim_path,
usd_path=_FRANKA_SOFT_LIFT_BLOCK_TET_USD,
material=DeformableMaterial(
youngs_modulus=_FRANKA_SOFT_LIFT_YOUNGS_MODULUS,
poissons_ratio=_FRANKA_SOFT_LIFT_POISSONS_RATIO,
density=300.0,
physx=PhysxDeformableTuning(
rest_offset=None,
contact_offset=None,
linear_damping=None,
static_friction=10.0,
dynamic_friction=5.0,
),
newton=NewtonDeformableTuning(particle_radius=0.01),
),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.95, 0.85, 0.1)),
local_bounding_box=AxisAlignedBoundingBox(
min_point=tuple(-extent for extent in half_extents),
max_point=half_extents,
),
initial_pose=initial_pose if initial_pose is not None else _FRANKA_SOFT_LIFT_BLOCK_INITIAL_POSE,
)
self.add_variation(DeformableInitialPoseVariation(self.name))


_PROCEDURAL_DEFORMABLE_CLOTH_SIZE = (0.22, 0.22)
_PROCEDURAL_DEFORMABLE_CABLE_LENGTH = 0.4
_PROCEDURAL_DEFORMABLE_CABLE_RADIUS = 0.012
Expand Down
15 changes: 15 additions & 0 deletions isaaclab_arena/assets/usd/franka_soft_lift_block_tet.usda
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#usda 1.0
(
defaultPrim = "FrankaSoftLiftBlock"
metersPerUnit = 1
upAxis = "Z"
)

def Xform "FrankaSoftLiftBlock"
{
def TetMesh "sim_mesh"
{
point3f[] points = [(-0.15, -0.025, -0.025), (-0.15, -0.025, 0.025), (-0.15, 0.025, -0.025), (-0.15, 0.025, 0.025), (-0.13, -0.025, -0.025), (-0.13, -0.025, 0.025), (-0.13, 0.025, -0.025), (-0.13, 0.025, 0.025), (-0.11, -0.025, -0.025), (-0.11, -0.025, 0.025), (-0.11, 0.025, -0.025), (-0.11, 0.025, 0.025), (-0.09, -0.025, -0.025), (-0.09, -0.025, 0.025), (-0.09, 0.025, -0.025), (-0.09, 0.025, 0.025), (-0.07, -0.025, -0.025), (-0.07, -0.025, 0.025), (-0.07, 0.025, -0.025), (-0.07, 0.025, 0.025), (-0.05, -0.025, -0.025), (-0.05, -0.025, 0.025), (-0.05, 0.025, -0.025), (-0.05, 0.025, 0.025), (-0.03, -0.025, -0.025), (-0.03, -0.025, 0.025), (-0.03, 0.025, -0.025), (-0.03, 0.025, 0.025), (-0.01, -0.025, -0.025), (-0.01, -0.025, 0.025), (-0.01, 0.025, -0.025), (-0.01, 0.025, 0.025), (0.01, -0.025, -0.025), (0.01, -0.025, 0.025), (0.01, 0.025, -0.025), (0.01, 0.025, 0.025), (0.03, -0.025, -0.025), (0.03, -0.025, 0.025), (0.03, 0.025, -0.025), (0.03, 0.025, 0.025), (0.05, -0.025, -0.025), (0.05, -0.025, 0.025), (0.05, 0.025, -0.025), (0.05, 0.025, 0.025), (0.07, -0.025, -0.025), (0.07, -0.025, 0.025), (0.07, 0.025, -0.025), (0.07, 0.025, 0.025), (0.09, -0.025, -0.025), (0.09, -0.025, 0.025), (0.09, 0.025, -0.025), (0.09, 0.025, 0.025), (0.11, -0.025, -0.025), (0.11, -0.025, 0.025), (0.11, 0.025, -0.025), (0.11, 0.025, 0.025), (0.13, -0.025, -0.025), (0.13, -0.025, 0.025), (0.13, 0.025, -0.025), (0.13, 0.025, 0.025), (0.15, -0.025, -0.025), (0.15, -0.025, 0.025), (0.15, 0.025, -0.025), (0.15, 0.025, 0.025)]
int4[] tetVertexIndices = [(0, 1, 7, 3), (0, 3, 7, 2), (0, 2, 7, 6), (0, 6, 7, 4), (0, 4, 7, 5), (0, 5, 7, 1), (4, 5, 11, 7), (4, 7, 11, 6), (4, 6, 11, 10), (4, 10, 11, 8), (4, 8, 11, 9), (4, 9, 11, 5), (8, 9, 15, 11), (8, 11, 15, 10), (8, 10, 15, 14), (8, 14, 15, 12), (8, 12, 15, 13), (8, 13, 15, 9), (12, 13, 19, 15), (12, 15, 19, 14), (12, 14, 19, 18), (12, 18, 19, 16), (12, 16, 19, 17), (12, 17, 19, 13), (16, 17, 23, 19), (16, 19, 23, 18), (16, 18, 23, 22), (16, 22, 23, 20), (16, 20, 23, 21), (16, 21, 23, 17), (20, 21, 27, 23), (20, 23, 27, 22), (20, 22, 27, 26), (20, 26, 27, 24), (20, 24, 27, 25), (20, 25, 27, 21), (24, 25, 31, 27), (24, 27, 31, 26), (24, 26, 31, 30), (24, 30, 31, 28), (24, 28, 31, 29), (24, 29, 31, 25), (28, 29, 35, 31), (28, 31, 35, 30), (28, 30, 35, 34), (28, 34, 35, 32), (28, 32, 35, 33), (28, 33, 35, 29), (32, 33, 39, 35), (32, 35, 39, 34), (32, 34, 39, 38), (32, 38, 39, 36), (32, 36, 39, 37), (32, 37, 39, 33), (36, 37, 43, 39), (36, 39, 43, 38), (36, 38, 43, 42), (36, 42, 43, 40), (36, 40, 43, 41), (36, 41, 43, 37), (40, 41, 47, 43), (40, 43, 47, 42), (40, 42, 47, 46), (40, 46, 47, 44), (40, 44, 47, 45), (40, 45, 47, 41), (44, 45, 51, 47), (44, 47, 51, 46), (44, 46, 51, 50), (44, 50, 51, 48), (44, 48, 51, 49), (44, 49, 51, 45), (48, 49, 55, 51), (48, 51, 55, 50), (48, 50, 55, 54), (48, 54, 55, 52), (48, 52, 55, 53), (48, 53, 55, 49), (52, 53, 59, 55), (52, 55, 59, 54), (52, 54, 59, 58), (52, 58, 59, 56), (52, 56, 59, 57), (52, 57, 59, 53), (56, 57, 63, 59), (56, 59, 63, 58), (56, 58, 63, 62), (56, 62, 63, 60), (56, 60, 63, 61), (56, 61, 63, 57)]
}
}
3 changes: 3 additions & 0 deletions isaaclab_arena/assets/usd/generate_deformable_tet_meshes.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ def main() -> None:
pts, tets = _structured_box_tets(length=0.08, half_width=0.02, half_height=0.02, num_segments=4)
_write_tet_usd(pts, tets, _OUT_DIR / "procedural_deformable_volume_block_tet.usda", "DeformableVolumeBlock")

pts, tets = _structured_box_tets(length=0.3, half_width=0.025, half_height=0.025, num_segments=15)
_write_tet_usd(pts, tets, _OUT_DIR / "franka_soft_lift_block_tet.usda", "FrankaSoftLiftBlock")

pts, tets = _structured_box_tets(length=0.4, half_width=0.012, half_height=0.012, num_segments=8)
_write_tet_usd(pts, tets, _OUT_DIR / "procedural_deformable_cable_tet.usda", "DeformableCable")

Expand Down
79 changes: 79 additions & 0 deletions isaaclab_arena/embodiments/franka/franka.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,85 @@ class FrankaCameraCfg(ArenaCameraCfg):
)


@configclass
class FrankaSoftLiftSceneCfg:
"""Source-parity Franka scene config for Isaac-Lift-Soft-Franka."""

robot: ArticulationCfg = FRANKA_PANDA_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")

ee_frame: FrameTransformerCfg = FrameTransformerCfg(
prim_path="{ENV_REGEX_NS}/Robot/panda_link0",
debug_vis=False,
target_frames=[
FrameTransformerCfg.FrameCfg(
prim_path="{ENV_REGEX_NS}/Robot/panda_hand",
name="end_effector",
offset=OffsetCfg(pos=[0.0, 0.0, 0.1034]),
),
],
)

def __post_init__(self) -> None:
self.robot.spawn.usd_path = _FRANKA_ROBOT_PRIM.robot_usd_path
self.robot.spawn.rigid_props.disable_gravity = True
self.robot.actuators["panda_hand"].effort_limit_sim = 500.0
self.robot.actuators["panda_hand"].stiffness = 1000.0
self.robot.actuators["panda_hand"].damping = 100.0


@configclass
class FrankaSoftLiftActionCfg:
"""Absolute pose IK plus binary gripper for Isaac-Lift-Soft-Franka."""

arm_action: ActionTermCfg = DifferentialInverseKinematicsActionCfg(
asset_name="robot",
joint_names=["panda_joint.*"],
body_name="panda_hand",
controller=DifferentialIKControllerCfg(
command_type="pose",
use_relative_mode=False,
ik_method="dls",
ik_params={"lambda_val": 0.6},
),
body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=[0.0, 0.0, 0.107]),
)

gripper_action: ActionTermCfg = BinaryJointPositionActionCfg(
asset_name="robot",
joint_names=["panda_finger.*"],
open_command_expr={"panda_finger_.*": 0.05},
close_command_expr={"panda_finger_.*": 0.0},
)


@register_asset
class FrankaSoftLiftPandaEmbodiment(EmbodimentBase):
"""Plain Franka Panda embodiment for the soft-lift evaluation scene."""

name = "franka_soft_lift_panda"
tags = ["embodiment", "franka", "franka_soft_lift"]
default_arm_mode = ArmMode.SINGLE_ARM

def __init__(
self,
enable_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.scene_config = FrankaSoftLiftSceneCfg()
self.action_config = FrankaSoftLiftActionCfg()
self.camera_config = FrankaCameraCfg()
self.add_camera_variations(self.camera_config)

def get_ee_frame_name(self, arm_mode: ArmMode) -> str:
return "ee_frame"

def get_command_body_name(self) -> str:
return self.action_config.arm_action.body_name


@configclass
class FrankaObservationsCfg:
"""Observation specifications for the MDP."""
Expand Down
143 changes: 143 additions & 0 deletions isaaclab_arena/metrics/deformable_goal_reached_rate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Copyright (c) 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

"""Goal-reached metric for deformable lift evaluation."""

from __future__ import annotations

import numpy as np
import torch

import warp as wp
from isaaclab.envs.manager_based_rl_env import ManagerBasedEnv
from isaaclab.managers import SceneEntityCfg
from isaaclab.managers.recorder_manager import RecorderTerm, RecorderTermCfg
from isaaclab.utils.configclass import configclass
from isaaclab.utils.math import combine_frame_transforms

from isaaclab_arena.metrics.metric_base import MetricBase
from isaaclab_arena.metrics.metric_term_cfg import MetricTermCfg


def _deformable_goal_reached(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 One-line docstring would help

_deformable_goal_reached is the success predicate the whole metric hangs on, but the name doesn't convey what it returns. A short line — e.g. """Return a per-env bool tensor: the deformable COM is above minimal_heightand withinposition_tolerance of the commanded goal.""" — saves readers from re-deriving the frame math.

env: ManagerBasedEnv,
*,
command_name: str,
minimal_height: float,
position_tolerance: float,
robot_cfg: SceneEntityCfg,
asset_cfg: SceneEntityCfg,
) -> torch.Tensor:
robot = env.scene[robot_cfg.name]
asset = env.scene[asset_cfg.name]
command = env.command_manager.get_command(command_name)
desired_pos_b = command[:, :3]
desired_pos_w, _ = combine_frame_transforms(
wp.to_torch(robot.data.root_pos_w),
wp.to_torch(robot.data.root_quat_w),
desired_pos_b,
)
com_w = wp.to_torch(asset.data.root_pos_w)
distance = torch.linalg.norm(desired_pos_w - com_w, dim=1)
return (com_w[:, 2] > minimal_height) & (distance < position_tolerance)


class DeformableGoalReachedRecorder(RecorderTerm):
"""Record whether the deformable reached its command goal at any point in the episode."""

def __init__(self, cfg: RecorderTermCfg, env: ManagerBasedEnv):
super().__init__(cfg, env)
self.name = cfg.name
self.command_name = cfg.command_name
self.minimal_height = cfg.minimal_height
self.position_tolerance = cfg.position_tolerance
self.robot_cfg = cfg.robot_cfg
self.asset_cfg = cfg.asset_cfg
self._ever_reached = torch.zeros(env.num_envs, dtype=torch.bool, device=env.device)
self._first_reset = True

def _update_state(self) -> None:
self._ever_reached |= _deformable_goal_reached(
self._env,
command_name=self.command_name,
minimal_height=self.minimal_height,
position_tolerance=self.position_tolerance,
robot_cfg=self.robot_cfg,
asset_cfg=self.asset_cfg,
)

def record_post_step(self):
self._update_state()
return None, None

def record_pre_reset(self, env_ids):
if self._first_reset:
self._first_reset = False
return None, None
self._update_state()
reached = self._ever_reached[env_ids].clone()
self._ever_reached[env_ids] = False
return self.name, reached


@configclass
class DeformableGoalReachedRecorderCfg(RecorderTermCfg):
"""Recorder config for the deformable goal-reached metric."""

class_type: type[RecorderTerm] = DeformableGoalReachedRecorder
name: str = "deformable_goal_reached"
command_name: str = "deformable_pose"
minimal_height: float = 0.075
position_tolerance: float = 0.05
robot_cfg: SceneEntityCfg = SceneEntityCfg("robot")
asset_cfg: SceneEntityCfg = SceneEntityCfg("deformable")


def compute_deformable_goal_reached_rate(recorded_metric_data: list[np.ndarray]) -> float:
"""Compute the fraction of episodes whose deformable reached the commanded goal."""
if len(recorded_metric_data) == 0:
return 0.0
goal_reached = np.concatenate([np.asarray(data, dtype=bool).reshape(-1) for data in recorded_metric_data])
if goal_reached.size == 0:
return 0.0
return float(np.mean(goal_reached))


class DeformableGoalReachedRateMetric(MetricBase):
"""Non-terminating goal-reached rate for deformable lift evaluation."""

name = "deformable_goal_reached_rate"
recorder_term_name = "deformable_goal_reached"

def __init__(
self,
command_name: str = "deformable_pose",
minimal_height: float = 0.075,
position_tolerance: float = 0.05,
robot_cfg: SceneEntityCfg | None = None,
asset_cfg: SceneEntityCfg | None = None,
):
self.command_name = command_name
self.minimal_height = minimal_height
self.position_tolerance = position_tolerance
self.robot_cfg = robot_cfg if robot_cfg is not None else SceneEntityCfg("robot")
self.asset_cfg = asset_cfg if asset_cfg is not None else SceneEntityCfg("deformable")

def get_recorder_term_cfg(self) -> RecorderTermCfg:
return DeformableGoalReachedRecorderCfg(
name=self.recorder_term_name,
command_name=self.command_name,
minimal_height=self.minimal_height,
position_tolerance=self.position_tolerance,
robot_cfg=self.robot_cfg,
asset_cfg=self.asset_cfg,
)

def get_metric_term_cfg(self) -> MetricTermCfg:
return MetricTermCfg(
compute_metric_func=compute_deformable_goal_reached_rate,
params={},
recorder_term_name=self.recorder_term_name,
)
Loading
Loading