Probabilistic Robotics
Chapter 23PART VIPlanning and Acting under UncertaintyDifficulty: AdvancedEstimated reading time: 55 min

Stochastic MPC: MPPI and Friends

Control by sampling — a thousand imagined futures, scored and reweighted every frame. MPPI is the particle filter's twin, and this chapter proves it term by term.

Control inputs drawn from the optimal distribution achieve a lower cost, in expectation, than any other control distribution.
Grady Williams, Paul Drews, Brian Goldfain, James M. Rehg, and Evangelos A. TheodorouInformation Theoretic Model Predictive Control: Theory and Applications to Autonomous Driving (2017), §III-A

In this chapter

Chapter 20 produced a path. Chapter 21 and Chapter 22 produced policies on grids small enough to enumerate. None of them produce the thing a real robot actually needs, which is a pair of numbers — a linear and an angular velocity — every fifty milliseconds, in a room where somebody has moved a chair since the path was planned.

The classical answer is a feedback controller that tracks the path. It works beautifully until the world stops matching the map, and then it drives into the chair with excellent tracking error. The modern answer is to stop separating planning from execution: re-solve a short-horizon optimal control problem every cycle, execute only its first command, and throw the rest away. That is model predictive control, and its dominant sampling-based form, MPPI, turns out to be something the reader has already proved correct. It is importance sampling over control sequences: the particle filter of Chapter 8, aimed forward in time.

The problem with a plan

A path is a claim about a world that no longer exists. It was computed from a map, and the map was recorded before the chair moved, before somebody left a box in the corridor, before the person walking toward Rusty decided which side to pass on.

A tracking controller cannot fix this, because it is not being asked to. Give it a path and it will minimize the distance to that path, which is precisely the wrong objective when the path goes through a chair. Re-running the global planner instead is honest but expensive: at twenty hertz, with a planner that takes a hundred milliseconds and returns a structurally different path each time, Rusty would spend his life twitching between homotopy classes.

The receding-horizon answer is narrower and cheaper. Do not re-plan the route. Re-solve, every cycle, a short optimal control problem — two seconds, not two minutes — that already knows about the chair, and execute one step of its answer.

Watch a few cycles before reading on. Three things are worth naming now; each one becomes a theorem later in the chapter.

Nothing here is optimized in the usual sense. There is no gradient, no line search, no convexity. Two hundred perturbed plans are simulated forward, each is assigned a number, and the next plan is a weighted average. That is the entire algorithm, and it is the reason the obstacle cost is allowed to be a hard if statement over an occupancy grid.

The purple plan is not one of the orange rollouts. It is their weighted mean, and it is visibly smoother than any of them — the signature of an expectation rather than a selection. When the temperature is pushed toward zero this stops being true, and the plan snaps onto whichever single rollout got lucky. Watch the effective sample size collapse toward 1 when you do it.

The storm re-centres itself. Each cycle inherits the previous plan, shifted forward one step. That warm start is why two hundred samples suffice where a cold start would need thousands, and it is the exact analogue of the particle filter's prediction step.

Building intuition: search velocities, not paths

Before the path-integral machinery, the classical baseline — and it is a good one, still shipping in production stacks thirty years later. Fox, Burgard and Thrun's Dynamic Window Approach starts from a physical observation: a differential-drive robot cannot execute an arbitrary curve. Over one short control period it executes an arc, and which arcs are available is decided by its acceleration limits. So do not search over paths. Search over the velocities reachable in the next period.

That search space is two-dimensional and tiny, which is why DWA ran on 1997 hardware at speeds up to 95 cm/s. Its structure is worth internalizing because MPPI keeps one half of it and replaces the other:

  • Keep: commands are the decision variable, and the reachable set is a box set by acceleration limits.
  • Replace: each candidate is scored along one constant-curvature arc, evaluated at its endpoint. A hypothesis class of single arcs cannot express "slow, drift left, then turn hard right" — not badly, but at all.

The velocity plane on the left is where both controllers actually live, and putting them in the same axes is the fastest way to see what changed. DWA lays a grid over the reachable box and asks one question per cell: if I hold this command, where do I end up and what do I hit? MPPI scatters two hundred samples over the same plane, but each dot is the head of a whole sequence, and its weight depends on what the other twenty-four steps of that sequence cost.

Two honest observations from the bench, both of which the numbers later in this chapter pin down.

First, DWA threads the chairs down the centreline with five centimetres to spare, and no setting of its clearance weight changes that. The reason is worth the paragraph: Fox et al.'s clearance term is dist(v,ω)\mathrm{dist}(v,\omega), the distance travelled before the arc touches something. A gap you fit through at all returns the same value as a gap with a metre to spare, so the term is saturated and its weight is inert. Swap in the modern repair — score the minimum margin anywhere along the arc, which the widget lets you toggle — and the weight bites, but what it buys is paralysis: above wc0.1w_c \approx 0.1 Rusty stops 0.7 m short of the chair and never moves again. There is no setting in between, because the maneuver that keeps margin and makes progress is an S-curve, and an S-curve is not one arc.

Second, in the counter pocket both controllers stall. A local controller is local. Getting out of that pocket is Chapter 20's job, and no temperature setting substitutes for it.

The mathematics

Setting

Notation used in this chapter
SymbolMeaning
H, ΔtH,\ \Delta tHorizon in steps, and the control period. The plan spans H·Δt seconds; the robot commits to Δt of it.
U=(u0,,uH1)U = (u_0, \dots, u_{H-1})The control sequence being optimized. For Rusty, u_k = (v_k, ω_k)ᵀ.
V=(v0,,vH1)V = (v_0, \dots, v_{H-1})The sequence actually applied: the commanded mean plus injected noise, v_k = u_k + ε_k.
xk+1=f(xk,uk)x_{k+1} = f(x_k, u_k)Rollout dynamics — the noise-free Chapter 9 velocity model, integrated exactly.
S(V)=ϕ(xH)+kc(xk,uk)S(V) = \phi(x_H) + \sum_k c(x_k, u_k)Trajectory cost of one rollout: stage costs plus a terminal cost.
Σu\Sigma_uCovariance of the control perturbation. Its inverse appears in the weight, which is where the control cost comes from.
λ\lambdaTemperature. The chapter’s one headline parameter: how sharply exp(−S/λ) prefers cheap rollouts.
α, γ=λ(1α)\alpha,\ \gamma = \lambda(1-\alpha)Which base distribution defines the control cost, and the resulting coefficient of the cross term.
w(i), ηw^{(i)},\ \etaRollout weight and its normalizer — the same η the Bayes filter has used since Chapter 5.
F(S)\mathcal{F}(S)Free energy of the control system, −λ log E_p[exp(−S/λ)].

Receding-horizon control is the following loop, and its most consequential line is the last one.

Algorithmreceding_horizon(bel(x_t), U)Costone horizon optimization per control period
In
the current belief, the plan carried over from last cycle
Out
one command, and the plan for next cycle
  1. x0argmaxxbel(xt)x_0 \leftarrow \operatorname{argmax}_x \bel(x_t)    (or the mean; see the honesty note below)
  2. UargminU  E[ϕ(xH)+kc(xk,uk)]U^\star \leftarrow \operatorname{argmin}_U\; \E\big[\phi(x_H) + \sum_{k} c(x_k, u_k)\big] subject to xk+1=f(xk,uk)x_{k+1} = f(x_k, u_k)
  3. execute u0u^\star_0 for Δt\Delta t
  4. Ushift(U)U \leftarrow \text{shift}(U^\star)    (drop u0u^\star_0, slide, repeat the tail)
  5. discard everything else and go to 1

