Skip to content

Commit a2602be

Browse files
committed
Merge pull request udacity#16 from jared-weed/machine-learning-branch
Add robot motion planning capstone project
2 parents 0d0187c + 8d9990a commit a2602be

File tree

8 files changed

+355
-1
lines changed

8 files changed

+355
-1
lines changed

projects/capstone/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
# Project 5: Capstone
1+
# Project 5: Capstone
2+
3+
The Capstone Project for the Machine Learning Engineer Nanodegree does not have any requirements for code, libraries, or datasets. You are free to choose your project as you wish! For students who are unable to construct a capstone project on their own imagination, a pre-built project has been provided in `robot_motion_planning`.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import numpy as np
2+
3+
class Maze(object):
4+
def __init__(self, filename):
5+
'''
6+
Maze objects have two main attributes:
7+
- dim: mazes should be square, with sides of even length. (integer)
8+
- walls: passages are coded as a 4-bit number, with a bit value taking
9+
0 if there is a wall and 1 if there is no wall. The 1s register
10+
corresponds with a square's top edge, 2s register the right edge,
11+
4s register the bottom edge, and 8s register the left edge. (numpy
12+
array)
13+
14+
The initialization function also performs some consistency checks for
15+
wall positioning.
16+
'''
17+
with open(filename, 'rb') as f_in:
18+
19+
# First line should be an integer with the maze dimensions
20+
self.dim = int(f_in.next())
21+
22+
# Subsequent lines describe the permissability of walls
23+
walls = []
24+
for line in f_in:
25+
walls.append(map(int,line.split(',')))
26+
self.walls = np.array(walls)
27+
28+
# Perform validation on maze
29+
# Maze dimensions
30+
if self.dim % 2:
31+
raise Exception('Maze dimensions must be even in length!')
32+
if self.walls.shape != (self.dim, self.dim):
33+
raise Exception('Maze shape does not match dimension attribute!')
34+
35+
# Wall permeability
36+
wall_errors = []
37+
# vertical walls
38+
for x in range(self.dim-1):
39+
for y in range(self.dim):
40+
if (self.walls[x,y] & 2 != 0) != (self.walls[x+1,y] & 8 != 0):
41+
wall_errors.append([(x,y), 'v'])
42+
# horizontal walls
43+
for y in range(self.dim-1):
44+
for x in range(self.dim):
45+
if (self.walls[x,y] & 1 != 0) != (self.walls[x,y+1] & 4 != 0):
46+
wall_errors.append([(x,y), 'h'])
47+
48+
if wall_errors:
49+
for cell, wall_type in wall_errors:
50+
if wall_type == 'v':
51+
cell2 = (cell[0]+1, cell[1])
52+
print 'Inconsistent vertical wall betweeen {} and {}'.format(cell, cell2)
53+
else:
54+
cell2 = (cell[0], cell[1]+1)
55+
print 'Inconsistent horizontal wall betweeen {} and {}'.format(cell, cell2)
56+
raise Exception('Consistency errors found in wall specifications!')
57+
58+
59+
def is_permissible(self, cell, direction):
60+
"""
61+
Returns a boolean designating whether or not a cell is passable in the
62+
given direction. Cell is input as a list. Directions may be
63+
input as single letter 'u', 'r', 'd', 'l', or complete words 'up',
64+
'right', 'down', 'left'.
65+
"""
66+
dir_int = {'u': 1, 'r': 2, 'd': 4, 'l': 8,
67+
'up': 1, 'right': 2, 'down': 4, 'left': 8}
68+
try:
69+
return (self.walls[tuple(cell)] & dir_int[direction] != 0)
70+
except:
71+
print 'Invalid direction provided!'
72+
73+
74+
def dist_to_wall(self, cell, direction):
75+
"""
76+
Returns a number designating the number of open cells to the nearest
77+
wall in the indicated direction. Cell is input as a list. Directions
78+
may be input as a single letter 'u', 'r', 'd', 'l', or complete words
79+
'up', 'right', 'down', 'left'.
80+
"""
81+
dir_move = {'u': [0, 1], 'r': [1, 0], 'd': [0, -1], 'l': [-1, 0],
82+
'up': [0, 1], 'right': [1, 0], 'down': [0, -1], 'left': [-1, 0]}
83+
84+
sensing = True
85+
distance = 0
86+
curr_cell = list(cell) # make copy to preserve original
87+
while sensing:
88+
if self.is_permissible(curr_cell, direction):
89+
distance += 1
90+
curr_cell[0] += dir_move[direction][0]
91+
curr_cell[1] += dir_move[direction][1]
92+
else:
93+
sensing = False
94+
return distance
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import numpy as np
2+
3+
class Robot(object):
4+
def __init__(self, maze_dim):
5+
'''
6+
Use the initialization function to set up attributes that your robot
7+
will use to learn and navigate the maze. Some initial attributes are
8+
provided based on common information, including the size of the maze
9+
the robot is placed in.
10+
'''
11+
12+
self.location = [0, 0]
13+
self.heading = 'up'
14+
self.maze_dim = maze_dim
15+
16+
def next_move(self, sensors):
17+
'''
18+
Use this function to determine the next move the robot should make,
19+
based on the input from the sensors after its previous move. Sensor
20+
inputs are a list of three distances from the robot's left, front, and
21+
right-facing sensors, in that order.
22+
23+
Outputs should be a tuple of two values. The first value indicates
24+
robot rotation (if any), as a number: 0 for no rotation, +90 for a
25+
90-degree rotation clockwise, and -90 for a 90-degree rotation
26+
counterclockwise. Other values will result in no rotation. The second
27+
value indicates robot movement, and the robot will attempt to move the
28+
number of indicated squares: a positive number indicates forwards
29+
movement, while a negative number indicates backwards movement. The
30+
robot may move a maximum of three units per turn. Any excess movement
31+
is ignored.
32+
33+
If the robot wants to end a run (e.g. during the first training run in
34+
the maze) then returing the tuple ('Reset', 'Reset') will indicate to
35+
the tester to end the run and return the robot to the start.
36+
'''
37+
38+
rotation = 0
39+
movement = 0
40+
41+
return rotation, movement
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from maze import Maze
2+
import turtle
3+
import sys
4+
5+
if __name__ == '__main__':
6+
'''
7+
This function uses Python's turtle library to draw a picture of the maze
8+
given as an argument when running the script.
9+
'''
10+
11+
# Create a maze based on input argument on command line.
12+
testmaze = Maze( str(sys.argv[1]) )
13+
14+
# Intialize the window and drawing turtle.
15+
window = turtle.Screen()
16+
wally = turtle.Turtle()
17+
wally.speed(0)
18+
wally.hideturtle()
19+
wally.penup()
20+
21+
# maze centered on (0,0), squares are 20 units in length.
22+
sq_size = 20
23+
origin = testmaze.dim * sq_size / -2
24+
25+
# iterate through squares one by one to decide where to draw walls
26+
for x in range(testmaze.dim):
27+
for y in range(testmaze.dim):
28+
if not testmaze.is_permissible([x,y], 'up'):
29+
wally.goto(origin + sq_size * x, origin + sq_size * (y+1))
30+
wally.setheading(0)
31+
wally.pendown()
32+
wally.forward(sq_size)
33+
wally.penup()
34+
35+
if not testmaze.is_permissible([x,y], 'right'):
36+
wally.goto(origin + sq_size * (x+1), origin + sq_size * y)
37+
wally.setheading(90)
38+
wally.pendown()
39+
wally.forward(sq_size)
40+
wally.penup()
41+
42+
# only check bottom wall if on lowest row
43+
if y == 0 and not testmaze.is_permissible([x,y], 'down'):
44+
wally.goto(origin + sq_size * x, origin)
45+
wally.setheading(0)
46+
wally.pendown()
47+
wally.forward(sq_size)
48+
wally.penup()
49+
50+
# only check left wall if on leftmost column
51+
if x == 0 and not testmaze.is_permissible([x,y], 'left'):
52+
wally.goto(origin, origin + sq_size * y)
53+
wally.setheading(90)
54+
wally.pendown()
55+
wally.forward(sq_size)
56+
wally.penup()
57+
58+
window.exitonclick()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
12
2+
1,5,7,5,5,5,7,5,7,5,5,6
3+
3,5,14,3,7,5,15,4,9,5,7,12
4+
11,6,10,10,9,7,13,6,3,5,13,4
5+
10,9,13,12,3,13,5,12,9,5,7,6
6+
9,5,6,3,15,5,5,7,7,4,10,10
7+
3,5,15,14,10,3,6,10,11,6,10,10
8+
9,7,12,11,12,9,14,9,14,11,13,14
9+
3,13,5,12,2,3,13,6,9,14,3,14
10+
11,4,1,7,15,13,7,13,6,9,14,10
11+
11,5,6,10,9,7,13,5,15,7,14,8
12+
11,5,12,10,2,9,5,6,10,8,9,6
13+
9,5,5,13,13,5,5,12,9,5,5,12
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
14
2+
1,5,5,7,7,5,5,6,3,6,3,5,5,6
3+
3,5,6,10,9,5,5,15,14,11,14,3,7,14
4+
11,6,11,14,1,7,6,10,10,10,11,12,8,10
5+
10,9,12,10,3,12,11,14,11,14,10,3,5,14
6+
11,5,6,8,11,7,12,8,10,9,12,9,7,12
7+
11,7,13,7,14,11,5,5,13,5,4,3,13,6
8+
8,9,5,14,9,12,3,7,6,3,6,11,6,10
9+
3,5,5,14,3,6,9,12,11,12,10,10,10,10
10+
10,3,5,13,14,10,3,5,13,7,14,8,9,14
11+
9,14,3,6,11,14,9,5,6,10,10,3,6,10
12+
3,13,14,11,14,11,4,3,13,15,13,14,10,10
13+
10,3,15,12,9,12,3,13,5,14,3,12,11,14
14+
11,12,11,7,5,6,10,1,5,15,13,7,12,10
15+
9,5,12,9,5,13,13,5,5,12,1,13,5,12
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
16
2+
1,5,5,6,3,7,5,5,5,5,7,5,5,5,5,6
3+
3,5,6,10,10,9,6,3,5,5,13,7,5,5,6,10
4+
11,6,11,15,15,5,14,8,2,3,5,13,5,6,10,10
5+
10,10,10,10,11,5,13,5,12,9,7,6,3,15,13,14
6+
10,10,10,9,12,3,5,6,3,6,10,11,14,11,6,10
7+
9,14,9,4,3,13,6,11,14,10,9,12,11,12,10,10
8+
1,13,6,3,14,3,15,12,9,15,6,3,13,7,12,10
9+
3,6,10,10,9,14,8,3,6,8,10,9,7,13,7,12
10+
10,10,10,10,3,13,7,13,12,3,14,3,13,7,13,6
11+
10,10,10,11,12,3,14,3,6,10,10,10,3,15,7,14
12+
10,9,12,9,7,14,11,14,10,8,10,10,10,10,10,10
13+
11,5,5,6,10,11,14,11,15,6,9,13,14,10,10,10
14+
11,7,6,10,9,14,9,14,10,10,3,7,15,14,10,10
15+
10,10,9,12,2,9,5,15,14,10,10,10,10,11,14,10
16+
10,11,5,5,12,3,5,12,10,11,13,12,10,10,9,14
17+
9,13,5,5,5,13,5,5,13,13,5,5,12,9,5,12
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from maze import Maze
2+
from robot import Robot
3+
import sys
4+
5+
# global dictionaries for robot movement and sensing
6+
dir_sensors = {'u': ['l', 'u', 'r'], 'r': ['u', 'r', 'd'],
7+
'd': ['r', 'd', 'l'], 'l': ['d', 'l', 'u'],
8+
'up': ['l', 'u', 'r'], 'right': ['u', 'r', 'd'],
9+
'down': ['r', 'd', 'l'], 'left': ['d', 'l', 'u']}
10+
dir_move = {'u': [0, 1], 'r': [1, 0], 'd': [0, -1], 'l': [-1, 0],
11+
'up': [0, 1], 'right': [1, 0], 'down': [0, -1], 'left': [-1, 0]}
12+
dir_reverse = {'u': 'd', 'r': 'l', 'd': 'u', 'l': 'r',
13+
'up': 'd', 'right': 'l', 'down': 'u', 'left': 'r'}
14+
15+
# test and score parameters
16+
max_time = 1000
17+
train_score_mult = 1/30.
18+
19+
if __name__ == '__main__':
20+
'''
21+
This script tests a robot based on the code in robot.py on a maze given
22+
as an argument when running the script.
23+
'''
24+
25+
# Create a maze based on input argument on command line.
26+
testmaze = Maze( str(sys.argv[1]) )
27+
28+
# Intitialize a robot; robot receives info about maze dimensions.
29+
testrobot = Robot(testmaze.dim)
30+
31+
# Record robot performance over two runs.
32+
runtimes = []
33+
total_time = 0
34+
for run in range(2):
35+
print "Starting run {}.".format(run)
36+
37+
# Set the robot in the start position. Note that robot position
38+
# parameters are independent of the robot itself.
39+
robot_pos = {'location': [0, 0], 'heading': 'up'}
40+
41+
run_active = True
42+
hit_goal = False
43+
while run_active:
44+
# check for end of time
45+
total_time += 1
46+
if total_time > max_time:
47+
run_active = False
48+
print "Allotted time exceeded."
49+
break
50+
51+
# provide robot with sensor information, get actions
52+
sensing = [testmaze.dist_to_wall(robot_pos['location'], heading)
53+
for heading in dir_sensors[robot_pos['heading']]]
54+
rotation, movement = testrobot.next_move(sensing)
55+
56+
# check for a reset
57+
if (rotation, movement) == ('Reset', 'Reset'):
58+
if run == 0 and hit_goal:
59+
run_active = False
60+
runtimes.append(total_time)
61+
print "Ending first run. Starting next run."
62+
break
63+
elif run == 0 and not hit_goal:
64+
print "Cannot reset - robot has not hit goal yet."
65+
continue
66+
else:
67+
print "Cannot reset on runs after the first."
68+
continue
69+
70+
# perform rotation
71+
if rotation == -90:
72+
robot_pos['heading'] = dir_sensors[robot_pos['heading']][0]
73+
elif rotation == 90:
74+
robot_pos['heading'] = dir_sensors[robot_pos['heading']][2]
75+
elif rotation == 0:
76+
pass
77+
else:
78+
print "Invalid rotation value, no rotation performed."
79+
80+
# perform movement
81+
if abs(movement) > 3:
82+
print "Movement limited to three squares in a turn."
83+
movement = max(min(int(movement), 3), -3) # fix to range [-3, 3]
84+
while movement:
85+
if movement > 0:
86+
if testmaze.is_permissible(robot_pos['location'], robot_pos['heading']):
87+
robot_pos['location'][0] += dir_move[robot_pos['heading']][0]
88+
robot_pos['location'][1] += dir_move[robot_pos['heading']][1]
89+
movement -= 1
90+
else:
91+
print "Movement stopped by wall."
92+
movement = 0
93+
else:
94+
rev_heading = dir_reverse[robot_pos['heading']]
95+
if testmaze.is_permissible(robot_pos['location'], rev_heading):
96+
robot_pos['location'][0] += dir_move[rev_heading][0]
97+
robot_pos['location'][1] += dir_move[rev_heading][1]
98+
movement += 1
99+
else:
100+
print "Movement stopped by wall."
101+
movement = 0
102+
103+
# check for goal entered
104+
goal_bounds = [testmaze.dim/2 - 1, testmaze.dim/2]
105+
if robot_pos['location'][0] in goal_bounds and robot_pos['location'][1] in goal_bounds:
106+
hit_goal = True
107+
if run != 0:
108+
runtimes.append(total_time - sum(runtimes))
109+
run_active = False
110+
print "Goal found; run {} completed!".format(run)
111+
112+
# Report score if robot is successful.
113+
if len(runtimes) == 2:
114+
print "Task complete! Score: {:4.3f}".format(runtimes[1] + train_score_mult*runtimes[0])

0 commit comments

Comments
 (0)