Skip to main content
Contents5 / 8 ▾
05
Phase Five · Deciding Where to Go, and Actually Going

Path Planning & Motion Control

Four phases of estimation and mapping earn their keep here: turning a pose and a map into motor commands. Three layers — global route, local trajectory, tracking control — each running at its own rate, each failing its own way.

5.1The Three-Layer Stack

Navigation is a hierarchy because no single algorithm can be simultaneously globally optimal, dynamically feasible, and fast enough to dodge a pedestrian. So the field splits the problem by horizon: a global planner finds a route through the whole map and can afford to be slow; a local planner turns the next few metres of that route into a dynamically feasible trajectory around fresh obstacles, fast; a tracking controller turns the trajectory into wheel commands at control rate. Each layer trusts the one above for direction and the one below for execution, and every layer reads Phase 2’s state estimate.

THE NAVIGATION HIERARCHY global costmap / ESDF Phase 3's map, inflated local costmap rolling window, sensor-fresh global planner A* / RRT* · ~0.1–1 Hz local planner DWA / TEB / MPPI · 10–50 Hz tracking controller pure pursuit / MPC 50–200 Hz → v, ω path trajectory Phase 2's state estimate feeds every layer, at that layer's rate
Fig 5.1: horizon decreases left to right, rate increases. The global layer is allowed to be slow and optimal; the local layer must be feasible and fast; the controller must be simple and relentless. Most navigation bugs are one layer doing another layer’s job.

The planner’s world model is the costmap: the Phase 3 map with obstacles inflated. Planning treats the robot as a point, so every obstacle grows by the robot’s radius (the configuration-space trick), then by a decaying safety margin — cost falls off from lethal at the obstacle through "inscribed" (guaranteed collision for the footprint) to a soft exponential buffer that makes planners prefer the middle of corridors. The ESDF of Phase 3.4 is this idea done continuously: distance is the cost, and its gradient points to safety.

Configuration space (C-space)
The space of robot configurations. Inflating obstacles by the robot radius turns "will this footprint collide" into "is this point in collision" — planning’s favorite simplification.
Kinodynamic feasibility
Whether a trajectory respects the robot’s motion limits — velocity, acceleration, turning radius. Global paths usually aren’t; local trajectories must be.
Anytime planner
Returns a valid (suboptimal) solution fast, then improves it while time remains. RRT* and weighted-A* variants are the classic examples.
Recovery behavior
What runs when planning fails: rotate in place, back up, clear the costmap. Unsexy, and the difference between a stuck robot and a support ticket.

5.2Graph Search: A*, D* Lite, JPS

On a grid costmap, planning is shortest-path search, and A* is the reference algorithm: expand nodes in order of f = (cost so far) + (heuristic cost to go), and the heuristic’s honesty is the entire contract:

f(n) = g(n) + h(n)          g: best known cost from start
                            h: estimated cost to goal

h admissible  (never overestimates)           → A* returns the optimal path
h consistent  (h(n) ≤ c(n,n') + h(n'))        → each node expanded only once
h = 0                                          → Dijkstra (blind, fat search)
f = g + w·h,  w > 1  ("weighted A*")           → faster, cost ≤ w × optimal

grids: octile heuristic for 8-connected — max+min formula of |Δx|,|Δy|
WHAT EACH SEARCH EXPLORES Dijkstra (h=0): a disc A* (h honest): an ellipse w·h: corridor, ≤ w·optimal S G
Fig 5.2: the explored region shrinks as the heuristic gets bolder. Dijkstra pays for ignorance with a disc of expansions; admissible A* focuses toward the goal at no cost in optimality; weighted A* buys speed with a bounded-suboptimality corridor. Setup A measures all three.