Line 5 is the trick. The controller solves a problem it does not intend to finish, which buys two things: the optimization only has to be locally right, and every cycle gets a fresh measurement. Feedback enters not through a gain matrix but through re-solving.

Line 1 collapses a belief into a point, and the whole of Parts II–IV was about why that is a lie. Doing it properly means optimizing over belief trajectories — belief-space MPC, of which Chapter 22 is the exact-solution end. The cheap partial repair used in practice, and in this chapter's Rust, is to inflate the robot's footprint by the localization covariance: replace the body radius rr with r+κλmax(Σt)r + \kappa \sqrt{\lambda_{\max}(\Sigma_t)}, where Σt\Sigma_t is the MCL position covariance from Chapter 12 and κ2\kappa \approx 2. A robot that does not know where it is drives as though it were bigger.

DWA, formally

The dynamic window is the intersection of three sets. The actuator envelope Vs=[vmin,vmax]×[ωmax,ωmax]V_s = [v_{\min}, v_{\max}] \times [-\omega_{\max}, \omega_{\max}]; the reachable set

Vd=[vamaxΔt,  v+amaxΔt]×[ωω˙maxΔt,  ω+ω˙maxΔt]\htmlClass{term-prior}{V_d} = \big[v - a_{\max}\Delta t,\; v + a_{\max}\Delta t\big] \times \big[\omega - \dot\omega_{\max}\Delta t,\; \omega + \dot\omega_{\max}\Delta t\big]

around the current command; and the admissible set VaV_a, the commands from which the robot can still stop before whatever its arc runs into.

DerivationWhere the admissibility inequality comes from

Let dist(v,ω)\mathrm{dist}(v, \omega) be the arclength travelled along the arc of curvature ω/v\omega / v before the robot's body first touches an obstacle. Braking at amaxa_{\max} from speed vv takes tb=v/amaxt_b = |v| / a_{\max} and covers

db=vtb12amaxtb2=v22amax.d_b = |v| t_b - \tfrac{1}{2} a_{\max} t_b^2 = \frac{v^2}{2 a_{\max}}.

Requiring dbdist(v,ω)d_b \le \mathrm{dist}(v, \omega) gives the inequality above, which is Fox et al.'s admissibility condition. The same argument on ω\omega with the angular limit ω˙max\dot\omega_{\max} bounds the rotation.

Two things are worth noticing. The first is that dist\mathrm{dist} must be measured along the arc, not as a straight line — a fast arc that curves away from a wall is safe, and a slow one that curves into it is not. The second is that this condition is exact only if the robot keeps the command. It is a one-step guarantee, re-derived every cycle, which is why DWA at speed depends on a fast control loop rather than on a long horizon.

Algorithmdwa_plan(x, x_goal, u_prev, esdf)CostO(n_v · n_ω · H) — 651 arcs × 40 integration steps in this chapter's lab
In
pose, goal, previous command, an obstacle distance field
Out
the command (v, ω) maximizing the objective over the admissible window
  1. VdV_d \leftarrow reachable box around uprevu_{prev} from the acceleration limits
  2. for all (v,ω)(v, \omega) on a grid over VsVdV_s \cap V_d do
  3.     integrate the arc; dist\mathrm{dist} \leftarrow arclength to first contact
  4.     if v>2distamax|v| > \sqrt{2\,\mathrm{dist}\,a_{\max}} then mark inadmissible; continue
  5.     G(v,ω)σ(whheading+wcclearance+wvvelocity)G(v,\omega) \leftarrow \sigma\big(w_h \cdot \mathrm{heading} + w_c \cdot \mathrm{clearance} + w_v \cdot \mathrm{velocity}\big)
  6. endfor
  7. return argmaxG\operatorname{argmax} G over the admissible candidates

σ\sigma is normalization across the window: each term is scaled to [0,1][0,1] before the weighted sum, so that metres and radians cannot decide the outcome by their units. (Fox et al. call the three weights α,β,γ\alpha, \beta, \gamma; this chapter needs those letters shortly for the temperature family, so they are wh,wc,wvw_h, w_c, w_v here.) Everything else is a design choice, and every one of those choices is a place where a real deployment spends a week.

The optimal distribution

Now the modern half. Fix a base distribution p(V)p(V) over control sequences — think of it as a prior over what the robot might do — and define the free energy of the control system,

F(S)  =  λlogEp ⁣[exp ⁣(1λS(V))].\mathcal{F}(S) \;=\; -\lambda \log \E_{p}\!\left[\exp\!\left(-\tfrac{1}{\lambda} S(V)\right)\right].

This object is the bridge between "minimize a cost" and "sample from a distribution", and the bridge is a single inequality.

DerivationThe free-energy bound and the distribution that attains it

Statement. For every distribution qq absolutely continuous with respect to pp,

F(S)    Eq[S(V)]  +  λKL(qp),\mathcal{F}(S) \;\le\; \E_{q}[S(V)] \;+\; \lambda\, \KL(q \,\|\, p),

with equality if and only if q=qq = q^\star, where q(V)=1ηp(V)eS(V)/λq^\star(V) = \frac{1}{\eta} p(V) e^{-S(V)/\lambda} and η=Ep[eS/λ]\eta = \E_p[e^{-S/\lambda}].

Step 1 — insert the proposal. Multiply and divide inside the expectation by qq, which is the importance-sampling identity and nothing more:

F(S)=λlogEq ⁣[p(V)q(V)eS(V)/λ].\mathcal{F}(S) = -\lambda \log \E_{q}\!\left[\frac{p(V)}{q(V)}\, e^{-S(V)/\lambda}\right].

Step 2 — Jensen. The logarithm is concave, so logEq[Z]Eq[logZ]\log \E_q[Z] \ge \E_q[\log Z]. Multiplying by λ<0-\lambda < 0 flips it:

F(S)λEq ⁣[logp(V)q(V)S(V)λ]=Eq[S(V)]+λEq ⁣[logq(V)p(V)].\mathcal{F}(S) \le -\lambda\, \E_q\!\left[\log \frac{p(V)}{q(V)} - \frac{S(V)}{\lambda}\right] = \E_q[S(V)] + \lambda\, \E_q\!\left[\log \frac{q(V)}{p(V)}\right].

Step 3 — name the second term. That expectation is exactly KL(qp)\KL(q \| p), giving the bound. Read it as an optimal control objective: expected cost, plus a temperature times the price of deviating from the base behaviour. The KL term is the control cost, and it arrived without being postulated.

Step 4 — equality. Jensen is tight exactly when its argument is qq-almost-surely constant: p(V)q(V)eS(V)/λ=c\frac{p(V)}{q(V)} e^{-S(V)/\lambda} = c. Solving for qq gives q(V)p(V)eS(V)/λq(V) \propto p(V) e^{-S(V)/\lambda}, which normalizes to qq^\star. Substituting qq^\star back into the right-hand side recovers λlogη=F(S)-\lambda \log \eta = \mathcal{F}(S), so the bound is attained. \blacksquare

