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