Skip to content

Commit 2e83916

Browse files
committed
Fix cable routing contact and replay semantics
1 parent e4e14de commit 2e83916

7 files changed

Lines changed: 108 additions & 44 deletions

File tree

source/isaaclab_tasks/changelog.d/maximiliank-cable-routing-yam.minor.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ Fixed
2525
represented gripper actions by their binary command state, and made terminal success and failure
2626
rewards independent of control frequency. Newton actuator graph capture and complete-episode PPO
2727
startup are now enabled for training.
28-
* Applied explicit Newton contact materials to YAM and fixture assets, proxied every collidable YAM
29-
link into the cable solver, and declared the task's Newton and contrib runtime dependencies.
28+
* Applied explicit Newton contact materials to the fixtures and a task calibration layer that keeps
29+
the Menagerie actuator dynamics while targeting high friction to YAM fingers. Limited the cable
30+
solver proxy to each wrist/gripper subtree, and declared the task's Newton and contrib runtime
31+
dependencies.
32+
* Prevented simultaneous success and invalid-state terminations from receiving successful reset
33+
replay credit.
3034
* Avoided redundant or manager-order-dependent route evaluation and expensive ordinary cable
3135
generation when a full-scene replay reset will replace it.
3236
* Replaced legacy dense tangent-point optimization with a deterministic, fixed-sweep XPBD-style

source/isaaclab_tasks/isaaclab_tasks/contrib/cable_routing/assets/yam/README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# I2RT YAM asset snapshot
22

33
This directory contains the self-contained `i2rt_yam_default` USD package used
4-
by the cable-routing environment. It was copied on 2026-08-05 from NVIDIA's
5-
Robot Menagerie repository:
4+
by the cable-routing environment. The eight source USD files were copied on
5+
2026-08-05 from NVIDIA's Robot Menagerie repository:
66

77
- repository: `https://gitlab-master.nvidia.com/isaac-applications-deployment/robot_menagerie`
88
- `main` commit: `68ef1e0cc3e863a861b873893f038496e0dfe16b`
@@ -16,18 +16,22 @@ source to `google-deepmind/mujoco_menagerie` commit
1616
digest `5ae901629db944abd3e6cbb0eaf28ea78bbda3483c840d6db1167e999cc0348c`.
1717
That source is MIT licensed; the required notice is preserved in `LICENSE.md`.
1818

19-
All eight USD files are included and all layer references are relative. OpenUSD
20-
dependency traversal resolves seven dependent layers with no unresolved paths,
21-
so runtime does not require GitLab, Git LFS, Nucleus, or credentials.
19+
All eight source USD files are included and all layer references are relative.
20+
The `i2rt_yam_cable_routing.usda` task layer composes that unmodified snapshot,
21+
retains its authored actuator dynamics, and targets the calibrated high-friction
22+
contact material to the finger collision subtrees. OpenUSD dependency traversal
23+
resolves every layer with no unresolved paths, so runtime does not require
24+
GitLab, Git LFS, Nucleus, or credentials.
2225

2326
- entry-layer SHA-256: `c1bedf1d978d1147f82d1c2cb5e56da1b5003eb14ec78bd0be89258c021404bc`
2427
- eight-file package size: `1,887,475` bytes
2528
- deterministic package-manifest SHA-256: `7a1532b694a51f263a9e09b60d212d40104aee871bcfa6e8c6705fd566698d47`
2629

27-
The manifest digest is produced from the pristine USD directory with:
30+
The manifest digest is produced from the pristine eight-file source package
31+
(and intentionally excludes the task layer) with:
2832

2933
```bash
30-
find . -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum
34+
find i2rt_yam i2rt_yam_default.usda -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum
3135
```
3236

3337
Set `ISAACLAB_CABLE_ROUTING_YAM_USD_PATH` to an alternate local YAM USD for an
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:ffcdbb1e477694e26cd58d1a1f605752327fae774cd5e6dd6f7f4433599f8d88
3+
size 2338

