Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cbe24bb
Rigid object is passing both unit and integration tests (most of them…
AntoineRichard Jan 14, 2026
599739e
Changed Newton version to latest
AntoineRichard Jan 14, 2026
8aa4c6d
pre-commits
AntoineRichard Jan 14, 2026
80282da
test articulation WIP
AntoineRichard Jan 16, 2026
eb7845c
improved CPU support, fixed some minor bugs and ran all the tests wit…
AntoineRichard Jan 20, 2026
308e1c9
moved and renamed
AntoineRichard Jan 20, 2026
9dca442
bringing more of the rigid object back.
AntoineRichard Jan 20, 2026
9af2fea
Fix some tolerances in the rigid object test. Might be good to do a c…
AntoineRichard Jan 20, 2026
fd5eb20
tested friction Ok. Restitution not OK.
AntoineRichard Jan 20, 2026
3653d3d
various fixes.
AntoineRichard Jan 20, 2026
b3328ff
WIP
AntoineRichard Jan 20, 2026
f8f51ea
addressed some of our Lizard overloard comments.
AntoineRichard Jan 21, 2026
14ffe75
pre-commits
AntoineRichard Jan 21, 2026
1befd82
Skipping failing tests + various test fix. We no longer offset robot …
AntoineRichard Jan 21, 2026
2b19553
pre-commits
AntoineRichard Jan 21, 2026
7cf49ff
removing prints
AntoineRichard Jan 21, 2026
9bc30d0
perf opt
AntoineRichard Jan 21, 2026
11da408
Improved doc-strings
AntoineRichard Jan 21, 2026
7749b3b
pre-commits
AntoineRichard Jan 21, 2026
5974d98
Should be ready to merge / test
AntoineRichard Jan 21, 2026
79b0949
Fixed some more Nits.
AntoineRichard Jan 21, 2026
b78e9b1
forgot to include common...
AntoineRichard Jan 22, 2026
89c1b27
pre-commits
AntoineRichard Jan 22, 2026
68736d0
includes bug fixes in contact sensor and added tests of test_contact_…
ooctipus Jan 28, 2026
6acad9a
added collision tests
ooctipus Jan 28, 2026
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
263 changes: 263 additions & 0 deletions scripts/demos/finger_collision_visualization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers.
# All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause

"""Visualization script for finger collision isolation test with Newton physics.

This script visualizes a sphere colliding with an Allegro hand fingertip.
Use this to debug collision behavior and verify finger deflection.

Usage:
cd /home/zhengyuz/Projects/isaaclab
source ~/miniconda3/etc/profile.d/conda.sh
conda activate newton_isaaclab

# Run with visualization (headless=False)
python scripts/demos/finger_collision_visualization.py --target_finger index

# Run headless with debug output
python scripts/demos/finger_collision_visualization.py --target_finger thumb --headless
"""

import argparse
import torch
import warp as wp

import isaaclab.sim as sim_utils
from isaaclab.assets import Articulation, RigidObject, RigidObjectCfg
from isaaclab.sim import build_simulation_context
from isaaclab.sim._impl.newton_manager_cfg import NewtonCfg
from isaaclab.sim._impl.solvers_cfg import MJWarpSolverCfg
from isaaclab.sim.simulation_cfg import SimulationCfg

# Import hand configuration
from isaaclab_assets.robots.allegro import ALLEGRO_HAND_CFG

##
# Configuration
##

# Finger tip positions relative to hand root in default orientation
ALLEGRO_FINGERTIP_OFFSETS = {
"index": (-0.052, -0.252, 0.052),
"middle": (-0.001, -0.252, 0.052),
"ring": (0.054, -0.252, 0.052),
"thumb": (-0.168, -0.039, 0.080),
}

# Joint names for each finger
ALLEGRO_FINGER_JOINTS = {
"index": ["index_joint_0", "index_joint_1", "index_joint_2", "index_joint_3"],
"middle": ["middle_joint_0", "middle_joint_1", "middle_joint_2", "middle_joint_3"],
"ring": ["ring_joint_0", "ring_joint_1", "ring_joint_2", "ring_joint_3"],
"thumb": ["thumb_joint_0", "thumb_joint_1", "thumb_joint_2", "thumb_joint_3"],
}

# Newton solver configuration
SOLVER_CFG = MJWarpSolverCfg(
njmax=100,
nconmax=100,
ls_iterations=20,
cone="elliptic",
impratio=100,
ls_parallel=True,
integrator="implicit",
)

NEWTON_CFG = NewtonCfg(
solver_cfg=SOLVER_CFG,
num_substeps=1,
debug_mode=False,
use_cuda_graph=False,
)


def main():
parser = argparse.ArgumentParser(description="Visualize finger collision isolation test")
parser.add_argument("--target_finger", type=str, default="index", choices=["index", "middle", "ring", "thumb"])
parser.add_argument("--headless", action="store_true", help="Run in headless mode")
parser.add_argument("--device", type=str, default="cuda:0", choices=["cuda:0", "cpu"])
parser.add_argument("--sphere_type", type=str, default="primitive", choices=["primitive", "mesh"])
args = parser.parse_args()

target_finger = args.target_finger
device = args.device
sim_dt = 1.0 / 240.0
drop_steps = 480 # 2 seconds

# Hand position
hand_pos = (0.0, 0.0, 0.5)

# Zero gravity globally - ball will have initial downward velocity
sim_cfg = SimulationCfg(
dt=sim_dt,
create_stage_in_memory=False,
newton_cfg=NEWTON_CFG,
device=device,
gravity=(0.0, 0.0, 0.0),
)

print(f"\n{'='*60}")
print(f"Finger Collision Visualization")
print(f"{'='*60}")
print(f"Target finger: {target_finger}")
print(f"Device: {device}")
print(f"Sphere type: {args.sphere_type}")
print(f"Headless: {args.headless}")
print(f"Gravity: (0, 0, 0) - using initial velocity instead")
print(f"{'='*60}\n")

with build_simulation_context(sim_cfg=sim_cfg, auto_add_lighting=True) as sim:
sim._app_control_on_stop_handle = None

# Create hand configuration
hand_cfg = ALLEGRO_HAND_CFG.copy()
hand_cfg.prim_path = "/World/Hand"
hand_cfg.init_state.pos = hand_pos

# Create ground plane
ground_cfg = sim_utils.GroundPlaneCfg()
ground_cfg.func("/World/ground", ground_cfg)

# Create the hand
hand = Articulation(hand_cfg)

# Get fingertip offset for target finger
fingertip_offset = ALLEGRO_FINGERTIP_OFFSETS[target_finger]
print(f"Fingertip offset for '{target_finger}': {fingertip_offset}")

# Position sphere above fingertip
drop_height = 0.10 # 10cm above
sphere_pos = (
hand_pos[0] + fingertip_offset[0],
hand_pos[1] + fingertip_offset[1],
hand_pos[2] + fingertip_offset[2] + drop_height,
)
print(f"Sphere initial position: {sphere_pos}")

# Create sphere
if args.sphere_type == "primitive":
sphere_spawn = sim_utils.SphereCfg(
radius=0.035,
rigid_props=sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
linear_damping=0.0,
angular_damping=0.0,
),
collision_props=sim_utils.CollisionPropertiesCfg(collision_enabled=True),
mass_props=sim_utils.MassPropertiesCfg(mass=0.2),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0)),
)
else:
sphere_spawn = sim_utils.MeshSphereCfg(
radius=0.035,
rigid_props=sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
linear_damping=0.0,
angular_damping=0.0,
),
collision_props=sim_utils.CollisionPropertiesCfg(collision_enabled=True),
mass_props=sim_utils.MassPropertiesCfg(mass=0.2),
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0)),
)

