In this project, we'll be localizing a robot in a 2D grid world. The basis for simultaneous localization and mapping (SLAM) is to gather information from a robot's sensors and motions over time, and then use information about measurements and motion to re-construct a map of the world.
As you've learned, robot motion and sensors have some uncertainty associated with them. For example, imagine a car driving up hill and down hill; the speedometer reading will likely overestimate the speed of the car going up hill and underestimate the speed of the car going down hill because it cannot perfectly account for gravity. Similarly, we cannot perfectly predict the motion of a robot. A robot is likely to slightly overshoot or undershoot a target location.
In this notebook, we'll look at the
robot class that is partially given to you for the upcoming SLAM notebook. First, we'll create a robot and move it around a 2D grid world. Then, you'll be tasked with defining a
sense function for this robot that allows it to sense landmarks in a given world! It's important that you understand how this robot moves, senses, and how it keeps track of different landmarks that it sees in a 2D grid world, so that you can work with it's movement and sensor data.
Before we start analyzing robot motion, let's load in our resources and define the
robot class. You can see that this class initializes the robot's position and adds measures of uncertainty for motion. You'll also see a
sense() function which is not yet implemented, and you will learn more about that later in this notebook.
# import some resources import numpy as np import matplotlib.pyplot as plt import random %matplotlib inline
# the robot class class robot: # -------- # init: # creates a robot with the specified parameters and initializes # the location (self.x, self.y) to the center of the world # def __init__(self, world_size = 100.0, measurement_range = 30.0, motion_noise = 1.0, measurement_noise = 1.0): self.measurement_noise = 0.0 self.world_size = world_size self.measurement_range = measurement_range self.x = world_size / 2.0 self.y = world_size / 2.0 self.motion_noise = motion_noise self.measurement_noise = measurement_noise self.landmarks =  self.num_landmarks = 0 # returns a positive, random float # returns random value between [-1.0, 1.0) def rand(self): return random.random() * 2.0 - 1.0 # -------- # move: attempts to move robot by dx, dy. If outside world # boundary, then the move does nothing and instead returns failure # def move(self, dx, dy): x = self.x + dx + self.rand() * self.motion_noise y = self.y + dy + self.rand() * self.motion_noise if x < 0.0 or x > self.world_size or y < 0.0 or y > self.world_size: return False else: self.x = x self.y = y return True # -------- # sense: returns x- and y- distances to landmarks within visibility range # because not all landmarks may be in this range, the list of measurements # is of variable length. Set measurement_range to -1 if you want all # landmarks to be visible at all times # ## TODO: complete the sense function def sense(self): ''' This function does not take in any parameters, instead it references internal variables (such as self.landamrks) to measure the distance between the robot and any landmarks that the robot can see (that are within its measurement range). This function returns a list of landmark indices, and the measured distances (dx, dy) between the robot's position and said landmarks. This function should account for measurement_noise and measurement_range. One item in the returned list should be in the form: [landmark_index, dx, dy]. ''' measurements =  ## TODO: iterate through all of the landmarks in a world ## TODO: For each landmark ## 1. compute dx and dy, the distances between the robot and the landmark ## 2. account for measurement noise by *adding* a noise component to dx and dy ## - The noise component should be a random value between [-1.0, 1.0)*measurement_noise ## - Feel free to use the function self.rand() to help calculate this noise component ## - It may help to reference the `move` function for noise calculation ## 3. If either of the distances, dx or dy, fall outside of the internal var, measurement_range ## then we cannot record them; if they do fall in the range, then add them to the measurements list ## as list.append([index, dx, dy]), this format is important for data creation done later ## TODO: return the final, complete list of measurements for index, landmark in enumerate(self.landmarks): rx = self.x ry = self.y lx = landmark ly = landmark # 1. compute dx and dy, the distances between the robot and the landmark # The measurements are with respect to robot's position, # hence it's position has to be subtracted from the landmarks' position and not the other way around. dx = lx - rx dy = ly - ry # 2. account for measurement noise by *adding* a noise component to dx and dy # - The noise component should be a random value between [-1.0, 1.0)*measurement_noise # - Feel free to use the function self.rand() to help calculate this noise component # - It may help to reference the `move` function for noise calculation dx = dx + self.rand() * self.measurement_noise dy = dy + self.rand() * self.measurement_noise # 3. If either of the distances, dx or dy, fall outside of the internal var, measurement_range # then we cannot record them; if they do fall in the range, then add them to the measurements list # as list.append([index, dx, dy]), this format is important for data creation done later if abs(dx) > self.measurement_range or abs(dy) > self.measurement_range: continue measurements.append([index, dx, dy]) return measurements # -------- # make_landmarks: # make random landmarks located in the world # def make_landmarks(self, num_landmarks): self.landmarks =  for i in range(num_landmarks): self.landmarks.append([round(random.random() * self.world_size), round(random.random() * self.world_size)]) self.num_landmarks = num_landmarks # called when print(robot) is called; prints the robot's location def __repr__(self): return 'Robot: [x=%.5f y=%.5f]' % (self.x, self.y)
Define a world and a robot¶
Next, let's instantiate a robot object. As you can see in
__init__ above, the robot class takes in a number of parameters including a world size and some values that indicate the sensing and movement capabilities of the robot.
In the next example, we define a small 10x10 square world, a measurement range that is half that of the world and small values for motion and measurement noise. These values will typically be about 10 times larger, but we just want to demonstrate this behavior on a small scale. You are also free to change these values and note what happens as your robot moves!
world_size = 10.0 # size of world (square) measurement_range = 5.0 # range at which we can sense landmarks motion_noise = 0.2 # noise in robot motion measurement_noise = 0.2 # noise in the measurements # instantiate a robot, r r = robot(world_size, measurement_range, motion_noise, measurement_noise) # print out the location of r print(r)
Robot: [x=5.00000 y=5.00000]
Visualizing the World¶
In the given example, we can see/print out that the robot is in the middle of the 10x10 world at (x, y) = (5.0, 5.0), which is exactly what we expect!
However, it's kind of hard to imagine this robot in the center of a world, without visualizing the grid itself, and so in the next cell we provide a helper visualization function,
display_world, that will display a grid world in a plot and draw a red
o at the location of our robot,
r. The details of how this function works can be found in the
helpers.py file in the home directory; you do not have to change anything in this
# import helper function from helpers import display_world # define figure size plt.rcParams["figure.figsize"] = (5,5) # call display_world and display the robot in it's grid world print(r) display_world(int(world_size), [r.x, r.y])
Robot: [x=5.00000 y=5.00000]
Now you can really picture where the robot is in the world! Next, let's call the robot's
move function. We'll ask it to move some distance
(dx, dy) and we'll see that this motion is not perfect by the placement of our robot
o and by the printed out position of
Try changing the values of
dy and/or running this cell multiple times; see how the robot moves and how the uncertainty in robot motion accumulates over multiple movements.
dx = 1, does the robot move exactly one spot to the right? What about
dx = -1? What happens if you try to move the robot past the boundaries of the world?¶
# choose values of dx and dy (negative works, too) dx = -1 dy = -2 r.move(dx, dy) # print out the exact location print(r) # display the world after movement, not that this is the same call as before # the robot tracks its own movement display_world(int(world_size), [r.x, r.y])
Robot: [x=3.82654 y=3.10412]
Next, let's create landmarks, which are measurable features in the map. You can think of landmarks as things like notable buildings, or something smaller such as a tree, rock, or other feature.
The robot class has a function
make_landmarks which randomly generates locations for the number of specified landmarks. Try changing
num_landmarks or running this cell multiple times to see where these landmarks appear. We have to pass these locations as a third argument to the
display_world function and the list of landmark locations is accessed similar to how we find the robot position
Each landmark is displayed as a purple
x in the grid world, and we also print out the exact
[x, y] locations of these landmarks at the end of this cell.
# create any number of landmarks num_landmarks = 3 r.make_landmarks(num_landmarks) # print out our robot's exact location print(r) # display the world including these landmarks display_world(int(world_size), [r.x, r.y], r.landmarks) # print the locations of the landmarks print('Landmark locations [x,y]: ', r.landmarks)
Robot: [x=3.82654 y=3.10412]
Landmark locations [x,y]: [[3, 10], [1, 2], [1, 7]]
Once we have some landmarks to sense, we need to be able to tell our robot to try to sense how far they are away from it. It will be up to you to code the
sense function in our robot class.
sense function uses only internal class parameters and returns a list of the the measured/sensed x and y distances to the landmarks it senses within the specified
TODO: Implement the
##TODO's in the class code above to complete the
sense function for the robot class. Once you have tested out your code, please copy your complete
sense code to the
robot_class.py file in the home directory. By placing this complete code in the
robot_class Python file, we will be able to refernce this class in a later notebook.
The measurements have the format,
[i, dx, dy] where
i is the landmark index (0, 1, 2, ...) and
dy are the measured distance between the robot's location (x, y) and the landmark's location (x, y). This distance will not be perfect since our sense function has some associated
In the example in the following cell, we have given our robot a range of
5.0 so any landmarks that are within that range of our robot's location, should appear in a list of measurements. Not all landmarks are guaranteed to be in our visibility range, so this list will be variable in length.
Note: the robot's location is often called the pose or
[Pxi, Pyi] and the landmark locations are often written as
[Lxi, Lyi]. You'll see this notation in the next notebook.
# try to sense any surrounding landmarks measurements = r.sense() # this will print out an empty list if `sense` has not been implemented print(measurements)
[[1, -2.9000596767394335, -0.9178807796354672], [2, -2.9474328448361815, 3.828379182602073]]
Refer back to the grid map above. Do these measurements make sense to you? Are all the landmarks captured in this list (why/why not)?
Putting it all together¶
To perform SLAM, we'll collect a series of robot sensor measurements and motions, in that order, over a defined period of time. Then we'll use only this data to re-construct the map of the world with the robot and landmark locations. You can think of SLAM as peforming what we've done in this notebook, only backwards. Instead of defining a world and robot and creating movement and sensor data, it will be up to you to use movement and sensor measurements to reconstruct the world!
In the next notebook, you'll see this list of movements and measurements (which you'll use to re-construct the world) listed in a structure called
data. This is an array that holds sensor measurements and movements in a specific order, which will be useful to call upon when you have to extract this data and form constraint matrices and vectors.
data is constructed over a series of time steps as follows:
data =  # after a robot first senses, then moves (one time step) # that data is appended like so: data.append([measurements, [dx, dy]]) # for our example movement and measurement print(data)
# in this example, we have only created one time step (0) time_step = 0 # so you can access robot measurements: print('Measurements: ', data[time_step]) # and its motion for a given time step: print('Motion: ', data[time_step])
Final robot class¶
Before moving on to the last notebook in this series, please make sure that you have copied your final, completed
sense function into the
robot_class.py file in the home directory. We will be using this file in the final implementation of slam!