Potential field path planning explained
How artificial potential fields steer a robot: an attractive pull to the goal, repulsive pushes from obstacles, and the local minima where it gets stuck. Change the gains, press Run and watch a robot find its way, or not.
A potential field steers a robot towards a goal without planning a route first. The goal pulls the robot towards it, every obstacle nearby pushes it away, and at each moment the robot drives in the direction of the pull and the pushes added together. Oussama Khatib described the method in the 1980s for robot arms and mobile robots, and the same idea is still used as a local layer that dodges obstacles while a planner such as A* chooses the route. It needs no map and only a few lines of code. It also has one serious flaw: the robot can stop somewhere that is not the goal, and stay there. On this page a small robot crosses a 2 metre mat, and each demo below is a real program you can change and run.
In the overhead view of each demo, the blue line is where the robot has been. The green line is the pull towards the goal and the red line is the push away from the obstacles, both drawn from the robot, 2 cm long for every cm/s. The chart shows to goal cm, how far the robot is from the goal, and push cm/s, the size of the push. When the robot is getting there, the to goal line falls towards zero. A flat line above zero means it is stuck.
The idea in one loop
each tick:
pull = a velocity towards the goal
push = a velocity away from each obstacle within reach,
bigger the closer the obstacle is
drive at pull + push
The name comes from physics. Picture the mat as a landscape: the goal is the bottom of a valley and every obstacle is a hill. The height of the landscape at each point is the potential, and the pull and the push together are its downhill slope. The robot rolls downhill, and the bottom of the valley is the goal.
The pull
The simplest pull is a fixed speed straight at the goal. That works from far away, but the robot then arrives at full speed and overshoots. The demos ease it off near the goal:
pull = min(PULL, 4 + 0.3 * gap)
ax, ay = pull * dx / gap, pull * dy / gap
gap is the distance to the goal and (dx, dy) is the step from the robot to the goal, so (dx / gap, dy / gap) is one centimetre long and points at the goal. Far away the pull is PULL, 13 cm/s. Within 30 cm of the goal it falls by 0.3 cm/s for every centimetre, down to 4 cm/s. It never goes below 4 because below about 15 percent of full speed this robot's motors do not move it at all, so a pull that shrank to zero would leave the robot parked a few centimetres short. The program stops on distance instead, when the robot is within 8 cm of the goal.
In the textbook language, a pull of fixed size comes from a valley shaped like a cone, and a pull that grows with the distance comes from a bowl. This one is a cone far away and a bowl close in.
The push
if d < REACH:
m = PUSH * (1 / d - 1 / REACH) / d ** 2
d is the distance from the robot to the nearest point of the obstacle, and the push points straight away from that point. The shape does three jobs:
- Beyond
REACHthere is no push at all, so an obstacle on the far side of the mat changes nothing. 1 / d - 1 / REACHis zero at the edge of the reach, so the push grows from nothing instead of switching on with a jolt.- The
1 / d²makes it grow very fast close in. WithPUSH = 120000andREACH = 45, the push is 0.2 cm/s at 40 cm, 8.3 at 20 cm, 23.7 at 15 cm and 93 at 10 cm.
That last number is far faster than the robot can drive, so the demos cap each push at 60 cm/s and the total at 14 cm/s.
For a rectangular block, the nearest point is the robot's position clamped into the rectangle on each axis: (min(max(x, bx), bx + bw), min(max(y, by), by + bh)). The demos treat the four edges of the mat as obstacles too. Their nearest points are (x, 0), (x, 200), (0, y) and (200, y).
Adding them up
The velocity the robot drives at is the pull plus every push. The BugBot can drive sideways as well as forwards, so it can move in any direction without turning first. The move function in the demos holds the heading at 0 and turns a velocity across the mat into forward and sideways commands, with the inverse kinematics from the University lessons.
The pull and the push are velocities here, in cm/s. In physics they would be forces that speed the robot up. Many robot programs use the sum directly as the velocity to drive at, as these demos do, which is simpler and stops the robot as soon as the forces balance.
Every distance on this page is measured from the robot's centre. The simulated robot is a circle 7 cm across, so its body reaches 3.5 cm closer to things than the numbers say.
Sliding round a block
The robot starts at (30, 40). A block 30 cm wide and 90 cm tall stands between it and the goal, from x = 80 to 110 and from y = 10 to 100. The goal is at (170, 160), up and to the right, beyond the block's top.
The program
from bugbot import *
import math
connect()
# change these numbers and press Run
GOAL = (170, 160) # where the robot is going, cm
PULL = 13 # pull gain, cm/s
PUSH = 120000 # push gain
REACH = 45 # no push beyond this, cm
START = (30, 40) # where the robot starts
BLOCKS = [(80, 10, 30, 90)] # x, y, width, height
TOP = 14 # top speed, cm/s
def nearest(x, y):
# the nearest point of each mat edge and block
pts = [(x, 0), (x, 200), (0, y), (200, y)]
for bx, by, bw, bh in BLOCKS:
pts.append((min(max(x, bx), bx + bw),
min(max(y, by), by + bh)))
return pts
def forces(x, y):
# the pull: straight at the goal, easing off in
# the last 30 cm, but never below 4 cm/s
dx, dy = GOAL[0] - x, GOAL[1] - y
gap = math.hypot(dx, dy)
pull = min(PULL, 4 + 0.3 * gap)
ax, ay = pull * dx / gap, pull * dy / gap
# the push: away from everything within REACH
rx = ry = 0
for cx, cy in nearest(x, y):
ex, ey = x - cx, y - cy
d = math.hypot(ex, ey)
if 0 < d < REACH:
m = PUSH * (1 / d - 1 / REACH) / d ** 2
m = min(60, m)
rx, ry = rx + m * ex / d, ry + m * ey / d
return ax, ay, rx, ry
def move(vx, vy):
# drive at (vx, vy) cm/s across the mat
s = math.hypot(vx, vy)
if s > TOP:
vx, vy = vx * TOP / s, vy * TOP / s
h = math.radians(heading())
fwd = vx * math.sin(h) + vy * math.cos(h)
side = vx * math.cos(h) - vy * math.sin(h)
spin = (heading() + 180) % 360 - 180
# 20 cm/s forwards or 15 sideways is 100 %
drive(fwd * 5, side * 100 / 15, -0.8 * spin)
def arrow(name, x, y, vx, vy, colour):
draw(name, [(x, y), (x + 2 * vx, y + 2 * vy)],
colour, "line")
trail, close = [], 99
for tick in range(450): # 45 seconds
px, py = position()
x, y = START[0] + px, START[1] + py
gap = math.hypot(GOAL[0] - x, GOAL[1] - y)
if gap < 8:
break
ax, ay, rx, ry = forces(x, y)
move(ax + rx, ay + ry)
trail.append((x, y))
draw("path", trail[::5], "blue", "line")
arrow("pull", x, y, ax, ay, "green")
arrow("push", x, y, rx, ry, "red")
plot("to goal cm", gap)
plot("push cm/s", math.hypot(rx, ry))
for bx, by, bw, bh in BLOCKS:
cx = min(max(x, bx), bx + bw)
cy = min(max(y, by), by + bh)
close = min(close, math.hypot(x - cx, y - cy))
wait(0.1)
stop()
if gap < 8:
print("at the goal after", tick / 10, "s")
else:
print("stuck", round(gap), "cm from the goal, at",
round(x), round(y))
print("closest to a block:", round(close, 1), "cm")
For the first 3.5 seconds the pull has it all its own way, and the robot heads diagonally up and to the right. About 20 cm from the block, where the push is 8.3 cm/s, the push cancels the part of the pull aimed into the block and leaves the part along its face. The robot slides straight up the face, 19 to 20 cm off it, for about 6 seconds. The push is largest, 9.9 cm/s, at 9.5 s, as the robot passes the top corner. Then the pull swings it round towards the goal and the push fades to nothing.
The robot never decided to go round the top rather than the bottom. The goal is up and to the right, so the part of the pull along the face points up, and that settles it.
The gains
Three numbers set the behaviour: PULL, PUSH and REACH. Change PUSH and REACH in the demo above and the closest the robot's centre comes to the block, and the time to the goal, come out as:
| PUSH 30000 | PUSH 120000 | PUSH 500000 | |
|---|---|---|---|
| REACH 25 | 11.3 cm, 17.8 s | 16.0 cm, 18.5 s | 19.9 cm, 19.3 s |
| REACH 45 | 12.9 cm, 17.9 s | 19.1 cm, 19.0 s | 26.9 cm, 20.7 s |
| REACH 70 | 13.5 cm, 18.0 s | 20.5 cm, 19.4 s | stuck 10 cm short |
A stronger push, or a longer reach, keeps the robot further from the block and costs a little time. At PUSH = 500000 and REACH = 70 it goes wrong in a different place: the goal is 30 cm from the right edge of the mat and 40 cm from the top, both inside the reach, and the pushes from the edges hold the robot 10 cm short of the goal. There is more on that below.
PULL works the other way. With PULL = 8 the robot keeps 22.2 cm from the block and takes 30.9 s. With PULL = 20 it comes within 16.5 cm and takes 15.5 s, and with PULL = 40 within 13.3 cm in 14.7 s. A stronger pull wins against the push closer to the block, so the robot cuts closer and arrives sooner.
Stuck: a local minimum
The same program with the goal moved to (170, 40), directly behind the block.
The program
from bugbot import *
import math
connect()
# change these numbers and press Run
GOAL = (170, 40) # where the robot is going, cm
PULL = 13 # pull gain, cm/s
PUSH = 120000 # push gain
REACH = 45 # no push beyond this, cm
START = (30, 40) # where the robot starts
BLOCKS = [(80, 10, 30, 90)] # x, y, width, height
TOP = 14 # top speed, cm/s
def nearest(x, y):
# the nearest point of each mat edge and block
pts = [(x, 0), (x, 200), (0, y), (200, y)]
for bx, by, bw, bh in BLOCKS:
pts.append((min(max(x, bx), bx + bw),
min(max(y, by), by + bh)))
return pts
def forces(x, y):
# the pull: straight at the goal, easing off in
# the last 30 cm, but never below 4 cm/s
dx, dy = GOAL[0] - x, GOAL[1] - y
gap = math.hypot(dx, dy)
pull = min(PULL, 4 + 0.3 * gap)
ax, ay = pull * dx / gap, pull * dy / gap
# the push: away from everything within REACH
rx = ry = 0
for cx, cy in nearest(x, y):
ex, ey = x - cx, y - cy
d = math.hypot(ex, ey)
if 0 < d < REACH:
m = PUSH * (1 / d - 1 / REACH) / d ** 2
m = min(60, m)
rx, ry = rx + m * ex / d, ry + m * ey / d
return ax, ay, rx, ry
def move(vx, vy):
# drive at (vx, vy) cm/s across the mat
s = math.hypot(vx, vy)
if s > TOP:
vx, vy = vx * TOP / s, vy * TOP / s
h = math.radians(heading())
fwd = vx * math.sin(h) + vy * math.cos(h)
side = vx * math.cos(h) - vy * math.sin(h)
spin = (heading() + 180) % 360 - 180
# 20 cm/s forwards or 15 sideways is 100 %
drive(fwd * 5, side * 100 / 15, -0.8 * spin)
def arrow(name, x, y, vx, vy, colour):
draw(name, [(x, y), (x + 2 * vx, y + 2 * vy)],
colour, "line")
trail, close = [], 99
for tick in range(450): # 45 seconds
px, py = position()
x, y = START[0] + px, START[1] + py
gap = math.hypot(GOAL[0] - x, GOAL[1] - y)
if gap < 8:
break
ax, ay, rx, ry = forces(x, y)
move(ax + rx, ay + ry)
trail.append((x, y))
draw("path", trail[::5], "blue", "line")
arrow("pull", x, y, ax, ay, "green")
arrow("push", x, y, rx, ry, "red")
plot("to goal cm", gap)
plot("push cm/s", math.hypot(rx, ry))
for bx, by, bw, bh in BLOCKS:
cx = min(max(x, bx), bx + bw)
cy = min(max(y, by), by + bh)
close = min(close, math.hypot(x - cx, y - cy))
wait(0.1)
stop()
if gap < 8:
print("at the goal after", tick / 10, "s")
else:
print("stuck", round(gap), "cm from the goal, at",
round(x), round(y))
print("closest to a block:", round(close, 1), "cm")
Now the robot, the middle of the block's face and the goal are on one straight line. The pull points straight at the block and the push points straight back, so there is no sideways part left to slide along. At 17.3 cm from the block the push is 14.3 cm/s and the pull is 13 cm/s. What is left, 1.3 cm/s backwards, is slower than the motors can drive, so the robot stops. A perfect motor would not help: it would settle 17.7 cm from the block, where the push is exactly 13 cm/s, and stay there.
This is a local minimum: a dip in the landscape that is not the goal. The robot rolls into it, and every direction out of it is uphill, so it has no reason to leave. It is not a mistake in the arithmetic, and the gains cannot fix it. Try them: with PUSH = 5000 the robot stops 6.0 cm from the block, with PUSH = 1000000 it stops 29.2 cm away, and with PUSH = 1000 it drives into the block first. It never reaches the goal.
Three shapes cause local minima again and again:
- A wall straight across the line to the goal, as here.
- A U shape, or any obstacle with a hollow in it. The robot drives into the hollow and is pushed back by both sides and the end.
- Two obstacles with a gap between them. The pushes from both sides add up across the gap and close it, even when the robot would fit through. The next demo shows it.
A doorway that closes
A different mat: a wall across it from y = 120 to 126, with a doorway 20 cm wide from x = 90 to 110. The robot starts at (100, 25), directly below the doorway, and the goal is at (100, 175), directly above it. The gains are the same as before.
The program
from bugbot import *
import math
connect()
# change these numbers and press Run
GOAL = (100, 175) # where the robot is going, cm
PULL = 13 # pull gain, cm/s
PUSH = 120000 # push gain
REACH = 45 # no push beyond this, cm
START = (100, 25) # where the robot starts
# two walls with a 20 cm doorway between them
BLOCKS = [(0, 120, 90, 6), (110, 120, 90, 6)]
TOP = 14 # top speed, cm/s
def nearest(x, y):
# the nearest point of each mat edge and block
pts = [(x, 0), (x, 200), (0, y), (200, y)]
for bx, by, bw, bh in BLOCKS:
pts.append((min(max(x, bx), bx + bw),
min(max(y, by), by + bh)))
return pts
def forces(x, y):
# the pull: straight at the goal, easing off in
# the last 30 cm, but never below 4 cm/s
dx, dy = GOAL[0] - x, GOAL[1] - y
gap = math.hypot(dx, dy)
pull = min(PULL, 4 + 0.3 * gap)
ax, ay = pull * dx / gap, pull * dy / gap
# the push: away from everything within REACH
rx = ry = 0
for cx, cy in nearest(x, y):
ex, ey = x - cx, y - cy
d = math.hypot(ex, ey)
if 0 < d < REACH:
m = PUSH * (1 / d - 1 / REACH) / d ** 2
m = min(60, m)
rx, ry = rx + m * ex / d, ry + m * ey / d
return ax, ay, rx, ry
def move(vx, vy):
# drive at (vx, vy) cm/s across the mat
s = math.hypot(vx, vy)
if s > TOP:
vx, vy = vx * TOP / s, vy * TOP / s
h = math.radians(heading())
fwd = vx * math.sin(h) + vy * math.cos(h)
side = vx * math.cos(h) - vy * math.sin(h)
spin = (heading() + 180) % 360 - 180
# 20 cm/s forwards or 15 sideways is 100 %
drive(fwd * 5, side * 100 / 15, -0.8 * spin)
def arrow(name, x, y, vx, vy, colour):
draw(name, [(x, y), (x + 2 * vx, y + 2 * vy)],
colour, "line")
trail, close = [], 99
for tick in range(450): # 45 seconds
px, py = position()
x, y = START[0] + px, START[1] + py
gap = math.hypot(GOAL[0] - x, GOAL[1] - y)
if gap < 8:
break
ax, ay, rx, ry = forces(x, y)
move(ax + rx, ay + ry)
trail.append((x, y))
draw("path", trail[::5], "blue", "line")
arrow("pull", x, y, ax, ay, "green")
arrow("push", x, y, rx, ry, "red")
plot("to goal cm", gap)
plot("push cm/s", math.hypot(rx, ry))
for bx, by, bw, bh in BLOCKS:
cx = min(max(x, bx), bx + bw)
cy = min(max(y, by), by + bh)
close = min(close, math.hypot(x - cx, y - cy))
wait(0.1)
stop()
if gap < 8:
print("at the goal after", tick / 10, "s")
else:
print("stuck", round(gap), "cm from the goal, at",
round(x), round(y))
print("closest to a block:", round(close, 1), "cm")
The robot is 7 cm wide and the doorway is 20 cm, so there is room. The field does not see it that way. Below the doorway, the nearest points of the two walls are the corners of the doorway, 20.6 and 22.1 cm from the robot. Their sideways pushes cancel, and their backward pushes add up to 11.6 cm/s against a pull of 13. The 1.4 cm/s left over is too slow for the motors, and the robot stops. The two pushes have closed a gap the robot could drive through.
Try PUSH = 10000: the robot goes through the doorway and reaches the goal after 13.5 s, its centre never closer than 9.6 cm to a wall. REACH = 12, with PUSH back at 120000, also gets through, in 14.3 s. PUSH = 30000 still stops, 10 cm short of the wall. A weaker or shorter push lets the robot use narrow gaps, and the price is that it runs closer to walls everywhere else.
Oscillation in a narrow gap
Now try PUSH = 1000000 and REACH = 12, a push that is very steep close in. The robot gets within 6 cm of the wall and never settles. It shakes from side to side by about 2 cm, back and forth every 0.7 seconds or so, for the rest of the run, and the push on the chart jumps between 0 and 60 cm/s from one tick to the next.
The push is so steep that a movement of a centimetre changes it from nothing to the cap. The robot takes a moment to respond to each command, so by the time one push has taken effect it has already moved past the balance point, and the next push is the other way. In a long, narrow corridor the same thing makes a robot bounce from one wall to the other. The robot here is slow and its motors ignore small commands, which keeps its swings small.
Goals near obstacles
In the first demo, try GOAL = (185, 175). It is in the green corner, 15 cm from the right edge of the mat and 25 cm from the top. The robot stops 10 cm short of it, at (177, 169). There the pull has eased to 7 cm/s, the right edge 23 cm away pushes back at 4.8 cm/s and the top edge at 1.3 cm/s. What is left, 3 cm/s, is just under the slowest the motors can drive.
This is a local minimum too, and it is caused by the goal's position. The closer the robot gets, the weaker the pull and the stronger the push, so a goal close to an obstacle can be one the field never lets the robot reach. Research papers call it the GNRON problem: goals non-reachable with obstacles nearby. A shorter reach fixes this case: with REACH = 25 the robot reaches (185, 175) after 19.7 s. The usual general fix is to make the push weaker as the robot gets close to the goal, so that near the goal the pull always wins.
Escaping a local minimum
A field cannot get itself out of a local minimum, because every way out starts uphill. So programs detect that the robot is stuck and do something else for a while:
- Drive somewhere at random for a second, then go back to the field. It sometimes works and nothing guarantees it.
- Follow the obstacle's edge until the straight line to the goal is clear, then go back to the field. This is the idea behind the Bug algorithms of Lumelsky and Stepanov, which are proved to reach the goal on a flat mat whenever a route exists.
- Fill the dip in. Put an imaginary obstacle where the robot got stuck, so the dip becomes a hill and the robot rolls out of it. With enough of them the robot can fill a hollow in, slowly.
- Use a field with only one minimum. A navigation function is a potential built so that the goal is its only minimum. The distance to the goal worked out over a grid map, going round obstacles, is one. It works, and it needs the whole map and a search over it, which is what the field was supposed to avoid.
- Plan the route with something else. A planner such as A* finds the route on a map, and the field only follows it and dodges what the map did not show.
The demo below adds the second one. If the robot has moved less than 2 cm in the last 2 seconds and is not at the goal, it is stuck. It then ignores the field and follows the block, keeping it on its right at the distance where it got stuck. As soon as the straight line from the robot to the goal misses the block, grown by 10 cm for safety, it goes back to the field.
The program
from bugbot import *
import math
connect()
# change these numbers and press Run
GOAL = (170, 40) # where the robot is going, cm
PULL = 13 # pull gain, cm/s
PUSH = 120000 # push gain
REACH = 45 # no push beyond this, cm
START = (30, 40) # where the robot starts
BLOCKS = [(80, 10, 30, 90)] # x, y, width, height
TOP = 14 # top speed, cm/s
def nearest(x, y):
# the nearest point of each mat edge and block
pts = [(x, 0), (x, 200), (0, y), (200, y)]
for bx, by, bw, bh in BLOCKS:
pts.append((min(max(x, bx), bx + bw),
min(max(y, by), by + bh)))
return pts
def forces(x, y):
# the pull: straight at the goal, easing off in
# the last 30 cm, but never below 4 cm/s
dx, dy = GOAL[0] - x, GOAL[1] - y
gap = math.hypot(dx, dy)
pull = min(PULL, 4 + 0.3 * gap)
ax, ay = pull * dx / gap, pull * dy / gap
# the push: away from everything within REACH
rx = ry = 0
for cx, cy in nearest(x, y):
ex, ey = x - cx, y - cy
d = math.hypot(ex, ey)
if 0 < d < REACH:
m = PUSH * (1 / d - 1 / REACH) / d ** 2
m = min(60, m)
rx, ry = rx + m * ex / d, ry + m * ey / d
return ax, ay, rx, ry
def move(vx, vy):
# drive at (vx, vy) cm/s across the mat
s = math.hypot(vx, vy)
if s > TOP:
vx, vy = vx * TOP / s, vy * TOP / s
h = math.radians(heading())
fwd = vx * math.sin(h) + vy * math.cos(h)
side = vx * math.cos(h) - vy * math.sin(h)
spin = (heading() + 180) % 360 - 180
# 20 cm/s forwards or 15 sideways is 100 %
drive(fwd * 5, side * 100 / 15, -0.8 * spin)
def arrow(name, x, y, vx, vy, colour):
draw(name, [(x, y), (x + 2 * vx, y + 2 * vy)],
colour, "line")
def block_near(x, y):
# the nearest point of the nearest block
pts = nearest(x, y)[4:] # not the mat edges
here = (x, y)
return min(pts, key=lambda p: math.dist(p, here))
def line_clear(x, y):
# does the straight line to the goal miss every
# block, grown by 10 cm?
for k in range(51):
px = x + (GOAL[0] - x) * k / 50
py = y + (GOAL[1] - y) * k / 50
for bx, by, bw, bh in BLOCKS:
if (bx - 10 < px < bx + bw + 10 and
by - 10 < py < by + bh + 10):
return False
return True
trail, follow, hold, calm = [], False, 0, 0
for tick in range(450): # 45 seconds
px, py = position()
x, y = START[0] + px, START[1] + py
gap = math.hypot(GOAL[0] - x, GOAL[1] - y)
if gap < 8:
break
trail.append((x, y))
calm = calm + 1
# stuck: moved under 2 cm in the last 2 seconds
if (not follow and calm > 20
and math.dist(trail[-21], (x, y)) < 2):
follow = True
cx, cy = block_near(x, y)
hold = math.hypot(x - cx, y - cy)
print("stuck at", tick / 10,
"s: follow the block")
# free: the straight line to the goal is clear
if follow and line_clear(x, y):
follow, calm = False, 0
print("clear at", tick / 10,
"s: back to the field")
ax, ay, rx, ry = forces(x, y)
if follow:
# go round with the block on the right, at
# the distance where the robot got stuck
cx, cy = block_near(x, y)
d = math.hypot(x - cx, y - cy)
nx, ny = (x - cx) / d, (y - cy) / d
out = 0.8 * (hold - d)
move(10 * ny + out * nx, -10 * nx + out * ny)
else:
move(ax + rx, ay + ry)
draw("path", trail[::5], "blue", "line")
arrow("pull", x, y, ax, ay, "green")
arrow("push", x, y, rx, ry, "red")
plot("to goal cm", gap)
plot("push cm/s", math.hypot(rx, ry))
wait(0.1)
stop()
if gap < 8:
print("at the goal after", tick / 10, "s")
else:
print("stuck", round(gap), "cm from the goal")
The robot stops at about 3.5 s, and the stall check notices at 4.7 s. It goes up the face of the block, 17 to 20 cm off it, and across the top, 18 to 20 cm above it. At 17.6 s, at about (112, 120), the line to the goal is clear, and the field carries the robot down the far side to the goal.
Watch the chart. While the robot goes round, its distance to the goal rises from 107 cm to 126.5 cm. Escaping a local minimum means going further away for a while, and a field on its own never chooses to do that.
The program picks which way to go round before it starts, and it is a guess. Here, going round the top works. On another mat the same choice could lead into a longer way round, which is why the Bug algorithms have rules for when to give up on the edge and when to leave it.
Potential fields and planners
| Potential field | A* on a grid | |
|---|---|---|
| Needs | the obstacles near the robot | a map of the whole area |
| Work | a few sums every tick | a search, once for each plan |
| Finds a route when there is one | not always: it can stop in a local minimum | yes, on the grid |
| Says when there is no route | no, it just stops | yes |
| Copes with something moving into the way | at once | only after planning again |
The two are good at different things, so real robots often use both. A global planner such as A* works out the route on a map, and a local layer, a potential field or something like it, follows the route and steers round whatever the map did not show. A path follower such as pure pursuit can do the following part. The University lessons below build a potential field, then plan a route with A* on a grid and drive it.
How to choose the gains
- Set
PULLto the speed you want the robot to cruise at, and cap the total a little above it. - Set
REACHto how far away obstacles should start to matter. Too short and the robot turns aside late. Too long and walls far away bend its path, and goals near walls become hard to reach. - Raise
PUSHuntil the robot passes obstacles with the room you want. Print the closest distance, as the demos do, and read it off. - Check the narrowest gap the robot has to use. If the push closes it, lower
PUSHorREACH. In the doorway, 120000 closed a 20 cm gap and 10000 went through. - If the robot shakes near walls, the push is too steep close in. Lower
PUSHor raiseREACH. - Test a wall straight across the line to the goal. No gains get past it, so the program needs a way out, such as the stall check above.
Questions
What is potential field path planning?
It is a way to move a robot to a goal without planning a route. The goal pulls the robot towards it, each obstacle within a set distance pushes it away, and the robot drives in the direction of the sum. The pull and the push are the slope of an imaginary landscape in which the goal is the lowest point and the obstacles are hills, so the robot rolls downhill to the goal.
How do you calculate the attractive and repulsive forces?
The attractive pull points at the goal. It is either a fixed size, or proportional to the distance to the goal, or, as on this page, a fixed size far away that eases off close in. The repulsive push from each obstacle points away from its nearest point, and its size is PUSH × (1/d − 1/REACH) / d² when the distance d is less than REACH, and zero beyond. Add the pull and all the pushes as vectors, and cap the result at the robot's top speed.
What is a local minimum in a potential field?
It is a place that is not the goal where the pull and the pushes cancel, so the robot stops. Every direction out of it is uphill, so the robot stays there. On this page a block straight across the line to the goal made one: the robot stopped 17.3 cm from the block and 107 cm from the goal, and no choice of gains got it past.
How do you escape a local minimum?
Detect that the robot is stuck, for example that it has moved less than 2 cm in 2 seconds away from the goal, and do something else for a while. Follow the obstacle's edge until the line to the goal is clear, drive somewhere at random, or place an imaginary obstacle where the robot got stuck. On this page, following the block's edge got the robot from the local minimum to the goal in 26.4 s. The reliable fix is to plan the route with a search such as A* and use the field only to follow it.
Why does a robot oscillate in a narrow passage?
In a narrow passage the robot is close to walls on both sides, where the push is steep. A small movement towards one wall makes that wall's push much bigger, and because the robot takes a moment to respond, it has already gone past the middle by the time the push acts. The next push is the other way. Lowering the push gain, or making the push less steep close in, calms it.
Why can't my robot reach a goal near a wall?
Near the goal the pull is small and the push from the wall is large, and close enough to the wall the push wins, so the robot stops short. On this page a goal 15 cm from the edge of the mat left the robot 10 cm short. Shorten the reach of the push, or make the push weaker as the robot gets close to the goal.
Is the potential field method complete?
No. A complete planner finds a route whenever one exists and reports when none does. A potential field only looks at the slope where the robot is standing, so it can stop in a local minimum with a route in plain view, and it cannot tell the difference between that and a goal it cannot reach. Search on a grid, such as A*, is complete on its grid.
What is the difference between potential fields and A*?
A searches a map and returns a whole route before the robot moves, and it finds a route whenever the grid has one. A potential field works out one velocity at a time from the obstacles near the robot, with no map and no search, which makes it quick and good at dodging things that move, but it can get stuck. Many robots use A for the route and a local method, such as a potential field, to follow it.
How do you implement a potential field in Python?
Each tick, read the robot's position. Work out the pull as a vector towards the goal. For each obstacle, find its nearest point, and if it is within the reach add a push away from it. Add them up, cap the speed, turn the velocity into motor commands, wait a tenth of a second and do it again. Stop when the robot is close enough to the goal. The first demo on this page does exactly that in about 80 lines of Python.
Is potential field path planning on the GCSE or A level specification?
Not by name. None of the GCSE or A level Computer Science specifications (AQA, OCR, Edexcel, Eduqas) include potential fields. The maths is adding vectors, which is in GCSE Maths, and unit vectors and forces, which are in A level Maths. It makes a good A level Computer Science programming project, and a stuck robot is a clear way to show why search algorithms such as A* matter.
Learn it step by step
These lessons build the same ideas one at a time, each with tasks the simulator marks.
- U9.1 The configuration space Planning, University
- U9.5 Potential fields Planning, University
- U9.7 Project: plan a route and drive it Planning, University