Two readings of the same formula. Statistically, qq^\star is a Boltzmann distribution: a prior tilted by an exponentiated cost. Bayesianly, it is a posterior — with the prior p(V)p(V) and the "likelihood" eS(V)/λe^{-S(V)/\lambda}, so that a cost is a negative log-likelihood and λ\lambda is the noise level you are willing to assume about your own objective. Every posterior in this book has had that shape; this one just happens to be over the future.

Convention note. Williams et al. define F=logEP[eS/λ]F = \log \E_P[e^{-S/\lambda}] and call λ\lambda the inverse temperature, so their bound reads λFEQ[S]+λKL(QP)-\lambda F \le \E_Q[S] + \lambda \KL(Q\|P). The statement is identical; this book absorbs the λ-\lambda into F\mathcal{F} and calls λ\lambda the temperature, because larger λ\lambda flattens the tilt exactly as temperature flattens a Boltzmann distribution.

The twin theorem: MPPI is importance sampling

We now have a target, qq^\star, and no way to sample from it — the familiar predicament of Chapter 8, one dimension per horizon step deeper. The familiar remedy applies.

DerivationFrom q* to the MPPI update, cross term and all

Step 1 — what we actually want. Since qq^\star is a distribution and the robot must emit a single sequence, project: choose the Gaussian q(U,Σu)q(\cdot \mid U, \Sigma_u) closest to qq^\star in KL. Expanding KL(qq(U,Σu))\KL(q^\star \| q(\cdot|U,\Sigma_u)) and dropping the terms that do not involve UU leaves a quadratic minimization whose solution is moment matching,

uk=Eq[vk],k=0,,H1.\htmlClass{term-posterior}{u_k^\star} = \E_{q^\star}[v_k], \qquad k = 0, \dots, H-1.

The optimal open-loop plan is the mean of the optimal control distribution. Not its mode, not its best sample.

Step 2 — a proposal we can sample. Let U^\hat U be the plan we are carrying (the shifted result of last cycle) and draw V(i)=U^+ϵ(i)V^{(i)} = \hat U + \epsilon^{(i)}, ϵk(i)N(0,Σu)\epsilon^{(i)}_k \sim \Normal(0, \Sigma_u). Self-normalized importance sampling gives

Eq[vk]=Eq(U^) ⁣[w(V)vk],w(V)=q(V)q(VU^,Σu).\E_{q^\star}[v_k] = \E_{q(\cdot|\hat U)}\!\big[w(V)\, v_k\big], \qquad w(V) = \frac{q^\star(V)}{q(V \mid \hat U, \Sigma_u)}.

Step 3 — expand the ratio. Split it through the base distribution:

w(V)  =  1ηeS(V)/λp(V)q(VU^,Σu).w(V) \;=\; \frac{1}{\eta}\, e^{-S(V)/\lambda}\, \frac{p(V)}{q(V \mid \hat U, \Sigma_u)} .

Take the base to be Gaussian about some nominal U~\tilde U, i.e. p=q(U~,Σu)p = q(\cdot \mid \tilde U, \Sigma_u). Then the log of that last ratio is a difference of two quadratics, and the quadratic terms in vv cancel:

logp(V)q(VU^)=12k[(vku~k)TΣu1(vku~k)(vku^k)TΣu1(vku^k)]=constk(u^ku~k)TΣu1vk.\log \frac{p(V)}{q(V|\hat U)} = -\tfrac{1}{2}\sum_k \Big[(v_k - \tilde u_k)\T \Sigma_u^{-1}(v_k - \tilde u_k) - (v_k - \hat u_k)\T \Sigma_u^{-1}(v_k - \hat u_k)\Big] = \text{const} - \sum_k (\hat u_k - \tilde u_k)\T \Sigma_u^{-1} v_k .

Step 4 — the cross term. Substitute vk=u^k+ϵkv_k = \hat u_k + \epsilon_k and drop everything that does not depend on the sample (self-normalization kills it). Writing the standard family U~=αU^\tilde U = \alpha \hat U with α[0,1]\alpha \in [0,1], so that u^ku~k=(1α)u^k\hat u_k - \tilde u_k = (1-\alpha)\hat u_k, and setting γ=λ(1α)\gamma = \lambda(1-\alpha):

S~(i)  =  S(V(i))state cost  +  γk=0H1u^kTΣu1ϵk(i).\htmlClass{term-measurement}{\tilde S^{(i)}} \;=\; \underbrace{S(V^{(i)})}_{\text{state cost}} \;+\; \gamma \sum_{k=0}^{H-1} \htmlClass{term-prior}{\hat u_k}\T \Sigma_u^{-1} \htmlClass{term-prediction}{\epsilon_k^{(i)}} .

This second term is the one every quick tutorial drops. It is not decoration: it is the quadratic control cost, arriving through the likelihood ratio rather than by decree. Choosing α\alpha chooses what "control cost" means.

  • α=0\alpha = 0: the base is the uncontrolled system, U~0\tilde U \equiv 0, so γ=λ\gamma = \lambda and the objective charges λ2kukTΣu1uk\tfrac{\lambda}{2}\sum_k u_k\T\Sigma_u^{-1}u_k — genuine control effort. A robot under this base is reluctant to move at all unless the state cost pays for it.
  • α=1\alpha = 1: the base is the current plan, so γ=0\gamma = 0 and the cross term vanishes. Effort is free; what the KL term now charges is 12k(uku^k)TΣu1(uku^k)\tfrac{1}{2}\sum_k (u_k - \hat u_k)\T \Sigma_u^{-1}(u_k - \hat u_k), the deviation from last cycle's plan. This is a trust region, it is why α=1\alpha = 1 implementations produce smooth motion, and it is the setting this chapter's lab uses.

Step 5 — weights and the update. Subtract ρ=miniS~(i)\rho = \min_i \tilde S^{(i)} before exponentiating. Self-normalized weights are invariant to any constant shift of the cost, so this changes nothing mathematically and everything numerically: it guarantees the largest exponent is exactly e0=1e^0 = 1.

w(i)=1ηexp ⁣(1λ(S~(i)ρ)),η=jexp ⁣(1λ(S~(j)ρ)),\htmlClass{term-posterior}{w^{(i)}} = \frac{1}{\eta}\exp\!\Big(-\tfrac{1}{\lambda}\big(\tilde S^{(i)} - \rho\big)\Big), \qquad \eta = \sum_{j} \exp\!\Big(-\tfrac{1}{\lambda}\big(\tilde S^{(j)} - \rho\big)\Big),
uk    u^k  +  i=1Kw(i)ϵk(i).\htmlClass{term-posterior}{u_k} \;\leftarrow\; \htmlClass{term-prior}{\hat u_k} \;+\; \sum_{i=1}^{K} \htmlClass{term-posterior}{w^{(i)}}\, \htmlClass{term-prediction}{\epsilon_k^{(i)}} .

That is the whole algorithm, and it is the self-normalized importance-sampling estimator of Eq[vk]\E_{q^\star}[v_k] — biased at finite KK, consistent as KK \to \infty, exactly like every particle filter estimate in Part II. \blacksquare