sphere_cfg = RigidObjectCfg(
prim_path="/World/DropSphere",
spawn=sphere_spawn,
init_state=RigidObjectCfg.InitialStateCfg(pos=sphere_pos),
)
drop_sphere = RigidObject(sphere_cfg)

# Reset simulation
sim.reset()
hand.reset()
drop_sphere.reset()

# Let hand settle
print("\nSettling hand...")
settle_steps = 30
for _ in range(settle_steps):
hand.write_data_to_sim()
# Always use render=False for Newton physics (no omni.kit dependency)
sim.step(render=False)
hand.update(sim_dt)

# Reset sphere and give initial velocity
drop_sphere.reset()
initial_velocity = torch.tensor([[0.0, 0.0, -1.5, 0.0, 0.0, 0.0]], device=device)
drop_sphere.write_root_velocity_to_sim(initial_velocity)
print(f"Sphere initial velocity: {initial_velocity[0, :3].tolist()} m/s")

# Record initial joint positions
initial_joint_pos = wp.to_torch(hand.data.joint_pos).clone()
joint_names = hand.data.joint_names

print(f"\nJoint names: {joint_names}")
print(f"\nInitial joint positions: {initial_joint_pos[0].tolist()}")

# Track deflection
peak_deflection = {finger: 0.0 for finger in ["index", "middle", "ring", "thumb"]}

