Lab Intro
In this lab, we will build off of what we saw in the previous lab and perform grid localization on the real robot using a Bayes filter update step. The robot executes a 360° in-place rotation at each of four marked positions in the map, collecting 18 ToF sensor readings at 20° increments. These readings are then fed into a provided Bayes filter implementation to estimate the robot's pose with the goal being to understand the gap between simulation and real-world localization performance.
Simulation Test
As a baseline, we first ran the provided lab11_sim.ipynb notebook to verify the Bayes filter implementation on the virtual robot. The result confirms what we observed in Lab 10: without filtering, the odometry estimate drifts significantly from the true position over time, while the Bayes filter belief stays close to the ground truth throughout the trajectory.
Sim results after comparing Bayesian Filter vs Odometry vs Ground Truth.
Implementation
The core task of this lab was implementing the perform_observation_loop() member function of the RealRobot class. This function is responsible for triggering the robot's 360° scan and returning the 18 ToF readings as a numpy column array in meters, which the Bayes filter then consumes directly.
On the Arduino side, we reused the COLLECT_MAP_DATA finite state machine developed in Lab 9, which accepts PID tuning parameters and step configuration without requiring a reflash which was convenient, so we could just send the command for 18 steps at 20° increments. To improve reading stability, the settling time between each turn and measurement was increased to 1000ms since each step size was now twice as large as what we had tested before. Overall however, the implementation remained exactly the same as before.
Robot performing a 360° observation loop at one of the marked positions.
On the Python side, perform_observation_loop() registers a BLE notification handler that parses incoming scan data, sends the COLLECT_MAP_DATA command, then waits asynchronously using await asyncio.sleep() until all 18 readings arrive. Because await requires an async context, the function is declared async, which in turn required adding async and await to get_observation_data() in localization.py. Rather than running the Bayes filter immediately after each scan, readings were first saved to CSV files so that data collection and processing could be done separately. This was practical given limited lab access time.
Implementation of perform_observation_loop().
Scan data saved to CSV immediately after collection for offline processing.
Saved CSV data injected into the Bayes filter update step and plotted against ground truth.
Results
The robot was placed at each of the four marked positions and the Bayes filter update step was run using the collected scan data. The belief (filter output) is plotted in blue and the ground truth in green. Errors are reported in meters.
Position (-3 ft, -2 ft)
Belief vs ground truth at (-3, -2) ft.
Printed belief and error values at (-3, -2) ft.
The filter localized to one grid cell east of the true position, producing an error of (0.3048, 0.0000) m, exactly one foot in the x direction.
Position (0 ft, 3 ft)
Belief vs ground truth at (0, 3) ft.
Printed belief and error values at (0, 3) ft.
The filter localized to one grid cell west of the true position, producing an error of (-0.3048, 0.0000) m, exactly one foot in the x direction.
Position (5 ft, 3 ft)
Belief vs ground truth at (5, 3) ft.
Printed belief and error values at (5, 3) ft.
The filter localized to one grid cell east of the true position, producing an error of (0.3048, 0.0000) m, exactly one foot in the x direction.
Position (5 ft, -3 ft)
Belief vs ground truth at (5, -3) ft.
Printed belief and error values at (5, -3) ft.
The filter localized exactly to the ground truth position, producing zero error. The belief and ground truth points overlap in the plot.
Discussion
Three out of four positions produced an error of exactly 0.3048m (one foot) in the x direction, while one position, (5, -3) ft, was localized perfectly with zero error. All errors were purely horizontal with no vertical component, which points to grid quantization as the primary source of error rather than sensor noise or scanning inaccuracy. The Bayes filter operates on a discrete grid where each cell is one foot wide, meaning the belief can only snap to cell centers. If the true position falls near a cell boundary, the filter may commit to the adjacent cell, producing an apparent error of exactly one grid cell width, which is precisely what we observe here. The believed heading angles across the four positions were approximately 10°, 30°, 10°, and 10° respectively, all reasonably close to the expected 0° given that the robot was placed facing the same direction at each position and the angular grid resolution introduces similar quantization effects.
The (5, -3) ft position localized best, likely because it sits in a geometrically distinctive corner of the map. It is close to the right and bottom walls with a long open diagonal across the map interior visible in certain scan directions. This combination of near and far readings gives the filter strong distinguishing information that maps unambiguously to that specific cell. The other positions, particularly the two upper corners, share more similar wall geometries with each other, making them harder to distinguish and more susceptible to committing to the wrong adjacent cell.
A possible source of error is that the robot does not rotate perfectly on its axis. Slight translational drift during the scan means each of the 18 readings is taken from a slightly different position, which effectively adds noise to the scan pattern and can shift the filter's peak belief by one cell in ambiguous cases.
Conclusion
This lab demonstrated that a Bayes filter update step alone, driven by 18 ToF readings from a 360° scan, is sufficient to localize a real robot to within one grid cell at all four tested positions. The results highlight a key real-world limitation of grid-based localization: position accuracy is fundamentally bounded by grid resolution, and errors will appear quantized even when the underlying sensor data is good. Despite the noisy and imperfect scanning behavior of the physical robot, the filter produced reliable and consistent results across all positions.