← all projects
Robotics · December 2025 · 20 min read

Nonlinear MPC for Autonomous Racing

A controller for a full-scale Dallara AV-24 in the Indy Autonomous Challenge, built on two timescales: a minimum-time lap solved offline with a double-track Pacejka tyre model, and a progress-maximizing MPC that drives it at 100 Hz. My part is the offline layer, which produces the racing lines the online controller follows. Validated in software in the loop, in simulation at Laguna Seca (117.6 s, 25% faster than the baseline controller), and on the physical car.

PythonC++17CasADiacadosIPOPTHPIPMROS2pybind11

The Indy Autonomous Challenge races full-scale Dallara AV-24 open-wheel cars with no driver, at circuits like Indianapolis Motor Speedway and at speeds above 290 km/h. This capstone is a ground-up redesign of UC Berkeley’s controller for that car: a nonlinear model predictive controller that stops tracking a fixed speed plan and instead maximizes how fast the car makes progress around the track. My contribution is Chapter 2: the offline trajectory optimization layer that defines the global racing strategy the online controller executes.

The film below is that layer’s output. It is the minimum-time lap of Laguna Seca from the double-track optimizer, driven over the surveyed track at the speed the optimizer computed every 3 metres.

loading the track…
The track is the surveyed Laguna Seca edges with their heights, and the car follows the minimum-time line from the double-track optimizer, at the speed it computed every 3 m. Only the camera, the terrain shading and the car model are made for the screen.
Dallara AV-24 autonomous race car on trackMEng capstone team with the autonomous race car at the track

1. What it had to do

A race controller is judged on lap time, but it has to earn the right to go fast by being predictable first. The requirements, as measurable statements:

  1. Beat the existing controller’s lap time on the same car and track.
  2. Deliver a control command every 10 ms, every time, at 100 Hz, with no solve that runs long.
  3. Stay inside the track limits with an adjustable safety margin, and never let the optimizer become infeasible when the car is already at the edge.
  4. Never command front braking and rear drive at the same time.
  5. Act on where the car will be, not where it was. Sensing to actuation takes 20 to 100 ms, which at 200 km/h is several metres.
  6. Switch racing lines during a race, for the pit lane or an overtake, without retuning.
  7. Reach the real car safely, in stages, before it has authority over the steering and throttle.

Section 6 checks each one against the results, including the ones that were only partly met.

2. Architecture

System architecture: off-car Python code generation on the left, on-car C++ 100 Hz control loop on the right

Offline, before the race: Python and CasADi build the problems and generate C code with acados. On the car: a C++ pipeline runs the state estimator and the MPC every 10 ms.

A controller at racing speed faces two problems on timescales that do not fit together. Planning decides where to brake, which line to take through a sequence of corners, and how to trade the exit of one corner against the entry to the next. That needs the whole 3.6 km lap at once. Execution has to reject disturbances, absorb estimation error, and stay physically feasible inside a single 10 ms cycle. No real-time solver can do both.

So the system is split in two. A full-lap minimum-time problem is solved offline, with no time limit, using a detailed vehicle model. Its answer is written into the Track Trajectory Library (TTL). An MPC running online at 100 Hz takes the TTL as its task and spends its whole budget on following it well under real conditions. The offline layer decides what the car should do over the lap. The online controller works out how to do it right now.

3. Models, states, and the interface between the layers

The two layers use different vehicle models on purpose, each as detailed as its time budget allows.

Offline: double trackOnline: bicycle
Tyre contact patches4, each on its own2, one per axle
Tyre modelPacejka Magic Formula, including past the peaksmooth linear saturation
Load transfer between wheelsper wheelaveraged per axle
Aerodynamicsdownforce and dragleft out
States6, plus load dynamics6
Solve timeminutesunder 10 ms

The online dynamic model’s state is x=[u, d, ψ, vx, vy, r]⊤\mathbf x = [u,\ d,\ \psi,\ v_x,\ v_y,\ r]^\top: progress along the racing line uu, lateral offset from it dd, heading error ψ\psi, the velocity in the car’s own frame vx,vyv_x, v_y, and the yaw rate rr. These live in a Frenet frame that follows the racing line, because “how far along” and “how far to the side” are exactly the quantities the cost and the track limits are written in. Survey data, GPS, and the TTL itself live in a local East-North-Up frame.

