Skip to content

Commit 03811e6

Browse files
authored
Merge pull request AtsushiSakai#107 from daniel-s-ingram/master
Robotic arm navigating obstacles with A*
2 parents 03678a5 + 79ccc76 commit 03811e6

File tree

1 file changed

+244
-0
lines changed

1 file changed

+244
-0
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""
2+
Obstacle navigation using A* on a toroidal grid
3+
4+
Author: Daniel Ingram (daniel-s-ingram)
5+
"""
6+
from math import pi
7+
import numpy as np
8+
import matplotlib.pyplot as plt
9+
from matplotlib.colors import from_levels_and_colors
10+
11+
plt.ion()
12+
13+
#Simulation parameters
14+
M = 100
15+
obstacles = [[1.75, 0.75, 0.6], [0.55, 1.5, 0.5], [0, -1, 0.25]]
16+
17+
def main():
18+
arm = NLinkArm([1, 1], [0, 0])
19+
grid = get_occupancy_grid(arm, obstacles)
20+
plt.imshow(grid)
21+
plt.show()
22+
#(50,50) (58,56)
23+
route = astar_torus(grid, (10, 50), (58, 56))
24+
for node in route:
25+
theta1 = 2*pi*node[0]/M-pi
26+
theta2 = 2*pi*node[1]/M-pi
27+
arm.update_joints([theta1, theta2])
28+
arm.plot(obstacles=obstacles)
29+
30+
def detect_collision(line_seg, circle):
31+
"""
32+
Determines whether a line segment (arm link) is in contact
33+
with a circle (obstacle).
34+
Credit to: http://doswa.com/2009/07/13/circle-segment-intersectioncollision.html
35+
Args:
36+
line_seg: List of coordinates of line segment endpoints e.g. [[1, 1], [2, 2]]
37+
circle: List of circle coordinates and radius e.g. [0, 0, 0.5] is a circle centered
38+
at the origin with radius 0.5
39+
40+
Returns:
41+
True if the line segment is in contact with the circle
42+
False otherwise
43+
"""
44+
a_vec = np.array([line_seg[0][0], line_seg[0][1]])
45+
b_vec = np.array([line_seg[1][0], line_seg[1][1]])
46+
c_vec = np.array([circle[0], circle[1]])
47+
radius = circle[2]
48+
line_vec = b_vec - a_vec
49+
line_mag = np.linalg.norm(line_vec)
50+
circle_vec = c_vec - a_vec
51+
proj = circle_vec.dot(line_vec/line_mag)
52+
if proj <= 0:
53+
closest_point = a_vec
54+
elif proj >= line_mag:
55+
closest_point = b_vec
56+
else:
57+
closest_point = a_vec + line_vec*proj/line_mag
58+
if np.linalg.norm(closest_point-c_vec) > radius:
59+
return False
60+
else:
61+
return True
62+
63+
def get_occupancy_grid(arm, obstacles):
64+
"""
65+
Discretizes joint space into M values from -pi to +pi
66+
and determines whether a given coordinate in joint space
67+
would result in a collision between a robot arm and obstacles
68+
in its environment.
69+
70+
Args:
71+
arm: An instance of NLinkArm
72+
obstacles: A list of obstacles, with each obstacle defined as a list
73+
of xy coordinates and a radius.
74+
75+
Returns:
76+
Occupancy grid in joint space
77+
"""
78+
grid = [[0 for _ in range(M)] for _ in range(M)]
79+
theta_list = [2*i*pi/M for i in range(-M//2, M//2+1)]
80+
for i in range(M):
81+
for j in range(M):
82+
arm.update_joints([theta_list[i], theta_list[j]])
83+
points = arm.points
84+
collision_detected = False
85+
for k in range(len(points)-1):
86+
for obstacle in obstacles:
87+
line_seg = [points[k], points[k+1]]
88+
collision_detected = detect_collision(line_seg, obstacle)
89+
if collision_detected:
90+
break
91+
if collision_detected:
92+
break
93+
grid[i][j] = int(collision_detected)
94+
return np.array(grid)
95+
96+
def astar_torus(grid, start_node, goal_node):
97+
"""
98+
Finds a path between an initial and goal joint configuration using
99+
the A* Algorithm on a tororiadal grid.
100+
101+
Args:
102+
grid: An occupancy grid (ndarray)
103+
start_node: Initial joint configuation (tuple)
104+
goal_node: Goal joint configuration (tuple)
105+
106+
Returns:
107+
Obstacle-free route in joint space from start_node to goal_node
108+
"""
109+
colors = ['white', 'black', 'red', 'pink', 'yellow', 'green', 'orange']
110+
levels = [0, 1, 2, 3, 4, 5, 6, 7]
111+
cmap, norm = from_levels_and_colors(levels, colors)
112+
113+
grid[start_node] = 4
114+
grid[goal_node] = 5
115+
116+
parent_map = [[() for _ in range(M)] for _ in range(M)]
117+
118+
X, Y = np.meshgrid([i for i in range(M)], [i for i in range(M)])
119+
heuristic_map = np.abs(X - goal_node[1]) + np.abs(Y - goal_node[0])
120+
for i in range(heuristic_map.shape[0]):
121+
for j in range(heuristic_map.shape[1]):
122+
heuristic_map[i,j] = min(heuristic_map[i,j],
123+
i + 1 + heuristic_map[M-1,j],
124+
M - i + heuristic_map[0,j],
125+
j + 1 + heuristic_map[i,M-1],
126+
M - j + heuristic_map[i,0]
127+
)
128+
129+
explored_heuristic_map = np.full((M, M), np.inf)
130+
distance_map = np.full((M, M), np.inf)
131+
explored_heuristic_map[start_node] = heuristic_map[start_node]
132+
distance_map[start_node] = 0
133+
while True:
134+
grid[start_node] = 4
135+
grid[goal_node] = 5
136+
137+
current_node = np.unravel_index(np.argmin(explored_heuristic_map, axis=None), explored_heuristic_map.shape)
138+
min_distance = np.min(explored_heuristic_map)
139+
if (current_node == goal_node) or np.isinf(min_distance):
140+
break
141+
142+
grid[current_node] = 2
143+
explored_heuristic_map[current_node] = np.inf
144+
145+
i, j = current_node[0], current_node[1]
146+
147+
neighbors = []
148+
if i-1 >= 0:
149+
neighbors.append((i-1, j))
150+
else:
151+
neighbors.append((M-1, j))
152+
153+
if i+1 < M:
154+
neighbors.append((i+1, j))
155+
else:
156+
neighbors.append((0, j))
157+
158+
if j-1 >= 0:
159+
neighbors.append((i, j-1))
160+
else:
161+
neighbors.append((i, M-1))
162+
163+
if j+1 < M:
164+
neighbors.append((i, j+1))
165+
else:
166+
neighbors.append((i, 0))
167+
168+
for neighbor in neighbors:
169+
if grid[neighbor] == 0 or grid[neighbor] == 5:
170+
distance_map[neighbor] = distance_map[current_node] + 1
171+
explored_heuristic_map[neighbor] = heuristic_map[neighbor]
172+
parent_map[neighbor[0]][neighbor[1]] = current_node
173+
grid[neighbor] = 3
174+
'''
175+
plt.cla()
176+
plt.imshow(grid, cmap=cmap, norm=norm, interpolation=None)
177+
plt.show()
178+
plt.pause(1e-5)
179+
'''
180+
181+
if np.isinf(explored_heuristic_map[goal_node]):
182+
route = []
183+
print("No route found.")
184+
else:
185+
route = [goal_node]
186+
while parent_map[route[0][0]][route[0][1]] is not ():
187+
route.insert(0, parent_map[route[0][0]][route[0][1]])
188+
189+
print("The route found covers %d grid cells." % len(route))
190+
for i in range(1, len(route)):
191+
grid[route[i]] = 6
192+
plt.cla()
193+
plt.imshow(grid, cmap=cmap, norm=norm, interpolation=None)
194+
plt.show()
195+
plt.pause(1e-2)
196+
197+
return route
198+
199+
class NLinkArm(object):
200+
"""
201+
Class for controlling and plotting a planar arm with an arbitrary number of links.
202+
"""
203+
def __init__(self, link_lengths, joint_angles):
204+
self.n_links = len(link_lengths)
205+
if self.n_links is not len(joint_angles):
206+
raise ValueError()
207+
208+
self.link_lengths = np.array(link_lengths)
209+
self.joint_angles = np.array(joint_angles)
210+
self.points = [[0, 0] for _ in range(self.n_links+1)]
211+
212+
self.lim = sum(link_lengths)
213+
self.update_points()
214+
215+
def update_joints(self, joint_angles):
216+
self.joint_angles = joint_angles
217+
self.update_points()
218+
219+
def update_points(self):
220+
for i in range(1, self.n_links+1):
221+
self.points[i][0] = self.points[i-1][0] + self.link_lengths[i-1]*np.cos(np.sum(self.joint_angles[:i]))
222+
self.points[i][1] = self.points[i-1][1] + self.link_lengths[i-1]*np.sin(np.sum(self.joint_angles[:i]))
223+
224+
self.end_effector = np.array(self.points[self.n_links]).T
225+
226+
def plot(self, obstacles=[]):
227+
plt.cla()
228+
229+
for obstacle in obstacles:
230+
circle = plt.Circle((obstacle[0], obstacle[1]), radius=0.5*obstacle[2], fc='k')
231+
plt.gca().add_patch(circle)
232+
233+
for i in range(self.n_links+1):
234+
if i is not self.n_links:
235+
plt.plot([self.points[i][0], self.points[i+1][0]], [self.points[i][1], self.points[i+1][1]], 'r-')
236+
plt.plot(self.points[i][0], self.points[i][1], 'k.')
237+
238+
plt.xlim([-self.lim, self.lim])
239+
plt.ylim([-self.lim, self.lim])
240+
plt.draw()
241+
plt.pause(1e-5)
242+
243+
if __name__ == '__main__':
244+
main()

0 commit comments

Comments
 (0)