Two SOTA refinements matter on real robots. D* Lite solves the replanning problem: mid-route, a door turns out closed — a fresh A* from scratch wastes everything learned. D* Lite searches backward from the goal (so cost-to-goal values stay valid as the robot moves) and, when edge costs change, repairs only the locally inconsistent nodes — those where the one-step lookahead rhs(u) = min over successors of c(u,s’) + g(s’) disagrees with g(u). Typical repairs touch a few percent of the map. Jump Point Search attacks uniform grids’ symmetry: a thousand equal-cost staircase paths cross an open room, and plain A* dutifully expands them all. JPS proves that only jump points (where an optimal path is forced to turn, at obstacle corners) need expansion, and leaps straight lines between them — order-of-magnitude speedups, exact same optimal path, zero preprocessing; JPS+ precomputes the jump distances for another large factor.

Setup A: A* with a tunable heuristic — one evening, zero dependencies. Goal: one grid, one concave trap, three heuristic weights — and the expansion counts that make Fig 5.2 empirical. Save as astar.cc, compile with g++ -O2 -std=c++17 astar.cc -o astar:

// astar.cc — A* on a grid; w=0 Dijkstra, w=1 optimal, w>1 bounded-suboptimal.
#include <algorithm>
#include <cmath>
#include <iostream>
#include <queue>
#include <vector>

int main() {
  std::vector<std::string> map = {          // S start, G goal, # wall
    "..................................................",
    "..................................................",
    "......S...............#..........................",
    ".......................#.........................",
    "........................#........................",
    ".........########################.................",
    ".........#......................#.................",
    ".........#......................#.................",
    ".........#........########......#.................",
    ".........#........#......#......#.................",
    ".........#........#..G...#......#.................",
    ".........#........#......#......#.................",
    ".........#........#......#####..#.................",
    ".........#........#..............#................",
    ".........##########...............#...............",
    "..................................................",
    "..................................................",
  };
  const int H = map.size(), W = map[0].size();
  int sx, sy, gx, gy;
  for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
    if (map[y][x] == 'S') { sx = x; sy = y; }
    if (map[y][x] == 'G') { gx = x; gy = y; }
  }
  const int DX[8] = {1,-1,0,0,1,1,-1,-1}, DY[8] = {0,0,1,-1,1,-1,1,-1};

  for (double w : {0.0, 1.0, 2.5}) {
    std::vector<double> g(W * H, 1e18);
    std::vector<int> par(W * H, -1);
    auto h = [&](int x, int y) {              // octile distance (admissible)
      double dx = std::abs(x - gx), dy = std::abs(y - gy);
      return std::max(dx, dy) + 0.41421356 * std::min(dx, dy);
    };
    using QE = std::pair<double, int>;        // (f, index)
    std::priority_queue<QE, std::vector<QE>, std::greater<>> open;
    g[sy * W + sx] = 0.0;
    open.push({w * h(sx, sy), sy * W + sx});
    long expanded = 0;

    while (!open.empty()) {
      auto [f, id] = open.top(); open.pop();
      const int x = id % W, y = id / W;
      if (f > g[id] + w * h(x, y) + 1e-9) continue;   // stale entry
      ++expanded;
      if (x == gx && y == gy) break;
      for (int k = 0; k < 8; ++k) {
        const int nx = x + DX[k], ny = y + DY[k];
        if (nx < 0 || ny < 0 || nx >= W || ny >= H || map[ny][nx] == '#') continue;
        const double nc = g[id] + ((k < 4) ? 1.0 : 1.41421356);
        if (nc < g[ny * W + nx]) {
          g[ny * W + nx] = nc;  par[ny * W + nx] = id;
          open.push({nc + w * h(nx, ny), ny * W + nx});
        }
      }
    }
    auto shown = map;                          // overlay the path
    for (int id = gy * W + gx; id != -1; id = par[id])
      if (shown[id / W][id % W] == '.') shown[id / W][id % W] = 'o';
    std::cout << "w = " << w << "   expanded " << expanded
              << " nodes   path cost " << g[gy * W + gx] << "\n";
    for (auto& row : shown) std::cout << "  " << row << "\n";
  }
}

Expected: all three find the goal inside the concave pocket, but the ledger differs — w = 0 (Dijkstra) expands the most nodes, w = 1 expands markedly fewer at the same path cost, and w = 2.5 expands the fewest while returning a slightly longer path (never more than 2.5× optimal, usually far closer). The trap is the teaching device: the heuristic confidently pulls the search into the pocket’s mouth, and the search must flood back out — heuristics help exactly as much as the world cooperates.

