Skip to content

Commit 03def46

Browse files
committed
Add more test coverages for duplicate body frame
1 parent 4417996 commit 03def46

File tree

2 files changed

+83
-48
lines changed

2 files changed

+83
-48
lines changed

source/isaaclab/isaaclab/sensors/frame_transformer/frame_transformer.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,9 @@ def _initialize_impl(self):
227227
" rigid body. The class only supports transformations between rigid bodies."
228228
)
229229

230-
# Get relative prim path as unique body identifier (e.g., "Robot/left_hand" instead of just "left_hand")
231-
# This allows tracking bodies with the same name at different hierarchy levels
230+
# Get the name of the body: use relative prim path for unique identification
232231
body_name = _get_relative_body_path(matching_prim_path)
233-
# Use leaf name if frame isn't specified by user (for backwards compatibility)
232+
# Use leaf name of prim path if frame name isn't specified by user
234233
frame_name = frame if frame is not None else matching_prim_path.rsplit("/", 1)[-1]
235234

236235
# Keep track of which frames are associated with which bodies
@@ -321,8 +320,7 @@ def extract_env_num_and_prim_path(item: str) -> tuple[int, str]:
321320
# -- target frames: use relative prim path for unique identification
322321
self._target_frame_body_names = [_get_relative_body_path(prim_path) for prim_path in sorted_prim_paths]
323322

324-
# -- source frame: extract relative path from config pattern
325-
# Handle patterns like "{ENV_REGEX_NS}/Robot/torso" or "/World/envs/env_.*/Robot/torso" -> "Robot/torso"
323+
# -- source frame: use relative prim path for unique identification
326324
self._source_frame_body_name = _get_relative_body_path(self.cfg.prim_path)
327325
source_frame_index = self._target_frame_body_names.index(self._source_frame_body_name)
328326

source/isaaclab/test/sensors/test_frame_transformer.py

Lines changed: 80 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -593,10 +593,25 @@ def test_sensor_print(sim):
593593

