Fast Robots - Lab 10: Localization [sim]

Overview

In this lab, we implemented grid-based localization using a Bayes Filter and simulated odometry. The goal was to estimate the position of a robot moving through a known map based on noisy sensor readings and control inputs. We executed this on a discretized grid using a virtual robot simulator.


The Bayes Filter

The Bayes Filter provides a probabilistic framework for estimating a robot’s position over time using control inputs (odometry) and observations (sensor measurements). Like the Kalman Filter, the Bayes Filter maintains a belief distribution over all possible positions and continuously refines it through prediction and update steps.


Algorithm Implementation

compute_control()

To model robot motion, we decomposed movement between two poses into:

  1. A rotation toward the target position,
  2. A translation to that location,
  3. A final rotation to align with the desired orientation.

This function allows the filter to model motion in a structured, incremental way.

def compute_control(cur_pose, prev_pose):
    """ Given the current and previous odometry poses, this function extracts
    the control information based on the odometry motion model.

    Args:
        cur_pose  ([Pose]): Current Pose
        prev_pose ([Pose]): Previous Pose 

    Returns:
        [delta_rot_1]: Rotation 1  (degrees)
        [delta_trans]: Translation (meters)
        [delta_rot_2]: Rotation 2  (degrees)
    """
    x_cur = cur_pose[0]
    y_cur = cur_pose[1]
    ang_cur = cur_pose[2] * (math.pi / 180)

    x_prev = prev_pose[0]
    y_prev = prev_pose[1]
    ang_prev = prev_pose[2] * (math.pi / 180)
    
    r = math.sqrt((x_cur-x_prev)**2 + (y_cur-y_prev)**2)
    theta = math.atan2((x_cur-x_prev), (y_cur-y_prev))

    delta_rot_1 = (theta - ang_prev) * (180/math.pi)
    delta_trans = r
    delta_rot_2 = (ang_cur - ang_prev - delta_rot_1) * (180/math.pi)
    
    return delta_rot_1, delta_trans, delta_rot_2

odom_motion_model()

This function computes the probability of transitioning from prev_pose to cur_pose given a control input u, using Gaussian distributions around expected motion values.

Each sub-motion is assumed to be independent, and the product of their probabilities gives the total transition likelihood.

def odom_motion_model(cur_pose, prev_pose, u):
    """ Odometry Motion Model

    Args:
        cur_pose  ([Pose]): Current Pose
        prev_pose ([Pose]): Previous Pose
        (rot1, trans, rot2) (float, float, float): A tuple with control data in the format 
                                                   format (rot1, trans, rot2) with units (degrees, meters, degrees)

    Returns:
        prob [float]: Probability p(x'|x, u)
    """

    #odometry readings
    delta_rot_1, delta_trans, delta_rot_2 = compute_control(cur_pose, prev_pose)
    delta_rot_1 = mapper.normalize_angle(delta_rot_1)
    delta_rot_2 = mapper.normalize_angle(delta_rot_2)

    #ideal
    delta_rot_1_hat, delta_trans_hat, delta_rot_2_hat = u
    delta_rot_1_hat = mapper.normalize_angle(delta_rot_1_hat)
    delta_rot_2_hat = mapper.normalize_angle(delta_rot_2_hat)

    #compute probs
    p1 = loc.gaussian(delta_rot_1, delta_rot_1_hat, loc.odom_rot_sigma)
    p2 = loc.gaussian(delta_trans, delta_trans_hat, loc.odom_trans_sigma)
    p3 = loc.gaussian(delta_rot_2, delta_rot_2_hat, loc.odom_rot_sigma)

    prob = p1 * p2 * p3
    return prob

prediction_step()

We compute the prior belief bel_bar by looping through all potential past poses and computing the probability that the robot arrived at each new pose. For performance, we ignore states with negligible belief (e.g., < 0.0001).

This step models how motion spreads uncertainty in the belief space.

def prediction_step(cur_odom, prev_odom):
    """ Prediction step of the Bayes Filter.
    Update the probabilities in loc.bel_bar based on loc.bel from the previous time step and the odometry motion model.

    Args:
        cur_odom  ([Pose]): Current Pose
        prev_odom ([Pose]): Previous Pose
    """

    u = compute_control(cur_odom, prev_odom)

    cells_X, cells_Y, cells_A = mapper.MAX_CELLS_X, mapper.MAX_CELLS_Y, mapper.MAX_CELLS_A
    temp = np.zeros((cells_X, cells_Y, cells_A))

    for i in range(cells_X):
        for j in range(cells_Y):
            for k in range(cells_A):
                
                if(loc.bel[i,j,k] > .0001):
                
                    for a in range(cells_X):
                        for b in range(cells_Y):
                            for c in range(cells_A):

                                cur_pose = mapper.from_map(a,b,c)
                                prev_pose = mapper.from_map (i,j,k)
                                prob = odom_motion_model(cur_pose, prev_pose, u)
                                belief = loc.bel[i,j,k]
                                temp += prob * belief

    sum_val = np.sum(temp)
    loc.bel_bar = np.true_divide(temp, sum_val) ##normalized bel_bar

sensor_model()

The sensor model computes the likelihood of receiving the current sensor reading for each possible pose by comparing it to pre-cached ground truth sensor data.

def sensor_model(obs):
    """ This is the equivalent of p(z|x).

    Args:
        obs ([ndarray]): A 1D array consisting of the true observations for a specific robot pose in the map 

    Returns:
        [ndarray]: Returns a 1D array of size 18 (=loc.OBS_PER_CELL) with the likelihoods of each individual sensor measurement
    """
    prob_array = []
    
    for i in range(18):
        #create a gaussian distribuion for each meas
        gauss = loc.gaussian(loc.obs_range_data[i], obs[i], loc.sensor_sigma)
        prob_array.append(gauss)
    
    return prob_array

update_step()

In the update step, we combine the predicted belief with sensor likelihoods to compute a new, corrected belief distribution.

The final belief is normalized and used to localize the robot on the grid.

def update_step():
    """ Update step of the Bayes Filter.
    Update the probabilities in loc.bel based on loc.bel_bar and the sensor model.
    """

    cells_X, cells_Y, cells_A = mapper.MAX_CELLS_X, mapper.MAX_CELLS_Y, mapper.MAX_CELLS_A

    for i in range(cells_X):
        for j in range(cells_Y):
            for k in range(cells_A):
                p = sensor_model(mapper.get_views(i,j,k))
                prob = np.prod(p)
                loc.bel[i,j,k] = loc.bel_bar[i,j,k] * prob

    loc.bel = loc.bel / np.sum(loc.bel)

Simulation Results

Our simulation showed that even with inaccurate odometry (red), the Bayes Filter (blue) was able to rapidly converge to the true position (green) of the robot. Despite large errors early in the trajectory, the filter corrected itself with sensor updates and achieved high accuracy.

Notably:

  • Errors were highest at corners of the grid but recovered quickly.
  • Raw Odom (red) is absolutely useless alone.
  • The center of the map occasionally produced ambiguous results
  • The belief consistently reached a confidence of prob = 1.0 at the correct grid cell.

Stunt 1

Localization Simulation

mapping

mapping


Discussion

The Bayes Filter proved highly effective for this task. Compared to the Kalman Filter, it supports non-linear motion and non-Gaussian distributions, making it more general and robust. It was particularly impressive how well the filter performed despite significant odometry drift.