source/isaaclab_tasks/isaaclab_tasks/contrib/cable_routing/cable_routing_env_cfg.py

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
# SPDX-License-Identifier: BSD-3-Clause
55

6-
"""Manager-based bimanual YAM cable routing with Newton MJWarp/VBD coupling."""
6+
"""Manager-based two-F1-peg cable-routing milestone with Newton MJWarp/VBD coupling."""
77

88
from __future__ import annotations
99

@@ -37,11 +37,14 @@
3737

3838
from . import mdp
3939

40+
YAM_SOURCE_USD_PATH = str(Path(__file__).resolve().parent / "assets" / "yam" / "i2rt_yam_default.usda")
41+
"""Pinned, unmodified Robot Menagerie YAM source package."""
42+
4043
YAM_USD_PATH = os.environ.get(
4144
"ISAACLAB_CABLE_ROUTING_YAM_USD_PATH",
42-
str(Path(__file__).resolve().parent / "assets" / "yam" / "i2rt_yam_default.usda"),
45+
str(Path(__file__).resolve().parent / "assets" / "yam" / "i2rt_yam_cable_routing.usda"),
4346
)
44-
"""Pinned Robot Menagerie YAM, optionally replaced by an explicit local asset path."""
47+
"""Task-calibrated YAM layer, optionally replaced by an explicit local asset path."""
4548

