Model Predictive Control (MPC) vs PID: Constraints, Prediction Horizons and a Hands-On OSQP and acados Tutorial (2026)
A PID loop knows one thing about your plant: the error right now, plus a little history. It does not know that the valve will hit its stop in four seconds, that the product temperature must never exceed a quality limit, or that the next disturbance is already visible upstream. Model predictive control (MPC) is the control law that does know, because at every sample it solves a small optimization problem over a predicted future, with the limits written into the problem as hard constraints.
That difference is why MPC runs the most economically valuable loops in refineries and chemical plants, and why it is now showing up in quadrotors, autonomous vehicles and edge gateways beside a PLC. In this article I build MPC from the ground up: the prediction model, the cost, the constraints and the receding horizon; the linear case as a quadratic program; the nonlinear case as sequential quadratic programming with real-time iterations; how to size the horizon and weights; and what it takes to run the optimizer inside a latency budget. There is runnable Python with OSQP that compares MPC against PID on a constrained step, and an acados sketch for the nonlinear case.
What this covers: the MPC formulation, linear MPC as a QP, nonlinear MPC with SQP-RTI, stability and terminal sets, tuning, explicit MPC, solver choice (OSQP, acados, CasADi, do-mpc, HPIPM), PLC and edge deployment, digital-twin-in-the-loop, and an honest list of failure modes.
Context and Background
Industrial MPC is older than most engineers assume. Model-based predictive controllers entered refineries in the late 1970s: Richalet and colleagues published model predictive heuristic control (IDCOM) in 1978, and Cutler and Ramaker’s dynamic matrix control (DMC) at Shell followed at about the same time. Qin and Badgwell’s widely cited survey of industrial MPC technology (Control Engineering Practice, 2003) documents thousands of installed applications, concentrated in refining and petrochemicals. Those first-generation controllers used step-response models and a layered architecture: a slow economic optimizer set targets, and a faster dynamic MPC layer tracked them, sitting above the regulatory PID loops that actually moved valves.
That layering is still the dominant pattern in process plants. MPC rarely replaces PID outright; it supervises it. The MPC computes setpoints for dozens of PID loops, because the PID loops already provide fast, robust, well-understood actuator-level regulation, and operators trust them. If you want the loop-level foundation first, my article on PID controller tuning, anti-windup and cascade control covers what MPC sits on top of.
What changed in the last fifteen years is compute and tooling. Embedded-quality QP solvers now run in microseconds to milliseconds on commodity CPUs. Automatic differentiation through CasADi lets you write a nonlinear model once and get exact derivatives. Code generation produces dependency-free C. So MPC moved from “expensive supervisory software” to “a library you call in a 10 ms loop”. For the plant-wide, digital-twin-centred view of the same idea, see our MPC with digital twins overview; this article focuses on the optimizer, the maths and the solvers.
The competitive landscape of open tools is also worth knowing. OSQP is an Apache 2.0 licensed solver for convex quadratic programs. According to its documentation it uses a custom ADMM-based first-order method that needs only one matrix factorization, in the setup phase. acados is a BSD 2-Clause licensed framework whose core is written in C on top of the BLASFEO linear algebra library, with Python and MATLAB interfaces and CasADi for modelling (Verschueren et al., acados paper). do-mpc is a Python toolbox that wraps CasADi and IPOPT for robust and nonlinear MPC prototyping; I cite it from general knowledge rather than a source I re-read this week.
The Core Idea: Predict, Optimize, Apply One Move, Repeat
Direct answer. Model predictive control computes the control input by minimizing a cost over a finite prediction horizon, using a plant model and subject to input and state constraints. It applies only the first computed move, then re-measures and re-solves at the next sample. This receding-horizon loop gives feedback, and the constraints make it fundamentally different from PID.

