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
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#usda 1.0
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
(
defaultPrim = "World"
)

# Keep the shared simulation topology, round visual mesh, and material.
# Newton uses the simulation tetrahedra directly; the PhysX collision mesh is redundant.
def Xform "World" (
prepend references = @Rope.usd@
)
{
over "collision_mesh" (
active = false
)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from isaaclab_arena.embodiments.common.arm_mode import ArmMode
from isaaclab_arena.embodiments.franka.franka import FrankaIKEmbodiment
from isaaclab_arena.utils.cameras import ArenaCameraCfg
from isaaclab_arena.utils.physics_backend import PhysicsBackend
from isaaclab_arena.utils.pose import Pose
from isaaclab_physx.renderers import IsaacRtxRendererCfg, IsaacRtxRendererGlobalSettingsCfg

Expand Down Expand Up @@ -159,6 +160,35 @@ def __init__(
self.event_config = FrankaRopeEventsCfg()
self.reward_config = None

def _configure_physics_backend(self, backend: PhysicsBackend) -> None:
"""Apply Newton gravity compensation and mesh contact settings to the Franka."""
if backend is PhysicsBackend.NEWTON:
from isaaclab_newton.renderers import NewtonWarpRendererCfg
from isaaclab_newton.sim.schemas import (
MujocoRigidBodyCfg,
NewtonMaterialPropertiesCfg,
NewtonSDFCollisionPropertiesCfg,
)

from .newton_cfg import spawn_rope_franka_newton

spawn = self.scene_config.robot.spawn
# Match the shared high-PD Franka's PhysX-only disable_gravity setting.
spawn.rigid_props = MujocoRigidBodyCfg(gravcomp=1.0)
spawn.make_uninstanceable = True
spawn.func = spawn_rope_franka_newton
# Full-surface soft contacts require SDFs for the mesh colliders.
spawn.collision_props = NewtonSDFCollisionPropertiesCfg(sdf_max_resolution=64)
spawn.physics_material = NewtonMaterialPropertiesCfg(
static_friction=3.0, dynamic_friction=3.0, restitution=0.0
)
# Maintain pose tracking under the deformable solver's contact feedback.
self.scene_config.robot.actuators["panda_shoulder"].stiffness = 800.0
self.scene_config.robot.actuators["panda_forearm"].stiffness = 800.0
# Read rigid poses and deformable triangles directly from Newton state.
for camera_name in self.camera_config.camera_names():
getattr(self.camera_config, camera_name).renderer_cfg = NewtonWarpRendererCfg()


@register_retargeter
class FrankaRopeKeyboardRetargeter(RetargetterBase):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from isaaclab_arena.environments.arena_environment_factory import ArenaEnvironmentCfg, ArenaEnvironmentFactory
from isaaclab_arena.utils.pose import Pose

from .rope_asset import FrankaRopeAsset
from .rope_asset import FrankaRopeAsset, rope_newton_spawn_cfg
from .task import FrankaRopeTask

if TYPE_CHECKING:
Expand All @@ -31,13 +31,35 @@ class FrankaRopeArenaEnvironmentCfg(ArenaEnvironmentCfg):


def _configure_franka_rope_env(env_cfg: Any) -> Any:
"""Apply rope timing and asset settings for PhysX."""
"""Apply rope timing and asset/solver settings for Arena's selected backend."""

from isaaclab_newton.physics import NewtonCfg

env_cfg.decimation = 5
env_cfg.sim.dt = 0.01
env_cfg.sim.render_interval = 2
env_cfg.num_rerenders_on_reset = 1
env_cfg.scene.replicate_physics = False
if isinstance(env_cfg.sim.physics, NewtonCfg):
from isaaclab_newton.sim.schemas import MujocoCollisionCfg

from .newton_cfg import rope_newton_physics_cfg, spawn_rope_table_newton

env_cfg.sim.physics = rope_newton_physics_cfg()
# Refresh Kit once per control step; the solver still advances at 1 ms.
env_cfg.sim.render_interval = env_cfg.decimation
env_cfg.scene.object.spawn = rope_newton_spawn_cfg()
# A discontinuous half-mesh reset can invert the connecting tetrahedra.
# Blend the same sampled poses through the middle half of the rope.
env_cfg.events.reset_object_position.params["transition_length"] = 0.4
env_cfg.scene.replicate_physics = True
env_cfg.scene.table.spawn.make_uninstanceable = True
env_cfg.scene.table.spawn.func = spawn_rope_table_newton
# A firm, critically damped table contact in MJWarp. Priority keeps the
# table response from being softened by the gripper's default material.
# These MuJoCo parameters do not change VBD's rope contact properties.
env_cfg.scene.table.spawn.collision_props = MujocoCollisionCfg(priority=1, solref=(0.004, 1.0))
else:
env_cfg.scene.replicate_physics = False
env_cfg.teleop_devices = DevicesCfg(
devices={
"keyboard": Se3KeyboardCfg(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def reset_rope_nodal_state(
pose_range: dict[str, tuple[float, float]],
partial_pose_range: dict[str, tuple[float, float]],
asset_cfg: SceneEntityCfg = SceneEntityCfg("object"),
transition_length: float | None = None,
) -> None:
"""Reset and randomize the rope, including a transform of one rope half.

Expand All @@ -87,6 +88,8 @@ def reset_rope_nodal_state(
pose_range: Translation [m] and rotation [rad] ranges applied to all nodes.
partial_pose_range: Additional translation [m] and rotation [rad] ranges applied to one rope half.
asset_cfg: Deformable rope entity.
transition_length: Length [m] over which to blend the half-rope transform.
None preserves the original piecewise reset used by PhysX.
"""

rope: DeformableObject = env.scene[asset_cfg.name]
Expand All @@ -101,6 +104,24 @@ def reset_rope_nodal_state(
)
node_ids = torch.tensor(_ROPE_PARTIAL_NODE_IDS, device=rope.device)
position, quaternion = _sample_pose_delta(partial_pose_range, len(env_ids), rope.device)
nodal_state[..., node_ids, :3] = rope.transform_nodal_pos(nodal_state[..., node_ids, :3], position, quaternion)
if transition_length is None:
nodal_state[..., node_ids, :3] = rope.transform_nodal_pos(nodal_state[..., node_ids, :3], position, quaternion)
else:
assert transition_length > 0.0
nodes = nodal_state[..., :3]
center = nodes.mean(dim=1, keepdim=True)
half_center = nodes[:, node_ids].mean(dim=1, keepdim=True)
axis = torch.nn.functional.normalize(half_center - center, dim=-1)
distance = ((nodes - center) * axis).sum(dim=-1, keepdim=True)
fraction = (distance / transition_length + 0.5).clamp(0.0, 1.0)
# A C2-continuous transition avoids tearing/inverting the tetrahedra at
# the boundary of the two halves. End sections retain their rigid poses.
weight = fraction**3 * (10.0 - 15.0 * fraction + 6.0 * fraction**2)
transformed = (
math_utils.quat_apply(quaternion[:, None, :].expand(-1, nodes.shape[1], -1), nodes - half_center)
+ half_center
+ position[:, None, :]
)
nodal_state[..., :3] = nodes + weight * (transformed - nodes)

rope.write_nodal_state_to_sim_index(nodal_state, env_ids=env_ids)
141 changes: 141 additions & 0 deletions autodata_examples/envs/isaac_lab_arena/franka_rope/newton_cfg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Newton rigid--deformable physics for the shared Franka rope task."""

from __future__ import annotations

from typing import TYPE_CHECKING

from isaaclab.sim.spawners.from_files import UsdFileCfg, spawn_from_usd
from isaaclab.sim.utils import clone
from isaaclab_contrib.coupling import CouplerEntryCfg, CouplerProxyCfg, CouplerProxyMappingCfg
from isaaclab_newton.physics import (
MJWarpSolverCfg,
NewtonCfg,
NewtonCollisionPipelineCfg,
NewtonSoftContactCfg,
VBDSolverCfg,
)

if TYPE_CHECKING:
from pxr import Usd


@clone
def spawn_rope_franka_newton(
prim_path: str,
cfg: UsdFileCfg,
translation: tuple[float, float, float] | None = None,
orientation: tuple[float, float, float, float] | None = None,
**kwargs,
) -> Usd.Prim:
"""Use analytic finger colliders for stable volumetric rope contacts.

The boxes occupy the original collision meshes' bounds. Visual meshes and
articulation joints remain those of the shared Franka.

Args:
prim_path: Prim path or pattern at which to spawn the robot.
cfg: Robot USD spawner configuration.
translation: Translation relative to the parent [m].
orientation: Rotation quaternion in XYZW order.
**kwargs: Additional arguments forwarded to the USD spawner.

Returns:
The spawned robot prim.
"""
from pxr import Gf, UsdGeom

prim = spawn_from_usd(prim_path, cfg, translation, orientation, **kwargs)
for finger in ("panda_leftfinger", "panda_rightfinger"):
collider = prim.GetStage().GetPrimAtPath(f"{prim.GetPath()}/{finger}/collisions/collisions")
assert collider.IsValid() and collider.GetTypeName() == "Mesh", "Expected the legacy Franka finger collider"
points = UsdGeom.Mesh(collider).GetPointsAttr().Get()
lower = Gf.Vec3d(*(min(point[axis] for point in points) for axis in range(3)))
upper = Gf.Vec3d(*(max(point[axis] for point in points) for axis in range(3)))
collider.SetTypeName("Cube")
cube = UsdGeom.Cube(collider)
cube.CreateSizeAttr(1.0)
cube.AddTranslateOp(opSuffix="rope_contact_center").Set((lower + upper) * 0.5)
cube.AddScaleOp(opSuffix="rope_contact_size").Set(upper - lower)
return prim


@clone
def spawn_rope_table_newton(
prim_path: str,
cfg: UsdFileCfg,
translation: tuple[float, float, float] | None = None,
orientation: tuple[float, float, float, float] | None = None,
**kwargs,
) -> Usd.Prim:
"""Use an analytic box for the Seattle table's existing unit-cube collider.

Args:
prim_path: Prim path or pattern at which to spawn the table.
cfg: Table USD spawner configuration.
translation: Translation relative to the parent [m].
orientation: Rotation quaternion in XYZW order.
**kwargs: Additional arguments forwarded to the USD spawner.

Returns:
The spawned table prim.
"""
from pxr import UsdGeom

prim = spawn_from_usd(prim_path, cfg, translation, orientation, **kwargs)
collider = prim.GetStage().GetPrimAtPath(f"{prim.GetPath()}/Collisions/Cube")
assert collider.IsValid() and collider.GetTypeName() == "Mesh", "Expected the Seattle table's cube mesh collider"
collider.SetTypeName("Cube")
UsdGeom.Cube(collider).CreateSizeAttr(1.0)
return prim


def rope_newton_physics_cfg() -> NewtonCfg:
"""Couple the Franka's rigid dynamics to the rope's volumetric VBD solve."""
return NewtonCfg(
solver_cfg=CouplerProxyCfg(
class_type="autodata_examples.envs.isaac_lab_arena.franka_rope.newton_visuals:NewtonRopeManager",
entries=[
CouplerEntryCfg(
name="rigid",
solver_cfg=MJWarpSolverCfg(cone="elliptic", ls_iterations=20, integrator="implicitfast"),
bodies=[r"/World/envs/env_[^/]+/Robot"],
include_static_shapes=True,
),
CouplerEntryCfg(
name="soft",
solver_cfg=VBDSolverCfg(
iterations=10,
rigid_body_particle_contact_buffer_size=4096,
particle_enable_self_contact=True,
particle_self_contact_radius=0.002,
particle_self_contact_margin=0.003,
),
all_particles=True,
),
],
proxies=[
CouplerProxyMappingCfg(
source="rigid",
destination="soft",
bodies=[
r"/World/envs/env_[^/]+/Robot/.*panda_hand",
r"/World/envs/env_[^/]+/Robot/.*panda_(left|right)finger",
],
mode="lagged",
collide_interval=1,
collision_pipeline=NewtonCollisionPipelineCfg(enable_rigid_soft_full_surface_contact=True),
),
],
iterations=1,
),
soft_contact_cfg=NewtonSoftContactCfg(
# Resolve contacts around the narrow rope, with enough friction to carry it.
soft_contact_ke=1.0e5,
soft_contact_kd=10.0,
soft_contact_mu=10.0,
),
num_substeps=10,
)
Loading
Loading