4649
MANIPULATIONNET_ASSET_DIR = Path(__file__).resolve().parent / "assets" / "manipulationnet"
4750
BOARD_USD_PATH = str(MANIPULATIONNET_ASSET_DIR / "board.usdc")
@@ -63,11 +66,13 @@
6366
ROUTE_AXIAL_CUTOFF = 0.5 * PEG_HEIGHT + CABLE_RADIUS
6467
"""Cable-center height range whose surface can overlap the finite peg [m]."""
6568
CABLE_CENTER_Z = BOARD_TOP_Z + CABLE_RADIUS + 0.002
66-
CABLE_CONTACT_FRICTION = 5.0
67-
YAM_CONTACT_FRICTION = 5.0
69+
CABLE_CONTACT_FRICTION = 60.0
6870
FIXTURE_CONTACT_FRICTION = 0.5
6971
CONTACT_STIFFNESS = 4.0e4
72+
# Rigid-contact damping is distinct from cable bend damping, which the current
73+
# CableMaterialCfg and Newton USD cable importer do not expose.
7074
CONTACT_DAMPING = 1.0e-5
75+
FAILURE_TERMINATION_NAMES = ("invalid_cable", "invalid_robot_or_action")
7176
YAM_BASE_COLLISION_DEPTH = 0.017
7277
YAM_BASE_Z = TABLE_TOP_Z + YAM_BASE_COLLISION_DEPTH
7378
YAM_VISUAL_BASE_DEPTH = 0.07
@@ -173,7 +178,6 @@ def _make_yam_cfg(prim_path: str, position: tuple[float, float, float], yaw: flo
173178
spawn=sim_utils.UsdFileCfg(
174179
usd_path=YAM_USD_PATH,
175180
copy_from_source=False,
176-
physics_material=_make_rigid_contact_material(YAM_CONTACT_FRICTION),
177181
# The converted Menagerie package retains legacy
178182
# ``mjc:body:gravcomp`` metadata. Author the current Newton schema
179183
# spelling explicitly and route compensation through each drive so
@@ -199,29 +203,20 @@ def _make_yam_cfg(prim_path: str, position: tuple[float, float, float], yaw: flo
199203
actuators={
200204
"arm": ImplicitActuatorCfg(
201205
joint_names_expr=["joint[1-6]"],
202-
effort_limit_sim=40.0,
203-
velocity_limit_sim=2.0,
204-
stiffness=400.0,
205-
damping=40.0,
206-
armature=0.02,
206+
stiffness=None,
207+
damping=None,
207208
),
208209
"gripper_drive": ImplicitActuatorCfg(
209210
joint_names_expr=["left_finger"],
210-
effort_limit_sim=80.0,
211-
velocity_limit_sim=0.2,
212-
stiffness=2000.0,
213-
damping=100.0,
214-
armature=0.1,
211+
stiffness=None,
212+
damping=None,
215213
),
216214
# Robot Menagerie authors right_finger as a -1 mimic of left_finger.
217215
# Keeping its drive passive avoids fighting the Newton equality constraint.
218216
"gripper_passive": ImplicitActuatorCfg(
219217
joint_names_expr=["right_finger"],
220-
effort_limit_sim=1.0,
221-
velocity_limit_sim=0.2,
222218
stiffness=0.0,
223219
damping=0.0,
224-
armature=0.1,
225220
),
226221
},
227222
soft_joint_pos_limit_factor=0.95,
@@ -363,6 +358,7 @@ class CommandsCfg:
363358
board_origin_b=(0.0, 0.0, BOARD_TOP_Z),
364359
radial_cutoff=0.05,
365360
axial_cutoff=ROUTE_AXIAL_CUTOFF,
361+
failure_termination_names=FAILURE_TERMINATION_NAMES,
366362
)
367363

368364

@@ -510,13 +506,13 @@ class RewardsCfg:
510506
weight=20.0,
511507
params={
512508
"command_name": "route",
513-
"failure_termination_names": ("invalid_cable", "invalid_robot_or_action"),
509+
"failure_termination_names": FAILURE_TERMINATION_NAMES,
514510
},
515511
)
516512
failure = RewTerm(
517513
func=mdp.route_failure,
518514
weight=-20.0,
519-
params={"termination_names": ("invalid_cable", "invalid_robot_or_action")},
515+
params={"termination_names": FAILURE_TERMINATION_NAMES},
520516
)
521517
stretch = RewTerm(
522518
func=mdp.cable_stretch,
@@ -563,7 +559,7 @@ class TerminationsCfg:
563559

564560
@configclass
565561
class CableRoutingEnvCfg(ManagerBasedRLEnvCfg):
566-
"""Goal-conditioned bimanual cable-routing environment using Newton only."""
562+
"""Goal-conditioned one-meter, two-F1-peg cable-routing environment using Newton."""
567563

568564
scene: CableRoutingSceneCfg = CableRoutingSceneCfg(
569565
num_envs=256,
@@ -621,7 +617,7 @@ class CableRoutingEnvCfg(ManagerBasedRLEnvCfg):
621617
source="rigid",
622618
destination="cable",
623619
bodies=[
624-
r"/World/envs/env_.*/Yam(Left|Right)",
620+
r"/World/envs/env_.*/Yam(Left|Right)/Geometry/arm/link_1/link_2/link_3/link_4/link_5/link_6",
625621
r"/World/envs/env_.*/(Table|Board)",
626622
r"/World/envs/env_.*/Peg(0|1)",
627623
],

source/isaaclab_tasks/isaaclab_tasks/contrib/cable_routing/mdp/commands.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ class CableRoutingCommandCfg(CommandTermCfg):
267267
reset_replay: CableResetReplayCfg = CableResetReplayCfg()
268268
"""Success-conditioned full-scene reset replay configuration."""
269269

270+
failure_termination_names: tuple[str, ...] = ()
271+
"""Failure terms that invalidate terminal success before replay crediting."""
272+
270273
marker_z_offset: float = 0.040
271274
"""Height of the first ordered-step marker above a peg center [m]."""
272275

@@ -1354,6 +1357,13 @@ def _cable_rest_length(self) -> float:
13541357
"""Return the cached authored mean cable-control-point edge length [m]."""
13551358
return self._cable_rest_length_m
13561359

1360+
def _terminal_success(self, env_ids: torch.Tensor) -> torch.Tensor:
1361+
"""Return terminal success after failure terminations take precedence."""
1362+
succeeded = self.succeeded[env_ids].clone()
1363+
for term_name in self.cfg.failure_termination_names:
1364+
succeeded &= ~self._env.termination_manager.get_term(term_name)[env_ids]
1365+
return succeeded
1366+
13571367
def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]:
13581368
"""Credit finished episodes, then atomically restore state and route."""
13591369
all_ids = torch.arange(self.num_envs, device=self.device, dtype=torch.long)
@@ -1364,7 +1374,7 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]:
13641374
else:
13651375
ids = torch.as_tensor(env_ids, device=self.device, dtype=torch.long)
13661376

1367-
terminal_success = self.succeeded[ids].clone()
1377+
terminal_success = self._terminal_success(ids)
13681378
terminal_route = self.route_id[ids].clone()
13691379
source_before_reset: torch.Tensor | None = None
13701380
if self.reset_replay is not None and self.reset_replay.built:

source/isaaclab_tasks/test/contrib/test_cable_routing_cfg.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
BOARD_TOP_Z,
4141
BOARD_USD_PATH,
4242
CABLE_BEND_MODULUS,
43+
CABLE_CONTACT_FRICTION,
4344
CABLE_LENGTH,
4445
CABLE_NUM_SEGMENTS,
4546
CABLE_SEGMENT_LENGTH,
@@ -55,6 +56,7 @@
5556
YAM_GRIPPER_CLOSED_POS,
5657
YAM_GRIPPER_OPEN_POS,
5758
YAM_LATERAL_OFFSET,
59+
YAM_SOURCE_USD_PATH,
5860
YAM_USD_PATH,
5961
YAM_VISUAL_BASE_DEPTH,
6062
YAM_VISUAL_BASE_WIDTH,
@@ -210,6 +212,22 @@ def test_terminal_outcome_rewards_are_control_frequency_independent() -> None:
210212
torch.testing.assert_close(failure_return, torch.tensor((0.0, -20.0, -20.0)))
211213

212214

215+
def test_reset_replay_success_credit_excludes_invalid_terminal_states() -> None:
216+
"""A simultaneous route completion and invalid state is a failed replay outcome."""
217+
term_values = {
218+
"invalid_cable": torch.tensor((False, True, False)),
219+
"invalid_robot_or_action": torch.tensor((False, False, True)),
220+
}
221+
command = CableRoutingCommand.__new__(CableRoutingCommand)
222+
command.succeeded = torch.tensor((True, True, True))
223+
command.cfg = SimpleNamespace(failure_termination_names=tuple(term_values))
224+
command._env = SimpleNamespace(termination_manager=SimpleNamespace(get_term=lambda name: term_values[name]))
225+
226+
terminal_success = command._terminal_success(torch.arange(3))
227+
228+
torch.testing.assert_close(terminal_success, torch.tensor((True, False, False)))
229+
230+
213231
def test_cable_routing_ppo_starts_from_complete_1p2_second_rollouts() -> None:
214232
"""The first SuccessMonitor outcomes come from complete episodes and sufficiently long rollouts."""
215233
env_cfg = CableRoutingEnvCfg()
@@ -246,6 +264,7 @@ def test_cable_routing_manager_terms_use_menagerie_joint_and_body_names() -> Non
246264
assert term.params["asset_cfg"].joint_names == ["joint[1-6]"]
247265

248266
assert cfg.events.reset_cable.params["full_scene_replay_command_name"] == "route"
267+
assert cfg.commands.route.failure_termination_names == cfg.rewards.success.params["failure_termination_names"]
249268

250269

251270
def test_cable_routing_rewards_are_sparse_success_with_penalties_only() -> None:
@@ -533,8 +552,14 @@ def test_cable_routing_table_board_and_yams_have_front_edge_reachable_placement(
533552
assert yam.spawn.joint_drive_props[0].actuatorgravcomp is True
534553
assert set(yam.actuators) == {"arm", "gripper_drive", "gripper_passive"}
535554
assert yam.actuators["arm"].joint_names_expr == ["joint[1-6]"]
555+
assert yam.actuators["arm"].stiffness is None
556+
assert yam.actuators["arm"].damping is None
557+
assert yam.actuators["arm"].effort_limit_sim is None
558+
assert yam.actuators["arm"].velocity_limit_sim is None
559+
assert yam.actuators["arm"].armature is None
536560
assert yam.actuators["gripper_drive"].joint_names_expr == ["left_finger"]
537-
assert yam.actuators["gripper_drive"].stiffness == 2000.0
561+
assert yam.actuators["gripper_drive"].stiffness is None
562+
assert yam.actuators["gripper_drive"].damping is None
538563
assert yam.actuators["gripper_passive"].joint_names_expr == ["right_finger"]
539564
assert yam.actuators["gripper_passive"].stiffness == 0.0
540565
assert yam.init_state.joint_pos["left_finger"] == YAM_GRIPPER_OPEN_POS
@@ -562,11 +587,13 @@ def test_cable_routing_table_board_and_yams_have_front_edge_reachable_placement(
562587

563588

564589
def test_cable_routing_uses_complete_credential_free_yam_snapshot() -> None:
565-
"""Test the default YAM is the pinned, dependency-complete Menagerie package."""
590+
"""Test the default YAM composes a calibration layer over the pinned Menagerie package."""
566591
yam_path = Path(YAM_USD_PATH)
592+
source_path = Path(YAM_SOURCE_USD_PATH)
567593

568594
assert yam_path.is_file()
569-
assert yam_path.name == "i2rt_yam_default.usda"
595+
assert yam_path.name == "i2rt_yam_cable_routing.usda"
596+
assert source_path.name == "i2rt_yam_default.usda"
570597
expected_hashes = {
571598
"i2rt_yam_default.usda": "c1bedf1d978d1147f82d1c2cb5e56da1b5003eb14ec78bd0be89258c021404bc",
572599
"i2rt_yam/i2rt_yam.usda": "d51c03bf78f1724c09ce46ae4c95cf64269871af25450fbc075bc2eb7cf08b55",
@@ -577,12 +604,12 @@ def test_cable_routing_uses_complete_credential_free_yam_snapshot() -> None:
577604
"i2rt_yam/Payload/MaterialsLibrary.usdc": ("c20c4d119a844c7efbb74905589bef2bcab5eaeb0b5518f81d16f997d9da9adb"),
578605
"i2rt_yam/Payload/Physics.usda": "51f028b6a305fc788eb0286a7e67bf5d43cab54727ffcfeb20ef6502230b10c9",
579606
}
580-
usd_files = {path.relative_to(yam_path.parent).as_posix(): path for path in yam_path.parent.rglob("*.usd*")}
581-
assert usd_files.keys() == expected_hashes.keys()
607+
usd_files = {path.relative_to(source_path.parent).as_posix(): path for path in source_path.parent.rglob("*.usd*")}
608+
assert usd_files.keys() == expected_hashes.keys() | {"i2rt_yam_cable_routing.usda"}
582609
for relative_path, expected_hash in expected_hashes.items():
583610
assert hashlib.sha256(usd_files[relative_path].read_bytes()).hexdigest() == expected_hash
584611

585-
for usd_path in yam_path.parent.rglob("*.usd*"):
612+
for usd_path in source_path.parent.rglob("*.usd*"):
586613
if usd_path.suffix == ".usda":
587614
contents = usd_path.read_text(encoding="utf-8")
588615
assert "omniverse://" not in contents
@@ -594,6 +621,7 @@ def test_cable_routing_uses_complete_credential_free_yam_snapshot() -> None:
594621
package_root = yam_path.parent.resolve()
595622
resolved_layers = {Path(layer.identifier).resolve().relative_to(package_root).as_posix() for layer in layers}
596623
assert resolved_layers == {
624+
"i2rt_yam_cable_routing.usda",
597625
"i2rt_yam_default.usda",
598626
"i2rt_yam/Payload/Contents.usda",
599627
"i2rt_yam/Payload/Geometry.usda",
@@ -615,7 +643,7 @@ def test_cable_routing_uses_disjoint_newton_collision_ownership() -> None:
615643
assert physics.use_cuda_graph
616644
assert physics.default_shape_cfg.ke == 4.0e4
617645
assert physics.default_shape_cfg.gap == 0.001
618-
assert physics.default_shape_cfg.mu == 5.0
646+
assert physics.default_shape_cfg.mu == CABLE_CONTACT_FRICTION
619647

620648
coupler = physics.solver_cfg
621649
assert isinstance(coupler, CouplerProxyCfg)
@@ -651,15 +679,15 @@ def test_cable_routing_uses_disjoint_newton_collision_ownership() -> None:
651679
assert proxy.collide_interval == 2
652680
assert physics.num_substeps % proxy.collide_interval == 0
653681
assert proxy.bodies == [
654-
r"/World/envs/env_.*/Yam(Left|Right)",
682+
r"/World/envs/env_.*/Yam(Left|Right)/Geometry/arm/link_1/link_2/link_3/link_4/link_5/link_6",
655683
r"/World/envs/env_.*/(Table|Board)",
656684
r"/World/envs/env_.*/Peg(0|1)",
657685
]
658686
assert coupler.model_cfg is None
659687

688+
assert cfg.scene.yam_left.spawn.physics_material is None
689+
assert cfg.scene.yam_right.spawn.physics_material is None
660690
for asset_cfg, expected_friction in (
661-
(cfg.scene.yam_left, 5.0),
662-
(cfg.scene.yam_right, 5.0),
663691
(cfg.scene.table, 0.5),
664692
(cfg.scene.board, 0.5),
665693
(cfg.scene.peg_0, 0.5),

source/isaaclab_tasks/test/contrib/test_cable_routing_collisions.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
import math
1010
from pathlib import Path
1111

12-
from pxr import Sdf, Usd, UsdGeom, UsdPhysics
12+
from pxr import Sdf, Usd, UsdGeom, UsdPhysics, UsdShade
1313

1414
from isaaclab_tasks.contrib.cable_routing.cable_routing_env_cfg import (
1515
BOARD_SIZE,
1616
BOARD_THICKNESS,
1717
BOARD_USD_PATH,
18+
CABLE_CONTACT_FRICTION,
19+
CONTACT_DAMPING,
20+
CONTACT_STIFFNESS,
1821
PEG_HEIGHT,
1922
PEG_RADIUS,
2023
PEG_SHAFT_RADIUS,
@@ -207,6 +210,22 @@ def test_yam_menagerie_asset_uses_native_newton_primitive_colliders() -> None:
207210
assert all(UsdPhysics.CollisionAPI(prim).GetCollisionEnabledAttr().Get() for prim in colliders)
208211
assert all(not prim.IsA(UsdGeom.Mesh) for prim in colliders)
209212

213+
finger_collider = next(prim for prim in colliders if "link_left_finger" in str(prim.GetPath()))
214+
arm_collider = next(
215+
prim
216+
for prim in colliders
217+
if "link_left_finger" not in str(prim.GetPath()) and "link_right_finger" not in str(prim.GetPath())
218+
)
219+
finger_material, _ = UsdShade.MaterialBindingAPI(finger_collider).ComputeBoundMaterial(materialPurpose="physics")
220+
arm_material, _ = UsdShade.MaterialBindingAPI(arm_collider).ComputeBoundMaterial(materialPurpose="physics")
221+
assert finger_material.GetPrim().GetAttribute("physics:dynamicFriction").Get() == CABLE_CONTACT_FRICTION
222+
assert arm_material.GetPrim().GetAttribute("physics:dynamicFriction").Get() == 5.0
223+
for material in (finger_material, arm_material):
224+
assert material.GetPrim().GetAttribute("newton:contactStiffness").Get() == CONTACT_STIFFNESS
225+
assert math.isclose(
226+
material.GetPrim().GetAttribute("newton:contactDamping").Get(), CONTACT_DAMPING, rel_tol=1.0e-6
227+
)
228+
210229
# Each caging fingertip keeps a detailed open geometry instead of being filled by
211230
# a visual-mesh convex hull: three capsules, two boxes, and six small contact beads.
212231
for leaf_name in ("lf_down", "rf_down"):

0 commit comments

Comments
 (0)