Sanity checks: (1) make the heuristic inadmissible by doubling it inside h() at w=1 and find a map where the returned path is visibly not optimal. (2) Change the diagonal cost to 1.0 and watch paths develop unrealistic staircase kinks — cost models leak into behavior. (3) Count re-expansions with an inconsistent heuristic — the "each node once" guarantee is the consistency clause, not the admissibility one.

5.3Sampling-Based Planning: RRT*, PRM, and the Informed Trick

Grids die of dimensionality: fine discretization in 6-DoF (an arm, a drone in SE(3)) is astronomically many cells. Sampling-based planners skip the discretization: draw random configurations, keep the collision-free ones, connect near neighbors, and let the graph emerge. PRM builds that roadmap offline for many queries (LazyPRM* defers expensive collision checks until a candidate path actually proposes an edge, validating lazily); RRT grows a tree online from the start, pulled toward unexplored space; and RRT* adds the two moves that buy asymptotic optimality — as samples n → ∞, the best path in the tree converges to the true optimum:

RRT* per sample x_new:
  1. choose parent: among tree nodes within radius r_n, wire x_new
     to the neighbor minimizing (cost-to-come + edge cost)
  2. rewire: for each neighbor, if routing THROUGH x_new is cheaper,
     swap its parent — the tree continuously repairs itself

  r_n = γ · (log n / n)^(1/d)        shrinks as the tree densifies

Informed RRT*: once any solution of cost c_best exists, the only samples
that can improve it lie in the ellipsoid  ‖x−x_start‖ + ‖x−x_goal‖ ≤ c_best
→ sample only there; the ellipse shrinks with every improvement.
INFORMED RRT*: WHERE BETTER PATHS CAN LIVE start goal current best path: cost c_best samples out here cannot help the informed ellipse — shrinks as c_best improves
Fig 5.3: after a first solution exists, uniform sampling wastes almost everything — only the ellipse with the start and goal as foci can contain an improvement. Informed RRT* samples it directly, converging orders of magnitude faster in cluttered or long-thin problems.
Search and sampling are the same bet placed differently: A*-family methods buy optimality with structure (grids, heuristics) and die of dimension; RRT*-family methods buy dimension-independence with randomness and pay in convergence time. Ground robots on 2D costmaps mostly search; arms, drones, and kinodynamic problems mostly sample.

5.4Local Planning: DWA and TEB

The global path ignores dynamics and fresh obstacles; the local planner owns both. DWA — the dynamic window approach — is honest brute force in velocity space: consider only the (v, ω) pairs reachable within one control cycle under acceleration limits (the "dynamic window"), forward-simulate each for a couple of seconds through the local costmap, score, pick the best, repeat at 20 Hz:

window:  V = { (v, ω) :  |v − v_now| ≤ v̇_max·Δt,  |ω − ω_now| ≤ ω̇_max·Δt }
         ∩ { admissible: can still stop before the nearest obstacle }

score:   G(v, ω) = α·heading(goal alignment) + β·clearance + γ·velocity
→ simulate every candidate, take argmax, emit one (v, ω), repeat

DWA’s weakness is myopia: it commits to one arc at a time, so a robot facing a U-shaped obstacle happily maximizes progress into the pocket, then oscillates. TEB — timed elastic band — plans a whole trajectory instead: take the global path’s next stretch as a band of poses with time intervals, and optimize band and times jointly (a sparse graph problem, solved with g2o — Phase 1’s loop yet again) under objectives for time-optimality, obstacle clearance, and kinodynamic limits; the standalone C++ library plans in several path topologies in parallel (left of the pillar vs right) and picks the best, which is exactly the decision DWA cannot represent. The price of both: tuning. Weights α, β, γ and their TEB equivalents are robot- and site-specific, and every practitioner has a scar story.

5.5Path Tracking: Pure Pursuit and Stanley

Below planning sits the controller — given the trajectory, produce commands now. Two geometric controllers own the classical tier. Pure pursuit: pick the point on the path one lookahead distance Ld ahead, and command the unique circular arc that passes through it:

pure pursuit:  α = bearing of lookahead point in the robot frame
               κ = 2·sin(α) / L_d          curvature of the joining arc
               ω = v·κ                      (L_d often scheduled: L_d = k·v)

Stanley:       δ = θ_e + arctan( k·e_fa / (k_s + v) )
               heading error + cross-track correction at the front axle
PURE PURSUIT: CHASE A POINT, DRIVE AN ARC path robot, speed v circle of radius L_d lookahead point commanded arc: κ = 2 sin α / L_d
Fig 5.4: the controller aims not at the path but at a point sliding along it. Ld is the single personality knob — small chases the path aggressively and oscillates, large smooths everything and cuts corners. Setup B lets you feel both failure modes.

Pure pursuit is beloved because it is ten lines, has one knob, and degrades gracefully. Stanley (the controller that won the DARPA Grand Challenge aboard the eponymous VW) instead regulates the cross-track error at the front axle directly, plus heading alignment — it converges to the path exactly rather than orbiting it, at the cost of more sensitivity to noise at speed. Beyond both sits full MPC tracking, which is the next section wearing its controller hat.

5.6MPC and MPPI: Optimization at Control Rate

Model predictive control collapses the planner/controller boundary: at every tick, solve a short optimal-control problem — minimize tracking error plus effort over the next N steps, subject to the robot’s actual dynamics and constraints — apply the first control, shift the horizon, repeat:

MPC, each tick:   min_{u₀..u_{N−1}}  Σₖ ℓ(xₖ, uₖ) + ℓ_f(x_N)
                  s.t.  xₖ₊₁ = f(xₖ, uₖ),   uₖ ∈ U,   xₖ ∈ X_free

nonlinear f → NMPC: transcribe (multiple shooting) and solve the NLP with
CasADi (+IPOPT) or ACADO's code-generated fast solvers — kHz-rate on ARM.

MPPI (sampling instead of gradients):
  draw K control sequences  uₖ + εₖ,   rollout each on the GPU, cost Sₖ
  wₖ = exp(−(Sₖ − min S)/λ)             softmin weighting
  u  ← Σ wₖ(uₖ + εₖ) / Σ wₖ             the noise-weighted average, repeat

The two branches split on differentiability. NMPC (via CasADi or ACADO) wants smooth dynamics and costs, and repays them with tight constraint handling and mature solvers. MPPI — model predictive path integral control — asks nothing of the cost function: sample thousands of trajectories, weight by exponentiated cost, average. Collision indicators, discrete gates, learned costs — all legal, because nothing is differentiated; a modern GPU rolls out 10,000 samples of a 2 s horizon fast enough for 30–50 Hz control, which is why GPU-MPPI has become the SOTA local planner for aggressive ground robots and the default advanced controller in modern navigation stacks. Its failure mode is its honesty: with too few samples or too-cold λ it averages between modes ("half left, half right of the pillar") — the same multimodality TEB solves with parallel topologies.

Local planner / controllerMechanismStrengthsWatch out for
DWAsample (v,ω) window, simulate, scoresimple, cheap, predictablemyopic; U-shaped traps; tuning
TEB (standalone C++)optimize a timed pose band (g2o), parallel topologieskinodynamic, anticipates, chooses sidesheavier; needs a sane global path
NMPC (CasADi/ACADO)solve an NLP each tickconstraints honored exactly, elegant trackingneeds differentiable models; solver latency tails
GPU MPPI10k sampled rollouts, softmin averagearbitrary costs, dynamics as-is, embarrassingly parallelsample starvation; mode averaging; needs a GPU

Setup B: pure pursuit end to end — one evening, zero dependencies. Goal: a unicycle robot tracking a square path at three lookahead distances, with the cross-track ledger that turns Fig 5.4’s "one knob" claim into numbers. Save as pursuit.cc, compile with g++ -O2 -std=c++17 pursuit.cc -o pursuit:

// pursuit.cc — pure pursuit on a 4×4 m square path, unicycle model.
#include <cmath>
#include <iostream>
#include <vector>

