Model predictive control explained
What MPC does each tick: keep a model, try a set of plans a second or two ahead, score them against a cost, drive the first step of the best one. Runnable programs hold a robot 25 cm from a wall with MPC and with a PID controller, chart both errors, and keep a speed limit that is built into the plan rather than clamped afterwards.
Model predictive control, or MPC, puts a model of the machine inside the controller. Every tick it tries out a set of plans for the next second or two, scores each one against a cost, drives the first step of the best plan, and throws the rest of it away. Then it measures again and does the whole thing over. It grew up in oil refineries in the 1970s and it is now in engine control, battery chargers, drones and self-driving cars. On this page a small robot holds a gap to a wall, and each demo below is a real program you can change and run.
The chart under each robot shows the error: how many centimetres the robot is from the gap it is supposed to hold. Plus means too far from the wall, minus means too close. A good controller takes that line to zero and leaves it there, without breaking any of the limits you set.
The idea in one loop
each tick:
measure where you are and how fast you are going
for each plan you are willing to consider:
run the model forward a second or two
throw the plan away if it breaks a limit
add up how bad it looks: that is its cost
keep the cheapest plan and drive its first step
throw the rest of the plan away
Three names come out of that:
- The horizon is how far ahead each plan runs. Here it is 8 steps of 0.2 s, so 1.6 seconds.
- The cost function is how a plan is scored. Here it is the error added up over the horizon, squared, so big errors count for much more than small ones.
- Throwing away all but the first step, then planning again from a fresh measurement, is called a receding horizon. It is what keeps MPC honest when the model is wrong.
A model you can run forward
MPC cannot start until you can answer one question in code: if the robot is doing this and I send that command, what happens next? For this robot the answer is three lines. Full command is 20 cm/s, the motors do nothing below 15 percent, and the speed follows the command with a lag of about a quarter of a second.
The program
from bugbot import *
connect()
DT, TAU = 0.2, 0.25 # the tick, and the robot's lag in seconds
def model(v, cmd):
# one step of the model: under 15 % nothing moves, and
# 100 % is 20 cm/s, reached with a lag of TAU
want = 0.0 if abs(cmd) < 15 else 0.2 * cmd
return v + (want - v) * DT / TAU
plan = [100] * 10 + [40] * 10 + [10] * 5 + [-60] * 5
guess = 0.0
for cmd in plan:
drive(cmd, 0, 0)
wait(DT)
guess = model(guess, cmd)
plot("model", guess)
plot("robot", flow()[1]) # what the flow sensor underneath measures
stop()
The two lines have the same shape and they are not the same line. After the first step the model says 16 cm/s and the robot is doing 8.8. The model tops out at 20 cm/s and the robot settles near 18.5, because this robot's motors give about 9 percent less than the nominal figure. At 10 percent command the model says nothing happens, and nothing does.
A model this rough is enough. MPC does not trust it for long, because every tick it throws the plan away and starts again from what the sensors actually say.
The cost function
A plan is scored by a number, and the controller picks the smallest. The score for this job is the gap error added up over the horizon:
cost = sum over the horizon of (predicted gap - target gap)²
Squaring does two things. It makes the cost positive whichever side of the target the plan ends up on, and it makes one big error worse than several small ones, so the controller prefers a plan that is a little bit wrong all the way to one that is badly wrong once.
Anything you can measure or predict can go into the cost. Real controllers add a term for effort, so the machine does not thrash the actuators for a tiny gain, and a term for changing the command, so it does not jitter. Each term gets a weight, and choosing the weights is the MPC version of tuning gains.
MPC on the robot
The robot starts 105 cm from a wall and has to sit 25 cm from it. Every tick it reads the depth sensor and the flow sensor underneath, tries 121 plans, and drives the first step of the best one. A plan here is two commands: one for the first two steps, another for the rest of the horizon.
The program
from bugbot import *
connect()
TARGET = 25.0 # cm we want to keep from the wall
DT, STEPS = 0.2, 8 # a plan is 8 steps of 0.2 s: 1.6 s ahead
TAU = 0.25 # the robot's lag, seconds
CHOICES = range(-100, 101, 20)
def model(v, cmd):
want = 0.0 if abs(cmd) < 15 else 0.2 * cmd
return v + (want - v) * DT / TAU
def score(gap, v, first, then):
# drive `first` for 2 steps, `then` for the rest of the
# horizon, and add up how far from the target it sits
total = 0.0
for k in range(STEPS):
v = model(v, first if k < 2 else then)
gap = gap - v * DT
total = total + (gap - TARGET) ** 2
return total
for tick in range(50):
gap, v = distance(), flow()[1]
best, pick = None, (0, 0)
for first in CHOICES:
for then in CHOICES:
c = score(gap, v, first, then)
if best is None or c < best:
best, pick = c, (first, then)
drive(pick[0], 0, 0) # only the first step
# draw the plan on the mat, to see what it intends
ahead, y, sv = [], 20 + position()[1], v
for k in range(STEPS):
sv = model(sv, pick[0] if k < 2 else pick[1])
y = y + sv * DT
ahead.append((50, y))
draw("plan", ahead, "red", size=3)
plot("error", gap - TARGET)
wait(DT)
stop()
print("final gap:", distance(), "cm")
The red dots are the plan: where the model says the robot will be at each step of the horizon. Watch them pile up against the wall as the robot closes in, which is the controller seeing the stop coming before it happens.
The commands it chooses go 100, 100, ... 100, 80, 40, 0, and then a few short pulses of minus 20. Full speed until the gap is 32 cm, and the error is at zero by 4.8 s, 1 cm past the target at worst. Nobody chose those numbers. They fall out of the model and the cost.
The short reverse pulses at the end are the dead band showing up in the plan. The model knows that anything under 15 percent does nothing, so a steady small correction is not on the menu. A short pulse is, and the cost says a pulse is cheaper than sitting 1 cm out.
The same job with a PID controller
Here is the same gap, held by a PID controller instead. This one is a PD: gain 4 on the error, gain 1 on how fast the error is changing.
The program
from bugbot import *
connect()
TARGET = 25.0
KP, KD = 4.0, 1.0
DT = 0.2
last = None
for tick in range(60):
gap = distance()
error = gap - TARGET
rate = 0.0 if last is None else (error - last) / DT
last = error
drive(max(-100, min(100, KP * error + KD * rate)), 0, 0)
plot("error", error)
wait(DT)
stop()
print("final gap:", distance(), "cm")
It is not a bad controller. It brakes smoothly and does not overshoot at all. It ends 3 cm short and stays there, because at 3 cm of error 4 × 3 asks for 12 percent and the motors need 15 to move. That is steady state error, and an integral term would clear it, with the usual risk of windup.
So on this job MPC arrives about a second and a half sooner and with no steady error, and it got there without anyone tuning a gain. That is worth something, but it is not the reason industry pays for MPC.
Constraints are the reason industry uses it
A limit is not a suggestion. A chemical plant has a temperature that must not be exceeded, a battery charger has a current and a cell voltage it must not go over, a car has a wheel that must not slip. The plant is most profitable, and the charger fastest, when they run close to those limits without ever crossing them.
A PID controller cannot be told about a limit. You can clamp its output, but clamping happens after the decision, when it is already too late, and a clamped controller still winds up. MPC is told the limit before it chooses, because a plan that breaks the limit is thrown out of the running.
This demo adds one line: a plan whose predicted speed goes over 10 cm/s is not a candidate at all.
The program
from bugbot import *
connect()
TARGET = 25.0 # cm we want to keep from the wall
LIMIT = 10.0 # cm/s: no plan may go faster than this
DT, STEPS = 0.2, 8
TAU = 0.25
CHOICES = range(-100, 101, 20)
def model(v, cmd):
want = 0.0 if abs(cmd) < 15 else 0.2 * cmd
return v + (want - v) * DT / TAU
def score(gap, v, first, then):
total = 0.0
for k in range(STEPS):
v = model(v, first if k < 2 else then)
if abs(v) > LIMIT:
return None # breaks the limit: not a candidate
gap = gap - v * DT
total = total + (gap - TARGET) ** 2
return total
for tick in range(90):
gap, v = distance(), flow()[1]
best, pick = None, 0
for first in CHOICES:
for then in CHOICES:
c = score(gap, v, first, then)
if c is not None and (best is None or c < best):
best, pick = c, first
drive(pick, 0, 0)
plot("error", gap - TARGET)
plot("speed", v)
wait(DT)
stop()
print("final gap:", distance(), "cm")
The speed line rises to about 7.4 cm/s on average and stays there. The highest single reading is 8.9 cm/s, and that includes the flow sensor's own noise. The robot reaches the target at 11.4 s instead of 4.8 s, which is the price of the limit, and the limit is kept by construction rather than by luck.
Two honest details. The commands on offer are 20 percent apart, so the fastest plan that fits under 10 cm/s cruises at 8, not at 9.9. A finer set of commands would sit closer to the limit and take longer to search. And the limit is kept in the model's world: the real robot is a little slower than the model thinks, which here is the safe direction, but a model that was optimistic would let the real machine cross the line. That is why safety limits in real plants carry a margin, and why the hard interlocks stay in place underneath the controller.
How far ahead to look
The horizon has to be long enough to cover how long the machine takes to respond. Anything shorter and the controller is greedy: it takes the command that looks best over the next fraction of a second and drives straight into the thing it could not see.
This robot stops quickly. From full speed a full reverse command brings it to a stop in about a fifth of a second and under 2 cm, so even a short horizon is enough here. Change STEPS to 2 in the demo above, a horizon of 0.4 s, and the error chart comes out the same to within one tick. A car doing 30 mph needs about two seconds and twenty metres, so a car's MPC plans several seconds ahead, and a refinery column that takes an hour to settle is planned hours ahead.
Longer is not free. Every extra step is more model error piled on top of model error, and more arithmetic every tick. The usual answer is a horizon a few times the response time of the thing you are controlling, and no longer.
What MPC costs
- Arithmetic. The demo tries 121 plans of 8 steps every 0.2 s, which is about 5,000 model steps a second. That is fine in Python for a robot with one thing to decide. A car, or a plant with thirty valves, cannot search plans one at a time. Real MPC writes the cost as a quadratic program and solves it with an optimiser, which finds the best plan in one go instead of trying a list.
- A model. Feedback control needs no model at all. MPC is only as good as the one you give it, and getting that model is real work, whether by measurement or by fitting.
- A state. The model has to be started from somewhere, so you need to know the speed as well as the gap. That usually means an estimator such as a Kalman filter feeding the controller.
- Different tuning. You no longer tune three gains. You tune the horizon, the weights in the cost, and the limits, and those are easier to explain to somebody else because each one means something physical.
Where this is taught
- A command is a request is the model this page uses: the gain, the dead band and the lag that make a command different from what happens.
- Measuring your robot measures those three numbers on your own robot rather than taking them on trust.
- The feedback loop and Proportional control are the controller MPC is being compared with here.
- Saturation and windup is what a feedback controller does when it meets a limit, which is the problem MPC is built to solve.
- Predict and correct runs a model forward one step, the same move MPC makes over and over.
- Velocity profiles plans a move against limits offline, which is the other way of respecting them. The motion profiles guide goes through it.
- Fitting a model to data is where the model in the controller comes from when nobody handed you one.
Questions
What is model predictive control in simple terms?
It is a controller that keeps a model of the machine, tries out several plans for the next second or two, scores each plan against a cost, and drives the first step of the best one. Next tick it measures again and plans again from scratch.
How is MPC different from PID?
A PID controller reacts to the error it has now, using three gains and no model. MPC predicts what each possible plan would do, using a model, and picks the cheapest. PID cannot be told about a limit or about what is coming; MPC can, because both go into the plan before it acts.
What is the prediction horizon?
How far into the future each plan runs. On this page it is 8 steps of 0.2 s, so 1.6 seconds. It should cover how long the machine takes to respond to a command, and no more than a few times that, because model error grows with every step.
What is a receding horizon?
Planning a second or two ahead, using only the first step, and then planning again from a fresh measurement. The horizon keeps sliding forward, so the controller never commits to a plan it made with old information.
What is the cost function in MPC?
The number that scores a plan. Usually the tracking error squared and added up over the horizon, plus terms for effort and for changing the command, each with a weight. Tuning MPC is mostly choosing those weights.
Why is MPC used in industry?
Because it handles limits and several controlled variables at once. Plants make money by running near a limit without crossing it, and MPC is told the limits before it chooses. It started in oil refineries in the 1970s and it is now in engine management, power converters, battery charging and vehicle control.
What are the disadvantages of MPC?
It needs a model, it needs an estimate of the state to start the model from, and it needs enough computing power to solve an optimisation every tick. A badly identified model gives confident, wrong plans. For a single loop with no limits in play, a well tuned PID is simpler and does the job.
Can you write MPC in Python?
Yes. Every demo on this page is a complete Python program of about forty lines: a model function, a cost function, a loop over the plans you are willing to consider, and one drive() call with the first step of the winner. Industrial MPC replaces the loop over plans with an optimiser, but the shape of the program is the same.
Learn it step by step
These lessons build the same ideas one at a time, each with tasks the simulator marks.
- U1.2 A command is a request The robot as a system, University
- U1.6 Measuring your robot The robot as a system, University
- U5.1 The feedback loop Feedback control, University
- U5.2 Proportional control Feedback control, University
- U5.6 Saturation and windup Feedback control, University
- U6.2 Predict and correct State estimation, University
- U10.2 Velocity profiles Following a trajectory, University
- U12.2 Fitting a model to data Learning, and the capstone, University