Figure 1: The receding-horizon loop. The optimizer plans N moves, only the first is sent to the plant, the estimator updates the state, and the horizon shifts forward by one sample.
Figure 1 shows the loop. Read it as four ingredients wired together, and notice that each is a design decision, not a given.
The prediction model
MPC needs a model that maps the current state and a candidate input sequence to future outputs. In discrete time with sample period Ts the simplest form is linear: x(k+1) = A x(k) + B u(k), y(k) = C x(k). The model can come from first principles, system identification, step tests, or, increasingly, a calibrated digital twin. The model does not have to be perfect, because feedback corrects it every sample, but a model with the wrong gain sign or a badly wrong time constant will produce confidently wrong plans. Model quality sets the ceiling on MPC performance, and in industrial practice most of the project effort goes into plant testing and model maintenance, not into the optimizer.
The cost function
The standard tracking cost sums stage costs over the horizon of N steps and adds a terminal term: J = sum over k of (x_k – x_ref)’ Q (x_k – x_ref) + (u_k – u_ref)’ R (u_k – u_ref) + (x_N – x_ref)’ P (x_N – x_ref). Q penalizes tracking error, R penalizes control effort, and P is a terminal weight whose choice matters for stability (more below). Economic MPC replaces the quadratic tracking cost with a direct economic objective such as energy cost or throughput, which is where much of the money is made in the process industries.
The constraints
Constraints are what justify the whole machinery. Actuator limits (u_min <= u <= u_max), rate limits (|u_k – u_(k-1)| <= du_max), and state or output limits (y <= y_max) are written directly as linear inequalities on the decision variables. The optimizer then plans around them rather than discovering them by hitting them. This is the heart of constrained optimal control: a PID loop treats a saturated actuator as a nuisance to be patched with anti-windup, whereas MPC treats it as a known feature of the problem and chooses a trajectory that uses the available range as efficiently as possible.
Why only the first move is applied
The solver returns N moves, but the model is imperfect and disturbances arrive. Applying the entire plan open loop would be feedforward, with no way to correct errors. Applying one move and re-solving from the new measured state turns the optimization into a feedback law, u = kappa(x), that happens to be defined implicitly by an optimization problem. That is the receding horizon principle, and it is why MPC is robust to model error despite being computed from a model.
Linear MPC is a quadratic program
For linear dynamics, a quadratic cost and linear constraints, the problem is a convex quadratic program (QP): minimize 0.5 z’Hz + q’z subject to l <= Az <= u. Two formulations are common. In the sparse (simultaneous) form, the decision vector z stacks all states and inputs, and the dynamics enter as equality constraints. The matrices are large but extremely sparse and block-banded. In the condensed (sequential) form, you eliminate the states by substituting the dynamics, leaving only the N x nu inputs as variables, with a smaller but dense Hessian.
The trade is classic. Condensing shrinks the variable count but the dense linear algebra scales roughly cubically with horizon length, while the sparse form has structure that Riccati-style or interior-point solvers exploit to scale linearly in N. For short horizons and few inputs, condensing is usually fastest. For long horizons or many states, keep it sparse. First-order methods like OSQP’s ADMM tolerate either, and the sparse form is the natural fit because the solver’s one-time factorization exploits the sparsity.
MPC vs PID on a Constrained Step: Where the Gap Actually Opens
The honest answer to “MPC vs PID” is that on a single, well-behaved, unconstrained loop with a first-order-plus-dead-time response, a well-tuned PID is hard to beat and far cheaper to maintain. MPC earns its keep in specific situations. Let me be precise about them, then show the numbers from a runnable experiment later in this article.
| Situation | PID | MPC |
|---|---|---|
| Single loop, unconstrained, benign dynamics | Excellent, trivial to maintain | Overkill |
| Actuator saturates during large moves | Needs anti-windup patches; overshoot risk | Plans around the limit natively |
| Hard output limit (quality, safety margin) | Cannot enforce; only back off the setpoint | Enforces as a constraint |
| Strong interaction between loops (MIMO) | Decouplers, detuning, many loops fighting | One multivariable problem |
| Long dead time or known upcoming disturbance | Smith predictor, feedforward add-ons | Prediction and feedforward are built in |
| Setpoint trajectory known ahead (batch, path) | Reacts after error appears | Previews the reference |
| Cost to commission and maintain | Low | High: model, estimator, solver, monitoring |
Three mechanisms explain the gap, and each is worth stating plainly.
First, constraint awareness. A PID with integral action winds up when the actuator saturates, because the integrator keeps accumulating error the plant cannot respond to. Back-calculation or clamping fixes the worst of it, but the controller still does not know the limit exists in advance. MPC knows, so it decelerates the approach early enough that the final trajectory stays inside bounds.
Second, preview. A PID acts only on the present error and its derivative. MPC sees the future reference and the predicted response, so it can start moving before an error develops. That matters with dead time, where reacting late is baked into the structure of the loop.
Third, multivariable coordination. Two PID loops on a coupled process, say a reactor temperature and a pressure sharing one feed valve, fight each other unless decoupled by hand. MPC treats both outputs and all inputs in one cost, so the coupling is in the model and the trade-offs are in the weights.
None of these is magic. The price is that you must supply a model, an estimator, a solver, and a way to detect when any of them is wrong. I will return to that price in the failure-modes section.
A note on terminology: MPC is not “a better PID”
Several practitioners describe MPC as an advanced PID. It is more useful to think of the two as different tools in a hierarchy. PID is a model-free (or very low-order-model) feedback law, cheap to evaluate and good at rejecting slow disturbances at the actuator. MPC is a model-based optimizer, expensive to evaluate and good at coordinating multiple variables against limits. In the common layered architecture, MPC outputs go to PID setpoints. If you find yourself replacing every PID in a plant with a single MPC, stop and ask which failure mode you are trading for which.
Nonlinear MPC: NLP, SQP and the Real-Time Iteration
When the plant is meaningfully nonlinear (a pH neutralization curve, a chemical reactor with Arrhenius kinetics, a quadrotor or a vehicle with tyre forces) the linear model is valid only near an operating point. Nonlinear MPC (NMPC) keeps the full model x(k+1) = f(x(k), u(k)) and solves a nonlinear program (NLP) at each sample: the cost may still be quadratic, but the equality constraints from the dynamics are now nonlinear, and the problem is no longer convex in general.

