@@ -583,154 +583,6 @@ def test_05_no_axis_swap(self):
583583 self .assertLess (x_change_from_y_move , 0.15 ,
584584 msg = f"X changed by { x_change_from_y_move } when only Y moved - AXIS SWAP BUG!" )
585585
586- def test_06_marker_data_shifts_with_robot_movement (self ):
587- """
588- Test that actual elevation data shifts correctly when robot moves.
589-
590- This test:
591- 1. Places robot at origin
592- 2. Injects a marker pointcloud at world position (1.0, 0.0, 0.5)
593- 3. Verifies elevation data appears in the grid
594- 4. Moves robot +0.5m in X
595- 5. Verifies the marker data shifted correctly in the grid
596-
597- This tests the actual cp.roll shifting, not just the pose tracking.
598- """
599- self ._require_dds ()
600-
601- resolution = 0.1 # From test config
602-
603- # Position robot at origin
604- for _ in range (30 ):
605- self .fixture .publish_tf (0.0 , 0.0 , 0.0 )
606- self .spin_for (0.1 )
607-
608- self ._prime_map_with_pointcloud (repeats = 5 )
609-
610- self .wait_for_gridmap_with_spin (timeout = 10.0 )
611- center_before_marker = self .fixture .get_gridmap_center ()
612- self .assertIsNotNone (center_before_marker , "No GridMap received before marker injection" )
613-
614- # Inject marker pointcloud at (1.0, 0.0, 0.5) in world frame
615- # This is 1m ahead of robot in X direction
616- marker_x = center_before_marker [0 ] + 1.0
617- marker_y = center_before_marker [1 ]
618- marker_z = 0.5
619- for _ in range (30 ):
620- self .fixture .publish_marker_pointcloud (marker_x , marker_y , marker_z )
621- self .spin_for (0.2 )
622-
623- # Wait for map to be updated
624- self .wait_for_gridmap_with_spin (timeout = 10.0 )
625-
626- # Get elevation data and find the marker
627- gridmap = self .fixture .last_gridmap
628- self .assertIsNotNone (gridmap , "No GridMap received" )
629-
630- elevation_data = self ._extract_elevation_layer (gridmap )
631- self .assertIsNotNone (elevation_data , "Could not extract elevation layer" )
632- self .fixture .get_logger ().info (
633- f"Elevation stats before move: min={ np .nanmin (elevation_data ):.4f} , "
634- f"max={ np .nanmax (elevation_data ):.4f} , nan_fraction={ np .isnan (elevation_data ).mean ():.3f} "
635- )
636-
637- # Find cells with non-zero elevation (the marker)
638- initial_marker_cells = self ._find_nonzero_cells (elevation_data , threshold = 0.01 )
639- if len (initial_marker_cells ) == 0 :
640- self .skipTest (
641- "Marker not registered in map - pointcloud may not be reaching node in this environment."
642- )
643-
644- # Record initial marker position in grid
645- initial_marker_center = np .mean (initial_marker_cells , axis = 0 )
646- self .fixture .get_logger ().info (f"Initial marker at grid cells: { initial_marker_cells } " )
647-
648- # Move robot +0.5m in X direction (relative to current center)
649- target_x = center_before_marker [0 ] + 0.5
650- target_y = center_before_marker [1 ]
651- for _ in range (30 ):
652- self .fixture .publish_tf (target_x , target_y , 0.0 )
653- self .spin_for (0.1 )
654-
655- self ._prime_map_with_pointcloud (repeats = 5 , tf_x = target_x , tf_y = target_y )
656-
657- # Wait for map to update
658- self .wait_for_gridmap_with_spin (timeout = 10.0 )
659-
660- # Get new elevation data
661- gridmap = self .fixture .last_gridmap
662- new_elevation_data = self ._extract_elevation_layer (gridmap )
663- self .assertIsNotNone (new_elevation_data ,
664- "Could not extract elevation layer after movement" )
665- self .fixture .get_logger ().info (
666- f"Elevation stats after move: min={ np .nanmin (new_elevation_data ):.4f} , "
667- f"max={ np .nanmax (new_elevation_data ):.4f} , nan_fraction={ np .isnan (new_elevation_data ).mean ():.3f} "
668- )
669-
670- # Find marker in new position
671- new_marker_cells = self ._find_nonzero_cells (new_elevation_data , threshold = 0.01 )
672- self .assertGreater (len (new_marker_cells ), 0 ,
673- "Marker data lost after robot movement" )
674-
675- new_marker_center = np .mean (new_marker_cells , axis = 0 )
676- self .fixture .get_logger ().info (f"New marker at grid cells: { new_marker_cells } " )
677-
678- # The marker should have shifted in the grid
679- # Robot moved +0.5m in X, so the marker (at fixed world position) should
680- # appear to shift BACKWARD relative to the robot-centered grid.
681- #
682- # In grid coordinates (row=Y, col=X):
683- # - X movement → column shift (shift[1] in row-major indexing)
684- # - Y movement → row shift (shift[0] in row-major indexing)
685- #
686- # With the axis-swap bug, X robot movement would incorrectly cause row shift.
687- shift = new_marker_center - initial_marker_center
688- self .fixture .get_logger ().info (f"Grid shift: { shift } (row, col)" )
689-
690- expected_shift_cells = int (0.5 / resolution ) # 5 cells
691-
692- # CRITICAL: Check DIRECTION, not just magnitude
693- # Robot moved +X, so marker should shift in column direction (shift[1]),
694- # NOT in row direction (shift[0])
695- col_shift = abs (shift [1 ]) # Should be ~5 cells
696- row_shift = abs (shift [0 ]) # Should be ~0 cells
697-
698- self .assertGreater (col_shift , expected_shift_cells * 0.5 ,
699- f"Marker should shift ~{ expected_shift_cells } cells in column (X) direction, "
700- f"got col_shift={ col_shift } " )
701- self .assertLess (row_shift , expected_shift_cells * 0.3 ,
702- f"Marker should NOT shift in row (Y) direction for X-only robot movement, "
703- f"got row_shift={ row_shift } - AXIS SWAP BUG!" )
704-
705- def _extract_elevation_layer (self , gridmap : GridMap ) -> np .ndarray :
706- """Extract the elevation layer from a GridMap message."""
707- try :
708- if 'elevation' not in gridmap .layers :
709- return None
710- idx = gridmap .layers .index ('elevation' )
711- data_msg = gridmap .data [idx ]
712-
713- # Extract dimensions from layout
714- if len (data_msg .layout .dim ) >= 2 :
715- cols = data_msg .layout .dim [0 ].size
716- rows = data_msg .layout .dim [1 ].size
717- else :
718- # Assume square
719- size = int (np .sqrt (len (data_msg .data )))
720- rows , cols = size , size
721-
722- data = np .array (data_msg .data , dtype = np .float32 ).reshape (rows , cols )
723- return data
724- except Exception as e :
725- self .fixture .get_logger ().warning (f"Failed to extract elevation: { e } " )
726- return None
727-
728- def _find_nonzero_cells (self , data : np .ndarray , threshold : float = 0.1 ) -> np .ndarray :
729- """Find grid cells with values above threshold (non-empty cells)."""
730- # Filter out NaN and find cells with elevation
731- valid_mask = ~ np .isnan (data ) & (np .abs (data ) > threshold )
732- indices = np .argwhere (valid_mask )
733- return indices
734586
735587
736588# ============================================================================
0 commit comments