The TTL is the contract between the two layers. Every field the online controller reads is defined here, so either side can change as long as it keeps the contract:

FieldSymbolRole in online control
Positionx,y,zx, y, zthe racing line, in East-North-Up
Headingψ\psidirection of the track tangent
Progressssindexes the line and the Frenet projection
Reference speedvrefv_\text{ref}speed target in the MPC cost
Curvatureκ(s)\kappa(s)feedforward steering
BoundariesbL,bRb_L, b_Rtrack limit constraints
Bankingβ(s)\beta(s)normal load and lateral force correction
Gradeϕ(s)\phi(s)the downhill or uphill pull of gravity
Nominal dynamic stateax,vy,r,δa_x, v_y, r, \deltawarm start for the MPC horizon

4. Subsystems

Each part is described the same way: its job, what goes in and out, the design and why, and how it was checked.

4.1 Track model

Job
Turn surveyed GPS points into a smooth track the optimizer can differentiate.
In
surveyed inner and outer barrier points
Out
centre line, widths, curvature, banking and grade at any distance along the track
Verified by
curvature continuous through the whole lap; Laguna Seca hairpin reproduced at R = 14 m

The surveyed coordinates of the inner and outer barriers are converted into a local East-North-Up frame and fitted with cubic B-splines. A smoothing parameter balances staying close to the survey points against a smooth shape. The splines give exact derivatives of curvature, banking, and grade at any distance along the track, which CasADi’s automatic differentiation needs.

Cubic B-spline fit of Laguna Seca: the full circuit, the hairpin at R = 14 m, and the curvature along the lap

The full circuit, the 14 m hairpin in detail, and the curvature along the lap. Cubic B-splines keep curvature continuous, which a well-posed optimization needs.

The centre line is resampled every 3 m to define the optimizer’s grid. At each point the distances to the boundaries wL(sk),wR(sk)w_L(s_k), w_R(s_k), the curvature κ(sk)\kappa(s_k), the banking β(sk)\beta(s_k) and the grade ϕ(sk)\phi(s_k) are read from the splines and passed in as fixed parameters.

4.2 Offline minimum-time optimizer (my part)

Job
Find the fastest lap the car can physically drive, and the library of racing lines built from it.
In
the track model and the double-track vehicle model with its tyre and aero parameters
Out
the TTL: line, speed, and nominal state every 3 m
Verified by
IPOPT reports an optimal solution; Laguna Seca: 106.1 s, 53 to 230 km/h

The problem is simple to state: find the controls that get the car round the lap in the least time, subject to the vehicle’s dynamics, the track limits, and the lap closing on itself.

min⁡x(⋅), u(⋅), T  Tsubject tox˙=f(x,u),g(x)≤0,u∈U,x(0)=x(T)\min_{x(\cdot),\,u(\cdot),\,T} \; T \quad \text{subject to} \quad \dot{x} = f(x, u), \quad g(x) \leq 0, \quad u \in \mathcal{U}, \quad x(0) = x(T)

To solve it numerically, the track is cut into about 1,200 segments of 3 m. The total time TT becomes a sum of per-segment times Δtk\Delta t_k, each one a decision variable:

J=∑k=0N−1Δtk  +  wu∑k=0N−1∥uk∥2Δtk  +  wΔu∑k=0N−2∥uk+1−ukΔtk∥2ΔtkJ = \sum_{k=0}^{N-1} \Delta t_k \;+\; w_u \sum_{k=0}^{N-1} \lVert u_k \rVert^2 \Delta t_k \;+\; w_{\Delta u} \sum_{k=0}^{N-2} \left\lVert \frac{u_{k+1} - u_k}{\Delta t_k} \right\rVert^2 \Delta t_k

The first term is the lap time. The two small terms penalize control effort and how fast the controls change, which stops the solver from producing on-off commands the real car cannot follow. The lateral position eke_k is kept inside the track with margins dL,dRd_L, d_R that can be set per section, wider on straights and tighter where an error would be costly:

−wR(sk)+dR  ≤  ek  ≤  wL(sk)−dL-w_R(s_k) + d_R \;\leq\; e_k \;\leq\; w_L(s_k) - d_L

The result has about 15,000 decision variables.

The vehicle model. The double-track model represents all four tyres separately, which captures what a two-wheel model averages away:

  • Individual wheel loads. Load moves forward under braking and outward in a corner, wheel by wheel. Braking into a left-hander, the right front tyre carries the most load and so has the most grip.
  • Pacejka Magic Formula. Each tyre’s force comes from the full Magic Formula with load-dependent coefficients (BB, CC, EE, Fz0F_{z0}), including the region past the peak where more slip gives less force.
  • Banking and grade. Both vary along the track and change the normal load and the lateral balance. At the Corkscrew they change the vertical loads by up to 40%.

Each tyre is held inside its own friction ellipse:

(Fx,ijμFz,ij)2+(Fy,ijμFz,ij)2≤1\left(\frac{F_{x,ij}}{\mu F_{z,ij}}\right)^2 + \left(\frac{F_{y,ij}}{\mu F_{z,ij}}\right)^2 \leq 1

Modelling wheel spin directly would add four stiff states and make the problem far larger. Instead the slip ratio is solved at equilibrium from the torque command at each point, which removes those states without losing the coupling between braking and cornering grip.

Solving it. The problem is written symbolically in CasADi, which gives exact gradients, Jacobians and Hessians by automatic differentiation. That matters for the Magic Formula, whose nested nonlinear terms are badly served by finite differences. IPOPT, an interior-point solver, solves it with a tolerance of 10−510^{-5}. A cold start takes about 200 iterations and 200 s. Starting from a previous solution takes 50 to 90 iterations and about 50 s.

Before IPOPT runs, a quasi-steady forward simulation along the centre line computes the highest speed the car could hold at each point under steady cornering. That answer is feasible but slow, and it gives IPOPT a starting point that keeps it out of its expensive recovery phase from the first iteration.

A scaling bug. The scaling of the warm start was being applied twice: once in Python when building the initial guess, and again inside the CasADi interface. IPOPT then fell into its recovery phase on every solve, so a warm start was no faster than a cold one. Fixing it brought warm-started solves from about 200 s down to about 50 s.

What moves the lap time. A sensitivity study on Laguna Seca between two solver configurations:

ChangeLap time
Friction coefficient μ\mu 1.1 → 1.25−4.4 s
Track margins 1.5 m → 0.3 m−2.3 s
Regularization weights, 25 times smaller−1.1 s
Steering rate weight TδT_\delta 0.75 → 0.3 s−0.3 s
Signed curvature, banking and grade, and the optimized speed profile along the 3.6 km Laguna Seca lap

Top: curvature, separating left and right turns. Middle: banking and grade from the survey, with the Corkscrew near 2.5 km. Bottom: the optimized speed. It does not just slow for each corner on its own. It sets the exit of one corner up for the entry to the next.

The track library. The optimizer’s solution is written out as a TTL file. Running the same optimizer with different margin and lane settings gives a library of lines per track, and the online controller picks between them by race situation: a pit command switches to TTL 27, and an overtake biases toward the left or right line.

Five TTL variants over Laguna Seca: optimal, centre, left, right, and pitThe optimal line coloured by speed from 60 to 200 km/h, with the safety margin bands

The hairpin inset on the left is where the lines differ most: the optimal line clips the apex harder than the constrained ones, using the full width to carry more speed.

4.3 Online MPC

Job
Drive the car around the track as fast as it can go right now, inside the limits.
In
the estimated state in the Frenet frame and the TTL
Out
steering and front and rear torque commands, every 10 ms
Verified by
closed-loop laps in the C++ pipeline test; dSPACE VEOS software in the loop; AWSIM at Laguna Seca; the physical car

Instead of tracking a precomputed speed schedule, the online controller maximizes the rate of progress along the track, s˙\dot s. That lets it choose its own speed and line within the corridor the TTL defines, from the car’s actual state, rather than following a plan made for a car that is not quite where the plan assumed. The cost at each step kk is

