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.

Guidefree, runs in your browser

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.
The MPC cycle: measure, try every plan over the horizon, score each one, drive the first step, repeatmeasuregap and speedtry the plans121 of themscore each oneerror squareddrive step 1bin the restevery 0.2 s, from a fresh measurement: the receding horizon
One tick of MPC. Each plan reaches 1.6 seconds ahead, the controller drives the first 0.2 seconds of the winning one, and then measures again and plans from scratch.

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 model rises faster than the robot does and tops out at 20 cm/s when the robot manages about 18.5, but the shape is right.
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()
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

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.

The gap each of four plans predicts over the horizon, from a gap of 32 cm at 18.5 cm per second, with the cost of each00.40.81.21.6010203040seconds aheadpredicted gap, cmtarget 25 cmthe wallthe cost of each plan:its error squared, added upfull speed: cost 162760 percent: cost 379nothing: cost 29880 then 0: cost 14the winnernow
The four plans scored from the state the robot is in 4.2 s into the run: 32 cm from the wall, doing 18.5 cm/s. Full speed runs into the wall and costs 1627. Holding 60 percent ends up 13 cm past the target and costs 379. The winner eases off and costs 14, and only its first 0.2 s is driven.

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.

MPC runs at full speed until the gap is 32 cm, brakes, and reaches 25 cm at 4.8 s, going 1 cm past at worst.
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")
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

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 PD controller closes most of the gap by 5 s, then stalls 3 cm short, because 3 cm of error asks for 12 percent and the motors need 15.
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")
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

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.

The gap error measured for MPC and for the PD controller over the last part of the approach345678910111205101520time, secondserror, cmPD: stops 3 cm shortMPC: on target at 4.8 sboth were flat out until 3.2 s
The same job, measured. Up to 3.2 s both controllers are flat out and the lines lie on top of each other. MPC brakes later, reaches the target at 4.8 s and sits on it; the PD controller stops 3 cm short, where its command falls under the motors' dead band.

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.

Every plan that would go over 10 cm/s is thrown away, so the robot cruises at about 7.4 cm/s and never reads above 8.9, and the move takes 11.4 s instead of 4.8.
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")
Put this demo on your own site

Paste it into a school website, Moodle, Google Sites or a blog. More options on the embed page.

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.

The speed each command reaches over the horizon, with the plans that break the 10 cm per second limit ruled out00.40.81.21.605101520seconds aheadpredicted speed, cm/sone line per command,held for the whole horizonthe 10 cm/s limit100 %: thrown away60 %: thrown away40 %: 8 cm/s, the fastest allowed20 %: 4 cm/s
Starting from a standstill, the model says 60 and 100 percent would pass 10 cm/s inside the horizon, so those plans are not candidates at all. The fastest one left is 40 percent, which settles at 8 cm/s, and that is what the robot cruises at.

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

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.

  1. U1.2 A command is a request The robot as a system, University
  2. U1.6 Measuring your robot The robot as a system, University
  3. U5.1 The feedback loop Feedback control, University
  4. U5.2 Proportional control Feedback control, University
  5. U5.6 Saturation and windup Feedback control, University
  6. U6.2 Predict and correct State estimation, University
  7. U10.2 Velocity profiles Following a trajectory, University
  8. U12.2 Fitting a model to data Learning, and the capstone, University
Open the lessons