struct P { double x, y; };

int main() {
  std::vector<P> path;                       // dense square, 5 cm spacing
  auto edge = [&](P a, P b) {
    const int n = 80;
    for (int i = 0; i < n; ++i)
      path.push_back({a.x + (b.x - a.x) * i / n, a.y + (b.y - a.y) * i / n});
  };
  edge({0,0},{4,0}); edge({4,0},{4,4}); edge({4,4},{0,4}); edge({0,4},{0,0});

  for (double Ld : {0.15, 0.6, 1.8}) {
    double x = 0.2, y = -0.3, th = 0.3;      // start off-path, misaligned
    const double v = 0.5, dt = 0.02;
    double sum_e = 0.0, max_e = 0.0;  int steps = 0;

    for (double t = 0.0; t < 40.0; t += dt, ++steps) {
      size_t near = 0;  double dbest = 1e18;  // nearest path sample
      for (size_t i = 0; i < path.size(); ++i) {
        const double d = std::hypot(path[i].x - x, path[i].y - y);
        if (d < dbest) { dbest = d; near = i; }
      }
      sum_e += dbest;  max_e = std::max(max_e, dbest);

      size_t tgt = near;                      // walk forward to lookahead
      while (std::hypot(path[tgt].x - x, path[tgt].y - y) < Ld)
        tgt = (tgt + 1) % path.size();

      const double ax = path[tgt].x - x, ay = path[tgt].y - y;
      const double alpha = std::atan2(ay, ax) - th;      // bearing in body
      const double kappa = 2.0 * std::sin(alpha) / Ld;   // the whole law
      const double w = std::max(-2.5, std::min(2.5, v * kappa));

      th += w * dt;                           // unicycle integration
      x  += v * std::cos(th) * dt;
      y  += v * std::sin(th) * dt;
    }
    std::cout << "Ld = " << Ld << " m   mean cross-track "
              << sum_e / steps << " m   max " << max_e << " m\n";
  }
}

Expected: Ld = 0.6 wins the ledger — small mean error, modest corner overshoot. Ld = 0.15 hunts: the robot overreacts to the nearest path points, ω saturates, and the max error spikes from oscillation. Ld = 1.8 glides — beautiful mean behavior between corners, but every corner is cut by tens of centimetres because the controller was already chasing the next edge. That triangle — stability, accuracy, anticipation, pick two — is the entire tuning experience of geometric tracking, in three printed lines.

Sanity checks: (1) schedule Ld = 0.4 + 0.8·v with varying v and watch both failure modes soften — gain scheduling is the standard production answer. (2) Start the robot at heading th = 2.5 (nearly backwards): pure pursuit recovers, one of the reasons it is loved. (3) Log ω and plot its variance vs Ld — actuator wear is a tuning criterion too, and this is where you see it.

5.7Choosing the Stack, and How It Fails

SituationGlobalLocal / control
Indoor 2D, static-ish worldA* (JPS+ if maps are big)DWA or TEB + pure pursuit
Frequent map changes en routeD* LiteTEB / MPPI
High-DoF or kinodynamic (drone, arm, car-like)Informed RRT* / LazyPRM*NMPC (CasADi/ACADO)
Aggressive offroad / dense dynamics, GPU aboardA* seedGPU MPPI
Crowds of people (see Phase 8)A*MPPI or a learned policy with a classical fallback
Three classic planning failures. The oscillating doorway: global path through a door the robot’s inflated footprint barely fits; the local planner alternately commits and aborts — fix the inflation radius and give the local planner the corridor width, not just lethal cells. The dynamic-obstacle freeze: costmaps treat a walking human as a static wall recomputed 10× a second, so the robot brakes forever — velocity-aware costs (or MPPI/DRL with predictive models) are the real fix. The stale-map lie: global plans through a door that closed an hour ago; D* Lite repairs cheaply, but only if the map layer actually admits the change — clamped occupancy (Phase 3.2) is what makes that possible. All three share a moral: the stack fails at the seams between layers, so log every layer’s input when debugging, not just the final command.
Plan globally with relaxed dynamics, locally with real dynamics, and control with almost none — then spend your engineering budget on the seams: inflation radii, replan triggers, and recovery behaviors. A mediocre planner in a well-tuned stack beats a brilliant one in a badly stitched stack, every time.