AlgorithmMPPI(x_0, U, K, λ, Σ_u)CostO(K · H) dynamics and cost evaluations — embarrassingly parallel over K
In
current state, the plan carried over, sample count, temperature, perturbation covariance
Out
the command to execute, and the improved plan
  1. for i=1i = 1 to KK do    (in parallel)
  2.     xx0x \leftarrow x_0;   S~(i)0\tilde S^{(i)} \leftarrow 0
  3.     for k=0k = 0 to H1H-1 do
  4.         ϵk(i)N(0,Σu)\epsilon_k^{(i)} \sim \Normal(0, \Sigma_u);   vkg(uk+ϵk(i))v_k \leftarrow g(u_k + \epsilon_k^{(i)})
  5.         xf(x,vk)x \leftarrow f(x, v_k)
  6.         S~(i)S~(i)+c(x,vk)+γukTΣu1ϵk(i)\tilde S^{(i)} \leftarrow \tilde S^{(i)} + c(x, v_k) + \gamma\, u_k\T \Sigma_u^{-1} \epsilon_k^{(i)}
  7.     endfor
  8.     S~(i)S~(i)+ϕ(x)\tilde S^{(i)} \leftarrow \tilde S^{(i)} + \phi(x)
  9. endfor
  10. wComputeWeights(S~(1:K),λ)w \leftarrow \texttt{ComputeWeights}(\tilde S^{(1{:}K)}, \lambda)
  11. for k=0k = 0 to H1H-1 do    ukuk+iw(i)ϵk(i)u_k \leftarrow u_k + \sum_i w^{(i)} \epsilon_k^{(i)}
  12. USavitzkyGolay(U)U \leftarrow \texttt{SavitzkyGolay}(U)
  13. execute u0u_0;   shift UU   (and report ESS=1/i(w(i))2\mathrm{ESS} = 1/\sum_i (w^{(i)})^2)
AlgorithmComputeWeights(S̃_1:K, λ)CostO(K)
In
the K tilted rollout costs and the temperature
Out
self-normalized weights
  1. ρminiS~(i)\rho \leftarrow \min_i \tilde S^{(i)}
  2. ηiexp((S~(i)ρ)/λ)\eta \leftarrow \sum_{i} \exp\big(-(\tilde S^{(i)} - \rho)/\lambda\big)
  3. for i=1i = 1 to KK do    w(i)1ηexp((S~(i)ρ)/λ)w^{(i)} \leftarrow \tfrac{1}{\eta}\exp\big(-(\tilde S^{(i)} - \rho)/\lambda\big)
  4. return ww

Set the two algorithms side by side and there is nothing left to argue about.

sub-stepparticle filter (Ch. 8)MPPI
hypothesisa state x(i)x^{(i)}a control sequence U(i)U^{(i)}
proposalmotion model from the previous beliefGaussian around the previous plan
weightw(i)p(ztx(i))w^{(i)} \propto p(z_t \mid x^{(i)})w(i)eS~(i)/λw^{(i)} \propto e^{-\tilde S^{(i)}/\lambda}
estimatex^=iw(i)x(i)\hat x = \sum_i w^{(i)} x^{(i)}UU^+iw(i)ϵ(i)U \leftarrow \hat U + \sum_i w^{(i)} \epsilon^{(i)}
renewalresample, then predict forwardexecute u0u_0, shift, re-centre
failureparticle deprivation: no sample near the truthno rollout near the good maneuver
gaugeESS=1/i(w(i))2\mathrm{ESS} = 1/\sum_i (w^{(i)})^2the same formula, unchanged

The correspondence is not an analogy, and the widget is not a metaphor: both panels run library code that the rest of this book already uses. The left one calls lowVarianceResample from lib/filters/pf.ts; the right one calls the same Mppi class that drives the corridor at the top of this chapter, restricted to one dimension.

One asymmetry is real and worth stating. The particle filter's weights are handed to it — the likelihood is physics, and there is no knob. MPPI's weights come from a cost you invented, so λ\lambda is a free parameter: it says how much you believe your own objective. That freedom is the subject of the next section.

Temperature, and the two limits

DerivationThe λ → 0 and λ → ∞ limits

Order the sampled costs so that S~(1)<S~(2)\tilde S^{(1)} < \tilde S^{(2)} \le \dots, and assume the minimum is unique. Then

w(i)=e(S~(i)S~(1))/λje(S~(j)S~(1))/λ.w^{(i)} = \frac{e^{-(\tilde S^{(i)} - \tilde S^{(1)})/\lambda}}{\sum_j e^{-(\tilde S^{(j)} - \tilde S^{(1)})/\lambda}} .

As λ0+\lambda \to 0^+ every exponent with S~(i)>S~(1)\tilde S^{(i)} > \tilde S^{(1)} has a numerator going to zero, so w(1)1w^{(1)} \to 1 and the update becomes uku^k+ϵk(1)u_k \leftarrow \hat u_k + \epsilon_k^{(1)}: the single cheapest rollout wins outright, and ESS1\mathrm{ESS} \to 1. MPPI degenerates into random search with KK candidates, which is why the executed command starts jittering — a different sample wins every cycle, and the difference between the winner and the runner-up is Monte-Carlo noise.

As λ\lambda \to \infty every exponent goes to 00 and w(i)1/Kw^{(i)} \to 1/K, so ESSK\mathrm{ESS} \to K and the update is uku^k+1Kiϵk(i)u_k \leftarrow \hat u_k + \frac1K\sum_i \epsilon_k^{(i)}. The perturbations are zero-mean, so the correction is O(σ/K)O(\sigma/\sqrt{K}) — noise, not signal. The plan stops responding to the cost at all.

Neither limit is a failure of the derivation: they are the correct behaviour of an exponential tilt at the two ends of its range. The engineering question is where in between to sit, and the ESS is the instrument that answers it.

Here is that trade-off measured, on the corridor run of w23.1 (seed 23, K=200K = 200, H=25H = 25, Δt=0.1\Delta t = 0.1 s, α=1\alpha = 1). "Jitter" is the mean Δu|\Delta u| between consecutive executed commands — the number that shows up on screen as twitching.

λ\lambdaESS (of 200)time to goalmin clearancejitter
0.51.517.9 s0.16 m0.583
26.118.8 s0.21 m0.416
826.015.2 s0.23 m0.156
30108.618.9 s0.21 m0.103
120187.834.1 s0.11 m0.080

Read the extremes. At λ=0.5\lambda = 0.5 the controller is fast to react and horrible to ride, and its clearance suffers because a single lucky rollout is allowed to steer. At λ=120\lambda = 120 it is serene, twice as slow, and cuts its margin in half — the weights are nearly uniform, so the barrier cost barely moves the plan. The useful band is the one where the ESS sits somewhere around a tenth of KK: enough samples voting to average out the noise, few enough that the cheap ones dominate.

The same gauge, the same reading, the same fix as Chapter 8: if the ESS is pinned near 1, your proposal does not cover the target. In a filter you fix it with a better proposal; here you fix it by raising λ\lambda, by widening Σu\Sigma_u, or — most often — by smoothing the cost so that the rollouts are not all equally catastrophic.

Constraints, and how sampling gets away with them

Input limits. Do not reject samples and do not solve a QP. Push the clamp into the dynamics: xk+1=f(xk,g(vk))x_{k+1} = f(x_k, g(v_k)) with gg the saturation. The sampler stays a plain Gaussian, and the non-smooth gg costs nothing because the update law never differentiates ff. Clamp before integrating, so that the trajectory you score is one the actuators would actually produce.