print(f"\nRunning simulation for {drop_steps} steps ({drop_steps * sim_dt:.2f}s)...")
print("-" * 60)

# Run simulation
for step in range(drop_steps):
hand.write_data_to_sim()
drop_sphere.write_data_to_sim()
# Always use render=False for Newton physics (no omni.kit dependency)
sim.step(render=False)
hand.update(sim_dt)
drop_sphere.update(sim_dt)

# Track deflection
current_joint_pos = wp.to_torch(hand.data.joint_pos)[0]
for finger_name in ["index", "middle", "ring", "thumb"]:
finger_deflection = 0.0
for joint_name in ALLEGRO_FINGER_JOINTS[finger_name]:
if joint_name in joint_names:
idx = joint_names.index(joint_name)
finger_deflection += abs(current_joint_pos[idx].item() - initial_joint_pos[0, idx].item())
peak_deflection[finger_name] = max(peak_deflection[finger_name], finger_deflection)

# Print progress every 100 steps
if step % 100 == 0 or step == drop_steps - 1:
sphere_pos = wp.to_torch(drop_sphere.data.root_pos_w)[0]
sphere_vel = wp.to_torch(drop_sphere.data.root_lin_vel_w)[0]
print(
f"Step {step:4d}: "
f"sphere_z={sphere_pos[2]:.4f}, vel_z={sphere_vel[2]:.4f}, "
f"deflections: {', '.join([f'{k}={v:.4f}' for k, v in peak_deflection.items()])}"
)

# Print results
print("\n" + "=" * 60)
print("RESULTS")
print("=" * 60)
target_peak = peak_deflection[target_finger]
print(f"Target finger: {target_finger}")
print(f"Target peak deflection: {target_peak:.6f}")
print("\nAll finger deflections:")
for finger_name, deflection in peak_deflection.items():
marker = " <-- TARGET" if finger_name == target_finger else ""
print(f" {finger_name:8s}: {deflection:.6f}{marker}")

# Check test conditions
print("\nTest conditions:")
if target_peak > 0.01:
print(f" [PASS] Target finger deflected > 0.01 ({target_peak:.6f})")
else:
print(f" [FAIL] Target finger deflection too small ({target_peak:.6f} <= 0.01)")

for finger_name in ["index", "middle", "ring", "thumb"]:
if finger_name != target_finger:
if target_peak >= peak_deflection[finger_name]:
print(f" [PASS] {target_finger} >= {finger_name} ({target_peak:.4f} >= {peak_deflection[finger_name]:.4f})")
else:
print(f" [FAIL] {target_finger} < {finger_name} ({target_peak:.4f} < {peak_deflection[finger_name]:.4f})")

print("=" * 60)


if __name__ == "__main__":
main()
8 changes: 4 additions & 4 deletions source/isaaclab/isaaclab/app/app_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@
_SIMULATION_APP_AVAILABLE = False
_SimulationApp = None

with contextlib.suppress(ModuleNotFoundError):
import isaacsim # noqa: F401
from isaacsim import SimulationApp as _SimulationApp
# with contextlib.suppress(ModuleNotFoundError):
# import isaacsim # noqa: F401
# from isaacsim import SimulationApp as _SimulationApp

_SIMULATION_APP_AVAILABLE = True
# _SIMULATION_APP_AVAILABLE = True

# import logger
logger = logging.getLogger(__name__)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from isaaclab_newton.assets.articulation import ArticulationData as NewtonArticulationData


class Articulation(FactoryBase):
class Articulation(FactoryBase, BaseArticulation):
"""Factory for creating articulation instances."""