ℓ(xk,uk)=−wp s˙k+wd dk2+wψ ψk2+wδ δk2+wT (Tf,k2+Tr,k2)\ell(\mathbf{x}_k, \mathbf{u}_k) = -w_p\,\dot{s}_k + w_d\,d_k^2 + w_\psi\,\psi_k^2 + w_\delta\,\delta_k^2 + w_T\,(T_{f,k}^2 + T_{r,k}^2)

The first term rewards progress. The rest penalize lateral offset dd, heading error ψ\psi, steering δ\delta, and torque. A terminal term rewards speed at the end of the horizon, so the controller does not brake toward the end of its one second look-ahead just because nothing beyond it is counted.

The vehicle model. A dynamic bicycle model with the state of section 3. Progress along the line is

s˙=vxcos⁡ψ−vysin⁡ψ1−d κ(u)\dot{s} = \frac{v_x \cos\psi - v_y \sin\psi}{1 - d\,\kappa(u)}

and the body follows Newton and Euler in its own frame:

v˙x=Fxm+r vy,v˙y=Fym−r vx,r˙=lfFy,f−lrFy,rIzz\dot{v}_x = \frac{F_x}{m} + r\,v_y, \qquad \dot{v}_y = \frac{F_y}{m} - r\,v_x, \qquad \dot{r} = \frac{l_f F_{y,f} - l_r F_{y,r}}{I_{zz}}

A double-track model online would add stiff dynamics: body motion around 5 Hz and tyre dynamics around 50 Hz. That ten times gap forces tiny integration steps or implicit solvers, which blow the 10 ms budget. The bicycle model keeps the limit-handling physics that matter for prediction.

Tyres. The online tyre model is linear in slip and then saturated smoothly with a hyperbolic tangent, which enforces a friction ellipse and stays differentiable:

Fy,raw=−Cα α,Fx,raw=Cσ σF_{y,\text{raw}} = -C_\alpha\,\alpha, \qquad F_{x,\text{raw}} = C_\sigma\,\sigma ρ=∥Fraw∥,F=Fraw⋅Fmax⁡ tanh⁡(ρ/Fmax⁡)ρ,Fmax⁡=μFz\rho = \lVert\mathbf{F}_\text{raw}\rVert, \qquad \mathbf{F} = \mathbf{F}_\text{raw} \cdot \frac{F_{\max}\,\tanh(\rho / F_{\max})}{\rho}, \qquad F_{\max} = \mu F_z
Normalized lateral force against slip ratio at slip angles of 2, 4, 6 and 8 degrees: lateral force falls under braking

Why braking and cornering cannot be planned separately: at a 6° slip angle, even light braking (slip ratio 0.05) cuts the available lateral force from 81% to about 50%. Without this coupling the MPC would expect more cornering grip during braking than the tyres have.

Further layers bring the model closer to the car: aero downforce and drag, a lag on load transfer so weight does not move instantly, banking and grade (Fz=mgcos⁡θcos⁡ϕ+mv2κtan⁡ϕF_z = mg\cos\theta\cos\phi + mv^2\kappa\tan\phi), rolling resistance, and aligning torque.

Constraints. Torque bounds are hard: the front can only brake, the rear can brake or drive. Everything that the car might already be violating is soft, with a slack variable and a penalty, so the problem stays feasible at the track edge: the track limits (large L1L_1 penalty), the speed limit, and the rule against front braking with rear drive. That last one uses an L2L_2 penalty, because the product Tf⋅TrT_f \cdot T_r has a zero gradient at the nominal point, which breaks a standard regularity condition the solver relies on (LICQ).

Discretization. Direct multiple shooting over 50 steps at 50 Hz, one second ahead, with a two-stage Runge–Kutta (Heun) integrator: about 450 decision variables plus slacks.

4.4 State estimation and latency

Job
Give the MPC the state the car will have when its command takes effect.
In
GPS and vehicle measurements; the commands already sent but not yet felt
Out
the predicted state in the Frenet frame
Verified by
ghost mode on the car, on live sensor data
Meets
requirement 5

Between a measurement and the command it leads to, 20 to 100 ms pass. At 200 km/h, 50 ms of ignored delay is 2.8 m of position error, a large fraction of the track’s width. The estimator therefore simulates the car forward from the latest GPS fix through every command that has been issued but is not yet reflected in the measurements:

xMPC=ForwardSim(xGPS,  {uτ}τ∈[tmeas, tact])x_\text{MPC} = \text{ForwardSim}\bigl(x_\text{GPS},\; \lbrace u_\tau \rbrace_{\tau \in [t_\text{meas},\, t_\text{act}]}\bigr)

The MPC plans from where the car will be when the command lands.

4.5 Real-time execution

Job
Make every control cycle finish inside 10 ms.
In
the MPC problem and the previous cycle's solution
Out
one bounded-time solve per measurement
Verified by
pipeline tests; the on-car ghost run
Meets
requirement 2

The original controller solved the full problem to convergence every cycle, so its solve time was unpredictable. The redesign uses real-time iteration, split in two:

  1. Preparation: linearize the dynamics and build the quadratic subproblem from the previous cycle’s solution, before the new measurement arrives.
  2. Feedback: when the measurement arrives, solve that one prepared subproblem. Fast, and bounded.

Between two 10 ms cycles the optimal solution barely moves, so one warm-started Newton step lands close to the new optimum. The loop is driven by measurements, not a clock: each new measurement triggers exactly one step, which bounds the worst case to one step’s cost.

A single Newton step is only reliable if the subproblem is well conditioned. Three things make sure it is:

  1. Scaling every state and control to similar size. Mixing a 60 m/s speed and a 0.1 rad steering angle in one matrix makes it badly conditioned.
  2. Regularizing the Hessian by lifting its flat directions to a minimum eigenvalue, so the step cannot blow up along directions the problem barely constrains.
  3. Warm starting the primal solution, the dual variables and the active set from the previous cycle.

4.6 Software architecture

Job
Keep the controller correct while the model and the code change.
In
the Python problem definition
Out
generated C solvers and a C++17 pipeline on the car
Verified by
about 25,900 lines of tests; the Python solver and the C++ runtime cross-checked through pybind11

The central piece is Pipeline<TModel>, a C++17 template parameterized by the vehicle model. Switching between the kinematic and the dynamic bicycle is one template argument and a rebuild, with no change to the pipeline, the solver wrappers, or the tests. The Python generator writes C++ headers with typed structs that carry their dimensions, and static_assert checks their layout, so a mismatch between the generated solver and the runtime is a compile error rather than a bad command on the car.

The ROS2 layer is deliberately thin. The node only converts messages and manages the lifecycle, and a bridge node passes data between the controller and the rest of the autonomy stack without adding any safety-critical logic.

The codebase is about 7,800 lines of production code and about 25,900 lines of tests, 3.3 times as much test as code. Every feature started with a failing test, including the code written with an AI assistant under project rules, which let the architecture go through several large rewrites without regressions.

5. One lap, corner by corner

The optimal lap from section 4.2, read along its length. Braking zones are shaded.

Speed, cornering and braking g, and height along the 3.6 km Laguna Seca lap, with braking zones shaded and the Corkscrew and slowest corner marked

106.1 s, from 230 km/h at the end of the main straight to 53 km/h at the slowest corner. The track climbs about 50 m to the top of the Corkscrew and drops about 20 m through it in 240 m.

Three things stand out.

The slowest corner is entered braking and turning at once. The car brakes at about 1 g in a straight line from 120 km/h, then blends into the turn: 0.74 g of braking with 0.57 g of cornering, then cornering alone at 0.97 g at the apex. That is trail braking, and the optimizer finds it only because its tyre model couples braking and cornering grip.

Downforce changes where the grip is. In the g-g diagram below, the hardest cornering, about 1.35 g, happens at the highest speeds, because downforce grows with the square of speed. The slow corners top out lower.

The engine, not the tyres, limits acceleration. Accelerating never goes much past 0.55 g, while braking reaches 1.17 g. Out of slow corners the tyres have grip to spare.

The g-g diagram of the optimal lap: cornering against braking and accelerating, coloured by speed, inside a 1.4 g circle

Every 3 m of the lap as a point: sideways g against forward g, coloured by speed.

6. Verification

The controller reached the car one stage at a time, and each stage had to pass before the next was allowed.