Obstacles. Two options, and the difference is measured in ESS. An indicator cost cobs=M1[in collision]c_{obs} = M \cdot \mathbb{1}[\text{in collision}] is legal — no gradient is required — but in a tight corridor it makes most rollouts equally infinite, the weights collapse onto whichever sample squeaked through, and the ESS falls to 1. The smooth barrier over the signed distance field of Chapter 19,

cobs(x)=woexp ⁣(dESDF(x)rσo),\htmlClass{term-measurement}{c_{obs}(x)} = w_o \exp\!\left(-\frac{d_{\text{ESDF}}(x) - r}{\sigma_o}\right),

grades the rollouts instead of censoring them, which keeps the importance weights informative. This chapter's lab uses the barrier plus a flat 900 for actual collision: graded where grading helps, brutal where it must be.

Chance constraints. Inflating the footprint by the belief covariance, as in the callout above, is the cheap version. The honest version optimizes over belief trajectories and is a research field.

Implementation in Rust

The design is two traits and one struct. Making Dynamics and CostFn traits rather than closures is what leaves the socket open for the gradient methods at the end of this chapter: iLQR needs the same simulator, plus derivatives.

crates/ch23_mppi/src/dynamics.rs
use nalgebra::SVector;

/// A simulator. Note what is *not* required: no Jacobians, no differentiability,
/// no continuity. This trait is the entire interface MPPI needs from a model.
pub trait Dynamics<const NX: usize, const NU: usize>: Sync {
    fn step(&self, x: &SVector<f64, NX>, u: &SVector<f64, NU>, dt: f64) -> SVector<f64, NX>;

    /// Input constraints, pushed into the dynamics (Williams et al. §III-D3).
    /// Clamp before integrating: a plan scored on commands the motors would
    /// refuse is a plan scored on a lie.
    fn clamp(&self, u: SVector<f64, NU>) -> SVector<f64, NU>;
}

/// Rusty: unicycle kinematics, the Chapter 9 velocity model with the noise off.
pub struct DiffDrive {
    pub v_lim: (f64, f64),
    pub w_max: f64,
    pub a_max: f64,
}

impl Dynamics<3, 2> for DiffDrive {
    fn step(&self, x: &SVector<f64, 3>, u: &SVector<f64, 2>, dt: f64) -> SVector<f64, 3> {
        let (v, w) = (u[0], u[1]);
        let th = x[2];
        // The exact arc solution (Thrun et al., eq. 5.9). As ω → 0 the radius
        // blows up while the arc flattens; rather than trust that cancellation
        // numerically we switch to the straight-line limit, exactly as the
        // TypeScript port in `lib/sim/world.ts` does.
        if w.abs() < 1e-9 {
            SVector::<f64, 3>::new(x[0] + v * th.cos() * dt, x[1] + v * th.sin() * dt, th)
        } else {
            let r = v / w;
            let nt = th + w * dt;
            SVector::<f64, 3>::new(
                x[0] - r * th.sin() + r * nt.sin(),
                x[1] + r * th.cos() - r * nt.cos(),
                wrap_pi(nt),
            )
        }
    }

    fn clamp(&self, u: SVector<f64, 2>) -> SVector<f64, 2> {
        SVector::<f64, 2>::new(
            u[0].clamp(self.v_lim.0, self.v_lim.1),
            u[1].clamp(-self.w_max, self.w_max),
        )
    }
}

pub trait CostFn<const NX: usize, const NU: usize>: Sync {
    fn stage(&self, x: &SVector<f64, NX>, u: &SVector<f64, NU>, k: usize) -> f64;
    fn terminal(&self, x: &SVector<f64, NX>) -> f64;
}

/// Track Chapter 20's path, keep clear of Chapter 19's distance field.
pub struct TrackAndClear<'a> {
    pub path: &'a [SVector<f64, 2>],
    pub esdf: &'a ch19_maps::Esdf2,
    pub obstacles: &'a [Circle],
    pub robot_radius: f64,
    pub w_track: f64,
    pub w_progress: f64,
    pub w_obs: f64,
    pub sigma_o: f64,
    pub collision: f64,
}

impl CostFn<3, 2> for TrackAndClear<'_> {
    fn stage(&self, x: &SVector<f64, 3>, u: &SVector<f64, 2>, _k: usize) -> f64 {
        let (cross, s) = self.project(x[0], x[1]);
        let clear = self.clearance(x[0], x[1]);
        let obs = if clear <= 0.0 {
            // A discontinuity. Legal here, fatal for a gradient method.
            self.collision
        } else {
            self.w_obs * (-clear / self.sigma_o).exp()
        };
        self.w_track * cross * cross + obs - self.w_progress * s + 0.4 * u[1] * u[1]
    }

    fn terminal(&self, x: &SVector<f64, 3>) -> f64 {
        let (_, s) = self.project(x[0], x[1]);
        8.0 * (self.path_length() - s)
    }
}

The controller itself. The only subtle line is where the randomness happens: perturbations are drawn serially from a seeded generator and only then handed to rayon, because a parallel iterator that draws its own noise would make the run non-reproducible — and every simulation in this book is reproducible on purpose.

crates/ch23_mppi/src/mppi.rs
use nalgebra::{SMatrix, SVector};
use rand::{rngs::SmallRng, SeedableRng};
use rand_distr::{Distribution, Normal};

pub struct Mppi<D, C, const NX: usize, const NU: usize, const H: usize>
where
    D: Dynamics<NX, NU>,
    C: CostFn<NX, NU>,
{
    pub lambda: f64,
    pub sigma_u: SMatrix<f64, NU, NU>,
    pub k_samples: usize,
    /// γ = λ(1 − α). Zero means α = 1: the base distribution is the current
    /// plan, so the KL term charges deviation from it rather than effort.
    pub gamma: f64,
    nominal: [SVector<f64, NU>; H],
    dynamics: D,
    cost: C,
    rng: SmallRng,
}

pub struct MppiDiag<const NX: usize> {
    pub weights: Vec<f64>,
    pub ess: f64,
    pub s_min: f64,
    /// Every rollout, for the widget's storm. Native builds drop this.
    pub rollouts: Vec<Vec<SVector<f64, NX>>>,
}