Figure 2: One real-time iteration cycle. The nonlinear problem is turned into a single structured QP per sample, and the previous solution warm starts the next.
Sequential quadratic programming
The standard workhorse for NMPC is sequential quadratic programming (SQP). At the current guess, SQP linearizes the constraints and builds a quadratic model of the cost, solves the resulting QP for a step, updates the guess, and repeats until convergence. Each SQP iteration is itself a structured QP of the same shape as linear MPC. Direct multiple shooting, where the state at each node is a decision variable and continuity is enforced by equality constraints, is the usual transcription because it is numerically stable and exposes exactly the sparsity that structure-exploiting QP solvers want.
The real-time iteration scheme
Running SQP to convergence at every 10 ms sample is rarely affordable. The real-time iteration (RTI) scheme, introduced by Diehl, Bock and Schloeder (SIAM Journal on Control and Optimization, 2005), makes a bold simplification: perform exactly one SQP iteration per sample, then apply the first move. The insight is that consecutive problems differ only slightly (the state moved by one sample), so warm-started single iterations track the optimal solution closely, and the cost per sample becomes a fixed, predictable quantity rather than a variable one.
RTI also splits the work into two phases, which is the practical reason it suits real-time hardware. The preparation phase does everything that does not depend on the newest measurement: integrating the model, evaluating sensitivities, and building the QP matrices. This can run in the idle time between samples. The feedback phase then only inserts the new state estimate, solves the QP and returns the control, so the delay between measurement and actuation is far shorter than the full cycle time. acados implements exactly this, and its documentation lists the standard SQP, real-time iterations and advanced-step RTI among its supported algorithms (acados features).
What acados actually gives you
According to the acados paper, the framework is “modular”, written in C with the BLASFEO linear algebra library, with HPIPM as the default QP framework and interfaces for MATLAB and Python, and models expressed in CasADi (arXiv:1910.13753). The Python interface generates C code for a problem you define, compiles it, and gives you a solver object you call in your control loop. The generated solver is a self-contained C library that can be deployed on an embedded target without Python.
Here is the shape of a minimal NMPC problem in acados’s Python interface, a cart-pole-style swing-up pattern reduced to its skeleton. I have not executed this snippet in the writing environment, because acados needs a compiled C toolchain and its own build; treat it as a structural sketch and check the current acados examples for exact option names, since the API has been evolving (for instance, the horizon-length attribute naming has changed across releases).
# SKELETON ONLY - not executed here. Verify option names against your acados version.
import numpy as np
import casadi as ca
from acados_template import AcadosOcp, AcadosModel, AcadosOcpSolver
def make_model():
x = ca.SX.sym("x", 2) # [position, velocity]
u = ca.SX.sym("u", 1)
xdot = ca.SX.sym("xdot", 2)
f_expl = ca.vertcat(x[1], u[0] - 0.1 * x[1]) # toy nonlinear-ready dynamics
m = AcadosModel()
m.name = "toy_nmpc"
m.x, m.u, m.xdot = x, u, xdot
m.f_expl_expr = f_expl
m.f_impl_expr = xdot - f_expl
return m
ocp = AcadosOcp()
ocp.model = make_model()
Tf, N = 2.0, 40 # horizon length in seconds and shooting nodes
# Least-squares tracking cost on [x; u]
ocp.cost.cost_type = "LINEAR_LS"
ocp.cost.cost_type_e = "LINEAR_LS"
ocp.cost.W = np.diag([10.0, 1.0, 0.1])
ocp.cost.W_e = np.diag([10.0, 1.0])
ocp.cost.Vx = np.vstack([np.eye(2), np.zeros((1, 2))])
ocp.cost.Vu = np.vstack([np.zeros((2, 1)), np.eye(1)])
ocp.cost.Vx_e = np.eye(2)
ocp.cost.yref = np.zeros(3)
ocp.cost.yref_e = np.zeros(2)
# Hard input bounds, and an initial-state constraint that the loop overwrites each sample
ocp.constraints.lbu = np.array([-1.0])
ocp.constraints.ubu = np.array([1.0])
ocp.constraints.idxbu = np.array([0])
ocp.constraints.x0 = np.zeros(2)
ocp.solver_options.qp_solver = "PARTIAL_CONDENSING_HPIPM"
ocp.solver_options.nlp_solver_type = "SQP_RTI" # one SQP iteration per sample
ocp.solver_options.integrator_type = "ERK"
ocp.solver_options.tf = Tf
# horizon length: set via ocp.solver_options.N_horizon (newer) or ocp.dims.N (older)
solver = AcadosOcpSolver(ocp) # generates and compiles C code
x_now = np.zeros(2)
for k in range(100):
solver.set(0, "lbx", x_now); solver.set(0, "ubx", x_now) # feedback phase input
status = solver.solve() # one RTI step
u0 = solver.get(0, "u")
# send u0 to the plant, read the new measurement into x_now
The PARTIAL_CONDENSING option appears in the acados feature documentation, with tuning parameters for the condensing block size (acados features). The HPIPM suffix, SQP_RTI and the ERK integrator reflect my knowledge of the Python interface and the acados paper’s description; confirm them against your installed version.
Linear MPC in Practice: A Runnable OSQP Tutorial
Theory is cheap, so here is a complete experiment you can run. The plant is a small thermal process: two coupled lags with the heater driving the first and the product temperature measured on the second. It is discretized at Ts = 1 s using SciPy’s zero-order-hold cont2discrete. The heater duty is constrained to [0, 1], and there is a hard ceiling on the output at 1.05 times the setpoint. I compare MPC solved with OSQP against three PIDs: one with back-calculation anti-windup, one without, and a gently tuned one.
Setup before the code. The MPC uses the sparse formulation with N = 40 steps. The decision vector stacks the state errors and the input deviations from the steady-state input that holds the setpoint. The terminal weight P comes from the discrete algebraic Riccati equation, so beyond the horizon the cost approximates the unconstrained infinite-horizon cost. Because the matrices H and A never change, I build them once, call setup, and then, at each sample, only update the bound vectors with update(l=..., u=...). That reuses the factorization and warm starts the ADMM iterations, which is the usage pattern the OSQP documentation highlights for parametrized problems.
import time
import numpy as np
import scipy.sparse as sp
import osqp
from scipy.linalg import solve_discrete_are
from scipy.signal import cont2discrete
# --- Plant: two coupled thermal lags, output y = T2, input u = heater duty in [0, 1]
Ac = np.array([[-0.10, 0.05], [0.05, -0.08]])
Bc = np.array([[0.30], [0.0]])
Cc = np.array([[0.0, 1.0]])
Ad, Bd, Cd, _, _ = cont2discrete((Ac, Bc, Cc, np.zeros((1, 1))), 1.0)
nx, nu, N = 2, 1, 40
u_min, u_max, y_ceiling = 0.0, 1.0, 1.05 # hard limits (setpoint r = 1.0)
dc_gain = (Cd @ np.linalg.inv(np.eye(nx) - Ad) @ Bd).item()
u_ss = 1.0 / dc_gain # steady input that holds y = 1
x_ss = np.linalg.solve(np.eye(nx) - Ad, Bd * u_ss).ravel()
# --- Cost: error coordinates, terminal weight from the discrete Riccati equation
Q, R = np.diag([0.0, 10.0]), 0.1 * np.eye(nu)
P = solve_discrete_are(Ad, Bd, Q, R)
# --- Build the sparse QP once: z = [e_0..e_N, v_0..v_{N-1}]
n_x, n_u = (N + 1) * nx, N * nu
H = sp.block_diag([sp.kron(sp.eye(N), Q), sp.csc_matrix(P), sp.kron(sp.eye(N), R)], format="csc")
dyn = sp.kron(sp.eye(N + 1), -sp.eye(nx)) + sp.kron(sp.eye(N + 1, k=-1), sp.csc_matrix(Ad))
inp = sp.kron(sp.vstack([sp.csc_matrix((1, N)), sp.eye(N)]), sp.csc_matrix(Bd))
A_eq = sp.hstack([dyn, inp])
A_u = sp.hstack([sp.csc_matrix((n_u, n_x)), sp.eye(n_u)])
A_y = sp.hstack([sp.kron(sp.eye(N + 1), sp.csc_matrix(Cd)), sp.csc_matrix((N + 1, n_u))])
A = sp.vstack([A_eq, A_u, A_y], format="csc")
def limits(x_meas, r):
e0 = x_meas - x_ss * r
eq = np.concatenate([-e0, np.zeros(N * nx)])
lo = np.concatenate([eq, np.full(n_u, u_min - u_ss * r), np.full(N + 1, -np.inf)])
hi_y = np.full(N + 1, y_ceiling - r); hi_y[0] = np.inf
hi = np.concatenate([eq, np.full(n_u, u_max - u_ss * r), hi_y])
return lo, hi
lo, hi = limits(np.zeros(nx), 1.0)
solver = osqp.OSQP()
solver.setup(H, np.zeros(n_x + n_u), A, lo, hi, verbose=False,
eps_abs=1e-5, eps_rel=1e-5, polishing=True)
def mpc(x, r):
lo, hi = limits(x, r)
solver.update(l=lo, u=hi) # only the bounds change: factorization is reused
res = solver.solve()
return res.x[n_x] + u_ss * r # apply the first move only
class PID:
def __init__(self, kp, ki, antiwindup=True):
self.kp, self.ki, self.aw, self.I = kp, ki, antiwindup, 0.0
def __call__(self, x, r):
e = r - (Cd @ x).item()
u_raw = self.kp * e + self.I
u = min(max(u_raw, u_min), u_max)
self.I += self.ki * e + (0.5 * (u - u_raw) if self.aw else 0.0) # back-calculation
return u
def simulate(ctrl, steps=150, r=1.0):
x, ys, ts = np.zeros(nx), [], []
for _ in range(steps):
t0 = time.perf_counter(); u = ctrl(x, r); ts.append(time.perf_counter() - t0)
x = Ad @ x + Bd.ravel() * u
ys.append((Cd @ x).item())
return np.array(ys), np.array(ts)
for name, c in [("MPC", mpc), ("PID with anti-windup", PID(4.0, 0.25)),
("PID no anti-windup", PID(4.0, 0.25, False)), ("PID gentle", PID(1.0, 0.05))]:
y, t = simulate(c)
settle = np.where(np.abs(y - 1) > 0.02)[0]
print(f"{name:22s} overshoot {100*(y.max()-1):6.2f}% settle {settle[-1]+1 if len(settle) else 0:3d} s "
f"IAE {np.abs(1-y).sum():6.2f} mean {1e3*t.mean():.3f} ms max {1e3*t.max():.3f} ms")
What the experiment showed
I ran this exact script in a Python sandbox with osqp 1.1.3 and SciPy. A unit step from rest has a steady-state heater duty of about 0.37, so the actuator saturates at 1.0 during the climb and the saturation is what exposes the difference. These are results from a simulated plant on my machine, not field measurements, and the timing column will differ on yours.
| Controller | Overshoot | 2% settling time | IAE (sum of absolute error) |
|---|---|---|---|
| MPC (OSQP, N = 40) | 1.28% | 20 s | 10.56 |
| PID with back-calculation anti-windup | 0.21% | 64 s | 14.96 |
| PID, no anti-windup | 56.83% | 88 s | 23.21 |
| PID, gentle tuning | 15.84% | 70 s | 14.22 |
Read this table carefully, because it carries three lessons. First, the PID without anti-windup is the cautionary tale: the integrator charged while the heater was pinned at 1.0, and the output overshot by more than 56%, which would have breached the 5% ceiling many times over. Second, the PID with anti-windup is genuinely good: it barely overshoots. Its cost is speed, with a 2% settling time of 64 s against 20 s for MPC, because its tuning had to be conservative enough to respect the limit without knowing about it. Third, MPC reached the band more than three times faster while its overshoot of 1.28% stayed well under the 5% ceiling. In this run the output constraint was not the binding one; the saturation of the input and the predictive deceleration did the work. To see the output ceiling bind, tighten y_ceiling toward 1.005 and rerun.
The solve-time column is the other side of the trade. In my runs the MPC averaged a few tenths of a millisecond per sample, with a worst case of several milliseconds, against microseconds for PID, in a Python process on a general-purpose CPU. Both are comfortably inside a 1 s sample here, but the three-orders-of-magnitude gap is the real cost of the approach, and the worst case, not the average, is what matters for a hard real-time loop.
Reading OSQP’s status, not just its answer
One habit separates a demo from a deployable controller: always check the solver status. OSQP is a first-order method, so at loose tolerances it can return a solution with small constraint violations, and it reports infeasibility and iteration-limit outcomes explicitly. Production code should inspect res.info.status and fall back to the previous plan, a safe PID or a last-good control when the status is not “solved”. The same applies to acados, whose solve() returns a status integer. A controller that silently applies an unconverged first move is more dangerous than one that admits it failed.
Stability, Terminal Sets and Feasibility
A receding-horizon controller is not automatically stable. The finite horizon can be myopic: a controller that optimizes over the next two seconds may happily steer the plant toward a state from which no constraint-satisfying recovery exists. The theory that closes this gap is well established; Mayne, Rawlings, Rao and Scokaert’s survey in Automatica (2000) is the standard reference, and I am citing its conclusions from memory.
The recipe has three parts. Add a terminal cost P that upper-bounds the cost-to-go inside a region, typically from the Riccati solution of the unconstrained problem, which is what the code above does. Add a terminal constraint set, a region where a known stabilizing local controller (for example the LQR gain) keeps the plant inside the constraints forever. Then the optimal cost serves as a Lyapunov function, and recursive feasibility follows: if the problem is feasible now, it is feasible at the next sample.
In practice, many industrial implementations skip the terminal set and rely on a long enough horizon plus tuning, which works but gives up the guarantee. If you do skip it, you inherit a responsibility: verify closed-loop behaviour over the whole operating envelope in simulation, including disturbances that push the state toward constraint boundaries.
Soft constraints keep the problem feasible
Hard output constraints can make the QP infeasible. A large disturbance may push the predicted output past its limit regardless of what the inputs do, and an infeasible QP gives you no control at all. The standard remedy is a slack variable on state or output constraints, penalized heavily in the cost (an exact penalty, so that the slack is zero whenever feasibility permits). Keep input limits hard, since they represent physical hardware, and soften the output limits so the controller degrades gracefully instead of failing. Which constraints are hard, which are soft, and what penalty weight the slack carries is a safety decision as much as a tuning one.
Offset-free tracking and disturbance models
A pure model-based MPC has no integral action, so a persistent model error leaves a steady-state offset. The standard fix is to augment the model with a constant disturbance state and estimate it with a Kalman filter or a Luenberger observer, then compute the steady-state target (x_ss, u_ss) from the estimated disturbance each sample. That is what turns the controller into an offset-free tracker, and it is why the estimator in Figure 1 is not optional decoration. Neglecting it is the most common reason a freshly commissioned MPC “never quite reaches the setpoint”.
Tuning: Horizon Length, Weights and Sample Time
MPC has fewer knobs than people fear, and each has a physical meaning, which makes it easier to tune than a multi-loop PID cascade.
Sample time should resolve the fastest dynamics you care about. A common rule of thumb in the literature is roughly one tenth to one fifth of the dominant closed-loop time constant; treat it as a starting heuristic, not a standard. Too slow and the controller misses fast disturbances; too fast and you burn compute while lengthening the horizon in steps needed to cover the same physical time.
Prediction horizon N should cover the plant’s settling time, or at least the time the actuator takes to produce most of its effect. If the horizon ends before the output has responded to a move, the controller cannot see the consequences and behaves myopically. In my experiment, N = 40 samples at 1 s covers the faster mode of the plant (time constants of roughly 7 s and 26 s, from the eigenvalues of the continuous-time matrix) and most of the slower one, which is why the result was clean. A longer horizon improves the plan and enlarges the QP; the cost grows with N, so there is a real engineering trade.
Control horizon Nu, shorter than N, restricts the optimizer to a few free moves and holds the input constant beyond them. It reduces the number of decision variables and smooths the control, and DMC-style controllers use it heavily. The cost is optimality: you can no longer shape the late part of the trajectory.
Weights Q and R set the aggressiveness. Raising R relative to Q slows the response and reduces actuator wear. Penalizing the input rate (the change between moves) rather than the input itself is often more useful in practice, because it directly smooths valve movement. Scale the variables first: a weight of 10 on a pressure in kPa and on a temperature in kelvin means different things, so normalize each to engineering ranges before choosing weights.
A practical tuning workflow runs in this order: pick the sample time, set N to cover the settling time, start with an identity-scaled Q and a small R, then raise R or the rate penalty until the manipulated variable stops chattering. Add constraints last and test the loop against the extreme disturbance, not the typical one.
Explicit MPC and Other Ways to Avoid Online Optimization
If even a small QP is too expensive for your target, there are two escape routes. The first is explicit MPC: Bemporad, Morari, Dua and Pistikopoulos (Automatica, 2002) showed that the solution of a linear MPC problem is a continuous piecewise-affine function of the state, defined over a partition of the state space into polyhedral regions. You compute that partition offline with multiparametric programming; online, the controller only locates the current region (point location) and evaluates an affine law. The online cost is tiny and deterministic, so it fits very small microcontrollers and even FPGAs.
The limitation is combinatorial. The number of regions grows rapidly with the number of constraints and the horizon, so explicit MPC is practical for roughly two to five states and short horizons, and becomes unwieldy beyond that. It is also less flexible: changing a constraint or a weight means recomputing the partition. Treat it as a tool for fast, small, fixed problems such as motor drives and power converters.
The second route is approximation: train a neural network or fit a lookup table to imitate the MPC offline, then run the cheap approximation online. This can run on hardware that cannot afford a solver, but it offers no automatic constraint guarantee. Safe deployments project the network’s output onto the constraint set or wrap it with a safety filter. I treat this as an emerging technique and recommend validating it against the original MPC over the full operating envelope before relying on it.
Solvers, Compared
The solver landscape splits along two axes: convex QP versus general NLP, and generic versus structure-exploiting.
| Tool | Problem class | Method | Strengths | Watch out for |
|---|---|---|---|---|
| OSQP | Convex QP | ADMM, first order | One factorization, warm start, tolerant of rough data, C code generation, Apache 2.0 | Moderate accuracy by default; tune tolerances and rho |
| HPIPM | Structured QPs from OCPs | Interior point exploiting stage structure | Very fast on multistage problems, default QP framework in acados | Tied to the OCP structure |
| acados | NLP and QP optimal control | SQP and SQP-RTI with HPIPM, BLASFEO | Embedded C, Python and MATLAB interfaces, RTI | Build toolchain, API evolution |
| CasADi | Modelling and AD, NLP front end | Algorithmic differentiation, interfaces IPOPT and others | Exact derivatives, rapid prototyping | A framework, not a real-time solver by itself |
| do-mpc | Robust and nonlinear MPC toolbox | Built on CasADi and IPOPT | Quick prototyping, scenario-based robust MPC | Not aimed at hard-real-time embedded use |
| qpOASES-style active set | Small dense QPs | Active set | Fast for small problems, exact active sets | Scaling to long horizons |
I have not re-verified the latest release number of any of these tools for this article, and version numbers drift; check the project release pages before pinning. What matters more than the version is matching the tool to the problem. Linear MPC with a small number of states fits OSQP or a structured QP solver in a few lines of code. A nonlinear model with a tight sample period needs acados or a similar code-generated SQP-RTI pipeline. A research prototype where you want to change the model every hour is best written in CasADi with IPOPT, then ported once it stabilizes.
OSQP’s own documentation is explicit about what it offers: warm starting and cached factorization for parametrized problems, library-free C code generation that needs no memory manager, and detection of primal and dual infeasibility (OSQP documentation). Its underlying paper (Stellato, Banjac, Goulart, Bemporad and Boyd, “OSQP: An Operator Splitting Solver for Quadratic Programs”, arXiv:1711.08013) is the right primary source for the algorithm. Because ADMM converges at a rate that depends on problem scaling and the penalty parameter rho, badly scaled MPC problems converge slowly; scale the decision variables, and use the solver’s adaptive-rho feature, before concluding that the solver is too slow.
Real-Time Implementation: PLC, Edge Gateway and the Latency Budget
Where the optimizer physically runs decides what is possible.

