
A* ALGORITHM DEVELOPMENT
OVERVIEW
A* is a type of search algorithm that is commonly used for robotic path planning. As a final project for our Robotic Manipulation class for the MSR Program, we were supposed to implement this algorithm in any language of our choice (I chose Python) and demonstrate it with given robot radius, starting coordinates, obstacle coordinates, and goal coordinates during our final exam time.
The final algorithm takes into account the above specifications as well as the coordinates for potential nodes, the radii of the obstacles, and the radii of the boundary.
CODE
import numpy as np
import matplotlib.pyplot as plt
import time
import math
import itertools
import random
def collision_check(robot_rad,nodes_coord,obs_radii,obs_coord,region_length,region_width):
#Using this function, check to see whether the nodes are within obstacles, or too close to obstacles for the robot to actually go to; this includes walls near the location of the nodes
b= nodes_coord
a = []
#Here, we need to take into account the planar region within which the robot is allowed to travel
x_region_min = 0
y_region_min = 0
x_region_max = region_width
y_region_max = region_length
for i in range(len(nodes_coord)):
#Find the coordinates of the node
xcoordn = nodes_coord[i][0]
ycoordn = nodes_coord[i][1]
for j in range(len(obs_radii)):
#Create the minimum allowable distance between the center of the obstacle and the center of the robot such that the robot can actually travel past it
clearance = robot_rad+obs_radii[j]
#Find the center of the obstacle
x_obscent = obs_coord[j][0]
y_obscent = obs_coord[j][1]
#Find the distance between the obstacle center and the node center
dist = ((xcoordn-x_obscent)**2 + (ycoordn-y_obscent)**2)**0.5
if dist < clearance:
#If the distance between the node and the obstacle is less than the minimum allowable area the robot can travel between, then it is allowed to passs
a.append(i)
break
for k in reversed(range(len(nodes_coord))):
xcoordn = nodes_coord[k][0]
ycoordn = nodes_coord[k][1]
#Check here to see if the robot can travel to a node that is close to the wall
x_min_region_check = xcoordn - robot_rad
y_min_region_check = ycoordn - robot_rad
x_max_region_check = xcoordn + robot_rad
y_max_region_check = ycoordn + robot_rad
if x_min_region_check < x_region_min or x_max_region_check > x_region_max or y_min_region_check < y_region_min or y_max_region_check > y_region_max:
# print "if statement pop"
# print "pre pop b", b
b.pop(k)
# print "b", b
# nodes_coord.pop(k)
# print "nodes_coord", nodes_coord
elif k in a:
b.pop(k)
# nodes_coord.pop(k)
# print "b", b
return b
def edges(robot_rad,nodes_coord,obs_radii,obs_coord):
#Here, we are finding all of the possible paths between nodes
obs_number = len(obs_radii)
collision = []
edgelist = []
l = -1
for i in itertools.permutations(nodes_coord,2):
#Here, we are finding all of the different combinations of the nodes, taking into account the proximity with which the robot will be traveling next to obstacles on the way to that node
xcoordn1 = i[0][0]
xcoordn2 = i[1][0]
ycoordn1 = i[0][1]
ycoordn2 = i[1][1]
l = l+1
#Add every node combination to the edgelist as we go, then we can pop them out at the end
edgelist.append([[xcoordn1,ycoordn1],[xcoordn2,ycoordn2]])
for j in range(obs_number):
#Find the x and y coordinates of the center of the obstacle
x_obscent = obs_coord[j][0]
y_obscent = obs_coord[j][1]
#Need to check whether the line between the two nodes is a horizontal or verticle line, so that can be taken into account for the slope
if ycoordn1==ycoordn2:
xint = x_obscent
yint = ycoordn1
elif xcoordn1==xcoordn2:
yint = y_obscent
xint = xcoordn1
else:
#Find the equation of the line between the two nodes and the equation of the perpendicular line that goes through the center of the obstacle and intersects with the line between the nodes; find this point of intersection
m = (ycoordn2-ycoordn1)/((xcoordn2-xcoordn1)*1.0)
b1 = ycoordn1 - m*xcoordn1
#The slope of the intersecting line is negative 1 divided by the slope of the of the line that it's intersecting
b2 = y_obscent + (1/m)*x_obscent
xint = (b2-b1)/(m+(1/m))
yint = m*xint + b1
#Calculate the minimum distance the robot can travel between obstacles (the clearance)
clearance = robot_rad+obs_radii[j]
dist_ro = ((xint-x_obscent)**2 + (yint-y_obscent)**2)**0.5
#Checking the distance between the intersection point and each of the nodes to check whether the obstacle is sitting outside of the range of the two nodes or not
dist1 = ((xint-xcoordn1)**2 + (yint-ycoordn1)**2)**0.5
dist2 = ((xint-xcoordn2)**2 + (yint-ycoordn2)**2)**0.5
distn = ((xcoordn1-xcoordn2)**2 + (ycoordn1-ycoordn2)**2)**0.5
#Finding collisions when the distance that the robot has to travel is actually less than the allowable range that the robot has to travel between
if dist_ro < clearance and dist1 < distn and dist2 < distn:
collision.append(l)
# print "COLLISION o_O with:",obs_coord[j],"between:",i
for k in reversed(range(len(edgelist))):
if k in collision:
#Taking the node combinations between which there is a collision out since the robot can't actually travel between them- this will give us our paths possible
edgelist.pop(k)
return edgelist
def create_graph(paths_possible,nodes_coord):
#Creating a graph from the possible paths and the node coordinates... this was back calculated since I started with making the astar for question two.
nodes_to = []
nodes_to_ind = []
nodes_from = []
nodes_from_ind = []
dist= []
nodes_total = len(nodes_coord)
for i in range(len(paths_possible)):
#Create an array that has the nodes that we're leaving from and the ones that we're going to
nodes_to.append(paths_possible[i][0])
nodes_from.append(paths_possible[i][1])
for j in range(len(nodes_to)):
#We need to know which node numbers the paths possible elements correspond to in order to create teh graph
nodes_to_ind.append(nodes_coord.index(nodes_to[j]))
nodes_from_ind.append(nodes_coord.index(nodes_from[j]))
for k in range(len(nodes_to)):
#Need to calculate the distane between them
node1x = nodes_to[k][0]
node1y = nodes_to[k][1]
node2x = nodes_from[k][0]
node2y = nodes_from[k][1]
distn = (((node2x-node1x)**2 + (node2y-node1y)**2))**0.5
dist.append(distn)
#Fill a graph with zeros and add in distance values only where there is an edge between the two nodes
graph = [[0 for col in range(nodes_total)] for row in range(nodes_total)]
for p in range(len(nodes_to_ind)):
graph[nodes_to_ind[p]][nodes_from_ind[p]] = dist[p]
# print "graph: ", graph
return graph
def create_graph(paths_possible,nodes_coord):
#Creating a graph from the possible paths and the node coordinates... this was back calculated since I started with making the astar for question two.
nodes_to = []
nodes_to_ind = []
nodes_from = []
nodes_from_ind = []
dist= []
nodes_total = len(nodes_coord)
for i in range(len(paths_possible)):
#Create an array that has the nodes that we're leaving from and the ones that we're going to
nodes_to.append(paths_possible[i][0])
nodes_from.append(paths_possible[i][1])
for j in range(len(nodes_to)):
#We need to know which node numbers the paths possible elements correspond to in order to create teh graph
nodes_to_ind.append(nodes_coord.index(nodes_to[j]))
nodes_from_ind.append(nodes_coord.index(nodes_from[j]))
for k in range(len(nodes_to)):
#Need to calculate the distane between them
node1x = nodes_to[k][0]
node1y = nodes_to[k][1]
node2x = nodes_from[k][0]
node2y = nodes_from[k][1]
distn = (((node2x-node1x)**2 + (node2y-node1y)**2))**0.5
dist.append(distn)
#Fill a graph with zeros and add in distance values only where there is an edge between the two nodes
graph = [[0 for col in range(nodes_total)] for row in range(nodes_total)]
for p in range(len(nodes_to_ind)):
graph[nodes_to_ind[p]][nodes_from_ind[p]] = dist[p]
# print "graph: ", graph
return graph
def heuristic_cost(neighbor_node,goal_node):
#An optimistic estimate of the cost that would be incurred if the the robot travelled from the current node to it's neighbor node is the straight line distance between the neighbor node and the goal
xcoordn = neighbor_node[0]
ycoordn = neighbor_node[1]
xcoord_goal = goal_node[0]
ycoord_goal = goal_node[1]
h_cost = ((xcoordn-xcoord_goal)**2 + (ycoordn-ycoord_goal)**2)**0.5
return h_cost
def neighbor_cost(neighbor_node, current_node):
#The cost of travelling to the neighbor node from the current is the distance between the two
xcoordcn = current_node[0]
ycoordcn = current_node[1]
xcoord_neigh = neighbor_node[0]
ycoord_neigh = neighbor_node[1]
cost = (((xcoordcn-xcoord_neigh)**2 + (ycoordcn-ycoord_neigh)**2))**0.5
return cost
def neighbor_nodes(nodes_coord,paths_possible):
#This finds the neighbors possible for all of the nodes
nodes_from = []
nodes_to = []
neighbor_node = []
neighbors = []
for i in range(len(paths_possible)):
#Since paths possible contains a pair of nodes, need to take one column of that and make it into the nodes we're travelling from and the other into the nodes we're travelling to
nodes_from.append(paths_possible[i][0])
nodes_to.append(paths_possible[i][1])
for j in range(len(nodes_coord)):
#Find all of the parents for the nodes
parent = nodes_coord[j]
neighbor_temp = []
for k in range(len(nodes_from)):
if parent == nodes_to[k]:
neighbor_temp.append(nodes_from[k])
neighbors.append(neighbor_temp)
return neighbors
def neigh_from_graph(nodes_coord,graph):
#This finds the neighbors possible for all of the nodes if given a graph to input- use with Astar1
neighbor_node = []
for j in range(len(node_coord)):
neighbor_temp = []
for k in range(len(node_coord)):
if graph[j][k] != 0:
neighbor_temp.append(node_coord[k])
neighbors.append(neighbor_temp)
return neighbors
def new_list_open(paths_possible, nodes_coord, current, list_open, list_closed):
#This function finds the possible neighbors of the current node
##First use the neighbors function (above) to find all of the possible neihbors
neighbors = neighbor_nodes(nodes_coord,paths_possible)
##Find the index the current node in the nodes_coord array
current_ind = nodes_coord.index(current)
for i in range(len(neighbors[current_ind])):
##Check if the neighbors of the current node are in the closed nodes (already explored) list and, if not, add it to the list of open neighbors
if neighbors[current_ind][i] not in list_closed and neighbors[current_ind][i] not in list_open:
list_open.append(neighbors[current_ind][i])
return list_open
def new_list_open_graph(list_open,list_closed,neighbors):
#This function finds the possible neighbors of the current node
num_neighbors = len(neighbors)
for i in range(num_neighbors):
if neighbors[i] not in list_open and neighbors[i] not in list_closed:
list_open.append(neighbors[i])
return list_open
def sorting_open(past_cost,goal,list_open,nodes_coord):
est_total_cost = []
#Sorting the possible neighbors of the current node by the lowest cost
for i in range(len(list_open)):
node_index = nodes_coord.index(list_open[i])
est_total_cost.append((past_cost[node_index] +heuristic_cost(list_open[i],goal),list_open[i]))
#Sorting the tuple, which contains the open node and the heuristic cost of that node; sorting by the heuristic cost
cost_sorted = sorted(est_total_cost,key = lambda cost:cost[0])
temp_list =[]
for i in range(len(list_open)):
temp_list.append(cost_sorted[i][1])
return temp_list
def create_inputs(nodes_number, obstacles_number):
#the A* function takes as input: (robot_rad,obs_radii,obs_coord,nodes_coord,start,goal,region_length,region_width) so need to create those randomly.
#Initialize arrays:
obs_coord = []
obs_radii = []
nodes_coord = []
#Region inputs:
##Length:
region_length = round(100*random.random(),2)
##Width:
region_width = round(100*random.random(),2)
#Robot radius:
robot_rad = round(2*random.random(),2)
#Obstacles:
##Coordinates:
for i in range(obstacles_number):
obs_coord.append([round(100*random.random(),2),round(100*random.random(),2)])
##Radii:
for i in range(obstacles_number):
obs_radii.append(round(10*random.random(),2))
##Nodes Coordinates:
for i in range(nodes_number):
nodes_coord.append([round(100*random.random(),2),round(100*random.random(),2)])
return robot_rad, obs_radii, obs_coord, nodes_coord, region_length, region_width
def plot(obs_radii,obs_coord,nodes_coord,nodes_coord_unaltered,path,start,goal,paths_possible):
num_obs = len(obs_radii)
num_nodes = len(nodes_coord)
num_old_nodes = len(nodes_coord_unaltered)
##Finding the coordinates of the start and goal
xstart = start[0]
ystart = start[1]
xgoal = goal[0]
ygoal = goal[1]
#Plot the start and goal
plt.plot(xstart,ystart,'rs')
plt.plot(xgoal,ygoal,'rs')
#Plot the obstacles:
for i in range(num_obs):
obs = plt.Circle((obs_coord[i][0],obs_coord[i][1]), radius = obs_radii[i], fc = 'g')
o = plt.axes()
o.add_patch(obs)
#Plot the nodes:
##There are two sets of nodes that need to be plotted- the first are the nodes that are in collision of sorts with the obstacles or the wall of the planar region that the robot is exploring
###Available nodes:
x = []
y = []
for i in range(num_nodes-2):
x.append(nodes_coord[i+1][0])
y.append(nodes_coord[i+1][1])
plt.plot(x,y,'rd')
###Old nodes:
xold = []
yold = []
for i in range(num_old_nodes-2):
xold.append(nodes_coord_unaltered[i+1][0])
yold.append(nodes_coord_unaltered[i+1][1])
plt.plot(xold,yold,'b^')
##Plot Edges:
nodes_to = []
nodes_from = []
for i in range(len(paths_possible)):
nodes_to.append(paths_possible[i][0])
nodes_from.append(paths_possible[i][1])
for k in range(len(nodes_to)):
node1x = nodes_to[k][0]
node1y = nodes_to[k][1]
node2x = nodes_from[k][0]
node2y = nodes_from[k][1]
plt.plot([node1x,node2x],[node1y,node2y],'c--')
##Plotting path:
for i in range(len(path)-1):
x_path1 = path[i][0]
y_path1 = path[i][1]
x_path2 = path[i+1][0]
y_path2 = path[i+1][1]
plt.plot([x_path1,x_path2],[y_path1,y_path2],'k')
##Normalizing the axes:
plt.axis('equal')
plt.axis([0,110,0,110])
plt.show()
def a_star1(graph,nodes_coord,start,goal):
#This is just purely the a star algorithm; takes as input a graph, the node coordinates, the start, and the goal
print 'in the program!!!!! Lets look at the graph to see if the program worked- hopefully it will have :)'
print "Robot Radius: ", robot_rad
nodes_coord_unaltered = nodes_coord[:]
num_nodes = len(nodes_coord)
path = []
##Find all of the possible neighbors between the coordinates using the paths possible list
all_nbrs = neigh_from_graph(nodes_coord,graph)
if start not in nodes_coord:
print "No solution. The start conflicts with an obstacle O_o"
plot(obs_radii,obs_coord,nodes_coord,nodes_coord_unaltered,path,start,goal,paths_possible)
return
elif goal not in nodes_coord:
print "No solution. The goal conflicts with an obstacle o_O"
plot(obs_radii,obs_coord,nodes_coord,nodes_coord_unaltered,path,start,goal,paths_possible)
return
#Now for the meat of A*:
##Initialize lists
###Place start as the first element in list open since that is the one that we are currently at- will update this with the neighbor list later
list_open = [start]
list_closed = []
#The past cost of everything but the "start" node is infinity- the cost associated with start is 0 since we are already there
past_cost = float('inf')*np.ones(num_nodes)
past_cost[0] = 0
#Need to create a parent array that is the length of the number of nodes and has the start as its first parent since we are beginning there
parent = [0]*num_nodes
parent[0] = start
while len(list_open) != 0:
current = list_open.pop(0)
list_closed.append(current)
current_ind = nodes_coord.index(current)
current_nbrs = all_nbrs[current_ind]
list_open = new_list_open_graph(paths_possible, nodes_coord, current, list_open, list_closed)
#We want to check if the current node is equal to the goal node, to see if we're done or not
if current == goal:
print "Path successfully found!"
#Need to set the path to goal initially since we will keep inserting a parent element into the first index space of the path array
path = [goal]
node_index = nodes_coord.index(goal)
while path[0] != start:
#Need to take all of the nodes that we travelled to and liked from the parents index to the path
path.insert(0,parent[node_index])
node_index = nodes_coord.index(path[0])
print path
#Once path is obtained, need to plot the successful path :)
plot(obs_radii,obs_coord,nodes_coord,nodes_coord_unaltered,path,start,goal,paths_possible)
return
for neighbor in current_nbrs:
#Find the index of the current neighbor so we can find the cost from going from the current node to the neighbor node
neighbor_ind = nodes_coord.index(neighbor)
tentative_cost = past_cost[current_ind] + graph[current_ind][neighbor_ind]
if tentative_cost < past_cost[neighbor_ind]:
#If the tentative cost is below the past cost, then we should travel to that node; otherwise, it is not a good option
past_cost[neighbor_ind] = tentative_cost
parent[neighbor_ind] = current
#We need to get a new open list for the neighbor node once we travel to it
list_open = sorting_open(past_cost,goal,list_open,nodes_coord)
if len(path) == 0:
print "No solution. There are no possible paths to the goal."
plot(obs_radii,obs_coord,nodes_coord,nodes_coord_unaltered,path,start,goal,paths_possible)
def a_star2(robot_rad,obs_radii,obs_coord,nodes_coord,start,goal,region_length,region_width):
#This is the a star algorithm, with some additions; it takes the robot radius, obstacle coordinates/radii, as well as the node coordinates, start, goal, and region length and width as an input and checks for collisions of nodes with obstacles, finds the possible paths, as well as creates the graph. THEN it goes into the a star algorithm
print 'in the program!!!!! Lets look at the graph to see if the program worked- hopefully it will have :)'
print "Robot Radius: ", robot_rad
#Creating the graph:
##Adding the start and goal as nodes
nodes_coord.append(goal)
nodes_coord.insert(0,start)
nodes_coord_unaltered = nodes_coord[:]
num_nodes = len(nodes_coord)
##Now, check to see whether those nodes are within obstacles or too close to an obstacle for the robot to go to with it's radius; the result of collision check should return nodes that the robot should actually be allowed to go to
nodes_coord = collision_check(robot_rad,nodes_coord,obs_radii,obs_coord,region_length,region_width)
#If the start or goal node are not in the resulting nodes list, then one or the other is in conflict with an obstacle
path = []
##Find the possible paths between all of the nodes and check which ones are still possible after you check for collisions with obstacles
paths_possible = edges(robot_rad,nodes_coord,obs_radii,obs_coord)
##Find all of the possible neighbors between the coordinates using the paths possible list
all_nbrs = neighbor_nodes(nodes_coord,paths_possible)
##Create a graph using the paths possible and node coordinates
graph = create_graph(paths_possible,nodes_coord)
# print "paths_possible: ", paths_possible
if start not in nodes_coord:
print "No solution. The start conflicts with an obstacle O_o"
plot(obs_radii,obs_coord,nodes_coord,nodes_coord_unaltered,path,start,goal,paths_possible)
return
elif goal not in nodes_coord:
print "No solution. The goal conflicts with an obstacle o_O"
plot(obs_radii,obs_coord,nodes_coord,nodes_coord_unaltered,path,start,goal,paths_possible)
return
#Now for the meat of A*:
##Initialize lists
###Place start as the first element in list open since that is the one that we are currently at- will update this with the neighbor list later
list_open = [start]
list_closed = []
#The past cost of everything but the "start" node is infinity- the cost associated with start is 0 since we are already there
past_cost = float('inf')*np.ones(num_nodes)
past_cost[0] = 0
#Need to create a parent array that is the length of the number of nodes and has the start as its first parent since we are beginning there
parent = [0]*num_nodes
parent[0] = start
while len(list_open) != 0:
current = list_open.pop(0)
list_closed.append(current)
current_ind = nodes_coord.index(current)
current_nbrs = all_nbrs[current_ind]
list_open = new_list_open(paths_possible, nodes_coord, current, list_open, list_closed)
#We want to check if the current node is equal to the goal node, to see if we're done or not
if current == goal:
print "Path successfully found!"
#Need to set the path to goal initially since we will keep inserting a parent element into the first index space of the path array
path = [goal]
node_index = nodes_coord.index(goal)
while path[0] != start:
#Need to take all of the nodes that we travelled to and liked from the parents index to the path
path.insert(0,parent[node_index])
node_index = nodes_coord.index(path[0])
print path
#Once path is obtained, need to plot the successful path :)
plot(obs_radii,obs_coord,nodes_coord,nodes_coord_unaltered,path,start,goal,paths_possible)
return
for neighbor in current_nbrs:
#Find the index of the current neighbor so we can find the cost from going from the current node to the neighbor node
neighbor_ind = nodes_coord.index(neighbor)
tentative_cost = past_cost[current_ind] + neighbor_cost(neighbor, current)
if tentative_cost < past_cost[neighbor_ind]:
#If the tentative cost is below the past cost, then we should travel to that node; otherwise, it is not a good option
past_cost[neighbor_ind] = tentative_cost
parent[neighbor_ind] = current
#We need to get a new open list for the neighbor node once we travel to it
list_open = sorting_open(past_cost,goal,list_open,nodes_coord)
if len(path) == 0:
print "No solution. There are no possible paths to the goal."
plot(obs_radii,obs_coord,nodes_coord,nodes_coord_unaltered,path,start,goal,paths_possible)