impl<D, C, const NX: usize, const NU: usize, const H: usize> Mppi<D, C, NX, NU, H>
where
    D: Dynamics<NX, NU>,
    C: CostFn<NX, NU>,
{
    /// One control cycle. Returns the command to execute and the diagnostics
    /// the widget renders; the improved plan is left in `self.nominal`.
    pub fn plan(&mut self, x0: &SVector<f64, NX>, dt: f64) -> (SVector<f64, NU>, MppiDiag<NX>) {
        let sigma_inv = self.sigma_u.try_inverse().expect("Σ_u must be positive definite");
        let normals: Vec<Normal<f64>> = (0..NU)
            .map(|i| Normal::new(0.0, self.sigma_u[(i, i)].sqrt()).unwrap())
            .collect();

        // Draw every perturbation up front, from the seeded generator. This is
        // the price of determinism under rayon, and it is worth paying: the
        // same seed must produce the same storm on 1 core and on 32.
        let (k_samples, rng) = (self.k_samples, &mut self.rng);
        let eps: Vec<[SVector<f64, NU>; H]> = (0..k_samples)
            .map(|_| {
                std::array::from_fn(|_| SVector::<f64, NU>::from_fn(|i, _| normals[i].sample(rng)))
            })
            .collect();

        // Embarrassingly parallel: K independent simulations of H steps. On
        // wasm32 there are no threads, so the same closure runs serially — and
        // at K = 200, H = 25 that is 5 000 rollout steps a cycle, which this
        // book's TypeScript port measures at about 1.6 ms on a laptop core.
        #[cfg(not(target_arch = "wasm32"))]
        let costs: Vec<f64> = { use rayon::prelude::*; eps.par_iter().map(|e| self.rollout(x0, e, &sigma_inv, dt)).collect() };
        #[cfg(target_arch = "wasm32")]
        let costs: Vec<f64> = eps.iter().map(|e| self.rollout(x0, e, &sigma_inv, dt)).collect();

        let (weights, ess, s_min) = information_theoretic_weights(&costs, self.lambda);

        // u_k ← u_k + Σ_i w_i ε_k^(i): the self-normalized IS estimate.
        for k in 0..H {
            let mut delta = SVector::<f64, NU>::zeros();
            for (i, e) in eps.iter().enumerate() {
                delta += weights[i] * e[k];
            }
            self.nominal[k] = self.dynamics.clamp(self.nominal[k] + delta);
        }
        savitzky_golay_inplace(&mut self.nominal); // §III-D4: kill the Monte-Carlo chatter

        let u0 = self.nominal[0];
        (u0, MppiDiag { weights, ess, s_min, rollouts: vec![] })
    }

    /// S̃ = S(V) + γ Σ_k u_kᵀ Σ_u⁻¹ ε_k — state cost plus the cross term.
    fn rollout(
        &self,
        x0: &SVector<f64, NX>,
        eps: &[SVector<f64, NU>; H],
        sigma_inv: &SMatrix<f64, NU, NU>,
        dt: f64,
    ) -> f64 {
        let mut x = *x0;
        let mut s = 0.0;
        for k in 0..H {
            let u = self.dynamics.clamp(self.nominal[k] + eps[k]);
            x = self.dynamics.step(&x, &u, dt);
            s += self.cost.stage(&x, &u, k);
            s += self.gamma * self.nominal[k].dot(&(sigma_inv * eps[k]));
        }
        s + self.cost.terminal(&x)
    }

    /// The receding step: drop the executed command, slide, repeat the tail.
    /// The twin of the particle filter's prediction — the same belief, moved
    /// one step into the future and re-centred there.
    pub fn shift(&mut self) {
        for k in 0..H - 1 {
            self.nominal[k] = self.nominal[k + 1];
        }
    }
}

/// `ComputeWeights` — Williams et al., Algorithm 2.
pub fn information_theoretic_weights(costs: &[f64], lambda: f64) -> (Vec<f64>, f64, f64) {
    let rho = costs.iter().cloned().fold(f64::INFINITY, f64::min);
    let mut w: Vec<f64> = costs.iter().map(|s| (-(s - rho) / lambda).exp()).collect();
    let eta: f64 = w.iter().sum();
    w.iter_mut().for_each(|x| *x /= eta);
    let ess = 1.0 / w.iter().map(|x| x * x).sum::<f64>();
    (w, ess, rho)
}

A worked example you can check by hand

Take the smallest MPPI that is still MPPI: horizon H=1H = 1, K=3K = 3 rollouts, temperature λ=2\lambda = 2, perturbation σv=0.4\sigma_v = 0.4 m/s, and a nominal command u^=0.5\hat u = 0.5 m/s. Suppose the three perturbations come out ϵ=(+0.4,0,0.4)\epsilon = (+0.4,\, 0,\, -0.4) and the simulator returns state costs

S=(4,  2,  6).S = (4,\; 2,\; 6).

With α=1\alpha = 1 (the base is the current plan, γ=0\gamma = 0, no cross term) the shift is ρ=2\rho = 2, so the unnormalized weights are e1,e0,e2=0.36788,1,0.13534e^{-1},\, e^{0},\, e^{-2} = 0.36788,\, 1,\, 0.13534, summing to η=1.50321\eta = 1.50321. Normalizing:

w=(0.24473,  0.66524,  0.09003),ESS=1iwi2=10.51054=1.959.w = (0.24473,\; 0.66524,\; 0.09003), \qquad \mathrm{ESS} = \frac{1}{\sum_i w_i^2} = \frac{1}{0.51054} = 1.959 .

Two of three rollouts are effectively voting. The update is the weighted mean of the perturbations,

Δu=0.4(0.24473)+00.4(0.09003)=+0.06188,u0.5619 m/s.\Delta u = 0.4(0.24473) + 0 - 0.4(0.09003) = +0.06188, \qquad u \leftarrow 0.5619\ \text{m/s}.

Now switch to α=0\alpha = 0, the uncontrolled base, so γ=λ=2\gamma = \lambda = 2 and Σu1=1/0.16=6.25\Sigma_u^{-1} = 1/0.16 = 6.25. Each rollout picks up γu^Σu1ϵ=2(0.5)(6.25)ϵ=6.25ϵ\gamma\, \hat u\, \Sigma_u^{-1} \epsilon = 2 (0.5)(6.25)\epsilon = 6.25\,\epsilon:

S~=(6.5,  2,  3.5)    w=(0.06680,  0.63381,  0.29939),Δu=0.09303,u0.4070 m/s.\tilde S = (6.5,\; 2,\; 3.5) \;\Longrightarrow\; w = (0.06680,\; 0.63381,\; 0.29939), \qquad \Delta u = -0.09303, \qquad u \leftarrow 0.4070\ \text{m/s} .

The same three rollouts, the same state costs, and the update changes sign. Judged on state cost alone, going faster looked good; charged for the effort of going faster against a base distribution that would sit still, it no longer does. That is what the cross term is for, and why dropping it silently changes the problem you are solving.

crates/ch23_mppi/src/mppi.rs (tests)
#[test]
fn worked_example_ch23_three_rollouts() {
    // α = 1: no cross term. Weights are e^{-1}, e^{0}, e^{-2}, normalized.
    let (w, ess, rho) = information_theoretic_weights(&[4.0, 2.0, 6.0], 2.0);
    assert_relative_eq!(rho, 2.0, epsilon = 1e-12);
    assert_relative_eq!(w[0], 0.2447284, epsilon = 1e-6);
    assert_relative_eq!(w[1], 0.6652406, epsilon = 1e-6);
    assert_relative_eq!(w[2], 0.0900310, epsilon = 1e-6);
    assert_relative_eq!(ess, 1.958699, epsilon = 1e-5);

    let eps = [0.4, 0.0, -0.4];
    let du: f64 = w.iter().zip(eps).map(|(wi, e)| wi * e).sum();
    assert_relative_eq!(du, 0.0618787, epsilon = 1e-6);

    // α = 0: γ = λ = 2, Σ_u⁻¹ = 1/0.4² = 6.25, nominal u = 0.5.
    let tilted: Vec<f64> = [4.0, 2.0, 6.0]
        .iter()
        .zip(eps)
        .map(|(s, e)| s + 2.0 * 0.5 * 6.25 * e)
        .collect();
    assert_relative_eq!(tilted[0], 6.5, epsilon = 1e-12);

    let (w2, _, _) = information_theoretic_weights(&tilted, 2.0);
    let du2: f64 = w2.iter().zip(eps).map(|(wi, e)| wi * e).sum();
    assert_relative_eq!(du2, -0.0930347, epsilon = 1e-6);
    assert!(du2 < 0.0, "the control cost must reverse the update's sign");
}

