Lab 10: Localization (Simulation)

Goal: Implement grid localization using a Bayes Filter.

Prelab

First, I set up the simulation environment in the prelab following these directions.


Task 1: Square

Using the commander set_vel() and time.sleep() functions, I move the robot in a square. Below is a sample run.

Here are two separate runs that compare the ground truth and the odometry of the robot. We can see that they vary greatly.

Square run 1: ground truth vs odometry Square run 2: ground truth vs odometry

I set the duration of a velocity command to be 0.7 seconds. The ground truth is the same for every run, but odometry changes a lot. This is the motivation of the lab!


Task 2: Closed Loop Control

For this task, the robot drives forward until it is close to a wall, keeps rotating until it detects above a certain distance, drives forward, and repeats. I set the robot to turn for 0.7 seconds and its linear speed to 0.5 m/s so it does not move too fast. Here is a sample run.

To improve this closed loop control, I could set the distance threshold to stop and turn to be greater, which would prevent getting too close to the wall.


Localization

Localization is the process of determining where the robot is with respect to the environment. Odometry estimates the robot's position by tracking how much it has moved — but as we saw in the prelab, it is very unreliable.

For this lab, the robot state is 3-dimensional: (x, y, θ). We create a grid over the following continuous space:

[-1.6764, +1.9812) meters  /  [-5.5, 6.5) feet — x direction

[-1.3716, +1.3716) meters  /  [-4.5, +4.5) feet — y direction

[-180, +180) degrees — θ axis


Sensor Model

We use a Gaussian distribution to model sensor noise. This is a good fit for laser range finders like our ToF sensor.


Motion Model

At every time step, odometry data is recorded and described by three parameters:


Bayes Filter Algorithm

Every iteration of the Bayes Filter has two steps: a prediction step that incorporates input data (increasing uncertainty in belief), and an update step that incorporates sensor observations (reducing uncertainty).


Trajectory

Here is the sample trajectory run provided. As we can see, the odometry vs the ground truth was not great at all.


Implementation


compute_control()

Decomposes the change between two odometry poses into the three motion model parameters: rotation1, translation, and rotation2.

compute_control() Python
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)
    """
    dx = cur_pose[0] - prev_pose[0]
    dy = cur_pose[1] - prev_pose[1]

    delta_rot_1 = np.degrees(np.arctan2(dy, dx)) - prev_pose[2]
    delta_rot_2 = cur_pose[2] - prev_pose[2] - delta_rot_1
    delta_trans = np.sqrt(dx**2 + dy**2)

    delta_rot_1 = mapper.normalize_angle(delta_rot_1)
    delta_rot_2 = mapper.normalize_angle(delta_rot_2)

    return delta_rot_1, delta_trans, delta_rot_2

odom_motion_model()

Answers the question: "given that I executed control u, how likely is it that I end up at cur_pose if I started at prev_pose?" Returns the probability p(x'|x, u) by comparing the expected control to the actual control using Gaussian likelihoods.

odom_motion_model() Python
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 (rot1, trans, rot2) with
                                                   units (degrees, meters, degrees)

    Returns:
        prob [float]: Probability p(x'|x, u)
    """
    rot1, trans, rot2 = u
    rot1_hat, trans_hat, rot2_hat = compute_control(cur_pose, prev_pose)

    p_rot1  = loc.gaussian(mapper.normalize_angle(rot1_hat  - rot1),  0, loc.odom_rot_sigma)
    p_trans = loc.gaussian(trans_hat - trans, 0, loc.odom_trans_sigma)
    p_rot2  = loc.gaussian(mapper.normalize_angle(rot2_hat  - rot2),  0, loc.odom_rot_sigma)

    return p_rot1 * p_trans * p_rot2

prediction_step()

Implements the prediction step of the Bayes Filter. For every possible cell the robot could be in, contributions from all previous cells are summed, weighted by how likely each transition was. Cells with negligible belief (< 0.0001) are skipped for efficiency. The result is renormalized to maintain a valid probability distribution.

prediction_step() Python
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)
    loc.bel_bar = np.zeros_like(loc.bel_bar)

    for prev_x in range(mapper.MAX_CELLS_X):
        for prev_y in range(mapper.MAX_CELLS_Y):
            for prev_a in range(mapper.MAX_CELLS_A):
                if loc.bel[prev_x, prev_y, prev_a] < 0.0001:
                    continue
                for cur_x in range(mapper.MAX_CELLS_X):
                    for cur_y in range(mapper.MAX_CELLS_Y):
                        for cur_a in range(mapper.MAX_CELLS_A):
                            cur_pose  = mapper.from_map(cur_x,  cur_y,  cur_a)
                            prev_pose = mapper.from_map(prev_x, prev_y, prev_a)
                            loc.bel_bar[cur_x, cur_y, cur_a] += \
                                odom_motion_model(cur_pose, prev_pose, u) * loc.bel[prev_x, prev_y, prev_a]

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

sensor_model()

Returns the likelihood of a set of sensor readings given a specific pose. For each of the 18 observations per cell, it computes a Gaussian probability comparing the actual observation to the expected observation for that pose.

sensor_model() Python
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 = np.zeros(mapper.OBS_PER_CELL)
    obs_data   = loc.obs_range_data.flatten()
    for i in range(mapper.OBS_PER_CELL):
        prob_array[i] = loc.gaussian(obs_data[i], obs[i], loc.sensor_sigma)
    return prob_array

update_step()

Updates the belief by multiplying the predicted belief at each cell by the product of sensor likelihoods for that cell, then renormalizing.

update_step() Python
def update_step():
    """ Update step of the Bayes Filter.
    Update the probabilities in loc.bel based on loc.bel_bar and the sensor model.
    """
    for x in range(mapper.MAX_CELLS_X):
        for y in range(mapper.MAX_CELLS_Y):
            for a in range(mapper.MAX_CELLS_A):
                loc.bel[x, y, a] = np.prod(sensor_model(mapper.get_views(x, y, a))) * loc.bel_bar[x, y, a]
    loc.bel /= np.sum(loc.bel)

Running the Bayes Filter

After finishing the implementation, I ran the same trajectory as before, this time using the Bayes Filter to track the belief. Here is a video of the run.

Here is an image of the final position.

Final Bayes Filter run — belief vs ground truth

As we can see, the Bayes Filter does a good job of maintaining a belief that is close to the ground truth. It performs better when the robot is closer to walls, since sensor readings near obstacles are more reliable.


Collaboration

For this lab, I referenced Lucca's webpage for implementation guidance. During class, I got help with setting up the simulation environment, and consulted Claude with questions about optimizing the prediction and update steps with better Python. I also used Claude to style the website.