Motion planning is the problem of getting from a start to a goal without hitting anything. The catch is that "configuration" for a real robot can be high-dimensional, and the obstacles live in that space, not in the world you can see.
Configuration space
Planning happens in configuration space, where each point is a complete pose of the robot. A path is a curve through the free part of that space. For a 2D point robot this is just the map; for an arm or a drone with orientation it has many dimensions, which is what makes planning hard.
A*: search a graph
Discretize the space into a grid or graph and run A*, which expands nodes in order of cost-so-far plus a heuristic estimate of cost-to-go. With an admissible heuristic it returns the optimal path and never over-explores. It is the right tool in low dimensions, but the number of grid cells explodes as dimensions grow.
RRT: sample instead of enumerate
Rapidly-exploring random trees avoid the grid entirely: sample a random configuration, steer the nearest tree node toward it, and add the new node if the motion is collision-free. The tree quickly fills the space, which is what makes RRT practical in high dimensions. Plain RRT finds a path, not the best one; RRT* rewires the tree as it grows and converges to the optimal path over time.
Trade-offs
- A*: complete and optimal, but limited by the curse of dimensionality.
- RRT / RRT*: scales to many dimensions, but paths are jagged and usually need smoothing.
Tree-based planners like these are what I used for autonomous navigation in GPS-denied environments, where you cannot rely on a global map and have to plan over the robot's own state.
Step by step: A*
- Keep a priority queue ordered by f = g + h (cost so far plus heuristic).
- Pop the lowest-f node; if it is the goal, reconstruct the path.
- For each free neighbor, compute a tentative g.
- If it improves, record the parent and push it with its f.
- An admissible heuristic (Manhattan here) guarantees an optimal path.
import heapq
def astar(grid, start, goal):
"""grid: 2D list, 0 = free, 1 = wall. 4-connected, Manhattan heuristic."""
h = lambda a, b: abs(a[0]-b[0]) + abs(a[1]-b[1])
R, C = len(grid), len(grid[0])
pq = [(h(start, goal), 0, start)] # (f = g + h, g, node)
came, g = {}, {start: 0}
while pq:
_, gc, cur = heapq.heappop(pq)
if cur == goal:
path = [cur]
while cur in came:
cur = came[cur]; path.append(cur)
return path[::-1]
for dr, dc in ((1,0), (-1,0), (0,1), (0,-1)):
nr, nc = cur[0]+dr, cur[1]+dc
if 0 <= nr < R and 0 <= nc < C and grid[nr][nc] == 0:
ng = gc + 1
if ng < g.get((nr, nc), float('inf')):
g[(nr, nc)] = ng
came[(nr, nc)] = cur
heapq.heappush(pq, (ng + h((nr, nc), goal), ng, (nr, nc)))
return None
Step by step: RRT
- Grow a tree from the start.
- Sample a random point and find the nearest tree node.
- Step from that node toward the sample; keep it if it is collision-free.
- Stop when a new node reaches the goal, then backtrack parents for the path.
import numpy as np
def rrt(start, goal, is_free, bounds, step=0.5, n=5000, tol=0.5):
rng = np.random.default_rng(0)
nodes, parent = [np.array(start, float)], {0: None}
lo, hi = np.array(bounds[0]), np.array(bounds[1])
goal = np.array(goal, float)
for _ in range(n):
rand = rng.uniform(lo, hi)
i = min(range(len(nodes)), key=lambda k: np.linalg.norm(nodes[k] - rand))
d = rand - nodes[i]
new = nodes[i] + d / np.linalg.norm(d) * min(step, np.linalg.norm(d))
if not is_free(new):
continue
nodes.append(new); parent[len(nodes) - 1] = i
if np.linalg.norm(new - goal) < tol: # reached the goal
path, k = [], len(nodes) - 1
while k is not None:
path.append(nodes[k]); k = parent[k]
return path[::-1]
return None
References
- Hart, Nilsson & Raphael, A Formal Basis for the Heuristic Determination of Minimum Cost Paths (A*, 1968).
- LaValle, Rapidly-Exploring Random Trees (1998).
- Karaman & Frazzoli, Sampling-Based Algorithms for Optimal Motion Planning (RRT*, 2011).
- LaValle, Planning Algorithms (Cambridge, 2006).