Introduction

Imagine driving toward a stop sign. You do not compute the single correct amount of brake pressure for the whole approach and commit to it. You glance ahead, form a rough plan for the next few seconds, ease off the brake a little, then look again and revise. Model Predictive Control (MPC) is that habit turned into an algorithm.

At every tick, MPC uses a model of the system to predict how it will behave over a short window into the future — the horizon. It searches for the sequence of actions over that window that best reaches a goal without violating any limits: a maximum speed, a temperature ceiling, a fuel budget. Then it does something almost wasteful-looking: it throws away almost the whole plan and applies only its first action.

One tick later, with fresh measurements in hand, it plans again from scratch over a new window shifted one step forward — the receding horizon. That constant re-optimization is not indecision. It is how MPC stays exact about constraints while still reacting to a world that never behaves exactly like the model predicted.

Steer to the Target

Below is a cart on a line. Set a target position and a speed limit, then let the controller drive there. At every step it searches a short horizon of future acceleration sequences, keeps only the ones that never break the speed limit, picks the plan that lands closest to the target, and applies just its first acceleration.

<p class="hint">The dot is a cart on a track. At every tick, the controller looks <b>%horizon% steps</b> ahead over every possible sequence of accelerations, discards any sequence that would ever push the speed above the limit, keeps the one that lands closest to the target — then applies only its <b>first</b> move and throws the rest of the plan away.</p>
<div class="controls">
  <label>Target position
    <input type="range" id="target" min="-12" max="12" step="1" value="8">
    <span id="targetVal" class="val">8</span>
  </label>
  <label>Speed limit
    <input type="range" id="vmax" min="0.2" max="2" step="0.1" value="1">
    <span id="vmaxVal" class="val">1.0</span>
  </label>
  <label>Horizon length
    <input type="range" id="horizon" min="2" max="6" step="1" value="4">
    <span id="horizonVal" class="val">4</span>
  </label>
</div>
<canvas id="track" width="560" height="150"></canvas>
<div class="status" id="status">Press Run and watch it steer.</div>
<div class="btns">
  <button id="run" type="button">Run</button>
  <button id="step" type="button">Step once</button>
  <button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: .6rem; }
