@@ -592,7 +592,8 @@ def test_sensor_print(sim):
592592
593593
594594@pytest .mark .isaacsim_ci
595- def test_frame_transformer_duplicate_body_names (sim ):
595+ @pytest .mark .parametrize ("source_robot" , ["Robot" , "Robot_1" ])
596+ def test_frame_transformer_duplicate_body_names (sim , source_robot ):
596597 """Test tracking bodies with same leaf name at different hierarchy levels.
597598
598599 This test verifies that bodies with the same leaf name but different paths
@@ -612,6 +613,11 @@ def test_frame_transformer_duplicate_body_names(sim):
612613 `idx = target_frame_names.index("RF_SHANK")`. However, when multiple bodies share
613614 the same leaf name, this results in duplicate frame names. The transforms are
614615 still distinct because internal body tracking uses full relative paths.
616+
617+ Args:
618+ source_robot: The robot to use as the source frame ("Robot" or "Robot_1").
619+ This tests that both source frames work correctly when there are
620+ duplicate body names.
615621 """
616622
617623 # Create a custom scene config with two robots
@@ -630,31 +636,35 @@ class MultiRobotSceneCfg(InteractiveSceneCfg):
630636 init_state = ANYMAL_C_CFG .init_state .replace (pos = (2.0 , 0.0 , 0.6 )),
631637 )
632638
633- # Frame transformer tracking same-named bodies from both robots
634- frame_transformer : FrameTransformerCfg = FrameTransformerCfg (
635- prim_path = "{ENV_REGEX_NS}/Robot/base" ,
636- target_frames = [
637- # Explicit frame names (recommended when bodies share the same leaf name)
638- FrameTransformerCfg .FrameCfg (
639- name = "Robot_LF_SHANK" ,
640- prim_path = "{ENV_REGEX_NS}/Robot/LF_SHANK" ,
641- ),
642- FrameTransformerCfg .FrameCfg (
643- name = "Robot_1_LF_SHANK" ,
644- prim_path = "{ENV_REGEX_NS}/Robot_1/LF_SHANK" ,
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" ,
652- ),
653- ],
654- )
639+ # Frame transformer will be set after config creation (needs source_robot parameter)
640+ frame_transformer : FrameTransformerCfg = None # type: ignore
655641
656642 # Spawn things into stage
657643 scene_cfg = MultiRobotSceneCfg (num_envs = 2 , env_spacing = 10.0 , lazy_sensor_update = False )
644+
645+ # Frame transformer tracking same-named bodies from both robots
646+ # Source frame is parametrized to test both Robot/base and Robot_1/base
647+ scene_cfg .frame_transformer = FrameTransformerCfg (
648+ prim_path = f"{{ENV_REGEX_NS}}/{ source_robot } /base" ,
649+ target_frames = [
650+ # Explicit frame names (recommended when bodies share the same leaf name)
651+ FrameTransformerCfg .FrameCfg (
652+ name = "Robot_LF_SHANK" ,
653+ prim_path = "{ENV_REGEX_NS}/Robot/LF_SHANK" ,
654+ ),
655+ FrameTransformerCfg .FrameCfg (
656+ name = "Robot_1_LF_SHANK" ,
657+ prim_path = "{ENV_REGEX_NS}/Robot_1/LF_SHANK" ,
658+ ),
659+ # Implicit frame names (backward compatibility)
660+ FrameTransformerCfg .FrameCfg (
661+ prim_path = "{ENV_REGEX_NS}/Robot/RF_SHANK" ,
662+ ),
663+ FrameTransformerCfg .FrameCfg (
664+ prim_path = "{ENV_REGEX_NS}/Robot_1/RF_SHANK" ,
665+ ),
666+ ],
667+ )
658668 scene = InteractiveScene (scene_cfg )
659669
660670 # Play the simulator
@@ -681,11 +691,17 @@ class MultiRobotSceneCfg(InteractiveSceneCfg):
681691 assert len (rf_shank_indices ) == 2 , f"Expected 2 RF_SHANK indices, got { rf_shank_indices } "
682692
683693 # Acquire ground truth body indices
694+ robot_base_body_idx = scene .articulations ["robot" ].find_bodies ("base" )[0 ][0 ]
695+ robot_1_base_body_idx = scene .articulations ["robot_1" ].find_bodies ("base" )[0 ][0 ]
684696 robot_lf_shank_body_idx = scene .articulations ["robot" ].find_bodies ("LF_SHANK" )[0 ][0 ]
685697 robot_1_lf_shank_body_idx = scene .articulations ["robot_1" ].find_bodies ("LF_SHANK" )[0 ][0 ]
686698 robot_rf_shank_body_idx = scene .articulations ["robot" ].find_bodies ("RF_SHANK" )[0 ][0 ]
687699 robot_1_rf_shank_body_idx = scene .articulations ["robot_1" ].find_bodies ("RF_SHANK" )[0 ][0 ]
688700
701+ # Determine expected source frame based on parameter
702+ expected_source_robot = "robot" if source_robot == "Robot" else "robot_1"
703+ expected_source_base_body_idx = robot_base_body_idx if source_robot == "Robot" else robot_1_base_body_idx
704+
689705 # Define simulation stepping
690706 sim_dt = sim .get_physics_dt ()
691707
@@ -721,15 +737,31 @@ class MultiRobotSceneCfg(InteractiveSceneCfg):
721737 scene .update (sim_dt )
722738
723739 # Get frame transformer data
724- target_pos_w = scene .sensors ["frame_transformer" ].data .target_pos_w
740+ frame_transformer_data = scene .sensors ["frame_transformer" ].data
741+ source_pos_w = frame_transformer_data .source_pos_w
742+ source_quat_w = frame_transformer_data .source_quat_w
743+ target_pos_w = frame_transformer_data .target_pos_w
725744
726- # Get ground truth positions
745+ # Get ground truth positions and orientations (after scene.update() so they're current)
727746 robot_lf_pos_w = scene .articulations ["robot" ].data .body_pos_w [:, robot_lf_shank_body_idx ]
728747 robot_1_lf_pos_w = scene .articulations ["robot_1" ].data .body_pos_w [:, robot_1_lf_shank_body_idx ]
729748 robot_rf_pos_w = scene .articulations ["robot" ].data .body_pos_w [:, robot_rf_shank_body_idx ]
730749 robot_1_rf_pos_w = scene .articulations ["robot_1" ].data .body_pos_w [:, robot_1_rf_shank_body_idx ]
731750
732- # TEST 1: Explicit named frames (LF_SHANK) should have DIFFERENT positions
751+ # Get expected source frame positions and orientations (after scene.update() so they're current)
752+ expected_source_base_pos_w = scene .articulations [expected_source_robot ].data .body_pos_w [
753+ :, expected_source_base_body_idx
754+ ]
755+ expected_source_base_quat_w = scene .articulations [expected_source_robot ].data .body_quat_w [
756+ :, expected_source_base_body_idx
757+ ]
758+
759+ # TEST 1: Verify source frame is correctly resolved
760+ # The source_pos_w should match the expected source robot's base world position
761+ torch .testing .assert_close (source_pos_w , expected_source_base_pos_w , rtol = 1e-5 , atol = 1e-5 )
762+ torch .testing .assert_close (source_quat_w , expected_source_base_quat_w , rtol = 1e-5 , atol = 1e-5 )
763+
764+ # TEST 2: Explicit named frames (LF_SHANK) should have DIFFERENT world positions
733765 lf_pos_difference = torch .norm (target_pos_w [:, robot_lf_idx ] - target_pos_w [:, robot_1_lf_idx ], dim = - 1 )
734766 assert torch .all (lf_pos_difference > 1.0 ), (
735767 f"Robot_LF_SHANK and Robot_1_LF_SHANK should have different positions (got diff={ lf_pos_difference } ). "
@@ -740,7 +772,7 @@ class MultiRobotSceneCfg(InteractiveSceneCfg):
740772 torch .testing .assert_close (target_pos_w [:, robot_lf_idx ], robot_lf_pos_w )
741773 torch .testing .assert_close (target_pos_w [:, robot_1_lf_idx ], robot_1_lf_pos_w )
742774
743- # TEST 2 : Implicit named frames (RF_SHANK) should also have DIFFERENT positions
775+ # TEST 3 : Implicit named frames (RF_SHANK) should also have DIFFERENT world positions
744776 # Even though they have the same frame name, internal body tracking uses full paths
745777 rf_pos_difference = torch .norm (
746778 target_pos_w [:, rf_shank_indices [0 ]] - target_pos_w [:, rf_shank_indices [1 ]], dim = - 1
0 commit comments