Figure 3: A safe integration pattern. The PLC scan task stays in charge of the actuator, the edge MPC supplies a proposed move, and a fallback PID takes over if the solver reports a bad status or misses its deadline.
Why MPC rarely runs inside the PLC logic
IEC 61131-3 PLC runtimes are built for deterministic cyclic scans over simple data types, not for iterative numerical optimization with sparse matrices and unbounded iteration counts. Some vendors do offer MPC function blocks and libraries, but for custom formulations the more common pattern is to run the solver on an industrial PC or edge gateway and exchange values with the PLC. For the timing background, see our deep dive on the PLC scan cycle, determinism and jitter.
The interface is usually OPC UA or a fieldbus gateway, which adds latency and a failure mode: the MPC may be alive while the link is dead. Design for it explicitly. The PLC should treat the MPC output as a proposal with a freshness timestamp, apply its own clamps and rate limits, and revert to a fallback controller when the value is stale.
The latency budget
Write the budget as a sum and hold every term to account: sensor-to-gateway transfer, state estimation, the preparation phase (if using RTI), the feedback-phase solve, serialization and network transfer to the PLC, the PLC scan that applies the value, and actuator response. The binding term is the worst-case solve, not the mean. In my OSQP experiment the average solve was a fraction of a millisecond but the worst-case was roughly ten times larger, a ratio you should expect to see on any non-real-time operating system because of scheduler jitter, cache effects and varying ADMM iteration counts.
Three disciplines tame that tail. Cap the solver iterations and accept a suboptimal-but-feasible answer rather than overrunning the deadline (RTI does this structurally). Run the solver in a pinned, high-priority thread on a real-time-patched kernel if the loop is fast. Warm start from the shifted previous solution, which is cheap and cuts the iteration count. When the sample period is in the tens of milliseconds, a general-purpose Linux box with those measures is workable; at sub-millisecond rates you are in code-generated, bare-metal or RTOS territory.
Safety layering
An MPC is an optimizer, not a safety function. If a failure could hurt people, the safety layer must be independent of it. That means a safety instrumented function on certified hardware, designed per the lifecycle in IEC 61508 functional safety, that trips regardless of what the MPC computes. A hard constraint inside the optimizer is a performance feature that keeps the process away from the limits, and the safety system remains the layer that actually enforces them. The same separation argument applies to cybersecurity: an optimizer that can write setpoints is an attractive target, so place it in a segmented zone and authenticate the OPC UA channel, as discussed in our zero trust architecture for industrial OT and IoT guide.
MPC in the Process Industry and in Robotics
The two big application families stress the technology differently, and it helps to know which one you are in.
Process industry. Plants are slow (minutes to hours), large (dozens of variables), and highly constrained, with economics driven by operating near limits. Linear models identified from step tests, sample times of seconds to minutes, and QP-based solvers dominate. The value is in pushing a distillation column, cracker or kiln closer to a quality or capacity constraint without violating it, a few percent at a time, across enormous throughput. Model maintenance is the persistent cost: plants drift, catalysts age, and an MPC with a stale model slowly degrades until operators switch it off. Surveys of industrial MPC repeatedly identify low service factors, meaning controllers that are installed but not engaged, as a practical weakness; I refer to this generally and do not quote a number.
Robotics and vehicles. Systems are fast (milliseconds), small (a few to a few dozen states), and nonlinear, with constraints from torque limits, joint limits and collision avoidance. This is the territory of NMPC with SQP-RTI, code generation and tight real-time scheduling, which is exactly what acados was built for. Torque-level whole-body control and path tracking for quadrotors are typical uses. When a manipulator needs compliant contact rather than free-space tracking, see our article on impedance and admittance control, which can sit beneath or beside an MPC layer.
Digital-Twin-in-the-Loop: Model, Validate, Deploy
A digital twin is a natural partner for MPC because the twin is, in effect, the prediction model plus a way to test it.