/// Self-normalized weights cannot see a constant shift of the cost — which is
/// exactly why subtracting ρ is safe.
#[test]
fn shift_invariance() {
    let (a, _, _) = information_theoretic_weights(&[3.0, 7.0, 11.0, 2.0], 1.5);
    let (b, _, _) = information_theoretic_weights(&[1003.0, 1007.0, 1011.0, 1002.0], 1.5);
    for (x, y) in a.iter().zip(b) {
        assert_relative_eq!(*x, y, epsilon = 1e-12);
    }
}

The TypeScript port runs the same assertions in lib/control/__checks_ch23__.ts, and the widgets on this page call the same functions the tests do. If the prose and the code ever disagree, the tests settle it.

Putting it together

Here is the corridor run of w23.1, executed headless over twelve seeds: Rusty tracks a path planned before two chairs moved, with a third obstacle being pushed along the corridor while he drives. K=200K = 200, H=25H = 25 (2.5 s), Δt=0.1\Delta t = 0.1 s, λ=8\lambda = 8, Σu=diag(0.222,0.552)\Sigma_u = \mathrm{diag}(0.22^2,\, 0.55^2), α=1\alpha = 1.

metricMPPIDWA (wc=0.2w_c = 0.2)
runs that reached the goal12 of 12 seeds1 of 1 (deterministic)
collisions00
worst clearance over all runs0.194 m0.050 m
mean cross-track error0.051 m0.000 m
time to goal (mean)16.8 s15.9 s
command jitter, mean Δu\lvert \Delta u \rvert0.1690.008
cost per cycle5 000 dynamics + cost evaluations651 arcs × 40 steps

Both controllers get there, and reading this table as "MPPI wins" would be the wrong lesson. DWA is smoother and marginally faster, because it always picks a single arc and the arc it picks is usually "straight ahead, fast". What it will not do is deviate laterally to buy margin — and, as the bench shows, cannot be made to by tuning. Its clearance term saturates the moment an arc is collision-free, and replacing it with a true minimum margin turns the controller timid rather than graceful: measured on this run, wc0.08w_c \le 0.08 still shaves the chairs at 0.050 m, and wc0.12w_c \ge 0.12 freezes 0.7 m short. MPPI's exponential barrier grades the last few centimetres continuously and its hypothesis is a whole 2.5-second maneuver, so it swings out and back and keeps four times the margin at the same speed.

The differences that are structural are these. MPPI evaluates a whole 2.5-second maneuver as one hypothesis, so it can commit to a plan whose first half looks worse. And MPPI does not care what the cost is made of — the barrier, the flat 900 for collision, and a raw occupancy lookup are all the same to it. Neither property is available to a controller that scores one arc by its endpoint.

When gradients beat samples

Rule of thumb. Sampling for contact-rich, discontinuous, or multi-modal problems. Gradients for smooth, high-dimensional, or certified ones.

MPPI (sampling)iLQR / DDP / SQP (gradients)
needsa simulator of ff, pointwise costsf\nabla f, c\nabla c, and a smooth world
cost functionsanything, including indicators and gridsmust be differentiable; ESDF, not occupancy
parallelismembarrassing; KK independent rolloutssequential backward pass
dimensionsample complexity grows badlyhandles tens of states comfortably
constraintssoft, via cost; jitters near boundarieshard, via KKT; tight and reliable
local minimaescapes shallow ones by sampling both sidescommits to one homotopy class
what it returnsa mean, re-estimated every cyclea locally optimal trajectory and a feedback gain

That last row is the one people forget. iLQR returns uk=uˉk+Kk(xkxˉk)u_k = \bar u_k + K_k (x_k - \bar x_k) — a time-varying feedback law, valid between control cycles. MPPI returns a single open-loop command and relies on re-solving fast enough that open loop never has time to matter. At 20 Hz on a diff-drive that is a fine bet; on a machine whose joints need commands hundreds of times faster it is not, which is why sampling controllers at those rates are wrapped in a fast ancillary feedback law that tracks the sampled plan between updates.

What MPPI is not

The counter pocket in w23.2 is in this chapter because it is the failure most likely to bite a reader who has just watched the storm and concluded that sampling solves planning. Rusty sits behind the kitchen counter; the goal is two metres north, through it. Escaping costs 1.9 m of travel in the wrong direction before a single metre is repaid.

MPPI does not escape it. Not at H=10H = 10, not at H=60H = 60: the perturbations are white noise around the current plan, so the probability that some rollout traces a coherent four-metre detour is negligible, and every rollout that starts the detour looks worse than standing still. DWA does not escape it either, and neither of them is broken. A local controller optimizes; it does not search. Give the same MPPI a reference path around the counter and it follows it out in 13.6 seconds. That is the layered architecture the whole of Part VI has been building toward, and the reason Chapter 20 exists.

The small end of a real continuum

It would be easy to read this chapter as a toy. It is not. The algorithm above is what Williams et al. ran on a fifth-scale rally car sliding around a dirt track: about 1 200 rollouts of 2.5 seconds at 40 Hz, roughly 4.8 million queries to the full nonlinear vehicle dynamics every second, on a GPU, over more than 100 km of autonomous driving. The same update law, with a stack of about a dozen pluggable cost critics, ships as a stock controller in ROS 2's Nav2 for exactly the kind of differential-drive robot Rusty is. The reader's rayon loop is the small end of that continuum, not a different thing.

And there is a thread left deliberately loose. Every cost in this chapter has been about where the robot should be. Nothing stops a term in c(xk,uk)c(x_k, u_k) from being about what the robot would learn — the expected entropy reduction of the map, say. Put that term in the cost and the same sampler that dodges chairs starts choosing where to look. Chapter 24 makes goals out of information.