5.8Reading List

A Formal Basis for the Heuristic Determination of Minimum Cost PathsHart, Nilsson & Raphael · IEEE Trans. SSC 1968A*. Fifty-plus years old, still the first algorithm every navigation stack runs. D* LiteKoenig & Likhachev · AAAI 2002Incremental replanning done right: repair only the locally inconsistent nodes when the world disagrees with the map. Online Graph Pruning for Pathfinding on Grid Maps (Jump Point Search)Harabor & Grastien · AAAI 2011Grid symmetry, executed: same optimal paths as A*, an order of magnitude fewer expansions, no preprocessing. Sampling-Based Algorithms for Optimal Motion PlanningKaraman & Frazzoli · IJRR 2011RRT* and PRM*: the paper that separated probabilistic completeness from asymptotic optimality and gave the r_n radius its form. Informed RRT*Gammell, Srinivasa & Barfoot · IROS 2014The ellipsoid of Fig 5.3: direct sampling of the only region that can improve the incumbent solution. The Dynamic Window Approach to Collision AvoidanceFox, Burgard & Thrun · IEEE RAM 1997The local planner that outlived its decade: velocity-space sampling under acceleration limits, scored by simulation. Integrated Online Trajectory Planning and Optimization in Distinctive TopologiesRösmann, Hoffmann & Bertram · Robotics and Autonomous Systems 2017TEB matured: timed elastic bands optimized in parallel homotopy classes — the "left or right of the pillar" decision made explicit. Information Theoretic MPC for Model-Based Reinforcement Learning (MPPI)Williams, Wagener, Goldfain, Drews, Rehg, Boots & Theodorou · ICRA 2017Sampling-based MPC with the softmin update — the math behind GPU-MPPI local planners and aggressive offroad driving. Implementation of the Pure Pursuit Path Tracking AlgorithmCoulter · CMU-RI-TR-92-01Ten pages, one arc formula, thirty years of deployed robots. Setup B is this memo, runnable. Autonomous Automobile Trajectory Tracking for Off-Road Driving (Stanley)Hoffmann, Tomlin, Montemerlo & Thrun · ACC 2007The DARPA-winning cross-track controller, with the stability analysis pure pursuit never had. CasADi: A Software Framework for Nonlinear Optimization and Optimal ControlAndersson, Gillis, Horn, Rawlings & Diehl · Mathematical Programming Computation 2019The tool that made writing an NMPC controller a weekend project instead of a thesis.

5.9Interview Questions

What do admissibility and consistency of a heuristic actually buy you?

Admissibility (h never overestimates the true cost-to-go) guarantees A* returns an optimal path: the goal cannot be popped with a suboptimal cost while a cheaper route’s frontier node still has lower f. Consistency (the triangle inequality h(n) ≤ c(n,n’) + h(n’)) additionally guarantees each node is expanded at most once, so no re-expansion bookkeeping is needed. An inadmissible heuristic — including any weighted heuristic w·h — trades optimality for speed with the bound cost ≤ w·optimal, which is often exactly the right trade on a robot that needs an answer in 10 ms.

Why is D* Lite preferred over re-running A* when the map changes mid-route?

Because most edge-cost changes invalidate almost none of the previous search. D* Lite searches backward from the goal, so its g-values are costs-to-goal — still meaningful as the robot moves — and maintains a one-step lookahead value rhs(u); when an edge changes, only nodes where g ≠ rhs (locally inconsistent) enter the queue, and repair propagates only as far as the inconsistency does. A closed door typically touches a few percent of the state space; A*-from-scratch pays for all of it, every time, at every one of the potentially hundreds of replans in a long mission.

RRT finds paths — what exactly do RRT*’s two extra steps add, and at what cost?