594594
@pytest.mark.isaacsim_ci
595595
def test_frame_transformer_duplicate_body_names(sim):
596-
"""Test that bodies with same name but different paths are tracked separately.
596+
"""Test tracking bodies with same leaf name at different hierarchy levels.
597597
598-
This test verifies the fix for body name collision when multiple bodies
599-
share the same name but exist at different hierarchy levels.
598+
This test verifies that bodies with the same leaf name but different paths
599+
(e.g., Robot/LF_SHANK vs Robot_1/LF_SHANK, or arm/link vs leg/link) are tracked
600+
separately using their full relative paths internally.
601+
602+
The test uses 4 target frames to cover both scenarios:
603+
604+
Explicit frame names (recommended when bodies share the same leaf name):
605+
User provides unique names like "Robot_LF_SHANK" and "Robot_1_LF_SHANK" to
606+
distinguish between bodies at different hierarchy levels. This makes it
607+
easy to identify which transform belongs to which body.
608+
609+
Implicit frame names (backward compatibility):
610+
When no name is provided, it defaults to the leaf body name (e.g., "RF_SHANK").
611+
This preserves backward compatibility for users who may have existing code like
612+
`idx = target_frame_names.index("RF_SHANK")`. However, when multiple bodies share
613+
the same leaf name, this results in duplicate frame names. The transforms are
614+
still distinct because internal body tracking uses full relative paths.
600615
"""
601616

602617
# Create a custom scene config with two robots
@@ -619,23 +634,21 @@ class MultiRobotSceneCfg(InteractiveSceneCfg):
619634
frame_transformer: FrameTransformerCfg = FrameTransformerCfg(
620635
prim_path="{ENV_REGEX_NS}/Robot/base",
621636
target_frames=[
622-
# Robot's LF_FOOT
637+
# Explicit frame names (recommended when bodies share the same leaf name)
623638
FrameTransformerCfg.FrameCfg(
624-
name="LF_FOOT",
639+
name="Robot_LF_SHANK",
625640
prim_path="{ENV_REGEX_NS}/Robot/LF_SHANK",
626-
offset=OffsetCfg(
627-
pos=euler_rpy_apply(rpy=(0, 0, -math.pi / 2), xyz=(0.08795, 0.01305, -0.33797)),
628-
rot=quat_from_euler_rpy(0, 0, -math.pi / 2),
629-
),
630641
),
631-
# Robot_1's LF_FOOT (same body name, different robot)
632642
FrameTransformerCfg.FrameCfg(
633-
name="LF_FOOT_1",
643+
name="Robot_1_LF_SHANK",
634644
prim_path="{ENV_REGEX_NS}/Robot_1/LF_SHANK",
635-
offset=OffsetCfg(
636-
pos=euler_rpy_apply(rpy=(0, 0, -math.pi / 2), xyz=(0.08795, 0.01305, -0.33797)),
637-
rot=quat_from_euler_rpy(0, 0, -math.pi / 2),
638-
),
645+
),
646+
# Implicit frame names (backward compatibility)
647+
FrameTransformerCfg.FrameCfg(
648+
prim_path="{ENV_REGEX_NS}/Robot/RF_SHANK",
649+
),
650+
FrameTransformerCfg.FrameCfg(
651+
prim_path="{ENV_REGEX_NS}/Robot_1/RF_SHANK",
639652
),
640653
],
641654
)
@@ -649,15 +662,29 @@ class MultiRobotSceneCfg(InteractiveSceneCfg):
649662

650663
# Get target frame names
651664
target_frame_names = scene.sensors["frame_transformer"].data.target_frame_names
652-
assert "LF_FOOT" in target_frame_names, "LF_FOOT should be in target frames"
653-
assert "LF_FOOT_1" in target_frame_names, "LF_FOOT_1 should be in target frames"
654665

655-
lf_foot_idx = target_frame_names.index("LF_FOOT")
656-
lf_foot_1_idx = target_frame_names.index("LF_FOOT_1")
666+
# Verify explicit frame names are present
667+
assert "Robot_LF_SHANK" in target_frame_names, f"Expected 'Robot_LF_SHANK', got {target_frame_names}"
668+
assert "Robot_1_LF_SHANK" in target_frame_names, f"Expected 'Robot_1_LF_SHANK', got {target_frame_names}"
669+
670+
# Without explicit names, both RF_SHANK frames default to same name "RF_SHANK"
671+
# This results in duplicate frame names (expected behavior for backwards compatibility)
672+
rf_shank_count = target_frame_names.count("RF_SHANK")
673+
assert rf_shank_count == 2, f"Expected 2 'RF_SHANK' entries (name collision), got {rf_shank_count}"
674+
675+
# Get indices for explicit named frames
676+
robot_lf_idx = target_frame_names.index("Robot_LF_SHANK")
677+
robot_1_lf_idx = target_frame_names.index("Robot_1_LF_SHANK")
678+
679+
# Get indices for implicit named frames (both named "RF_SHANK")
680+
rf_shank_indices = [i for i, name in enumerate(target_frame_names) if name == "RF_SHANK"]
681+
assert len(rf_shank_indices) == 2, f"Expected 2 RF_SHANK indices, got {rf_shank_indices}"
657682

658683
# Acquire ground truth body indices
659-
robot_lf_shank_idx = scene.articulations["robot"].find_bodies("LF_SHANK")[0][0]
660-
robot_1_lf_shank_idx = scene.articulations["robot_1"].find_bodies("LF_SHANK")[0][0]
684+
robot_lf_shank_body_idx = scene.articulations["robot"].find_bodies("LF_SHANK")[0][0]
685+
robot_1_lf_shank_body_idx = scene.articulations["robot_1"].find_bodies("LF_SHANK")[0][0]
686+
robot_rf_shank_body_idx = scene.articulations["robot"].find_bodies("RF_SHANK")[0][0]
687+
robot_1_rf_shank_body_idx = scene.articulations["robot_1"].find_bodies("RF_SHANK")[0][0]
661688

662689
# Define simulation stepping
663690
sim_dt = sim.get_physics_dt()
@@ -697,30 +724,40 @@ class MultiRobotSceneCfg(InteractiveSceneCfg):
697724
target_pos_w = scene.sensors["frame_transformer"].data.target_pos_w
698725

699726
# Get ground truth positions
700-
robot_lf_pos_w = scene.articulations["robot"].data.body_pos_w[:, robot_lf_shank_idx]
701-
robot_1_lf_pos_w = scene.articulations["robot_1"].data.body_pos_w[:, robot_1_lf_shank_idx]
702-
703-
# The two positions should be DIFFERENT (robots are at different locations)
704-
# If the bug exists, both would return the same position
705-
pos_difference = torch.norm(target_pos_w[:, lf_foot_idx] - target_pos_w[:, lf_foot_1_idx], dim=-1)
706-
707-
# The robots are 2m apart, so the feet should also be roughly 2m apart
708-
assert torch.all(pos_difference > 1.0), (
709-
f"LF_FOOT and LF_FOOT_1 should have different positions (got diff={pos_difference}). "
727+
robot_lf_pos_w = scene.articulations["robot"].data.body_pos_w[:, robot_lf_shank_body_idx]
728+
robot_1_lf_pos_w = scene.articulations["robot_1"].data.body_pos_w[:, robot_1_lf_shank_body_idx]
729+
robot_rf_pos_w = scene.articulations["robot"].data.body_pos_w[:, robot_rf_shank_body_idx]
730+
robot_1_rf_pos_w = scene.articulations["robot_1"].data.body_pos_w[:, robot_1_rf_shank_body_idx]
731+
732+
# TEST 1: Explicit named frames (LF_SHANK) should have DIFFERENT positions
733+
lf_pos_difference = torch.norm(target_pos_w[:, robot_lf_idx] - target_pos_w[:, robot_1_lf_idx], dim=-1)
734+
assert torch.all(lf_pos_difference > 1.0), (
735+
f"Robot_LF_SHANK and Robot_1_LF_SHANK should have different positions (got diff={lf_pos_difference}). "
710736
"This indicates body name collision bug."
711737
)
712738

713-
# Verify each frame transformer output matches the correct robot's body
714-
# (with some tolerance for the offset transformation)
715-
torch.testing.assert_close(
716-
target_pos_w[:, lf_foot_idx],
717-
robot_lf_pos_w,
718-
atol=0.5, # Allow tolerance for offset
719-
rtol=0.1,
739+
# Verify explicit named frames match correct robot bodies
740+
torch.testing.assert_close(target_pos_w[:, robot_lf_idx], robot_lf_pos_w)
741+
torch.testing.assert_close(target_pos_w[:, robot_1_lf_idx], robot_1_lf_pos_w)
742+
743+
# TEST 2: Implicit named frames (RF_SHANK) should also have DIFFERENT positions
744+
# Even though they have the same frame name, internal body tracking uses full paths
745+
rf_pos_difference = torch.norm(
746+
target_pos_w[:, rf_shank_indices[0]] - target_pos_w[:, rf_shank_indices[1]], dim=-1
720747
)
721-
torch.testing.assert_close(
722-
target_pos_w[:, lf_foot_1_idx],
723-
robot_1_lf_pos_w,
724-
atol=0.5, # Allow tolerance for offset
725-
rtol=0.1,
748+
assert torch.all(rf_pos_difference > 1.0), (
749+
f"The two RF_SHANK frames should have different positions (got diff={rf_pos_difference}). "
750+
"This indicates body name collision bug in internal body tracking."
726751
)
752+
753+
# Verify implicit named frames match correct robot bodies
754+
# Note: Order depends on internal processing, so we check both match one of the robots
755+
rf_positions = [target_pos_w[:, rf_shank_indices[0]], target_pos_w[:, rf_shank_indices[1]]]
756+
757+
# Each tracked position should match one of the ground truth positions
758+
for rf_pos in rf_positions:
759+
matches_robot = torch.allclose(rf_pos, robot_rf_pos_w, atol=1e-5)
760+
matches_robot_1 = torch.allclose(rf_pos, robot_1_rf_pos_w, atol=1e-5)
761+
assert (
762+
matches_robot or matches_robot_1
763+
), f"RF_SHANK position {rf_pos} doesn't match either robot's RF_SHANK position"

0 commit comments

Comments
 (0)