@@ -593,10 +593,25 @@ def test_sensor_print(sim):
593593
594594@pytest .mark .isaacsim_ci
595595def 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