Plain RRT is probabilistically complete but converges to a suboptimal path almost surely — early wiring is frozen forever. RRT* adds choose-parent (wire each new sample to the ball neighbor minimizing total cost-to-come) and rewire (re-parent ball neighbors through the new sample when cheaper), so path quality improves monotonically and asymptotically reaches the optimum; the ball radius shrinks as γ(log n/n)^{1/d} to keep expected work per sample logarithmic. Costs: more nearest-neighbor queries and collision checks per sample (the practical bottleneck), and slower early progress — hence anytime use: take RRT’s first answer, spend remaining budget on RRT*’s improvement, ideally inside the informed ellipsoid.

Your DWA robot drives into a U-shaped obstacle and oscillates. Why, and what fixes it?

DWA optimizes one short arc at a time: heading-to-goal dominates the score, and every step into the pocket genuinely improves it until the pocket closes. At the dead end, no arc scores well, and the tie-breaking flips between turn-left/turn-right — oscillation. Fixes by layer: make the global planner’s path (which sees the pocket in the costmap) a stronger attractor than the raw goal heading; use a local planner with a horizon — TEB’s band or MPPI’s rollouts see the dead end before entering; and have a recovery behavior (back out, replan) as the honest last resort. The lesson generalizes: local minima are escaped by information from a longer horizon, never by re-tuning the myopic scorer.

Pure pursuit’s lookahead distance: what exactly does it trade?

Ld is a low-pass filter on the path expressed in metres. Small Ld: the target point is nearly the closest path point, curvature commands are large and reactive — tight tracking, but oscillation and actuator chatter as the robot orbits the path; the κ = 2sinα/Ld gain literally diverges as Ld → 0. Large Ld: commands are smooth and stable, but the controller aims past corners and cuts them — geometric error proportional to path curvature. Production answer: schedule Ld with speed (Ld = k·v + floor), because the stability requirement scales with v while the corner-cutting penalty doesn’t.

When does MPPI beat gradient-based NMPC, and what is its characteristic failure?

MPPI wins when the cost or dynamics are non-differentiable, discontinuous, or learned — collision indicators, terrain classes, neural network costs — because it only ever evaluates rollouts, never differentiates them; and it parallelizes perfectly on a GPU, so horizon×samples scale with hardware, not with solver cleverness. NMPC wins on tight constraint satisfaction (exact bounds, terminal sets) and on CPUs. MPPI’s characteristic failure is averaging across modes: half the samples pass a pillar left, half right, and the softmin average drives at the pillar. Mitigations: lower temperature λ, warm-start from the previous solution, or maintain separate topological candidates TEB-style.

Why do costmaps inflate obstacles, and what breaks if inflation is wrong?

Inflation converts footprint collision checking into point checking: grow every obstacle by the robot’s inscribed radius and the planner may treat the robot as a point — the C-space transform, precomputed. The decaying cost skirt beyond the lethal core additionally biases plans toward clearance. Too small: the point-robot assumption is false and the robot clips corners it "legally" planned through. Too large: doorways and aisles close in the costmap while remaining physically passable — the planner reports "no path" through a door the robot fits; asymmetric footprints (long robots) make single-radius inflation wrong in yaw-dependent ways, which is when you graduate to footprint-aware or lattice planners. Inflation radius is the single most consequential number in a 2D nav stack.

Design the planning stack for a hospital delivery robot: corridors, elevators, beds on wheels, people.

Global: A* (JPS+ for large floors) on the Phase 3 occupancy map with conservative inflation; D* Lite mode for en-route changes like closed fire doors; elevator transitions as graph edges with their own logic. Local: TEB or MPPI at 20 Hz on a 6–8 m rolling costmap fused from lidar + depth (beds and IV stands are lidar-sparse — Phase 2’s complementary-sensor lesson); velocity-aware human costs so walkers aren’t treated as walls, with speed caps near people from a semantic layer. Control: pure pursuit with speed-scheduled lookahead — hospitals reward smoothness over agility. Seams: recovery behaviors that never reverse blindly (people stand behind robots), replan triggers on local/global divergence, and the two-frame contract from Phase 3.8 so loop-closure jumps never yank a robot mid-corridor. Then spend a week tuning inflation and DWA/TEB weights on site, because you will.