Skip to content

Commit 43ab313

Browse files
authored
New A star algorithm pr (AtsushiSakai#391)
* a star pr * a star pr * a star pr * fix line excede 79 error * fix pycodestyle errors, missing a whitespace * add test file * add test file * rerun CI * rerun CI * rerun CI * rerun CI * rerun CI * modified test file and rerun CI * rerun CI * fix CI error * modified code resubmit pr * fixed some minor error * modified pr as suggested
1 parent 6f16420 commit 43ab313

File tree

2 files changed

+396
-0
lines changed

2 files changed

+396
-0
lines changed
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
"""
2+
A* algorithm
3+
Author: Weicent
4+
randomly generate obstacles, start and goal point
5+
searching path from start and end simultaneously
6+
"""
7+
8+
import numpy as np
9+
import matplotlib.pyplot as plt
10+
import math
11+
12+
show_animation = True
13+
14+
15+
class Node:
16+
"""node with properties of g, h, coordinate and parent node"""
17+
18+
def __init__(self, G=0, H=0, coordinate=None, parent=None):
19+
self.G = G
20+
self.H = H
21+
self.F = G + H
22+
self.parent = parent
23+
self.coordinate = coordinate
24+
25+
def reset_f(self):
26+
self.F = self.G + self.H
27+
28+
29+
def hcost(node_coordinate, goal):
30+
dx = abs(node_coordinate[0] - goal[0])
31+
dy = abs(node_coordinate[1] - goal[1])
32+
hcost = dx + dy
33+
return hcost
34+
35+
36+
def gcost(fixed_node, update_node_coordinate):
37+
dx = abs(fixed_node.coordinate[0] - update_node_coordinate[0])
38+
dy = abs(fixed_node.coordinate[1] - update_node_coordinate[1])
39+
gc = math.hypot(dx, dy) # gc = move from fixed_node to update_node
40+
gcost = fixed_node.G + gc # gcost = move from start point to update_node
41+
return gcost
42+
43+
44+
def boundary_and_obstacles(start, goal, top_vertex, bottom_vertex, obs_number):
45+
"""
46+
:param start: start coordinate
47+
:param goal: goal coordinate
48+
:param top_vertex: top right vertex coordinate of boundary
49+
:param bottom_vertex: bottom left vertex coordinate of boundary
50+
:param obs_number: number of obstacles generated in the map
51+
:return: boundary_obstacle array, obstacle list
52+
"""
53+
# below can be merged into a rectangle boundary
54+
ay = list(range(bottom_vertex[1], top_vertex[1]))
55+
ax = [bottom_vertex[0]] * len(ay)
56+
cy = ay
57+
cx = [top_vertex[0]] * len(cy)
58+
bx = list(range(bottom_vertex[0] + 1, top_vertex[0]))
59+
by = [bottom_vertex[1]] * len(bx)
60+
dx = [bottom_vertex[0]] + bx + [top_vertex[0]]
61+
dy = [top_vertex[1]] * len(dx)
62+
63+
# generate random obstacles
64+
ob_x = np.random.randint(bottom_vertex[0] + 1,
65+
top_vertex[0], obs_number).tolist()
66+
ob_y = np.random.randint(bottom_vertex[1] + 1,
67+
top_vertex[1], obs_number).tolist()
68+
# x y coordinate in certain order for boundary
69+
x = ax + bx + cx + dx
70+
y = ay + by + cy + dy
71+
obstacle = np.vstack((ob_x, ob_y)).T.tolist()
72+
# remove start and goal coordinate in obstacle list
73+
obstacle = [coor for coor in obstacle if coor != start and coor != goal]
74+
obs_array = np.array(obstacle)
75+
bound = np.vstack((x, y)).T
76+
bound_obs = np.vstack((bound, obs_array))
77+
return bound_obs, obstacle
78+
79+
80+
def find_neighbor(node, ob, closed):
81+
# generate neighbors in certain condition
82+
ob_list = ob.tolist()
83+
neighbor: list = []
84+
for x in range(node.coordinate[0] - 1, node.coordinate[0] + 2):
85+
for y in range(node.coordinate[1] - 1, node.coordinate[1] + 2):
86+
if [x, y] not in ob_list:
87+
# find all possible neighbor nodes
88+
neighbor.append([x, y])
89+
# remove node violate the motion rule
90+
# 1. remove node.coordinate itself
91+
neighbor.remove(node.coordinate)
92+
# 2. remove neighbor nodes who cross through two diagonal
93+
# positioned obstacles since there is no enough space for
94+
# robot to go through two diagonal positioned obstacles
95+
96+
# top bottom left right neighbors of node
97+
top_nei = [node.coordinate[0], node.coordinate[1] + 1]
98+
bottom_nei = [node.coordinate[0], node.coordinate[1] - 1]
99+
left_nei = [node.coordinate[0] - 1, node.coordinate[1]]
100+
right_nei = [node.coordinate[0] + 1, node.coordinate[1]]
101+
# neighbors in four vertex
102+
lt_nei = [node.coordinate[0] - 1, node.coordinate[1] + 1]
103+
rt_nei = [node.coordinate[0] + 1, node.coordinate[1] + 1]
104+
lb_nei = [node.coordinate[0] - 1, node.coordinate[1] - 1]
105+
rb_nei = [node.coordinate[0] + 1, node.coordinate[1] - 1]
106+
107+
# remove the unnecessary neighbors
108+
if top_nei and left_nei in ob_list and lt_nei in neighbor:
109+
neighbor.remove(lt_nei)
110+
if top_nei and right_nei in ob_list and rt_nei in neighbor:
111+
neighbor.remove(rt_nei)
112+
if bottom_nei and left_nei in ob_list and lb_nei in neighbor:
113+
neighbor.remove(lb_nei)
114+
if bottom_nei and right_nei in ob_list and rb_nei in neighbor:
115+
neighbor.remove(rb_nei)
116+
neighbor = [x for x in neighbor if x not in closed]
117+
return neighbor
118+
119+
120+
def find_node_index(coordinate, node_list):
121+
# find node index in the node list via its coordinate
122+
ind = 0
123+
for node in node_list:
124+
if node.coordinate == coordinate:
125+
target_node = node
126+
ind = node_list.index(target_node)
127+
break
128+
return ind
129+
130+
131+
def find_path(open_list, closed_list, goal, obstacle):
132+
# searching for the path, update open and closed list
133+
# obstacle = obstacle and boundary
134+
flag = len(open_list)
135+
for i in range(flag):
136+
node = open_list[0]
137+
open_coordinate_list = [node.coordinate for node in open_list]
138+
closed_coordinate_list = [node.coordinate for node in closed_list]
139+
temp = find_neighbor(node, obstacle, closed_coordinate_list)
140+
for element in temp:
141+
if element in closed_list:
142+
continue
143+
elif element in open_coordinate_list:
144+
# if node in open list, update g value
145+
ind = open_coordinate_list.index(element)
146+
new_g = gcost(node, element)
147+
if new_g <= open_list[ind].G:
148+
open_list[ind].G = new_g
149+
open_list[ind].reset_f()
150+
open_list[ind].parent = node
151+
else: # new coordinate, create corresponding node
152+
ele_node = Node(coordinate=element, parent=node,
153+
G=gcost(node, element), H=hcost(element, goal))
154+
open_list.append(ele_node)
155+
open_list.remove(node)
156+
closed_list.append(node)
157+
open_list.sort(key=lambda x: x.F)
158+
return open_list, closed_list
159+
160+
161+
def node_to_coordinate(node_list):
162+
# convert node list into coordinate list and array
163+
coordinate_list = [node.coordinate for node in node_list]
164+
return coordinate_list
165+
166+
167+
def check_node_coincide(close_ls1, closed_ls2):
168+
"""
169+
:param close_ls1: node closed list for searching from start
170+
:param closed_ls2: node closed list for searching from end
171+
:return: intersect node list for above two
172+
"""
173+
# check if node in close_ls1 intersect with node in closed_ls2
174+
cl1 = node_to_coordinate(close_ls1)
175+
cl2 = node_to_coordinate(closed_ls2)
176+
intersect_ls = [node for node in cl1 if node in cl2]
177+
return intersect_ls
178+
179+
180+
def find_surrounding(coordinate, obstacle):
181+
# find obstacles around node, help to draw the borderline
182+
boundary: list = []
183+
for x in range(coordinate[0] - 1, coordinate[0] + 2):
184+
for y in range(coordinate[1] - 1, coordinate[1] + 2):
185+
if [x, y] in obstacle:
186+
boundary.append([x, y])
187+
return boundary
188+
189+
190+
def get_border_line(node_closed_ls, obstacle):
191+
# if no path, find border line which confine goal or robot
192+
border: list = []
193+
coordinate_closed_ls = node_to_coordinate(node_closed_ls)
194+
for coordinate in coordinate_closed_ls:
195+
temp = find_surrounding(coordinate, obstacle)
196+
border = border + temp
197+
border_ary = np.array(border)
198+
return border_ary
199+
200+
201+
def get_path(org_list, goal_list, coordinate):
202+
# get path from start to end
203+
path_org: list = []
204+
path_goal: list = []
205+
ind = find_node_index(coordinate, org_list)
206+
node = org_list[ind]
207+
while node != org_list[0]:
208+
path_org.append(node.coordinate)
209+
node = node.parent
210+
path_org.append(org_list[0].coordinate)
211+
ind = find_node_index(coordinate, goal_list)
212+
node = goal_list[ind]
213+
while node != goal_list[0]:
214+
path_goal.append(node.coordinate)
215+
node = node.parent
216+
path_goal.append(goal_list[0].coordinate)
217+
path_org.reverse()
218+
path = path_org + path_goal
219+
path = np.array(path)
220+
return path
221+
222+
223+
def random_coordinate(bottom_vertex, top_vertex):
224+
# generate random coordinates inside maze
225+
coordinate = [np.random.randint(bottom_vertex[0] + 1, top_vertex[0]),
226+
np.random.randint(bottom_vertex[1] + 1, top_vertex[1])]
227+
return coordinate
228+
229+
230+
def draw(close_origin, close_goal, start, end, bound):
231+
# plot the map
232+
if not close_goal.tolist(): # ensure the close_goal not empty
233+
# in case of the obstacle number is really large (>4500), the
234+
# origin is very likely blocked at the first search, and then
235+
# the program is over and the searching from goal to origin
236+
# will not start, which remain the closed_list for goal == []
237+
# in order to plot the map, add the end coordinate to array
238+
close_goal = np.array([end])
239+
plt.cla()
240+
plt.gcf().set_size_inches(11, 9, forward=True)
241+
plt.axis('equal')
242+
plt.plot(close_origin[:, 0], close_origin[:, 1], 'oy')
243+
plt.plot(close_goal[:, 0], close_goal[:, 1], 'og')
244+
plt.plot(bound[:, 0], bound[:, 1], 'sk')
245+
plt.plot(end[0], end[1], '*b', label='Goal')
246+
plt.plot(start[0], start[1], '^b', label='Origin')
247+
plt.legend()
248+
plt.pause(0.0001)
249+
250+
251+
def draw_control(org_closed, goal_closed, flag, start, end, bound, obstacle):
252+
"""
253+
control the plot process, evaluate if the searching finished
254+
flag == 0 : draw the searching process and plot path
255+
flag == 1 or 2 : start or end is blocked, draw the border line
256+
"""
257+
stop_loop = 0 # stop sign for the searching
258+
org_closed_ls = node_to_coordinate(org_closed)
259+
org_array = np.array(org_closed_ls)
260+
goal_closed_ls = node_to_coordinate(goal_closed)
261+
goal_array = np.array(goal_closed_ls)
262+
path = None
263+
if show_animation: # draw the searching process
264+
draw(org_array, goal_array, start, end, bound)
265+
if flag == 0:
266+
node_intersect = check_node_coincide(org_closed, goal_closed)
267+
if node_intersect: # a path is find
268+
path = get_path(org_closed, goal_closed, node_intersect[0])
269+
stop_loop = 1
270+
print('Path is find!')
271+
if show_animation: # draw the path
272+
plt.plot(path[:, 0], path[:, 1], '-r')
273+
plt.title('Robot Arrived', size=20, loc='center')
274+
plt.pause(0.01)
275+
plt.show()
276+
elif flag == 1: # start point blocked first
277+
stop_loop = 1
278+
print('There is no path to the goal! Start point is blocked!')
279+
elif flag == 2: # end point blocked first
280+
stop_loop = 1
281+
print('There is no path to the goal! End point is blocked!')
282+
if show_animation: # blocked case, draw the border line
283+
info = 'There is no path to the goal!' \
284+
' Robot&Goal are split by border' \
285+
' shown in red \'x\'!'
286+
if flag == 1:
287+
border = get_border_line(org_closed, obstacle)
288+
plt.plot(border[:, 0], border[:, 1], 'xr')
289+
plt.title(info, size=14, loc='center')
290+
plt.pause(0.01)
291+
plt.show()
292+
elif flag == 2:
293+
border = get_border_line(goal_closed, obstacle)
294+
plt.plot(border[:, 0], border[:, 1], 'xr')
295+
plt.title(info, size=14, loc='center')
296+
plt.pause(0.01)
297+
plt.show()
298+
return stop_loop, path
299+
300+
301+
def searching_control(start, end, bound, obstacle):
302+
"""manage the searching process, start searching from two side"""
303+
# initial origin node and end node
304+
origin = Node(coordinate=start, H=hcost(start, end))
305+
goal = Node(coordinate=end, H=hcost(end, start))
306+
# list for searching from origin to goal
307+
origin_open: list = [origin]
308+
origin_close: list = []
309+
# list for searching from goal to origin
310+
goal_open = [goal]
311+
goal_close: list = []
312+
# initial target
313+
target_goal = end
314+
# flag = 0 (not blocked) 1 (start point blocked) 2 (end point blocked)
315+
flag = 0 # init flag
316+
path = None
317+
while True:
318+
# searching from start to end
319+
origin_open, origin_close = \
320+
find_path(origin_open, origin_close, target_goal, bound)
321+
if not origin_open: # no path condition
322+
flag = 1 # origin node is blocked
323+
draw_control(origin_close, goal_close, flag, start, end, bound,
324+
obstacle)
325+
break
326+
# update target for searching from end to start
327+
target_origin = min(origin_open, key=lambda x: x.F).coordinate
328+
329+
# searching from end to start
330+
goal_open, goal_close = \
331+
find_path(goal_open, goal_close, target_origin, bound)
332+
if not goal_open: # no path condition
333+
flag = 2 # goal is blocked
334+
draw_control(origin_close, goal_close, flag, start, end, bound,
335+
obstacle)
336+
break
337+
# update target for searching from start to end
338+
target_goal = min(goal_open, key=lambda x: x.F).coordinate
339+
340+
# continue searching, draw the process
341+
stop_sign, path = draw_control(origin_close, goal_close, flag, start,
342+
end, bound, obstacle)
343+
if stop_sign:
344+
break
345+
return path
346+
347+
348+
def main(obstacle_number=1500):
349+
print(__file__ + ' start!')
350+
351+
top_vertex = [60, 60] # top right vertex of boundary
352+
bottom_vertex = [0, 0] # bottom left vertex of boundary
353+
354+
# generate start and goal point randomly
355+
start = random_coordinate(bottom_vertex, top_vertex)
356+
end = random_coordinate(bottom_vertex, top_vertex)
357+
358+
# generate boundary and obstacles
359+
bound, obstacle = boundary_and_obstacles(start, end, top_vertex,
360+
bottom_vertex,
361+
obstacle_number)
362+
363+
path = searching_control(start, end, bound, obstacle)
364+
if not show_animation:
365+
print(path)
366+
367+
368+
if __name__ == '__main__':
369+
main(obstacle_number=1500)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from unittest import TestCase
2+
import os
3+
import sys
4+
5+
sys.path.append(os.path.dirname(__file__) + '/../')
6+
7+
try:
8+
from PathPlanning.AStar import A_Star_searching_from_two_side as m
9+
except ImportError:
10+
raise
11+
12+
13+
class Test(TestCase):
14+
15+
def test1(self):
16+
m.show_animation = False
17+
m.main(800)
18+
19+
def test2(self):
20+
m.show_animation = False
21+
m.main(5000) # increase obstacle number, block path
22+
23+
24+
if __name__ == '__main__':
25+
test = Test()
26+
test.test1()
27+
test.test2()

0 commit comments

Comments
 (0)