Figure 4: A deployment loop where the twin supplies and updates the prediction model and also acts as the virtual plant for closed-loop testing before any change reaches the real process.
The workflow in Figure 4 has three uses. First, the twin supplies and recalibrates the prediction model from telemetry, which attacks the model-drift problem directly. Second, the twin serves as a virtual plant: you run the full closed loop, including the real solver code and the real estimator, against a higher-fidelity simulation, and gate deployment on constraint satisfaction under a set of disturbance scenarios. Third, the twin’s discrepancy against the real plant becomes a monitoring signal: if the one-step prediction error grows, the model is stale and the MPC’s promises are no longer trustworthy.
There is a subtlety about fidelity. The twin used to test the controller should be richer than the model the MPC predicts with; otherwise you are testing the controller against its own assumptions and will never find model-mismatch failures. Use a physics-based model (for instance in Modelica or Simulink via FMI) as the virtual plant and a reduced model for the optimizer. For large systems, reduced-order and surrogate models are what make the prediction step fast enough, and the verification discipline in our VVUQ for digital twins article applies to the twin that gates your deployments.
Trade-offs, Gotchas, and What Goes Wrong
MPC fails in recognisable ways. Most of them are about the things around the optimizer rather than the optimizer itself.
Model mismatch. The most common failure. A wrong gain or an unmodelled delay makes the predictions systematically optimistic, and the controller acts on them. Feedback and disturbance estimation absorb modest error, but not a wrong sign or a regime change such as a fouled heat exchanger. Monitor the prediction residual and alarm on it.
Infeasibility at the worst moment. Hard output constraints and a large disturbance produce an infeasible QP, which returns no control. The fix is soft constraints with a large exact penalty plus a defined fallback. Test infeasibility explicitly, since it is easy to miss in simulation where disturbances are gentle.
Solver time overruns. An average solve time says nothing about the deadline. The ADMM iteration count varies with the data; an interior-point solver has a more predictable count but costs more per iteration. Measure the tail on the target hardware, under load, with the real problem, and cap iterations.
State estimation errors. MPC is only as good as the state it starts from. A biased or noisy estimate drives a confident wrong plan. Treat the estimator with the same testing discipline as the controller.
Numerical scaling. Mixing variables with very different magnitudes degrades both ADMM and interior-point methods. Normalise states and inputs before building the QP.
Over-tuning to the simulation. Horizon, weights and constraints tuned against a clean model will look excellent in simulation and disappoint on a plant with noise, delay and quantized sensors. Inject measurement noise, input delay and model error in the test bench.
Operator trust and observability. Operators accept a PID because they can reason about it. An MPC that behaves unexpectedly, such as pre-emptively cutting a valve, gets switched to manual. Expose the predicted trajectory, the active constraints and the reason a move was chosen. An explainable MPC stays engaged longer.
Cost of ownership. The licence, the plant tests, the estimator, the monitoring and the people all cost money. If a cascade of PIDs with feedforward and anti-windup meets the specification, use it.
Practical Recommendations
Start by asking whether you need MPC at all. If you have one loop, benign dynamics and no active constraints, tune the PID properly and stop. If you have several interacting variables, hard limits that you currently handle by backing off the setpoint, or known disturbances and references you could preview, MPC is likely worth the investment.
Prototype in this order. Identify or derive a linear model and validate it against plant data. Build the QP with OSQP or CVXPY as in the tutorial, simulate against a richer virtual plant, and check the closed loop under noise, delay and model error. Only move to NMPC with acados if the linear model fails over the operating range you need; the nonlinear route costs more in tooling, tuning effort and certification of behaviour.
For deployment, keep the PLC in charge of the actuator, treat the MPC output as a proposal with a timestamp, and always keep a fallback controller and an independent safety function. Version the model alongside the controller and log the prediction error continuously.
- Confirm a PID with anti-windup and feedforward cannot meet the specification first.
- Identify the model from step tests and validate on held-out data before writing any solver code.
- Normalise all variables, then choose Ts, N and the weights in that order.
- Keep input limits hard, soften output limits with an exact penalty, and test infeasibility on purpose.
- Add a disturbance model and estimator for offset-free tracking.
- Measure worst-case solve time on target hardware under load and cap solver iterations.
- Check the solver status every sample and define the fallback behaviour.
- Test against a higher-fidelity digital twin, with noise, delay and model error injected.
- Monitor the one-step prediction error in production and alarm on model drift.
- Keep safety functions independent, and secure the OPC UA path that carries setpoints.
Frequently Asked Questions
What is the main difference between MPC and PID?
PID reacts to the present error, its accumulated history and its rate of change, with no knowledge of limits or the future. Model predictive control uses a plant model to predict the output over a horizon and optimizes the input sequence subject to explicit constraints, applying only the first move before re-solving. That makes MPC better for constrained, interacting or delayed processes, at the price of a model, a solver and more compute.
Does MPC replace PID controllers?
Usually not. In process plants MPC typically sits above PID: it computes setpoints that the regulatory PID loops track at the actuator level. This keeps the fast, trusted, low-level regulation and adds multivariable coordination and constraint handling above it. Replacing PID entirely makes sense mainly in fast robotics or vehicles, where the MPC output drives low-level actuator controllers or motor drives directly.
How long should the prediction horizon be?
Long enough to cover the plant’s settling time, or at least the time for a move to show most of its effect on the output. If the horizon ends first, the controller is myopic and may behave poorly or unstably. Beyond that, longer horizons cost compute, so use a terminal cost and, if needed, a shorter control horizon to keep the problem small. Validate the choice in closed-loop simulation.
When should I use OSQP and when acados?
Use OSQP for convex quadratic programs, which means linear MPC with quadratic costs and linear constraints. It is simple, supports warm starting and code generation, and is Apache 2.0 licensed. Use acados when the model is nonlinear, the sample time is short and you need SQP with real-time iterations in generated C. Many teams prototype the linear version first and move to acados only when the linear model fails.
Can MPC run on a PLC?
Sometimes, but not generally. IEC 61131-3 runtimes are built for deterministic cyclic scans, and iterative optimization with variable iteration counts fits poorly. Some vendors ship MPC function blocks for standard formulations. For custom problems the usual pattern is an industrial PC or edge gateway running the solver and exchanging proposed values with the PLC over OPC UA, with the PLC applying clamps and a fallback controller.
Is MPC stable?
Not automatically. A finite-horizon controller can steer a plant into a state from which constraints cannot be satisfied. The standard theory adds a terminal cost and a terminal constraint set so the optimal cost acts as a Lyapunov function and recursive feasibility holds. Many industrial systems skip the terminal set and rely on a long horizon and thorough simulation instead, which works but removes the formal guarantee.
Further Reading
- Zero trust architecture for industrial OT and IoT: securing the channel that carries MPC setpoints into the control network.
- Model predictive control with digital twins: the plant-wide, twin-centred companion to this article.
- PID controller tuning, anti-windup and cascade control: the regulatory layer MPC sits on.
- OSQP documentation: algorithm, interfaces and code generation.
- acados: a modular open-source framework for fast embedded optimal control: the primary reference for the framework and its design.
- acados features by example: SQP-RTI, advanced-step RTI and partial condensing.
By Riju — about
