python-motion-planning 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- curve_generation/__init__.py +9 -0
- curve_generation/bezier_curve.py +131 -0
- curve_generation/bspline_curve.py +271 -0
- curve_generation/cubic_spline.py +128 -0
- curve_generation/curve.py +64 -0
- curve_generation/dubins_curve.py +348 -0
- curve_generation/fem_pos_smooth.py +114 -0
- curve_generation/polynomial_curve.py +226 -0
- curve_generation/reeds_shepp.py +736 -0
- global_planner/__init__.py +3 -0
- global_planner/evolutionary_search/__init__.py +4 -0
- global_planner/evolutionary_search/aco.py +186 -0
- global_planner/evolutionary_search/evolutionary_search.py +87 -0
- global_planner/evolutionary_search/pso.py +356 -0
- global_planner/graph_search/__init__.py +28 -0
- global_planner/graph_search/a_star.py +124 -0
- global_planner/graph_search/d_star.py +291 -0
- global_planner/graph_search/d_star_lite.py +188 -0
- global_planner/graph_search/dijkstra.py +77 -0
- global_planner/graph_search/gbfs.py +78 -0
- global_planner/graph_search/graph_search.py +87 -0
- global_planner/graph_search/jps.py +165 -0
- global_planner/graph_search/lazy_theta_star.py +114 -0
- global_planner/graph_search/lpa_star.py +230 -0
- global_planner/graph_search/s_theta_star.py +133 -0
- global_planner/graph_search/theta_star.py +171 -0
- global_planner/graph_search/voronoi.py +200 -0
- global_planner/sample_search/__init__.py +6 -0
- global_planner/sample_search/informed_rrt.py +152 -0
- global_planner/sample_search/rrt.py +151 -0
- global_planner/sample_search/rrt_connect.py +147 -0
- global_planner/sample_search/rrt_star.py +77 -0
- global_planner/sample_search/sample_search.py +135 -0
- local_planner/__init__.py +19 -0
- local_planner/apf.py +144 -0
- local_planner/ddpg.py +630 -0
- local_planner/dqn.py +687 -0
- local_planner/dwa.py +212 -0
- local_planner/local_planner.py +262 -0
- local_planner/lqr.py +146 -0
- local_planner/mpc.py +214 -0
- local_planner/pid.py +158 -0
- local_planner/rpp.py +147 -0
- python_motion_planning-1.0.dist-info/LICENSE +674 -0
- python_motion_planning-1.0.dist-info/METADATA +873 -0
- python_motion_planning-1.0.dist-info/RECORD +65 -0
- python_motion_planning-1.0.dist-info/WHEEL +5 -0
- python_motion_planning-1.0.dist-info/top_level.txt +4 -0
- utils/__init__.py +19 -0
- utils/agent/__init__.py +0 -0
- utils/agent/agent.py +135 -0
- utils/environment/__init__.py +0 -0
- utils/environment/env.py +134 -0
- utils/environment/node.py +85 -0
- utils/environment/point2d.py +96 -0
- utils/environment/pose2d.py +91 -0
- utils/helper/__init__.py +3 -0
- utils/helper/math_helper.py +65 -0
- utils/planner/__init__.py +0 -0
- utils/planner/control_factory.py +31 -0
- utils/planner/curve_factory.py +29 -0
- utils/planner/planner.py +40 -0
- utils/planner/search_factory.py +51 -0
- utils/plot/__init__.py +0 -0
- utils/plot/plot.py +274 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'''
|
|
2
|
+
@file: s_theta_star.py
|
|
3
|
+
@breif: S-Theta* motion planning
|
|
4
|
+
@author: Wu Maojia
|
|
5
|
+
@update: 2024.6.23
|
|
6
|
+
'''
|
|
7
|
+
import heapq
|
|
8
|
+
from math import acos
|
|
9
|
+
|
|
10
|
+
from .theta_star import ThetaStar
|
|
11
|
+
from python_motion_planning.utils import Env, Node
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SThetaStar(ThetaStar):
|
|
15
|
+
"""
|
|
16
|
+
Class for S-Theta* motion planning.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
start (tuple): start point coordinate
|
|
20
|
+
goal (tuple): goal point coordinate
|
|
21
|
+
env (Env): environment
|
|
22
|
+
heuristic_type (str): heuristic function type
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
>>> import python_motion_planning as pmp
|
|
26
|
+
>>> planner = pmp.SThetaStar((5, 5), (45, 25), pmp.Grid(51, 31))
|
|
27
|
+
>>> cost, path, expand = planner.plan()
|
|
28
|
+
>>> planner.plot.animation(path, str(planner), cost, expand) # animation
|
|
29
|
+
>>> planner.run() # run both planning and animation
|
|
30
|
+
|
|
31
|
+
References:
|
|
32
|
+
[1] S-Theta*: low steering path-planning algorithm
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, start: tuple, goal: tuple, env: Env, heuristic_type: str = "euclidean") -> None:
|
|
36
|
+
super().__init__(start, goal, env, heuristic_type)
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return "S-Theta*"
|
|
40
|
+
|
|
41
|
+
def plan(self) -> tuple:
|
|
42
|
+
"""
|
|
43
|
+
S-Theta* motion plan function.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
cost (float): path cost
|
|
47
|
+
path (list): planning path
|
|
48
|
+
expand (list): all nodes that planner has searched
|
|
49
|
+
"""
|
|
50
|
+
# OPEN list (priority queue) and CLOSED list (hash table)
|
|
51
|
+
OPEN = []
|
|
52
|
+
heapq.heappush(OPEN, self.start)
|
|
53
|
+
CLOSED = dict()
|
|
54
|
+
|
|
55
|
+
while OPEN:
|
|
56
|
+
node = heapq.heappop(OPEN)
|
|
57
|
+
|
|
58
|
+
# exists in CLOSED list
|
|
59
|
+
if node.current in CLOSED:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# goal found
|
|
63
|
+
if node == self.goal:
|
|
64
|
+
CLOSED[node.current] = node
|
|
65
|
+
cost, path = self.extractPath(CLOSED)
|
|
66
|
+
return cost, path, list(CLOSED.values())
|
|
67
|
+
|
|
68
|
+
for node_n in self.getNeighbor(node):
|
|
69
|
+
# exists in CLOSED list
|
|
70
|
+
if node_n.current in CLOSED:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# path1
|
|
74
|
+
node_n.parent = node.current
|
|
75
|
+
node_n.h = self.h(node_n, self.goal)
|
|
76
|
+
|
|
77
|
+
alpha = 0.0
|
|
78
|
+
node_p = CLOSED.get(node.parent)
|
|
79
|
+
if node_p:
|
|
80
|
+
alpha = self.getAlpha(node_p, node_n)
|
|
81
|
+
node_n.g += alpha
|
|
82
|
+
|
|
83
|
+
if node_p:
|
|
84
|
+
self.updateVertex(node_p, node_n, alpha)
|
|
85
|
+
|
|
86
|
+
# goal found
|
|
87
|
+
if node_n == self.goal:
|
|
88
|
+
heapq.heappush(OPEN, node_n)
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
# update OPEN list
|
|
92
|
+
heapq.heappush(OPEN, node_n)
|
|
93
|
+
|
|
94
|
+
CLOSED[node.current] = node
|
|
95
|
+
return [], [], []
|
|
96
|
+
|
|
97
|
+
def updateVertex(self, node_p: Node, node_c: Node, alpha: float) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Update extend node information with current node's parent node.
|
|
100
|
+
|
|
101
|
+
Parameters:
|
|
102
|
+
node_p (Node): parent node
|
|
103
|
+
node_c (Node): current node
|
|
104
|
+
alpha (float): alpha angle
|
|
105
|
+
"""
|
|
106
|
+
# if alpha == 0 or self.lineOfSight(node_c, node_p): # "alpha == 0" will cause the path to penetrate obstacles
|
|
107
|
+
if self.lineOfSight(node_c, node_p):
|
|
108
|
+
# path 2
|
|
109
|
+
new_g = node_p.g + self.dist(node_c, node_p) + alpha
|
|
110
|
+
if new_g <= node_c.g:
|
|
111
|
+
node_c.g = new_g
|
|
112
|
+
node_c.parent = node_p.current
|
|
113
|
+
|
|
114
|
+
def getAlpha(self, node_p: Node, node_c: Node):
|
|
115
|
+
"""
|
|
116
|
+
α(t) represents the deviation in the trajectory to reach the goal node g
|
|
117
|
+
through the node t in relation to the straight-line distance between the parent of its
|
|
118
|
+
predecessor (t ∈ succ(p) and parent(p) = q) and the goal node.
|
|
119
|
+
|
|
120
|
+
Parameters:
|
|
121
|
+
node_p (Node): parent node
|
|
122
|
+
node_c (Node): current node
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
alpha (float): alpha angle
|
|
126
|
+
"""
|
|
127
|
+
d_qt = self.dist(node_p, node_c)
|
|
128
|
+
d_qg = self.dist(node_p, self.goal)
|
|
129
|
+
d_tg = self.dist(node_c, self.goal)
|
|
130
|
+
value = (d_qt * d_qt + d_qg * d_qg - d_tg * d_tg) / (2.0 * d_qt * d_qg)
|
|
131
|
+
value = max(-1.0, min(1.0, value))
|
|
132
|
+
cost = acos(value)
|
|
133
|
+
return cost
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: theta_star.py
|
|
3
|
+
@breif: Theta* motion planning
|
|
4
|
+
@author: Yang Haodong, Wu Maojia
|
|
5
|
+
@update: 2024.6.23
|
|
6
|
+
"""
|
|
7
|
+
import heapq
|
|
8
|
+
|
|
9
|
+
from .a_star import AStar
|
|
10
|
+
from python_motion_planning.utils import Env, Node
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ThetaStar(AStar):
|
|
14
|
+
"""
|
|
15
|
+
Class for Theta* motion planning.
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
start (tuple):
|
|
19
|
+
start point coordinate
|
|
20
|
+
goal (tuple):
|
|
21
|
+
goal point coordinate
|
|
22
|
+
env (Env):
|
|
23
|
+
environment
|
|
24
|
+
heuristic_type (str):
|
|
25
|
+
heuristic function type
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> import python_motion_planning as pmp
|
|
29
|
+
>>> planner = pmp.ThetaStar((5, 5), (45, 25), pmp.Grid(51, 31))
|
|
30
|
+
>>> cost, path, expand = planner.plan()
|
|
31
|
+
>>> planner.plot.animation(path, str(planner), cost, expand) # animation
|
|
32
|
+
>>> planner.run() # run both planning and animation
|
|
33
|
+
|
|
34
|
+
References:
|
|
35
|
+
[1] Theta*: Any-Angle Path Planning on Grids
|
|
36
|
+
[2] Any-angle path planning on non-uniform costmaps
|
|
37
|
+
"""
|
|
38
|
+
def __init__(self, start: tuple, goal: tuple, env: Env, heuristic_type: str = "euclidean") -> None:
|
|
39
|
+
super().__init__(start, goal, env, heuristic_type)
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return "Theta*"
|
|
43
|
+
|
|
44
|
+
def plan(self) -> tuple:
|
|
45
|
+
"""
|
|
46
|
+
Theta* motion plan function.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
cost (float): path cost
|
|
50
|
+
path (list): planning path
|
|
51
|
+
expand (list): all nodes that planner has searched
|
|
52
|
+
"""
|
|
53
|
+
# OPEN list (priority queue) and CLOSED list (hash table)
|
|
54
|
+
OPEN = []
|
|
55
|
+
heapq.heappush(OPEN, self.start)
|
|
56
|
+
CLOSED = dict()
|
|
57
|
+
|
|
58
|
+
while OPEN:
|
|
59
|
+
node = heapq.heappop(OPEN)
|
|
60
|
+
|
|
61
|
+
# exists in CLOSED list
|
|
62
|
+
if node.current in CLOSED:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# goal found
|
|
66
|
+
if node == self.goal:
|
|
67
|
+
CLOSED[node.current] = node
|
|
68
|
+
cost, path = self.extractPath(CLOSED)
|
|
69
|
+
return cost, path, list(CLOSED.values())
|
|
70
|
+
|
|
71
|
+
for node_n in self.getNeighbor(node):
|
|
72
|
+
# exists in CLOSED list
|
|
73
|
+
if node_n.current in CLOSED:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# path1
|
|
77
|
+
node_n.parent = node.current
|
|
78
|
+
node_n.h = self.h(node_n, self.goal)
|
|
79
|
+
|
|
80
|
+
node_p = CLOSED.get(node.parent)
|
|
81
|
+
|
|
82
|
+
if node_p:
|
|
83
|
+
self.updateVertex(node_p, node_n)
|
|
84
|
+
|
|
85
|
+
# goal found
|
|
86
|
+
if node_n == self.goal:
|
|
87
|
+
heapq.heappush(OPEN, node_n)
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
# update OPEN list
|
|
91
|
+
heapq.heappush(OPEN, node_n)
|
|
92
|
+
|
|
93
|
+
CLOSED[node.current] = node
|
|
94
|
+
return [], [], []
|
|
95
|
+
|
|
96
|
+
def updateVertex(self, node_p: Node, node_c: Node) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Update extend node information with current node's parent node.
|
|
99
|
+
|
|
100
|
+
Parameters:
|
|
101
|
+
node_p (Node): parent node
|
|
102
|
+
node_c (Node): current node
|
|
103
|
+
"""
|
|
104
|
+
if self.lineOfSight(node_c, node_p):
|
|
105
|
+
# path 2
|
|
106
|
+
if node_p.g + self.dist(node_c, node_p) <= node_c.g:
|
|
107
|
+
node_c.g = node_p.g + self.dist(node_c, node_p)
|
|
108
|
+
node_c.parent = node_p.current
|
|
109
|
+
|
|
110
|
+
def lineOfSight(self, node1: Node, node2: Node) -> bool:
|
|
111
|
+
"""
|
|
112
|
+
Judge collision when moving from node1 to node2 using Bresenham.
|
|
113
|
+
|
|
114
|
+
Parameters:
|
|
115
|
+
node1 (Node): start node
|
|
116
|
+
node2 (Node): end node
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
line_of_sight (bool): True if line of sight exists ( no collision ) else False
|
|
120
|
+
"""
|
|
121
|
+
if node1.current in self.obstacles or node2.current in self.obstacles:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
x1, y1 = node1.current
|
|
125
|
+
x2, y2 = node2.current
|
|
126
|
+
|
|
127
|
+
if x1 < 0 or x1 >= self.env.x_range or y1 < 0 or y1 >= self.env.y_range:
|
|
128
|
+
return False
|
|
129
|
+
if x2 < 0 or x2 >= self.env.x_range or y2 < 0 or y2 >= self.env.y_range:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
d_x = abs(x2 - x1)
|
|
133
|
+
d_y = abs(y2 - y1)
|
|
134
|
+
s_x = 0 if (x2 - x1) == 0 else (x2 - x1) / d_x
|
|
135
|
+
s_y = 0 if (y2 - y1) == 0 else (y2 - y1) / d_y
|
|
136
|
+
x, y, e = x1, y1, 0
|
|
137
|
+
|
|
138
|
+
# check if any obstacle exists between node1 and node2
|
|
139
|
+
if d_x > d_y:
|
|
140
|
+
tau = (d_y - d_x) / 2
|
|
141
|
+
while not x == x2:
|
|
142
|
+
if e > tau:
|
|
143
|
+
x = x + s_x
|
|
144
|
+
e = e - d_y
|
|
145
|
+
elif e < tau:
|
|
146
|
+
y = y + s_y
|
|
147
|
+
e = e + d_x
|
|
148
|
+
else:
|
|
149
|
+
x = x + s_x
|
|
150
|
+
y = y + s_y
|
|
151
|
+
e = e + d_x - d_y
|
|
152
|
+
if (x, y) in self.obstacles:
|
|
153
|
+
return False
|
|
154
|
+
# swap x and y
|
|
155
|
+
else:
|
|
156
|
+
tau = (d_x - d_y) / 2
|
|
157
|
+
while not y == y2:
|
|
158
|
+
if e > tau:
|
|
159
|
+
y = y + s_y
|
|
160
|
+
e = e - d_x
|
|
161
|
+
elif e < tau:
|
|
162
|
+
x = x + s_x
|
|
163
|
+
e = e + d_y
|
|
164
|
+
else:
|
|
165
|
+
x = x + s_x
|
|
166
|
+
y = y + s_y
|
|
167
|
+
e = e + d_y - d_x
|
|
168
|
+
if (x, y) in self.obstacles:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
return True
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: voronoi.py
|
|
3
|
+
@breif: Voronoi-based motion planning
|
|
4
|
+
@author: Yang Haodong, Wu Maojia
|
|
5
|
+
@update: 2024.6.23
|
|
6
|
+
"""
|
|
7
|
+
import heapq, math
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy.spatial import cKDTree, Voronoi
|
|
10
|
+
|
|
11
|
+
from .graph_search import GraphSearcher
|
|
12
|
+
from python_motion_planning.utils import Env, Node
|
|
13
|
+
|
|
14
|
+
class VoronoiPlanner(GraphSearcher):
|
|
15
|
+
"""
|
|
16
|
+
Class for Voronoi-based motion planning.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
start (tuple): start point coordinate
|
|
20
|
+
goal (tuple): goal point coordinate
|
|
21
|
+
env (Env): environment
|
|
22
|
+
heuristic_type (str): heuristic function type, default is euclidean
|
|
23
|
+
n_knn (int): number of edges from one sampled point
|
|
24
|
+
max_edge_len (float): maximum edge length
|
|
25
|
+
inflation_r (float): inflation range
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> import python_motion_planning as pmp
|
|
29
|
+
>>> planner = pmp.VoronoiPlanner((5, 5), (45, 25), pmp.Grid(51, 31))
|
|
30
|
+
>>> cost, path, _ = planner.plan() # planning results only
|
|
31
|
+
>>> planner.plot.animation(path, str(planner), cost, expand) # animation
|
|
32
|
+
>>> planner.run() # run both planning and animation
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self, start: tuple, goal: tuple, env: Env, heuristic_type: str = "euclidean", \
|
|
35
|
+
n_knn: int = 10, max_edge_len: float = 10.0, inflation_r: float = 1.0) -> None:
|
|
36
|
+
super().__init__(start, goal, env, heuristic_type)
|
|
37
|
+
# number of edges from one sampled point
|
|
38
|
+
self.n_knn = n_knn
|
|
39
|
+
# maximum edge length
|
|
40
|
+
self.max_edge_len = max_edge_len
|
|
41
|
+
# inflation range
|
|
42
|
+
self.inflation_r = inflation_r
|
|
43
|
+
|
|
44
|
+
def __str__(self) -> str:
|
|
45
|
+
return "Voronoi-based Planner"
|
|
46
|
+
|
|
47
|
+
def plan(self):
|
|
48
|
+
"""
|
|
49
|
+
Voronoi-based motion plan function.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
cost (float): path cost
|
|
53
|
+
path (list): planning path
|
|
54
|
+
expand (list): voronoi sampled nodes
|
|
55
|
+
"""
|
|
56
|
+
# sampling voronoi diagram
|
|
57
|
+
vor = Voronoi(np.array(list(self.env.obstacles)))
|
|
58
|
+
vx_list = [ix for [ix, _] in vor.vertices] + [self.start.x, self.goal.x]
|
|
59
|
+
vy_list = [iy for [_, iy] in vor.vertices] + [self.start.y, self.goal.y]
|
|
60
|
+
sample_num = len(vx_list)
|
|
61
|
+
expand = [Node((vx_list[i], vy_list[i])) for i in range(sample_num)]
|
|
62
|
+
|
|
63
|
+
# generate road map for voronoi nodes
|
|
64
|
+
road_map = {}
|
|
65
|
+
node_tree = cKDTree(np.vstack((vx_list, vy_list)).T)
|
|
66
|
+
|
|
67
|
+
for node in expand:
|
|
68
|
+
edges = []
|
|
69
|
+
_, index_list = node_tree.query([node.x, node.y], k=sample_num)
|
|
70
|
+
|
|
71
|
+
for i in range(1, len(index_list)):
|
|
72
|
+
node_n = expand[index_list[i]]
|
|
73
|
+
|
|
74
|
+
if not self.isCollision(node, node_n):
|
|
75
|
+
edges.append(node_n)
|
|
76
|
+
|
|
77
|
+
if len(edges) >= self.n_knn:
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
road_map[node] = edges
|
|
81
|
+
|
|
82
|
+
# calculate shortest path using graph search algorithm
|
|
83
|
+
cost, path = self.getShortestPath(road_map)
|
|
84
|
+
return cost, path, expand
|
|
85
|
+
|
|
86
|
+
def run(self):
|
|
87
|
+
"""
|
|
88
|
+
Running both plannig and animation.
|
|
89
|
+
"""
|
|
90
|
+
cost, path, expand = self.plan()
|
|
91
|
+
self.plot.animation(path, str(self), cost, expand)
|
|
92
|
+
|
|
93
|
+
def getShortestPath(self, road_map: dict, dijkstra: bool = True) -> list:
|
|
94
|
+
"""
|
|
95
|
+
Calculate shortest path using graph search algorithm(A*, Dijkstra, etc).
|
|
96
|
+
|
|
97
|
+
Parameters:
|
|
98
|
+
road_map (dict): road map for voronoi diagram, which store KNN for one voronoi node
|
|
99
|
+
dijkstra (bool): using Dijkstra if true, else A*
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
cost (float): path cost
|
|
103
|
+
path (list): planning path
|
|
104
|
+
"""
|
|
105
|
+
# OPEN list (priority queue) and CLOSED list (hash table)
|
|
106
|
+
OPEN = []
|
|
107
|
+
heapq.heappush(OPEN, self.start)
|
|
108
|
+
CLOSED = dict()
|
|
109
|
+
|
|
110
|
+
while OPEN:
|
|
111
|
+
node = heapq.heappop(OPEN)
|
|
112
|
+
|
|
113
|
+
# exists in CLOSED list
|
|
114
|
+
if node.current in CLOSED:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# goal found
|
|
118
|
+
if node == self.goal:
|
|
119
|
+
CLOSED[node.current] = node
|
|
120
|
+
return self.extractPath(CLOSED)
|
|
121
|
+
|
|
122
|
+
for node_n in road_map[node]:
|
|
123
|
+
# exists in CLOSED set
|
|
124
|
+
if node_n.current in CLOSED:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
node_n.parent = node.current
|
|
128
|
+
node_n.g = self.dist(node_n, node)
|
|
129
|
+
if not dijkstra:
|
|
130
|
+
node_n.h = self.h(node_n, self.goal)
|
|
131
|
+
|
|
132
|
+
# goal found
|
|
133
|
+
if node_n == self.goal:
|
|
134
|
+
heapq.heappush(OPEN, node_n)
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
# update OPEN set
|
|
138
|
+
heapq.heappush(OPEN, node_n)
|
|
139
|
+
|
|
140
|
+
CLOSED[node.current] = node
|
|
141
|
+
return [], []
|
|
142
|
+
|
|
143
|
+
def extractPath(self, closed_list: dict):
|
|
144
|
+
"""
|
|
145
|
+
Extract the path based on the CLOSED list.
|
|
146
|
+
|
|
147
|
+
Parameters:
|
|
148
|
+
closed_list (dict): CLOSED list
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
cost (float): the cost of planning path
|
|
152
|
+
path (list): the planning path
|
|
153
|
+
"""
|
|
154
|
+
cost = 0
|
|
155
|
+
node = closed_list[self.goal.current]
|
|
156
|
+
path = [node.current]
|
|
157
|
+
while node != self.start:
|
|
158
|
+
node_parent = closed_list[node.parent]
|
|
159
|
+
cost += self.dist(node, node_parent)
|
|
160
|
+
node = node_parent
|
|
161
|
+
path.append(node.current)
|
|
162
|
+
return cost, path
|
|
163
|
+
|
|
164
|
+
def isCollision(self, node1: Node, node2: Node) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Judge collision when moving from node1 to node2.
|
|
167
|
+
|
|
168
|
+
Parameters:
|
|
169
|
+
node1 (Node): start node
|
|
170
|
+
node2 (Node): end node
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
collision (bool): True if collision exists else False
|
|
174
|
+
"""
|
|
175
|
+
if node1.current in self.obstacles or node2.current in self.obstacles:
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
yaw = self.angle(node1, node2)
|
|
179
|
+
dist = self.dist(node1, node2)
|
|
180
|
+
|
|
181
|
+
if dist >= self.max_edge_len:
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
d_dist = self.inflation_r
|
|
185
|
+
n_step = round(dist / d_dist)
|
|
186
|
+
|
|
187
|
+
x, y = node1.current
|
|
188
|
+
for _ in range(n_step):
|
|
189
|
+
dist_to_obs, _ = self.env.obstacles_tree.query([x, y])
|
|
190
|
+
if dist_to_obs <= self.inflation_r:
|
|
191
|
+
return True
|
|
192
|
+
x += d_dist * math.cos(yaw)
|
|
193
|
+
y += d_dist * math.sin(yaw)
|
|
194
|
+
|
|
195
|
+
# goal point check
|
|
196
|
+
dist_to_obs, _ = self.env.obstacles_tree.query(node2.current)
|
|
197
|
+
if dist_to_obs <= self.inflation_r:
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
return False
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@file: informed_rrt.py
|
|
3
|
+
@breif: Informed RRT* motion planning
|
|
4
|
+
@author: Yang Haodong, Wu Maojia
|
|
5
|
+
@update: 2024.6.23
|
|
6
|
+
"""
|
|
7
|
+
import numpy as np
|
|
8
|
+
from functools import partial
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
|
|
11
|
+
from .rrt_star import RRTStar
|
|
12
|
+
from python_motion_planning.utils import Env, Node
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Ellipse:
|
|
16
|
+
"""
|
|
17
|
+
Ellipse sampling.
|
|
18
|
+
"""
|
|
19
|
+
@staticmethod
|
|
20
|
+
def transform(a: float, c: float, p1: tuple, p2: tuple) -> np.ndarray:
|
|
21
|
+
# center
|
|
22
|
+
center_x = (p1[0] + p2[0]) / 2
|
|
23
|
+
center_y = (p1[1] + p2[1]) / 2
|
|
24
|
+
|
|
25
|
+
# rotation
|
|
26
|
+
theta = - np.arctan2(p2[1] - p1[1], p2[0] - p1[0])
|
|
27
|
+
|
|
28
|
+
# transform
|
|
29
|
+
b = np.sqrt(a ** 2 - c ** 2)
|
|
30
|
+
T = np.array([[ a * np.cos(theta), b * np.sin(theta), center_x],
|
|
31
|
+
[-a * np.sin(theta), b * np.cos(theta), center_y],
|
|
32
|
+
[ 0, 0, 1]])
|
|
33
|
+
return T
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class InformedRRT(RRTStar):
|
|
37
|
+
"""
|
|
38
|
+
Class for Informed RRT* motion planning.
|
|
39
|
+
|
|
40
|
+
Parameters:
|
|
41
|
+
start (tuple): start point coordinate
|
|
42
|
+
goal (tuple): goal point coordinate
|
|
43
|
+
env (Env): environment
|
|
44
|
+
max_dist (float): Maximum expansion distance one step
|
|
45
|
+
sample_num (int): Maximum number of sample points
|
|
46
|
+
r (float): optimization radius
|
|
47
|
+
goal_sample_rate (float): heuristic sample
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
>>> import python_motion_planning as pmp
|
|
51
|
+
>>> planner = pmp.InformedRRT((5, 5), (45, 25), pmp.Map(51, 31))
|
|
52
|
+
>>> cost, path, expand = planner.plan() # planning results only
|
|
53
|
+
>>> planner.plot.animation(path, str(planner), cost, expand) # animation
|
|
54
|
+
>>> planner.run() # run both planning and animation
|
|
55
|
+
|
|
56
|
+
References:
|
|
57
|
+
[1] Optimal Sampling-based Path Planning Focused via Direct Sampling of an Admissible Ellipsoidal heuristic
|
|
58
|
+
"""
|
|
59
|
+
def __init__(self, start: tuple, goal: tuple, env: Env, max_dist: float = 0.5,
|
|
60
|
+
sample_num: int = 1500, r: float = 12.0, goal_sample_rate: float = 0.05) -> None:
|
|
61
|
+
super().__init__(start, goal, env, max_dist, sample_num, goal_sample_rate)
|
|
62
|
+
# optimization radius
|
|
63
|
+
self.r = r
|
|
64
|
+
# best planning cost
|
|
65
|
+
self.c_best = float("inf")
|
|
66
|
+
# distance between start and goal
|
|
67
|
+
self.c_min = self.dist(self.start, self.goal)
|
|
68
|
+
# ellipse sampling
|
|
69
|
+
self.transform = partial(Ellipse.transform, c=self.c_min / 2, p1=start, p2=goal)
|
|
70
|
+
|
|
71
|
+
def __str__(self) -> str:
|
|
72
|
+
return "Informed RRT*"
|
|
73
|
+
|
|
74
|
+
def plan(self) -> tuple:
|
|
75
|
+
"""
|
|
76
|
+
Informed-RRT* motion plan function.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
cost (float): path cost
|
|
80
|
+
path (list): planning path
|
|
81
|
+
expand (list): expanded (sampled) nodes list
|
|
82
|
+
"""
|
|
83
|
+
# Sampled list
|
|
84
|
+
sample_list = {self.start.current: self.start}
|
|
85
|
+
|
|
86
|
+
best_cost, best_path = float("inf"), None
|
|
87
|
+
|
|
88
|
+
# main loop
|
|
89
|
+
for i in range(self.sample_num):
|
|
90
|
+
# generate a random node in the map
|
|
91
|
+
node_rand = self.generateRandomNode()
|
|
92
|
+
|
|
93
|
+
# visited
|
|
94
|
+
if node_rand.current in sample_list:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# generate new node
|
|
98
|
+
node_new = self.getNearest(list(sample_list.values()), node_rand)
|
|
99
|
+
if node_new:
|
|
100
|
+
sample_list[node_new.current] = node_new
|
|
101
|
+
dist = self.dist(node_new, self.goal)
|
|
102
|
+
# goal found
|
|
103
|
+
if dist <= self.max_dist and not self.isCollision(node_new, self.goal):
|
|
104
|
+
self.goal.parent = node_new.current
|
|
105
|
+
self.goal.g = node_new.g + self.dist(self.goal, node_new)
|
|
106
|
+
sample_list[self.goal.current] = self.goal
|
|
107
|
+
cost, path = self.extractPath(sample_list)
|
|
108
|
+
if path and cost < best_cost:
|
|
109
|
+
best_cost, best_path = cost, path
|
|
110
|
+
self.c_best = best_cost
|
|
111
|
+
|
|
112
|
+
return best_cost, best_path, list(sample_list.values())
|
|
113
|
+
|
|
114
|
+
def run(self) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Running both plannig and animation.
|
|
117
|
+
"""
|
|
118
|
+
cost, path, expand = self.plan()
|
|
119
|
+
t = np.arange(0, 2 * np.pi + 0.1, 0.1)
|
|
120
|
+
x = [np.cos(it) for it in t]
|
|
121
|
+
y = [np.sin(it) for it in t]
|
|
122
|
+
z = [1 for _ in t]
|
|
123
|
+
fx = self.transform(self.c_best / 2) @ np.array([x, y, z])
|
|
124
|
+
fx[0, :] += x
|
|
125
|
+
fx[1, :] += y
|
|
126
|
+
self.plot.animation(path, str(self), cost, expand, ellipse=fx)
|
|
127
|
+
|
|
128
|
+
def generateRandomNode(self) -> Node:
|
|
129
|
+
"""
|
|
130
|
+
Generate a random node to extend exploring tree.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
node (Node): a random node based on sampling
|
|
134
|
+
"""
|
|
135
|
+
# ellipse sample
|
|
136
|
+
if self.c_best < float("inf"):
|
|
137
|
+
while True:
|
|
138
|
+
# unit ball sample
|
|
139
|
+
p = np.array([.0, .0, 1.])
|
|
140
|
+
while True:
|
|
141
|
+
x, y = np.random.uniform(-1, 1), np.random.uniform(-1, 1)
|
|
142
|
+
if x ** 2 + y ** 2 < 1:
|
|
143
|
+
p[0], p[1] = x, y
|
|
144
|
+
break
|
|
145
|
+
# transform to ellipse
|
|
146
|
+
p_star = self.transform(self.c_best / 2) @ p.T
|
|
147
|
+
if self.delta <= p_star[0] <= self.env.x_range - self.delta and \
|
|
148
|
+
self.delta <= p_star[1] <= self.env.y_range - self.delta:
|
|
149
|
+
return Node((p_star[0], p_star[1]), None, 0, 0)
|
|
150
|
+
# random sample
|
|
151
|
+
else:
|
|
152
|
+
return super().generateRandomNode()
|