.controls label { display: flex; flex-direction: column; font-size: .8rem; font-weight: 600; color: #1d3557; gap: .2rem; }
.controls input[type="range"] { width: 150px; }
.val { font: 700 13px ui-monospace, monospace; color: #333; }
canvas { width: 100%; max-width: 560px; height: 150px; background: #f4f6f8; border: 1px solid #cdd9e3; border-radius: 8px; display: block; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));

// Discrete-time model: velocity changes by the chosen acceleration each tick, position changes by velocity.
const DT = 1;
const ACCELS = [-0.2, 0, 0.2]; // The 3 discrete actions available each tick: brake, coast, accelerate.
const POS_MIN = -14, POS_MAX = 14;

let pos = 0, vel = 0, target = 8, vmax = 1.0, horizon = 4;
let running = false, timer = null, tick = 0;
const MAX_TICKS = 200;

// Simulates one candidate action sequence and scores it; infeasible if the speed limit is ever exceeded.
function simulate(p0, v0, actions, vmaxLocal, targetLocal) {
  let p = p0, v = v0, cost = 0;
  for (const a of actions) {
    v = v + a * DT;
    if (Math.abs(v) > vmaxLocal + 1e-9) return { cost: Infinity, feasible: false };
    p = p + v * DT;
    const err = targetLocal - p;
    cost += err * err;
  }
  cost += 0.5 * v * v; // Small penalty on ending velocity so the controller prefers to settle at the target rather than coast past it.
  return { cost, feasible: true };
}

function* sequences(depth) {
  if (depth === 0) { yield []; return; }
  for (const a of ACCELS) for (const rest of sequences(depth - 1)) yield [a, ...rest];
}

// The core MPC step: search every sequence over the horizon, keep the feasible one with lowest cost.
function mpcStep(p0, v0, targetLocal, vmaxLocal, horizonLocal) {
  let best = null, bestCost = Infinity;
  for (const seq of sequences(horizonLocal)) {
    const { cost, feasible } = simulate(p0, v0, seq, vmaxLocal, targetLocal);
    if (!feasible) continue;
    if (cost < bestCost) { bestCost = cost; best = seq; }
  }
  return best ? best[0] : 0; // Receding horizon — only the first action of the best plan is ever applied; everything else is recomputed next tick.
}

function readControls() {
  target = Number(document.getElementById('target').value);
  vmax = Number(document.getElementById('vmax').value);
  horizon = Number(document.getElementById('horizon').value);
  document.getElementById('targetVal').textContent = target;
  document.getElementById('vmaxVal').textContent = vmax.toFixed(1);
  document.getElementById('horizonVal').textContent = horizon;
}

function toX(p) {
  const canvas = document.getElementById('track');
  const w = canvas.width;
  return ((p - POS_MIN) / (POS_MAX - POS_MIN)) * w;
}

function draw() {
  const canvas = document.getElementById('track');
  const ctx = canvas.getContext('2d');
  const w = canvas.width, h = canvas.height, midY = h / 2;
  ctx.clearRect(0, 0, w, h);
  // track line
  ctx.strokeStyle = '#adb1b8'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(0, midY); ctx.lineTo(w, midY); ctx.stroke();
  // target marker
  const tx = toX(target);
  ctx.strokeStyle = '#e63946'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(tx, midY - 35); ctx.lineTo(tx, midY + 35); ctx.stroke();
  ctx.fillStyle = '#e63946'; ctx.font = '11px system-ui, sans-serif'; ctx.textAlign = 'center';
  ctx.fillText('target', tx, midY - 40);
  // cart
  const cx = toX(pos);
  ctx.fillStyle = '#1d3557';
  ctx.beginPath(); ctx.arc(cx, midY, 12, 0, Math.PI * 2); ctx.fill();
  // velocity indicator arrow
  if (Math.abs(vel) > 0.01) {
    const dir = vel > 0 ? 1 : -1;
    ctx.strokeStyle = '#457b9d'; ctx.lineWidth = 3;
    ctx.beginPath(); ctx.moveTo(cx, midY); ctx.lineTo(cx + dir * 22, midY); ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(cx + dir * 22, midY - 5);
    ctx.lineTo(cx + dir * 28, midY);
    ctx.lineTo(cx + dir * 22, midY + 5);
    ctx.fill();
  }
}

function setStatus(t, cls) {
  const s = document.getElementById('status');
  s.textContent = t; s.className = 'status ' + cls;
}

function stepOnce() {
  const a = mpcStep(pos, vel, target, vmax, horizon);
  vel = vel + a * DT;
  pos = pos + vel * DT;
  tick++;
  draw();
  const dist = Math.abs(pos - target);
  setStatus(i18nFmt('pos %pos% · vel %vel% · tick %tick%', { pos: pos.toFixed(2), vel: vel.toFixed(2), tick: tick }), 'info');
  if (dist < 0.15 && Math.abs(vel) < 0.05) {
    stop();
    setStatus(i18nFmt('Arrived and settled after %tick% ticks.', { tick: tick }), 'ok');
    return true;
  }
  if (tick >= MAX_TICKS) { stop(); return true; }
  return false;
}

function run() {
  if (running) return;
  readControls();
  running = true;
  document.getElementById('run').disabled = true;
  document.getElementById('step').disabled = true;
  timer = setInterval(() => { if (stepOnce()) stop(); }, 120);
}

function stop() {
  running = false;
  clearInterval(timer);
  document.getElementById('run').disabled = false;
  document.getElementById('step').disabled = false;
}

function resetAll() {
  stop();
  pos = 0; vel = 0; tick = 0;
  readControls();
  draw();
  setStatus('Press Run and watch it steer.', '');
}

document.getElementById('run').addEventListener('click', run);
document.getElementById('step').addEventListener('click', () => { readControls(); stepOnce(); });
document.getElementById('reset').addEventListener('click', resetAll);
['target', 'vmax', 'horizon'].forEach((id) => {
  document.getElementById(id).addEventListener('input', () => { if (!running) { readControls(); draw(); } });
});

readControls();
draw();
Result
Console

Push the speed limit down and watch the cart brake earlier — it is not reacting late, it is foreseeing the limit several steps ahead inside its horizon before it becomes urgent. Push the horizon length up or down and you trade foresight for raw compute: at every single tick, MPC resolves the whole small optimization problem again from scratch.

The Real Complexity

MPC is not one algorithm — it is a recipe: at every time step, solve an optimization problem, apply the first move, discard the rest, and repeat. How hard that recipe is depends entirely on what is inside the box.

  • The model. If the system's dynamics are linear and the cost is quadratic, the per-step problem is a quadratic program (QP) — convex, and solvable in polynomial time with well-understood methods. This is the workhorse case, called linear MPC.
  • The horizon. Searching NN steps ahead with mm possible actions per step naively costs O(mN)O(m^{N}) if you enumerate sequences — exactly the brute force the small demo above uses for a horizon short enough to stay fast. Real solvers avoid the explosion with convex optimization or dynamic programming instead of enumeration.
  • The constraints. Simple bounds like a speed limit keep the feasible region convex. But once decisions become discrete — turn this valve on or off, commit to this gear — the problem becomes mixed-integer programming, which is NP-hard in general, the same complexity class behind problems like job-shop scheduling or the knapsack problem.
  • The nonlinearity. If the dynamics themselves are nonlinear (a drone's rotation, a chemical reactor), the per-step problem loses convexity, and solvers can only guarantee a locally optimal plan, not a global one.

So the honest complexity of MPC is really a spectrum, from "solve a small QP a thousand times a second" to "solve an NP-hard puzzle before the plane needs to know what to do next." Engineers spend enormous effort keeping real systems on the cheap end of that spectrum — approximating, linearizing, and simplifying constraints — precisely so the controller can finish thinking before the world moves on.

Where It Matters

MPC became industry's favorite advanced controller because so many real systems share the same shape: a target to chase and hard limits that must never be crossed.

  • Chemical and oil refining: MPC has run large-scale process plants since the 1980s, juggling temperatures, flows and pressures that can never leave safe operating ranges.
  • Autonomous vehicles and robotics: self-driving cars and robot arms plan a short trajectory, respect speed and torque limits, then replan as sensors update — precisely the loop in the demo above.
  • Aerospace: spacecraft rendezvous, rocket landings and drone flight all use MPC variants to hit a target state while obeying fuel, thrust and attitude constraints.
  • Energy systems: power grids and building climate control use MPC to balance supply and demand ahead of time instead of only reacting after the fact.

Any time you hear "the system optimizes over a rolling window and only commits to the next step," you are looking at a relative of the same idea family that connects to linear programming and integer programming at its foundations.

Conclusion

Model Predictive Control turns the ordinary act of steering into a tiny optimization problem, solved again and again, one horizon at a time. Its trick is not cleverness at any single instant — it is the discipline of planning ahead, acting once, and never trusting the plan further than that.

Whether the per-step problem takes a millisecond or minutes depends on the shape hiding inside it: a friendly convex QP, or something as hard as any NP-hard puzzle. Either way, the receding horizon is the same humble idea driving refineries, rovers and robots alike — look ahead, commit briefly, and always be ready to think again.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/model-predictive-control/Content licensed under CC BY-NC 4.0.