Exercises

  1. Foundation exerciseDifficulty 2 of 3Derive the cross term, then delete it

    Starting from w(V)eS(V)/λp(V)/q(VU^,Σu)w(V) \propto e^{-S(V)/\lambda}\, p(V) / q(V \mid \hat U, \Sigma_u) with p=q(αU^,Σu)p = q(\cdot \mid \alpha\hat U, \Sigma_u), carry out the two-quadratic expansion in Step 3 of the twin derivation and confirm that the sample-dependent part of the log-ratio is exactly k(1α)u^kTΣu1ϵk-\sum_k (1-\alpha)\hat u_k\T \Sigma_u^{-1} \epsilon_k. Then explain, using the free-energy bound rather than intuition, what objective you are optimizing when you set γ=0\gamma = 0: what replaces the effort penalty, and why that makes the resulting motion smoother rather than faster.

  2. Foundation exerciseDifficulty 2 of 3Both limits, and the gauge between them

    Prove the two limits of the previous derivation carefully: that λ0+\lambda \to 0^+ gives best-of-KK (state the tie-breaking assumption you need) and that λ\lambda \to \infty gives an update of size O(σ/K)O(\sigma/\sqrt{K}). Then show that ESS=1\mathrm{ESS} = 1 and ESS=K\mathrm{ESS} = K are attained exactly in those limits, and use that to explain why the measured table above has its shortest time-to-goal in the middle rather than at either end.

  3. Foundation exerciseDifficulty 3 of 3Why the mean, not the mode

    Step 1 of the twin derivation projects qq^\star onto a Gaussian by minimizing KL(qq(U,Σu))\KL(q^\star \| q(\cdot|U,\Sigma_u)) and lands on moment matching. Do the algebra. Then argue what would change if you minimized the reverse KL instead, KL(q(U,Σu)q)\KL(q(\cdot|U,\Sigma_u) \| q^\star), and relate your answer to what happens in w23.1 as λ0\lambda \to 0. Which of the two divergences describes a controller that commits to one option in a bimodal cost landscape, and which describes one that steers between them into the obstacle?

  4. Conceptual exerciseDifficulty 1 of 3Predict, then check: starve the proposal

    In w23.1, set the exploration σv\sigma_v to roughly a quarter of its default (0.06 m/s) and leave everything else alone. Before you press play, predict: does Rusty still find the gap beside the first chair, and does the ESS go up or down? Run it. Explain what you see in the proposal-coverage language of Chapter 8 — and note which of the two possible ESS answers is the bad one here, which is the opposite of the filtering case.

  5. Conceptual exerciseDifficulty 2 of 3Try to make DWA safe

    In w23.2, run the corridor with DWA and note the minimum clearance. Predict what raising the clearance weight wcw_c from 0.02 to 0.5 will do, then do it — and explain the result from the definition of dist(v,ω)\mathrm{dist}(v,\omega) rather than from the plot. Now switch on minimum-margin clearance and find the largest wcw_c that still reaches the goal, and the smallest that freezes the robot. Report both, and say why there is no useful value in between: what shape of trajectory would be needed, and why is it not in DWA's hypothesis class? Finally, name the property of exp(d/σo)\exp(-d/\sigma_o) that a normalized linear term lacks.

  6. Practical exerciseDifficulty 2 of 3Colored noise

    White perturbations spend most of their probability mass on plans that reverse direction every step — plans no robot would consider. Implement temporally correlated sampling in mppi.rs: draw ϵk=βϵk1+1β2ξk\epsilon_k = \beta \epsilon_{k-1} + \sqrt{1-\beta^2}\, \xi_k with ξkN(0,Σu)\xi_k \sim \Normal(0, \Sigma_u), so the marginal variance is unchanged. Measure, on the corridor run over twelve seeds, the mean Δu|\Delta u|, the minimum clearance, and the time to goal as a function of β{0,0.3,0.6,0.9}\beta \in \{0, 0.3, 0.6, 0.9\}. At which β\beta does the controller stop being able to react to the moving obstacle, and why?

  7. Practical exerciseDifficulty 3 of 3A gradient rival on the same traits

    Implement one iLQR iteration against the same Dynamics and CostFn traits, adding only the two Jacobian methods it needs. Race it against MPPI on (a) the smooth-ESDF corridor and (b) a version whose obstacle cost is a raw occupancy-grid lookup with no smoothing. Report time to goal, minimum clearance, and iterations to convergence for both, and reproduce the top two rows of this chapter's scorecard with your own numbers. If iLQR fails on (b), say precisely at which line of your implementation it fails.

References

  1. Fox, D., Burgard, W., and Thrun, S. (1997) The Dynamic Window Approach to Collision Avoidance. IEEE Robotics & Automation Magazine 4(1), 23–33.doi:10.1109/100.580977 (opens in a new tab)

    The classical baseline of this chapter, from the same authors as the book's spine. Section III is the source of the admissibility inequality and the three-term objective; the RHINO experiments ran at up to 95 cm/s in populated corridors.

  2. Theodorou, E., Buchli, J., and Schaal, S. (2010) A Generalized Path Integral Control Approach to Reinforcement Learning. Journal of Machine Learning Research 11, 3137–3181.link to A Generalized Path Integral Control Approach to Reinforcement Learning (opens in a new tab)

    Where the exponentiated-cost weighting entered robotics, as PI². Read it for the continuous-time path-integral derivation this chapter deliberately replaces with the discrete-time information-theoretic one.

  3. Williams, G., Aldrich, A., and Theodorou, E. A. (2017) Model Predictive Path Integral Control: From Theory to Parallel Computation. Journal of Guidance, Control, and Dynamics 40(2), 344–357.doi:10.2514/1.G001921 (opens in a new tab)

    The paper that named MPPI and made the case for GPU rollouts. Its parallel-computation analysis is the reason the Rust here draws noise serially and parallelizes only the simulations.

  4. Williams, G., Drews, P., Goldfain, B., Rehg, J. M., and Theodorou, E. A. (2018) Information-Theoretic Model Predictive Control: Theory and Applications to Autonomous Driving. IEEE Transactions on Robotics 34(6), 1603–1622.doi:10.1109/TRO.2018.2865891 (opens in a new tab)

    This chapter's Foundation section follows its §III exactly: the free-energy bound, the optimal distribution, the importance-sampling weight with its cross term, and Algorithms 1–2. The preprint is arXiv:1707.02342, and the epigraph is from its §III-A.

  5. Macenski, S., Moore, T., Lu, D. V., Merzlyakov, A., and Ferguson, M. (2023) From the Desks of ROS Maintainers: A Survey of Modern & Capable Mobile Robotics Algorithms in the Robot Operating System 2. Robotics and Autonomous Systems 168, 104493.doi:10.1016/j.robot.2023.104493 (opens in a new tab)

    Deployment evidence for the claim that MPPI is now a default rather than a curiosity: written by the Nav2 maintainers, it surveys the controller stack that ships MPPI alongside DWB, the direct descendant of the 1997 paper above.

  6. Kazim, M., Hong, J., Kim, M.-G., and Kim, K.-K. K. (2024) Recent Advances in Path Integral Control for Trajectory Optimization: An Overview in Theoretical and Algorithmic Perspectives. Annual Reviews in Control 57, 100931.doi:10.1016/j.arcontrol.2023.100931 (opens in a new tab)

    The map of the field since 2018 — cross-entropy variants, covariance adaptation, smoothing schemes — and the best single source for what to read next after this chapter.

  7. Trevisan, E. and Alonso-Mora, J. (2024) Biased-MPPI: Informing Sampling-Based Model Predictive Control by Fusing Ancillary Controllers. IEEE Robotics and Automation Letters 9(6), 5871–5878.doi:10.1109/LRA.2024.3397083 (opens in a new tab)

    The modern answer to this chapter's counter-pocket failure: keep the proposal, but seed it with samples from controllers that already know the way out. The importance weights stay valid, which is exactly the point of deriving them properly.

  8. Homburger, H., Messerer, F., Diehl, M., and Reuter, J. (2025) Optimality and Suboptimality of MPPI Control in Stochastic and Deterministic Settings. IEEE Control Systems Letters.doi:10.1109/LCSYS.2025.3574151 (opens in a new tab)

    An honest accounting of what MPPI actually returns: the suboptimality of the sampled solution grows second-order in the noise scaling for smooth unconstrained problems. Read it before promising anyone that MPPI is optimal.