data: BaseArticulationData | NewtonArticulationData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from ..asset_base import AssetBase

if TYPE_CHECKING:
from isaaclab.utils.wrench_composer import WrenchComposer

from .articulation_cfg import ArticulationCfg
from .articulation_data import ArticulationData

Expand Down Expand Up @@ -176,12 +178,33 @@ def root_view(self):
"""
raise NotImplementedError()

@property
@abstractmethod
def instantaneous_wrench_composer(self) -> WrenchComposer:
"""Instantaneous wrench composer for the articulation."""
raise NotImplementedError()

@property
@abstractmethod
def permanent_wrench_composer(self) -> WrenchComposer:
"""Permanent wrench composer for the articulation."""
raise NotImplementedError()

"""
Operations.
"""

@abstractmethod
def reset(self, env_ids: Sequence[int] | None = None, mask: wp.array | torch.Tensor | None = None):
def reset(self, env_ids: Sequence[int] | None = None, env_mask: wp.array | torch.Tensor | None = None):
"""Reset the articulation.

Note: If both env_ids and env_mask are provided, then env_mask will be used. For performance reasons, it is
recommended to use the env_mask instead of env_ids.

Args:
env_ids: Environment indices. If None, then all indices are used.
env_mask: Environment mask. Shape is (num_instances,).
"""
raise NotImplementedError()

@abstractmethod
Expand Down Expand Up @@ -831,6 +854,8 @@ def write_joint_armature_to_sim(
def write_joint_friction_coefficient_to_sim(
self,
joint_friction_coeff: torch.Tensor | wp.array | float,
joint_dynamic_friction_coeff: torch.Tensor | wp.array | float | None = None,
joint_viscous_friction_coeff: torch.Tensor | wp.array | float | None = None,
joint_ids: Sequence[int] | slice | None = None,
env_ids: Sequence[int] | None = None,
joint_mask: torch.Tensor | wp.array | None = None,
Expand Down Expand Up @@ -860,6 +885,8 @@ def write_joint_friction_coefficient_to_sim(

Args:
joint_friction_coeff: Joint static friction coefficient. Shape is (len(env_ids), len(joint_ids)) or (num_instances, num_joints).
joint_dynamic_friction_coeff: Joint dynamic friction coefficient. Shape is (len(env_ids), len(joint_ids)) or (num_instances, num_joints).
joint_viscous_friction_coeff: Joint viscous friction coefficient. Shape is (len(env_ids), len(joint_ids)) or (num_instances, num_joints).
joint_ids: The joint indices to set the joint torque limits for. Defaults to None (all joints).
env_ids: The environment indices to set the joint torque limits for. Defaults to None (all environments).
joint_mask: The joint mask. Shape is (num_joints).
Expand All @@ -878,6 +905,17 @@ def write_joint_dynamic_friction_coefficient_to_sim(
):
raise NotImplementedError()

@abstractmethod
def write_joint_viscous_friction_coefficient_to_sim(
self,
joint_viscous_friction_coeff: torch.Tensor | wp.array | float,
joint_ids: Sequence[int] | slice | None = None,
env_ids: Sequence[int] | None = None,
joint_mask: torch.Tensor | wp.array | None = None,
env_mask: torch.Tensor | wp.array | None = None,
):
raise NotImplementedError()

"""
Operations - Setters.
"""
Expand Down
5 changes: 4 additions & 1 deletion source/isaaclab/isaaclab/assets/asset_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any

import warp as wp

import isaaclab.sim as sim_utils
import isaaclab.sim.utils.prims as prim_utils
from isaaclab.sim import SimulationContext
Expand Down Expand Up @@ -243,11 +245,12 @@ def set_debug_vis(self, debug_vis: bool) -> bool:
return True

@abstractmethod
def reset(self, env_ids: Sequence[int] | None = None):
def reset(self, env_ids: Sequence[int] | None = None, env_mask: wp.array | torch.Tensor | None = None):
"""Resets all internal buffers of selected environments.

Args:
env_ids: The indices of the object to reset. Defaults to None (all instances).
env_mask: The mask of the object to reset. Defaults to None (all instances).
"""
raise NotImplementedError

Expand Down
Loading
Loading