StageWhat it checks
Unit testsbicycle dynamics, tyre models, cost and constraint Jacobians, generated struct layout, acados compatibility, powertrain conversion, actuator safety filters
Pipeline teststhe C++ runtime against the Python reference solver through pybind11; closed-loop laps on synthetic and real tracks
dSPACE VEOSsoftware in the loop with the full vehicle dynamics, before the car
AWSIMthe full stack at Laguna Seca in simulation
Ghost moderunning on the real car beside the existing controller, on live sensors, publishing commands without sending them
Track testclosed loop on the physical car

The pybind11 pipeline tests catch a class of bug the unit tests cannot: errors that only appear once symbolic expressions are compiled and run.

Closed-loop pipeline test: 96.42 s lap, top speed 231 km/h, largest lateral deviation 1.218 m

The full C++ pipeline, from state estimation to actuator commands, in closed loop against the bicycle model: 96.42 s, 231 km/h, and at most 1.218 m from the racing line.

Simulation at Laguna Seca. In AWSIM on the full 3.6 km circuit:

NMPCPure pursuitPure pursuit, tuned speed profileOptimal
Best lap117.6 s157.7 s113.4 s106.0 s
Top speed207 km/h
Lateral spread1.50 m
Lap to lap spread2.0 s
Lap time against the baselines, convergence, speed against the optimal profile, and lateral tracking

The NMPC is 25% faster than pure pursuit and within 11% of the optimal lap.

Eight consecutive laps with overlapping speed, lateral position and steering

Eight laps in a row: speed, lateral position and steering lie on top of each other, so the closed loop repeats without drifting.

Steering, drive and brake force, and speed against curvature for the best lap

The best lap’s controls: sharp braking into corners and full drive on the straights, the trail braking pattern the coupled tyre model allows.

Ghost mode on the car. Before getting authority, the controller ran on the real Dallara on live sensor data:

Ghost run
diagnostics rate24.5 Hz
command topic rate20.2 Hz
solve time, 95th percentile24.9 ms
healthy solves75.6%
fell back to the previous solution3.4%

On the car. The controller drove the physical Dallara AV-24 on a closed track, holding a 40 mph speed command for about 1,400 seconds with the cross-track error under 2 m, until an off-track event at the end.

On-car track test: speed tracking, throttle, cross-track error and MPC enable flag over time

The track test. The off-track event at the end came from conflicting throttle commands: the torque safety filter did not handle an edge case that appeared after the solver’s cores were cut from 6 to 1 to reduce solve time variance.

Against the requirements from section 1:

#RequirementResult
1Beat the existing controller117.6 s against 157.7 s for pure pursuit, 25% fastermet
2A command every 10 msreal-time iteration reduces each cycle to one prepared subproblem; the on-car ghost run used the full solver instead, at 24.9 ms (95th percentile)partly
3Stay inside the track limitslargest deviation 1.218 m in the pipeline test, 1.50 m spread in AWSIM, under 2 m on the car until the final upsetmet in simulation
4No front braking with rear drivesoft constraint and safety filter in place; the track test found an edge case it missednot yet
5Plan from where the car will beforward simulation through pending commands, run on live sensors in ghost modebuilt
6Switch lines in a racefive lines per track from one optimizer; pit and overtake switching by TTLmet
7Reach the car safelysoftware in the loop, simulation, ghost mode, then closed loop on the carmet

7. Limits and next steps

  • A tuned baseline was faster. Pure pursuit with a hand-tuned speed profile ran 113.4 s against the NMPC’s 117.6 s. The NMPC needs no per-track speed tuning, and closing that gap is the next tuning target.
  • The on-car timing was measured on the slower solver. The ghost run used the full solver, not the real-time iteration that meets 10 ms. The next on-car run should measure the real-time version directly.
  • The safety filter has a gap. The off-track event traced to a case the torque filter did not cover once the solver ran on one core. That case needs a test of its own.
  • The online model is simpler than the offline one. One likely part of the 11% gap to the optimal lap is the bicycle model leaving unused grip that the double-track model accounts for.
#mpc#autonomous-racing#trajectory-optimization#optimal-control#real-time-systems#vehicle-dynamics

Related projects