Chapter 15: Local Motion Generation (Mobile-Specific)

Lesson 5: Lab: Compare Local Planners in Dense Obstacles

This lab operationalizes the local-motion concepts from Lessons 1–4 by running a controlled, reproducible benchmark of two mobile-specific local planning families—velocity-space sampling (DWA-style) and trajectory-band optimization (TEB-style)—in a corridor-like dense-obstacle world. You will implement a consistent simulation harness, define safety and efficiency metrics, and interpret the trade-offs using rigorous geometric and optimization-based reasoning.

1. Lab Goal and Assumptions

We assume a planar wheeled AMR with a unicycle kinematic model and a disc footprint in a 2D world containing static circular obstacles. The global reference is a smooth corridor centerline. Your task is to command admissible controls to reach the goal while maintaining clearance.

\( \mathbf{x}(t) \): robot state \( \mathbf{x}(t) = [x(t),\,y(t),\,\theta(t)]^\top \) and \( \mathbf{u}(t) \): control \( \mathbf{u}(t) = [v(t),\,\omega(t)]^\top \). The unicycle kinematics are

\[ \dot x(t)=v(t)\cos\theta(t),\quad \dot y(t)=v(t)\sin\theta(t),\quad \dot\theta(t)=\omega(t). \]

Obstacles are circles \( \mathcal{O}_i \): \( \|(x,y)-(x_i,y_i)\| \le r_i \). The robot is a disc of radius \( R \). A configuration \( (x,y) \) is collision-free if

\[ \forall i:\quad \|(x,y)-(x_i,y_i)\| - r_i - R \; > \; 0. \]

We will use the signed clearance function \( c(x,y) \): \( c(x,y)=\min_i \big(\|(x,y)-(x_i,y_i)\| - r_i - R\big) \). Safety requires \( c(x,y) > 0 \) along the executed trajectory.

2. What You Compare (Interfaces Only)

Lessons 3–4 introduced the algorithmic families. In this lab we treat each local planner as a black-box policy: given \( \mathbf{x}_k \), current \( v_k,\omega_k \), a short horizon \( T \), the local map/cost field, and a reference path, it returns one command \( (v_{k+1},\omega_{k+1}) \).

Velocity-space sampling (DWA-style). Sample candidate pairs \( (v,\omega) \) in a dynamic window defined by actuator limits over one step \( \Delta t \):

\[ v \in \Big[\max(0, v_k - a_{\max}\Delta t),\; \min(v_{\max}, v_k + a_{\max}\Delta t)\Big], \\ \omega \in \Big[\max(-\omega_{\max}, \omega_k - \alpha_{\max}\Delta t),\; \min(\omega_{\max}, \omega_k + \alpha_{\max}\Delta t)\Big]. \]

Each candidate is forward-simulated for horizon \( T \), collision-checked using clearance \( c(x,y) \), then scored via a weighted objective.

Trajectory-band optimization (TEB-style). Maintain a band of poses \( \mathbf{p}_0,\dots,\mathbf{p}_N \) near the path and optimize an energy that trades off smoothness, obstacle clearance, and time/feasibility constraints. The command is derived from the first segment direction (and a heading controller). In this lab we use a simplified educational proxy that still expresses the core optimization structure.

flowchart TD
  S["State x_k and current (v_k,w_k)"] --> I["Local map + reference path"]
  I --> DWA["Velocity-space sampling over (v,w)"]
  I --> TEB["Trajectory-band optimization over points"]
  DWA --> R1["Rollout + collision check"]
  R1 --> J1["Score candidates; pick best (v,w)"]
  TEB --> R2["Optimize band; take first segment"]
  R2 --> J2["Compute (v,w) via heading + limits"]
  J1 --> U["Apply command and step dynamics"]
  J2 --> U
        

3. Metrics and a Key Mathematical Guarantee

To compare planners meaningfully, we define metrics that capture: (i) task completion, (ii) efficiency, (iii) safety margin, and (iv) control effort/smoothness. For a discrete rollout at times \( t_k = k\Delta t \):

  • Success indicator \( \mathbb{I}_\text{succ} \): \( \mathbb{I}_\text{succ}=1 \) if \( \|\mathbf{p}_K-\mathbf{p}_g\| \le \varepsilon_g \) with no collisions.
  • Time-to-goal \( T_g \): \( T_g = K\Delta t \) on success.
  • Path length \( L \): \( L = \sum_{k=0}^{K-1} \|\mathbf{p}_{k+1}-\mathbf{p}_k\| \).
  • Minimum clearance \( c_{\min} \): \( c_{\min} = \min_k c(\mathbf{p}_k) \).
  • Angular effort proxy \( E_\omega \): \( E_\omega = \sum_{k=0}^{K-1} \omega_k^2\Delta t \) (penalizes aggressive turning).

Why discretized collision checking can be justified. Define the obstacle distance-to-set function (without robot radius) \( d(\mathbf{p}) \): \( d(\mathbf{p})=\min_i \big(\|\mathbf{p}-\mathbf{o}_i\|-r_i\big) \). A classical property is that \( d \) is 1-Lipschitz:

\[ |d(\mathbf{p})-d(\mathbf{q})| \; \le \; \|\mathbf{p}-\mathbf{q}\|,\quad \forall\mathbf{p},\mathbf{q}\in\mathbb{R}^2. \]

Proof sketch. For any obstacle set \( \mathcal{O} \), the distance to a closed set satisfies the triangle inequality bound \( d(\mathbf{p}) \le \|\mathbf{p}-\mathbf{q}\| + d(\mathbf{q}) \). Swapping \( \mathbf{p} \) and \( \mathbf{q} \) gives the claim. Therefore if the robot translation per step is bounded as \( \|\mathbf{p}_{k+1}-\mathbf{p}_k\| \le v_{\max}\Delta t \), then the clearance can drop by at most that amount between checks. A conservative discrete-time safety condition is:

\[ c(\mathbf{p}_k) \; > \; v_{\max}\Delta t \quad \Longrightarrow \quad c(\mathbf{p}(t)) \; > \; 0 \; \text{for all}\; t\in[t_k,t_{k+1}], \]

assuming the motion between samples is continuous and the obstacle set is static. This lemma motivates choosing \( \Delta t \) small enough when benchmarking dense obstacles.

4. Experimental Protocol (Reproducible)

We compare planners using the same seeds and world generator. Each trial generates a corridor-like environment by placing circular obstacles near (but not inside) the corridor, then runs the planner until success, collision, or a time limit. We record all metrics and aggregate across trials.

flowchart TD
  A["Set robot + sim params"] --> B["For each seed: generate dense world"]
  B --> C["Initialize state at start"]
  C --> D["Loop: planner -> command (v,w)"]
  D --> E["Roll forward dynamics dt"]
  E --> F["Compute metrics: clearance, length, effort"]
  F --> G["Stop if goal reached or collision or timeout"]
  G --> H["Aggregate across trials: mean + rates"]
  H --> I["Interpret trade-offs and tune weights"]
        

Fairness controls. (1) identical robot limits \( v_{\max},\omega_{\max},a_{\max},\alpha_{\max} \), (2) identical collision model (disc vs circles), (3) identical time step \( \Delta t \), and (4) identical reference path and termination criteria.

5. Python Implementation

The Python script builds the full benchmark harness, including environment generation, DWA rollout scoring, a simplified TEB-like band optimizer, and comparative summaries.

Chapter15_Lesson5.py


# Chapter15_Lesson5.py
# Lab: Compare Local Planners in Dense Obstacles (DWA vs simplified TEB-proxy)
# Requirements: Python 3.9+, numpy, matplotlib (optional for plotting)

import math
import numpy as np

# ---------- World / geometry ----------
def corridor_y(x: float) -> float:
    return 0.9 * math.sin(0.6 * x)

def reference_path(n=120, x0=0.5, x1=11.5):
    xs = np.linspace(x0, x1, n)
    ys = np.array([corridor_y(x) for x in xs])
    return np.c_[xs, ys]

def sample_dense_world(n_obs=45, seed=0):
    rng = np.random.default_rng(seed)
    obs = []
    xmin, xmax, ymin, ymax = 0.0, 12.0, -3.0, 3.0
    tries = 0
    while len(obs) < n_obs and tries < 20000:
        tries += 1
        x = rng.uniform(xmin + 0.5, xmax - 0.5)
        y = rng.uniform(ymin + 0.5, ymax - 0.5)
        r = rng.uniform(0.12, 0.32)
        if abs(y - corridor_y(x)) < 0.55 + r:
            continue
        ok = True
        for (ox, oy, orad) in obs:
            if (x - ox) ** 2 + (y - oy) ** 2 < (r + orad + 0.05) ** 2:
                ok = False
                break
        if ok:
            obs.append((x, y, r))
    return np.array(obs, dtype=float)

def clearance(p, obs, R):
    d = np.inf
    for (ox, oy, r) in obs:
        d = min(d, math.hypot(p[0] - ox, p[1] - oy) - r - R)
    return d

def wrap(a):
    return (a + math.pi) % (2 * math.pi) - math.pi

# ---------- Unicycle ----------
def step_unicycle(x, v, w, dt):
    return np.array([x[0] + v * math.cos(x[2]) * dt,
                     x[1] + v * math.sin(x[2]) * dt,
                     wrap(x[2] + w * dt)], float)

# ---------- DWA (short) ----------
def rollout_min_clear(x, v, w, obs, R, dt, T):
    steps = max(1, int(round(T / dt)))
    xc = x.copy()
    mc = np.inf
    for _ in range(steps):
        xc = step_unicycle(xc, v, w, dt)
        mc = min(mc, clearance(xc[:2], obs, R))
        if mc <= 0:
            break
    return xc, mc

def dwa_command(x, v0, w0, path, obs, prm):
    dt, T = prm["dt"], prm["T"]
    vmin = max(0.0, v0 - prm["a"] * dt); vmax = min(prm["vmax"], v0 + prm["a"] * dt)
    wmin = max(-prm["wmax"], w0 - prm["alpha"] * dt); wmax = min(prm["wmax"], w0 + prm["alpha"] * dt)

    best = (-1e18, 0.0, 0.0)
    goal = path[-1]

    for v in np.linspace(vmin, vmax, 9):
        for w in np.linspace(wmin, wmax, 17):
            xf, mc = rollout_min_clear(x, v, w, obs, prm["R"], dt, T)
            if mc <= prm["R"] + 0.05:
                continue
            goal_dist = np.linalg.norm(xf[:2] - goal)
            J = -0.6 * goal_dist + 1.8 * mc + 0.4 * (v / (prm["vmax"] + 1e-9)) - 0.12 * (w * w)
            if J > best[0]:
                best = (J, float(v), float(w))
    return best[1], best[2]

# ---------- TEB-proxy (short band optimization) ----------
def optimize_band(x, path, obs, prm, N=12, band_len=2.6, iters=20, step=0.12):
    # sample N points ahead along x (good enough for corridor reference)
    x0 = x[0]
    xs = np.linspace(x0, x0 + band_len, N)
    pts = np.c_[xs, np.array([corridor_y(xx) for xx in xs])]
    pts[0] = x[:2]

    for _ in range(iters):
        g = np.zeros_like(pts)

        # smoothness (2nd diff)
        g[1:-1] += 0.35 * (2 * pts[1:-1] - pts[:-2] - pts[2:])

        # obstacle repulsion (nearest obstacle)
        for i in range(1, N):
            best_d = np.inf; best_u = np.zeros(2)
            for (ox, oy, r) in obs:
                dvec = pts[i] - np.array([ox, oy])
                n = np.linalg.norm(dvec) + 1e-12
                d = n - r - prm["R"]
                if d < best_d:
                    best_d = d; best_u = dvec / n
            if best_d < 0.9:
                phi = math.exp(-4.5 * (best_d - 0.9))
                g[i] += 1.8 * (-4.5 * phi) * best_u

        # time preference (shorten segments)
        g[1:] += 0.25 * (pts[1:] - pts[:-1])

        pts[1:] -= step * g[1:]
        pts[0] = x[:2]
    return pts

def teb_command(x, v0, w0, path, obs, prm):
    pts = optimize_band(x, path, obs, prm)
    d = pts[1] - pts[0]
    heading = math.atan2(d[1], d[0])
    herr = wrap(heading - x[2])
    w = max(-prm["wmax"], min(prm["wmax"], 2.2 * herr))
    v = max(0.0, min(prm["vmax"], 0.8 * (np.linalg.norm(d) / prm["dt"])))
    v *= 1.0 / (1.0 + 1.2 * abs(w))
    # accel limits
    v = max(max(0.0, v0 - prm["a"] * prm["dt"]), min(prm["vmax"], v0 + prm["a"] * prm["dt"], v))
    w = max(w0 - prm["alpha"] * prm["dt"], min(w0 + prm["alpha"] * prm["dt"], w))
    return v, w

# ---------- Benchmark ----------
def run_one(planner, seed, prm):
    obs = sample_dense_world(prm["n_obs"], seed)
    path = reference_path()
    x = np.array([0.6, 0.0, 0.0], float)
    v = 0.0; w = 0.0

    L = 0.0; cmin = np.inf; w2 = 0.0
    goal = path[-1]

    for k in range(prm["max_steps"]):
        c = clearance(x[:2], obs, prm["R"])
        cmin = min(cmin, c)
        if c <= 0:
            return dict(success=False, collision=True, time=k * prm["dt"], L=L, cmin=cmin, w2=w2)
        if np.linalg.norm(x[:2] - goal) <= prm["goal_tol"]:
            return dict(success=True, collision=False, time=k * prm["dt"], L=L, cmin=cmin, w2=w2)

        if planner == "teb":
            vcmd, wcmd = teb_command(x, v, w, path, obs, prm)
        else:
            vcmd, wcmd = dwa_command(x, v, w, path, obs, prm)

        x2 = step_unicycle(x, vcmd, wcmd, prm["dt"])
        L += float(np.linalg.norm(x2[:2] - x[:2]))
        w2 += float((wcmd ** 2) * prm["dt"])
        x, v, w = x2, vcmd, wcmd

    return dict(success=False, collision=False, time=prm["max_steps"] * prm["dt"], L=L, cmin=cmin, w2=w2)

def summarize(results):
    n = len(results)
    succ = sum(r["success"] for r in results)
    col  = sum(r["collision"] for r in results)
    def mean(key): return float(np.mean([r[key] for r in results]))
    return dict(trials=n, success_rate=succ / n, collision_rate=col / n,
                time_mean=mean("time"), L_mean=mean("L"), cmin_mean=mean("cmin"), w2_mean=mean("w2"))

def compare(trials=30, seed0=0):
    prm = dict(R=0.25, vmax=0.9, wmax=1.6, a=1.2, alpha=2.5,
               dt=0.1, T=2.0, goal_tol=0.25, max_steps=800, n_obs=45)
    for name in ["dwa", "teb"]:
        res = [run_one(name, seed0 + i, prm) for i in range(trials)]
        print(name.upper(), summarize(res))

if __name__ == "__main__":
    compare(30, 0)
      

6. C++ Implementation

Chapter15_Lesson5.cpp


// Chapter15_Lesson5.cpp
// Lab: Compare Local Planners in Dense Obstacles (DWA vs simplified TEB-proxy)
// Build: g++ -O2 -std=c++17 Chapter15_Lesson5.cpp -o lab
// Run:   ./lab 30 0
#include <algorithm>
#include <array>
#include <cmath>
#include <iostream>
#include <random>
#include <vector>

struct Ob { double x,y,r; };
struct Res { bool success=false, collision=false; double time=0, L=0, cmin=1e9, w2=0; };

static inline double wrap(double a){ a = std::fmod(a + M_PI, 2*M_PI); if(a<0) a+=2*M_PI; return a - M_PI; }
static inline double hypot2(double x,double y){ return std::sqrt(x*x+y*y); }
static inline double corridor_y(double x){ return 0.9*std::sin(0.6*x); }

std::vector<std::array<double,2>> reference_path(int n=120){
  std::vector<std::array<double,2>> p; p.reserve(n);
  double x0=0.5,x1=11.5;
  for(int i=0;i<n;i++){
    double x = x0 + (x1-x0)*double(i)/(n-1);
    p.push_back({x, corridor_y(x)});
  }
  return p;
}

std::vector<Ob> sample_dense_world(int nObs, int seed){
  std::mt19937 rng(seed);
  std::uniform_real_distribution<double> Ux(0.5, 11.5), Uy(-2.5, 2.5), Ur(0.12, 0.32);
  std::vector<Ob> obs; obs.reserve(nObs);
  int tries=0;
  while((int)obs.size()<nObs && tries<20000){
    tries++;
    double x=Ux(rng), y=Uy(rng), r=Ur(rng);
    if(std::abs(y - corridor_y(x)) < 0.55 + r) continue;
    bool ok=true;
    for(auto &o: obs){
      double dx=x-o.x, dy=y-o.y;
      double rr=r+o.r+0.05;
      if(dx*dx+dy*dy < rr*rr){ ok=false; break; }
    }
    if(ok) obs.push_back({x,y,r});
  }
  return obs;
}

double clearance(double px,double py, const std::vector<Ob>& obs, double R){
  double d=1e18;
  for(auto &o: obs){
    d = std::min(d, hypot2(px-o.x, py-o.y) - o.r - R);
  }
  return d;
}

std::array<double,3> step_unicycle(const std::array<double,3>& x, double v,double w,double dt){
  return { x[0] + v*std::cos(x[2])*dt,
           x[1] + v*std::sin(x[2])*dt,
           wrap(x[2] + w*dt) };
}

std::pair<std::array<double,3>, double> rollout_min_clear(std::array<double,3> x, double v,double w,
                                                          const std::vector<Ob>& obs, double R, double dt, double T){
  int steps = std::max(1, (int)std::lround(T/dt));
  double mc=1e18;
  for(int i=0;i<steps;i++){
    x = step_unicycle(x,v,w,dt);
    mc = std::min(mc, clearance(x[0],x[1],obs,R));
    if(mc<=0) break;
  }
  return {x, mc};
}

struct Params{
  double R=0.25, vmax=0.9, wmax=1.6, a=1.2, alpha=2.5;
  double dt=0.1, T=2.0, goal_tol=0.25;
  int max_steps=800, n_obs=45;
};

std::pair<double,double> dwa_command(const std::array<double,3>& x, double v0,double w0,
                                     const std::vector<std::array<double,2>>& path,
                                     const std::vector<Ob>& obs, const Params& p){
  double vmin=std::max(0.0, v0 - p.a*p.dt), vmax=std::min(p.vmax, v0 + p.a*p.dt);
  double wmin=std::max(-p.wmax, w0 - p.alpha*p.dt), wmax=std::min(p.wmax, w0 + p.alpha*p.dt);

  double bestJ=-1e18, bestV=0, bestW=0;
  auto goal = path.back();

  for(int i=0;i<9;i++){
    double v = vmin + (vmax-vmin)*double(i)/8.0;
    for(int j=0;j<17;j++){
      double w = wmin + (wmax-wmin)*double(j)/16.0;
      auto rr = rollout_min_clear(x,v,w,obs,p.R,p.dt,p.T);
      double mc = rr.second;
      if(mc <= p.R + 0.05) continue;
      double gx = rr.first[0]-goal[0], gy = rr.first[1]-goal[1];
      double goalDist = hypot2(gx,gy);
      double J = -0.6*goalDist + 1.8*mc + 0.4*(v/(p.vmax+1e-9)) - 0.12*(w*w);
      if(J>bestJ){ bestJ=J; bestV=v; bestW=w; }
    }
  }
  return {bestV, bestW};
}

std::vector<std::array<double,2>> optimize_band(const std::array<double,3>& x,
                                                const std::vector<Ob>& obs, const Params& p,
                                                int N=12, double bandLen=2.6, int iters=20, double step=0.12){
  std::vector<std::array<double,2>> pts(N);
  for(int i=0;i<N;i++){
    double xx = x[0] + bandLen*double(i)/(N-1);
    pts[i] = {xx, corridor_y(xx)};
  }
  pts[0] = {x[0], x[1]};

  for(int it=0; it<iters; ++it){
    std::vector<std::array<double,2>> g(N, {0,0});

    // smoothness
    for(int i=1;i<N-1;i++){
      g[i][0] += 0.35*(2*pts[i][0] - pts[i-1][0] - pts[i+1][0]);
      g[i][1] += 0.35*(2*pts[i][1] - pts[i-1][1] - pts[i+1][1]);
    }

    // obstacle repulsion (nearest obstacle)
    for(int i=1;i<N;i++){
      double bestD=1e18; std::array<double,2> bestU{0,0};
      for(auto &o: obs){
        double vx=pts[i][0]-o.x, vy=pts[i][1]-o.y;
        double n = hypot2(vx,vy) + 1e-12;
        double d = n - o.r - p.R;
        if(d<bestD){ bestD=d; bestU={vx/n, vy/n}; }
      }
      if(bestD < 0.9){
        double phi = std::exp(-4.5*(bestD - 0.9));
        g[i][0] += 1.8*(-4.5*phi)*bestU[0];
        g[i][1] += 1.8*(-4.5*phi)*bestU[1];
      }
    }

    // time preference
    for(int i=1;i<N;i++){
      g[i][0] += 0.25*(pts[i][0]-pts[i-1][0]);
      g[i][1] += 0.25*(pts[i][1]-pts[i-1][1]);
    }

    for(int i=1;i<N;i++){
      pts[i][0] -= step*g[i][0];
      pts[i][1] -= step*g[i][1];
    }
    pts[0] = {x[0], x[1]};
  }
  return pts;
}

std::pair<double,double> teb_command(const std::array<double,3>& x, double v0,double w0,
                                     const std::vector<std::array<double,2>>& path,
                                     const std::vector<Ob>& obs, const Params& p){
  (void)path; // not needed in this proxy
  auto pts = optimize_band(x, obs, p);
  double dx=pts[1][0]-pts[0][0], dy=pts[1][1]-pts[0][1];
  double heading = std::atan2(dy,dx);
  double herr = wrap(heading - x[2]);

  double w = std::clamp(2.2*herr, -p.wmax, p.wmax);
  double v = std::clamp(0.8*(hypot2(dx,dy)/p.dt), 0.0, p.vmax);
  v *= 1.0/(1.0 + 1.2*std::abs(w));

  // accel limits
  v = std::clamp(v, std::max(0.0, v0 - p.a*p.dt), std::min(p.vmax, v0 + p.a*p.dt));
  w = std::clamp(w, w0 - p.alpha*p.dt, w0 + p.alpha*p.dt);
  return {v,w};
}

Res run_one(const std::string& planner, int seed, const Params& p){
  auto obs  = sample_dense_world(p.n_obs, seed);
  auto path = reference_path();
  auto goal = path.back();

  std::array<double,3> x{0.6,0.0,0.0};
  double v=0,w=0;
  Res r; r.time = p.max_steps*p.dt;

  for(int k=0;k<p.max_steps;k++){
    double c = clearance(x[0],x[1],obs,p.R);
    r.cmin = std::min(r.cmin, c);
    if(c<=0){ r.collision=true; r.time=k*p.dt; return r; }

    double gdist = hypot2(x[0]-goal[0], x[1]-goal[1]);
    if(gdist <= p.goal_tol){ r.success=true; r.time=k*p.dt; return r; }

    double vcmd, wcmd;
    if(planner=="teb"){ auto u = teb_command(x,v,w,path,obs,p); vcmd=u.first; wcmd=u.second; }
    else             { auto u = dwa_command(x,v,w,path,obs,p); vcmd=u.first; wcmd=u.second; }

    auto x2 = step_unicycle(x, vcmd, wcmd, p.dt);
    r.L  += hypot2(x2[0]-x[0], x2[1]-x[1]);
    r.w2 += (wcmd*wcmd)*p.dt;
    x=x2; v=vcmd; w=wcmd;
  }
  return r;
}

struct Summary{ int n=0; double succ=0,col=0, t=0,L=0,cmin=0,w2=0; };

Summary summarize(const std::vector<Res>& R){
  Summary s; s.n = (int)R.size();
  for(auto &r: R){
    s.succ += r.success?1:0;
    s.col  += r.collision?1:0;
    s.t    += r.time; s.L += r.L; s.cmin += r.cmin; s.w2 += r.w2;
  }
  s.t/=s.n; s.L/=s.n; s.cmin/=s.n; s.w2/=s.n;
  return s;
}

int main(int argc, char** argv){
  int trials = (argc>1)? std::atoi(argv[1]) : 30;
  int seed0  = (argc>2)? std::atoi(argv[2]) : 0;
  Params p;

  for(auto name: {std::string("dwa"), std::string("teb")}){
    std::vector<Res> R; R.reserve(trials);
    for(int i=0;i<trials;i++) R.push_back(run_one(name, seed0+i, p));
    auto s = summarize(R);
    std::cout << "\n" << (name=="dwa"?"DWA":"TEB") << " trials="<<s.n
              << " success_rate="<< (s.succ/s.n)
              << " collision_rate="<< (s.col/s.n)
              << " time_mean="<< s.t
              << " L_mean="<< s.L
              << " cmin_mean="<< s.cmin
              << " w2_mean="<< s.w2
              << "\n";
  }
  return 0;
}
      

7. Java Implementation

Chapter15_Lesson5.java


// Chapter15_Lesson5.java
// Lab: Compare Local Planners in Dense Obstacles (DWA vs simplified TEB-proxy)
// Build: javac Chapter15_Lesson5.java
// Run:   java Chapter15_Lesson5 30 0
import java.util.*;

public class Chapter15_Lesson5 {

  static class Ob { double x,y,r; Ob(double x,double y,double r){this.x=x;this.y=y;this.r=r;} }
  static class Res { boolean success=false, collision=false; double time=0,L=0,cmin=1e9,w2=0; }
  static class Params{
    double R=0.25,vmax=0.9,wmax=1.6,a=1.2,alpha=2.5,dt=0.1,T=2.0,goalTol=0.25;
    int maxSteps=800,nObs=45;
  }

  static double wrap(double a){
    a = (a + Math.PI) % (2*Math.PI);
    if(a < 0) a += 2*Math.PI;
    return a - Math.PI;
  }
  static double hypot(double x,double y){ return Math.hypot(x,y); }
  static double corridorY(double x){ return 0.9*Math.sin(0.6*x); }

  static double[][] referencePath(int n){
    double x0=0.5,x1=11.5;
    double[][] p = new double[n][2];
    for(int i=0;i<n;i++){
      double x = x0 + (x1-x0)*((double)i/(n-1));
      p[i][0]=x; p[i][1]=corridorY(x);
    }
    return p;
  }

  static ArrayList<Ob> sampleDenseWorld(int nObs, int seed){
    Random rng = new Random(seed);
    ArrayList<Ob> obs = new ArrayList<>(nObs);
    int tries=0;
    while(obs.size()<nObs && tries<20000){
      tries++;
      double x = 0.5 + 11.0*rng.nextDouble();
      double y = -2.5 + 5.0*rng.nextDouble();
      double r = 0.12 + 0.20*rng.nextDouble();
      if(Math.abs(y - corridorY(x)) < 0.55 + r) continue;

      boolean ok=true;
      for(Ob o: obs){
        double dx=x-o.x, dy=y-o.y;
        double rr=r+o.r+0.05;
        if(dx*dx+dy*dy < rr*rr){ ok=false; break; }
      }
      if(ok) obs.add(new Ob(x,y,r));
    }
    return obs;
  }

  static double clearance(double px,double py, ArrayList<Ob> obs, double R){
    double d=1e18;
    for(Ob o: obs){
      d = Math.min(d, hypot(px-o.x, py-o.y) - o.r - R);
    }
    return d;
  }

  static double[] stepUnicycle(double[] x, double v,double w,double dt){
    return new double[]{ x[0] + v*Math.cos(x[2])*dt,
                         x[1] + v*Math.sin(x[2])*dt,
                         wrap(x[2] + w*dt) };
  }

  static class Rollout { double[] xf; double minClear; Rollout(double[] xf,double mc){this.xf=xf; this.minClear=mc;} }

  static Rollout rolloutMinClear(double[] x0, double v,double w, ArrayList<Ob> obs, Params p){
    int steps = Math.max(1, (int)Math.round(p.T/p.dt));
    double[] x = x0.clone();
    double mc = 1e18;
    for(int i=0;i<steps;i++){
      x = stepUnicycle(x,v,w,p.dt);
      mc = Math.min(mc, clearance(x[0],x[1],obs,p.R));
      if(mc<=0) break;
    }
    return new Rollout(x,mc);
  }

  static double[] dwaCommand(double[] x, double v0,double w0, double[][] path, ArrayList<Ob> obs, Params p){
    double vmin=Math.max(0.0, v0 - p.a*p.dt), vmax=Math.min(p.vmax, v0 + p.a*p.dt);
    double wmin=Math.max(-p.wmax, w0 - p.alpha*p.dt), wmax=Math.min(p.wmax, w0 + p.alpha*p.dt);
    double bestJ=-1e18, bestV=0, bestW=0;
    double[] goal = path[path.length-1];

    for(int i=0;i<9;i++){
      double v = vmin + (vmax-vmin)*(i/8.0);
      for(int j=0;j<17;j++){
        double w = wmin + (wmax-wmin)*(j/16.0);
        Rollout rr = rolloutMinClear(x,v,w,obs,p);
        double mc = rr.minClear;
        if(mc <= p.R + 0.05) continue;
        double gx=rr.xf[0]-goal[0], gy=rr.xf[1]-goal[1];
        double goalDist = hypot(gx,gy);
        double J = -0.6*goalDist + 1.8*mc + 0.4*(v/(p.vmax+1e-9)) - 0.12*(w*w);
        if(J>bestJ){ bestJ=J; bestV=v; bestW=w; }
      }
    }
    return new double[]{bestV,bestW};
  }

  static double[][] optimizeBand(double[] x, ArrayList<Ob> obs, Params p, int N, double bandLen, int iters, double step){
    double[][] pts = new double[N][2];
    for(int i=0;i<N;i++){
      double xx = x[0] + bandLen*(i/(double)(N-1));
      pts[i][0]=xx; pts[i][1]=corridorY(xx);
    }
    pts[0][0]=x[0]; pts[0][1]=x[1];

    for(int it=0; it<iters; it++){
      double[][] g = new double[N][2];

      for(int i=1;i<N-1;i++){
        g[i][0] += 0.35*(2*pts[i][0] - pts[i-1][0] - pts[i+1][0]);
        g[i][1] += 0.35*(2*pts[i][1] - pts[i-1][1] - pts[i+1][1]);
      }

      for(int i=1;i<N;i++){
        double bestD=1e18, ux=0, uy=0;
        for(Ob o: obs){
          double vx=pts[i][0]-o.x, vy=pts[i][1]-o.y;
          double n = hypot(vx,vy) + 1e-12;
          double d = n - o.r - p.R;
          if(d<bestD){ bestD=d; ux=vx/n; uy=vy/n; }
        }
        if(bestD < 0.9){
          double phi = Math.exp(-4.5*(bestD - 0.9));
          g[i][0] += 1.8*(-4.5*phi)*ux;
          g[i][1] += 1.8*(-4.5*phi)*uy;
        }
      }

      for(int i=1;i<N;i++){
        g[i][0] += 0.25*(pts[i][0]-pts[i-1][0]);
        g[i][1] += 0.25*(pts[i][1]-pts[i-1][1]);
      }

      for(int i=1;i<N;i++){
        pts[i][0] -= step*g[i][0];
        pts[i][1] -= step*g[i][1];
      }
      pts[0][0]=x[0]; pts[0][1]=x[1];
    }
    return pts;
  }

  static double[] tebCommand(double[] x, double v0,double w0, double[][] path, ArrayList<Ob> obs, Params p){
    double[][] pts = optimizeBand(x, obs, p, 12, 2.6, 20, 0.12);
    double dx=pts[1][0]-pts[0][0], dy=pts[1][1]-pts[0][1];
    double heading = Math.atan2(dy,dx);
    double herr = wrap(heading - x[2]);

    double w = Math.max(-p.wmax, Math.min(p.wmax, 2.2*herr));
    double v = Math.max(0.0, Math.min(p.vmax, 0.8*(hypot(dx,dy)/p.dt)));
    v *= 1.0/(1.0 + 1.2*Math.abs(w));

    v = Math.max(Math.max(0.0, v0 - p.a*p.dt), Math.min(Math.min(p.vmax, v0 + p.a*p.dt), v));
    w = Math.max(w0 - p.alpha*p.dt, Math.min(w0 + p.alpha*p.dt, w));
    return new double[]{v,w};
  }

  static Res runOne(String planner, int seed, Params p){
    ArrayList<Ob> obs = sampleDenseWorld(p.nObs, seed);
    double[][] path = referencePath(120);
    double[] goal = path[path.length-1];

    double[] x = new double[]{0.6,0.0,0.0};
    double v=0,w=0;
    Res r = new Res();
    r.time = p.maxSteps*p.dt;

    for(int k=0;k<p.maxSteps;k++){
      double c = clearance(x[0],x[1],obs,p.R);
      r.cmin = Math.min(r.cmin, c);
      if(c<=0){ r.collision=true; r.time=k*p.dt; return r; }
      if(hypot(x[0]-goal[0], x[1]-goal[1]) <= p.goalTol){ r.success=true; r.time=k*p.dt; return r; }

      double[] u = planner.equals("teb") ? tebCommand(x,v,w,path,obs,p) : dwaCommand(x,v,w,path,obs,p);
      double vcmd=u[0], wcmd=u[1];

      double[] x2 = stepUnicycle(x, vcmd, wcmd, p.dt);
      r.L += hypot(x2[0]-x[0], x2[1]-x[1]);
      r.w2 += (wcmd*wcmd)*p.dt;
      x=x2; v=vcmd; w=wcmd;
    }
    return r;
  }

  static Map<String,Double> summarize(ArrayList<Res> R){
    int n=R.size();
    double succ=0,col=0,t=0,L=0,cmin=0,w2=0;
    for(Res r: R){
      succ += r.success?1:0; col += r.collision?1:0;
      t += r.time; L += r.L; cmin += r.cmin; w2 += r.w2;
    }
    LinkedHashMap<String,Double> s = new LinkedHashMap<>();
    s.put("trials", (double)n);
    s.put("success_rate", succ/n);
    s.put("collision_rate", col/n);
    s.put("time_mean", t/n);
    s.put("L_mean", L/n);
    s.put("cmin_mean", cmin/n);
    s.put("w2_mean", w2/n);
    return s;
  }

  public static void main(String[] args){
    int trials = (args.length>0) ? Integer.parseInt(args[0]) : 30;
    int seed0  = (args.length>1) ? Integer.parseInt(args[1]) : 0;
    Params p = new Params();

    for(String name: new String[]{"dwa","teb"}){
      ArrayList<Res> R = new ArrayList<>(trials);
      for(int i=0;i<trials;i++) R.add(runOne(name, seed0+i, p));
      System.out.println("\n" + name.toUpperCase() + " " + summarize(R));
    }
  }
}
      

8. MATLAB / Simulink Implementation

Chapter15_Lesson5.m


% Chapter15_Lesson5.m
% Lab: Compare Local Planners in Dense Obstacles (DWA vs simplified TEB-proxy)
% Run:
%   ComparePlanners(30, 0);

function ComparePlanners(trials, seed0)
  if nargin < 1, trials = 30; end
  if nargin < 2, seed0 = 0; end

  prm.R=0.25; prm.vmax=0.9; prm.wmax=1.6; prm.a=1.2; prm.alpha=2.5;
  prm.dt=0.1; prm.T=2.0; prm.goal_tol=0.25; prm.max_steps=800; prm.n_obs=45;

  fprintf('\nDWA: '); disp(Summarize(RunMany('dwa', trials, seed0, prm)));
  fprintf('\nTEB: '); disp(Summarize(RunMany('teb', trials, seed0, prm)));
end

function R = RunMany(name, trials, seed0, prm)
  R = repmat(struct('success',false,'collision',false,'time',0,'L',0,'cmin',inf,'w2',0), trials, 1);
  for i=1:trials
    R(i) = RunOne(name, seed0 + (i-1), prm);
  end
end

function S = Summarize(R)
  n = numel(R);
  succ = sum([R.success]); col = sum([R.collision]);
  S.trials = n;
  S.success_rate = succ/n;
  S.collision_rate = col/n;
  S.time_mean = mean([R.time]);
  S.L_mean = mean([R.L]);
  S.cmin_mean = mean([R.cmin]);
  S.w2_mean = mean([R.w2]);
end

% ---------- World / geometry ----------
function y = corridor_y(x), y = 0.9*sin(0.6*x); end

function path = reference_path()
  xs = linspace(0.5, 11.5, 120)';
  ys = arrayfun(@corridor_y, xs);
  path = [xs, ys];
end

function obs = sample_dense_world(n_obs, seed)
  rng(seed);
  obs = zeros(0,3);
  tries = 0;
  while size(obs,1) < n_obs && tries < 20000
    tries = tries + 1;
    x = 0.5 + 11.0*rand();
    y = -2.5 + 5.0*rand();
    r = 0.12 + 0.20*rand();
    if abs(y - corridor_y(x)) < 0.55 + r, continue; end
    ok = true;
    for k=1:size(obs,1)
      dx = x-obs(k,1); dy = y-obs(k,2);
      rr = r + obs(k,3) + 0.05;
      if dx*dx + dy*dy < rr*rr, ok=false; break; end
    end
    if ok, obs(end+1,:) = [x,y,r]; end %#ok<AGROW>
  end
end

function c = clearance(p, obs, R)
  d = inf;
  for i=1:size(obs,1)
    d = min(d, hypot(p(1)-obs(i,1), p(2)-obs(i,2)) - obs(i,3) - R);
  end
  c = d;
end

function a = wrap(a), a = mod(a+pi, 2*pi) - pi; end

% ---------- Unicycle ----------
function x2 = step_unicycle(x, v, w, dt)
  x2 = [x(1) + v*cos(x(3))*dt;
        x(2) + v*sin(x(3))*dt;
        wrap(x(3) + w*dt)];
end

% ---------- DWA ----------
function [xf, mc] = rollout_min_clear(x, v, w, obs, prm)
  steps = max(1, round(prm.T/prm.dt));
  mc = inf; xf = x;
  for k=1:steps
    xf = step_unicycle(xf, v, w, prm.dt);
    mc = min(mc, clearance(xf(1:2)', obs, prm.R));
    if mc <= 0, break; end
  end
end

function [vbest, wbest] = dwa_command(x, v0, w0, path, obs, prm)
  vmin = max(0, v0 - prm.a*prm.dt); vmax = min(prm.vmax, v0 + prm.a*prm.dt);
  wmin = max(-prm.wmax, w0 - prm.alpha*prm.dt); wmax = min(prm.wmax, w0 + prm.alpha*prm.dt);

  goal = path(end,:);
  bestJ = -1e18; vbest = 0; wbest = 0;

  for v = linspace(vmin, vmax, 9)
    for w = linspace(wmin, wmax, 17)
      [xf, mc] = rollout_min_clear(x, v, w, obs, prm);
      if mc <= prm.R + 0.05, continue; end
      goalDist = norm(xf(1:2)' - goal);
      J = -0.6*goalDist + 1.8*mc + 0.4*(v/(prm.vmax+1e-9)) - 0.12*w*w;
      if J > bestJ, bestJ = J; vbest = v; wbest = w; end
    end
  end
end

% ---------- TEB-proxy ----------
function pts = optimize_band(x, obs, prm, N, bandLen, iters, step)
  if nargin < 4, N=12; end
  if nargin < 5, bandLen=2.6; end
  if nargin < 6, iters=20; end
  if nargin < 7, step=0.12; end

  xs = linspace(x(1), x(1)+bandLen, N)';
  pts = [xs, arrayfun(@corridor_y, xs)];
  pts(1,:) = x(1:2)';

  for it=1:iters
    g = zeros(size(pts));

    g(2:end-1,:) = g(2:end-1,:) + 0.35*(2*pts(2:end-1,:) - pts(1:end-2,:) - pts(3:end,:));

    for i=2:N
      bestD = inf; bestU = [0,0];
      for k=1:size(obs,1)
        dvec = pts(i,:) - obs(k,1:2);
        n = norm(dvec) + 1e-12;
        d = n - obs(k,3) - prm.R;
        if d < bestD, bestD = d; bestU = dvec/n; end
      end
      if bestD < 0.9
        phi = exp(-4.5*(bestD - 0.9));
        g(i,:) = g(i,:) + 1.8*(-4.5*phi)*bestU;
      end
    end

    g(2:end,:) = g(2:end,:) + 0.25*(pts(2:end,:) - pts(1:end-1,:));

    pts(2:end,:) = pts(2:end,:) - step*g(2:end,:);
    pts(1,:) = x(1:2)';
  end
end

function [vcmd, wcmd] = teb_command(x, v0, w0, path, obs, prm) %#ok<INUSD>
  pts = optimize_band(x, obs, prm);
  d = pts(2,:) - pts(1,:);
  heading = atan2(d(2), d(1));
  herr = wrap(heading - x(3));

  wcmd = max(-prm.wmax, min(prm.wmax, 2.2*herr));
  vcmd = max(0, min(prm.vmax, 0.8*(norm(d)/prm.dt)));
  vcmd = vcmd / (1 + 1.2*abs(wcmd));

  vcmd = max(max(0, v0 - prm.a*prm.dt), min(min(prm.vmax, v0 + prm.a*prm.dt), vcmd));
  wcmd = max(w0 - prm.alpha*prm.dt, min(w0 + prm.alpha*prm.dt, wcmd));
end

% ---------- Simulation ----------
function r = RunOne(planner, seed, prm)
  obs = sample_dense_world(prm.n_obs, seed);
  path = reference_path();
  goal = path(end,:);

  x = [0.6;0.0;0.0]; v=0; w=0;
  L=0; cmin=inf; w2=0;

  for k=1:prm.max_steps
    c = clearance(x(1:2)', obs, prm.R);
    cmin = min(cmin, c);
    if c <= 0
      r = struct('success',false,'collision',true,'time',(k-1)*prm.dt,'L',L,'cmin',cmin,'w2',w2);
      return;
    end
    if norm(x(1:2)' - goal) <= prm.goal_tol
      r = struct('success',true,'collision',false,'time',(k-1)*prm.dt,'L',L,'cmin',cmin,'w2',w2);
      return;
    end

    if strcmpi(planner,'teb')
      [vcmd,wcmd] = teb_command(x,v,w,path,obs,prm);
    else
      [vcmd,wcmd] = dwa_command(x,v,w,path,obs,prm);
    end

    x2 = step_unicycle(x, vcmd, wcmd, prm.dt);
    L = L + norm(x2(1:2)-x(1:2));
    w2 = w2 + (wcmd*wcmd)*prm.dt;
    x = x2; v=vcmd; w=wcmd;
  end

  r = struct('success',false,'collision',false,'time',prm.max_steps*prm.dt,'L',L,'cmin',cmin,'w2',w2);
end

% ---------------- Optional: Simulink Harness Builder ----------------
% BuildSimulinkHarness('AMR_UnicycleHarness');

function BuildSimulinkHarness(modelName)
  if nargin < 1, modelName = 'AMR_UnicycleHarness'; end
  if bdIsLoaded(modelName), close_system(modelName,0); end
  new_system(modelName); open_system(modelName);
  set_param(modelName, 'StopTime', '20');

  add_block('simulink/Sources/From Workspace', [modelName '/Vin'], 'VariableName', 'Vin', 'Position', [30 60 140 90]);
  add_block('simulink/Sources/From Workspace', [modelName '/Win'], 'VariableName', 'Win', 'Position', [30 140 140 170]);

  add_block('simulink/Math Operations/Trigonometric Function', [modelName '/cos'], 'Operator', 'cos', 'Position', [220 40 270 80]);
  add_block('simulink/Math Operations/Trigonometric Function', [modelName '/sin'], 'Operator', 'sin', 'Position', [220 110 270 150]);
  add_block('simulink/Math Operations/Product', [modelName '/v*cos'], 'Position', [320 55 360 85]);
  add_block('simulink/Math Operations/Product', [modelName '/v*sin'], 'Position', [320 125 360 155]);

  add_block('simulink/Continuous/Integrator', [modelName '/x'], 'Position', [420 55 450 85]);
  add_block('simulink/Continuous/Integrator', [modelName '/y'], 'Position', [420 125 450 155]);
  add_block('simulink/Continuous/Integrator', [modelName '/theta'], 'Position', [220 200 250 230]);

  add_block('simulink/Sinks/To Workspace', [modelName '/x_out'], 'VariableName', 'x_out', 'SaveFormat', 'Array', 'Position', [520 55 600 85]);
  add_block('simulink/Sinks/To Workspace', [modelName '/y_out'], 'VariableName', 'y_out', 'SaveFormat', 'Array', 'Position', [520 125 600 155]);
  add_block('simulink/Sinks/To Workspace', [modelName '/theta_out'], 'VariableName', 'theta_out', 'SaveFormat', 'Array', 'Position', [320 200 410 230]);

  add_line(modelName, 'theta/1', 'cos/1'); add_line(modelName, 'theta/1', 'sin/1');
  add_line(modelName, 'Vin/1', 'v*cos/1'); add_line(modelName, 'cos/1', 'v*cos/2');
  add_line(modelName, 'Vin/1', 'v*sin/1'); add_line(modelName, 'sin/1', 'v*sin/2');
  add_line(modelName, 'v*cos/1', 'x/1'); add_line(modelName, 'v*sin/1', 'y/1');
  add_line(modelName, 'Win/1', 'theta/1');
  add_line(modelName, 'x/1', 'x_out/1'); add_line(modelName, 'y/1', 'y_out/1'); add_line(modelName, 'theta/1', 'theta_out/1');

  save_system(modelName);
end
      

9. Wolfram Mathematica Implementation

Chapter15_Lesson5.nb


(* Chapter15_Lesson5.nb (Wolfram Language script) *)
(* Lab: Compare Local Planners in Dense Obstacles (DWA vs simplified TEB-proxy) *)

ClearAll["Global`*"];

corridorY[x_] := 0.9*Sin[0.6*x];
referencePath[n_:120] := Table[{x, corridorY[x]}, {x, 0.5, 11.5, (11.5-0.5)/(n-1)}];

sampleDenseWorld[nObs_:45, seed_:0] := Module[
  {rng = RandomGeneratorState[], obs = {}, tries = 0, x, y, r, ok},
  SeedRandom[seed];
  While[Length[obs] < nObs && tries < 20000,
    tries++;
    x = RandomReal[{0.5, 11.5}];
    y = RandomReal[{-2.5, 2.5}];
    r = RandomReal[{0.12, 0.32}];
    If[Abs[y - corridorY[x]] < 0.55 + r, Continue[]];
    ok = True;
    Do[
      If[(x - o[[1]])^2 + (y - o[[2]])^2 < (r + o[[3]] + 0.05)^2, ok = False; Break[]],
      {o, obs}
    ];
    If[ok, obs = Append[obs, {x, y, r}]];
  ];
  RandomGeneratorState[rng];
  obs
];

clearance[p_, obs_, R_] := Min@Table[Norm[p - o[[{1,2}]]] - o[[3]] - R, {o, obs}];

wrap[a_] := Mod[a + Pi, 2 Pi] - Pi;

stepUnicycle[x_, v_, w_, dt_] := {
  x[[1]] + v*Cos[x[[3]]]*dt,
  x[[2]] + v*Sin[x[[3]]]*dt,
  wrap[x[[3]] + w*dt]
};

rolloutMinClear[x0_, v_, w_, obs_, R_, dt_, T_] := Module[
  {steps = Max[1, Round[T/dt]], x = x0, mc = Infinity},
  Do[
    x = stepUnicycle[x, v, w, dt];
    mc = Min[mc, clearance[x[[{1,2}]], obs, R]];
    If[mc <= 0, Break[]],
    {steps}
  ];
  {x, mc}
];

dwaCommand[x_, v0_, w0_, path_, obs_, prm_] := Module[
  {dt = prm["dt"], T = prm["T"], vmax = prm["vmax"], wmax = prm["wmax"],
   a = prm["a"], alpha = prm["alpha"], R = prm["R"], goal = Last[path],
   vmin, vmax2, wmin, wmax2, best = {-10^18, 0., 0.}, v, w, rr, xf, mc, goalDist, J},
  vmin = Max[0., v0 - a*dt]; vmax2 = Min[vmax, v0 + a*dt];
  wmin = Max[-wmax, w0 - alpha*dt]; wmax2 = Min[wmax, w0 + alpha*dt];
  Do[
    Do[
      rr = rolloutMinClear[x, v, w, obs, R, dt, T];
      xf = rr[[1]]; mc = rr[[2]];
      If[mc <= R + 0.05, Continue[]];
      goalDist = Norm[xf[[{1,2}]] - goal];
      J = -0.6*goalDist + 1.8*mc + 0.4*(v/(vmax + 10^-9)) - 0.12*w^2;
      If[J > best[[1]], best = {J, v, w}],
      {w, Subdivide[wmin, wmax2, 16]}
    ],
    {v, Subdivide[vmin, vmax2, 8]}
  ];
  best[[{2,3}]]
];

optimizeBand[x_, obs_, prm_, N_:12, bandLen_:2.6, iters_:20, step_:0.12] := Module[
  {R = prm["R"], pts, g, bestD, bestU, dvec, n, d, phi},
  pts = Table[{x[[1]] + bandLen*(i-1)/(N-1), corridorY[x[[1]] + bandLen*(i-1)/(N-1)]}, {i, 1, N}];
  pts[[1]] = x[[{1,2}]];
  Do[
    g = ConstantArray[{0., 0.}, N];
    (* smoothness *)
    Do[g[[i]] += 0.35*(2 pts[[i]] - pts[[i-1]] - pts[[i+1]]), {i, 2, N-1}];
    (* obstacle repulsion *)
    Do[
      {bestD, bestU} = {Infinity, {0., 0.}};
      Do[
        dvec = pts[[i]] - o[[{1,2}]];
        n = Norm[dvec] + 10^-12;
        d = n - o[[3]] - R;
        If[d < bestD, bestD = d; bestU = dvec/n],
        {o, obs}
      ];
      If[bestD < 0.9,
        phi = Exp[-4.5*(bestD - 0.9)];
        g[[i]] += 1.8*(-4.5*phi)*bestU
      ],
      {i, 2, N}
    ];
    (* time preference *)
    Do[g[[i]] += 0.25*(pts[[i]] - pts[[i-1]]), {i, 2, N}];
    Do[pts[[i]] -= step*g[[i]], {i, 2, N}];
    pts[[1]] = x[[{1,2}]],
    {iters}
  ];
  pts
];

tebCommand[x_, v0_, w0_, path_, obs_, prm_] := Module[
  {pts, d, heading, herr, w, v, vmax = prm["vmax"], wmax = prm["wmax"], a = prm["a"], alpha = prm["alpha"], dt = prm["dt"]},
  pts = optimizeBand[x, obs, prm];
  d = pts[[2]] - pts[[1]];
  heading = ArcTan[d[[1]], d[[2]]];
  herr = wrap[heading - x[[3]]];
  w = Clip[2.2*herr, {-wmax, wmax}];
  v = Clip[0.8*(Norm[d]/dt), {0., vmax}];
  v = v/(1 + 1.2*Abs[w]);
  v = Clip[v, {Max[0., v0 - a*dt], Min[vmax, v0 + a*dt]}];
  w = Clip[w, {w0 - alpha*dt, w0 + alpha*dt}];
  {v, w}
];

runOne[planner_, seed_, prm_] := Module[
  {obs = sampleDenseWorld[prm["n_obs"], seed], path = referencePath[], goal, x = {0.6, 0.0, 0.0},
   v = 0., w = 0., L = 0., cmin = Infinity, w2 = 0., c, u, x2, k},
  goal = Last[path];
  For[k = 0, k < prm["max_steps"], k++,
    c = clearance[x[[{1,2}]], obs, prm["R"]];
    cmin = Min[cmin, c];
    If[c <= 0, Return[<|"success"->False,"collision"->True,"time"->k*prm["dt"],"L"->L,"cmin"->cmin,"w2"->w2|>]];
    If[Norm[x[[{1,2}]] - goal] <= prm["goal_tol"], Return[<|"success"->True,"collision"->False,"time"->k*prm["dt"],"L"->L,"cmin"->cmin,"w2"->w2|>]];
    u = If[planner=="teb", tebCommand[x, v, w, path, obs, prm], dwaCommand[x, v, w, path, obs, prm]];
    x2 = stepUnicycle[x, u[[1]], u[[2]], prm["dt"]];
    L += Norm[x2[[{1,2}]] - x[[{1,2}]]];
    w2 += u[[2]]^2*prm["dt"];
    x = x2; v = u[[1]]; w = u[[2]];
  ];
  <|"success"->False,"collision"->False,"time"->prm["max_steps"]*prm["dt"],"L"->L,"cmin"->cmin,"w2"->w2|>
];

summarize[R_] := Module[
  {n = Length[R]},
  <|
    "trials"->n,
    "success_rate"->Mean[Boole[R[[All,"success"]]]],
    "collision_rate"->Mean[Boole[R[[All,"collision"]]]],
    "time_mean"->Mean[R[[All,"time"]]],
    "L_mean"->Mean[R[[All,"L"]]],
    "cmin_mean"->Mean[R[[All,"cmin"]]],
    "w2_mean"->Mean[R[[All,"w2"]]]
  |>
];

compare[trials_:30, seed0_:0] := Module[
  {prm = <|"R"->0.25,"vmax"->0.9,"wmax"->1.6,"a"->1.2,"alpha"->2.5,"dt"->0.1,"T"->2.0,
                 "goal_tol"->0.25,"max_steps"->800,"n_obs"->45|>, resDWA, resTEB},
  resDWA = Table[runOne["dwa", seed0+i, prm], {i, 0, trials-1}];
  resTEB = Table[runOne["teb", seed0+i, prm], {i, 0, trials-1}];
  Print["DWA ", summarize[resDWA]];
  Print["TEB ", summarize[resTEB]];
];

compare[30, 0];
      

10. Problems and Solutions

Problem 1 (Distance is 1-Lipschitz): Let \( d(\mathbf{p})=\inf_{\mathbf{z}\in\mathcal{O}}\|\mathbf{p}-\mathbf{z}\| \) be the distance from a point to a nonempty closed set \( \mathcal{O} \subset \mathbb{R}^2 \). Prove that \( |d(\mathbf{p})-d(\mathbf{q})| \le \|\mathbf{p}-\mathbf{q}\| \).

Solution: Fix arbitrary \( \mathbf{p},\mathbf{q} \). For any \( \mathbf{z}\in\mathcal{O} \), the triangle inequality gives \( \|\mathbf{p}-\mathbf{z}\| \le \|\mathbf{p}-\mathbf{q}\|+\|\mathbf{q}-\mathbf{z}\| \). Taking infimum over \( \mathbf{z} \) yields \( d(\mathbf{p}) \le \|\mathbf{p}-\mathbf{q}\| + d(\mathbf{q}) \). Swap \( \mathbf{p} \) and \( \mathbf{q} \) to get \( d(\mathbf{q}) \le \|\mathbf{p}-\mathbf{q}\| + d(\mathbf{p}) \). Combining implies \( |d(\mathbf{p})-d(\mathbf{q})| \le \|\mathbf{p}-\mathbf{q}\| \).

Problem 2 (Discrete clearance bound): Suppose \( \left\|\frac{d\mathbf{p}(t)}{dt}\right\| \le v_{\max} \) and the clearance function \( c(\mathbf{p}) = d(\mathbf{p}) - R \) uses distance \( d \) to obstacles. Show that if \( c(\mathbf{p}(t_k)) \; > \; v_{\max}\Delta t \), then \( c(\mathbf{p}(t)) \; > \; 0 \) for all \( t \in [t_k, t_{k+1}] \).

Solution: Since the distance-to-obstacle function \( d(\cdot) \) is 1-Lipschitz, for any \( t \in [t_k, t_{k+1}] \) we have

\[ d(\mathbf{p}(t)) \; \ge \; d(\mathbf{p}(t_k)) - \|\mathbf{p}(t) - \mathbf{p}(t_k)\|. \]

Also, by the fundamental theorem of calculus and the speed bound,

\[ \|\mathbf{p}(t) - \mathbf{p}(t_k)\| \; \le \; \int_{t_k}^{t} \left\|\frac{d\mathbf{p}(\tau)}{d\tau}\right\| d\tau \; \le \; \int_{t_k}^{t} v_{\max}\, d\tau \; = \; v_{\max}(t - t_k) \; \le \; v_{\max}\Delta t. \]

Combining the two inequalities yields

\[ d(\mathbf{p}(t)) \; \ge \; d(\mathbf{p}(t_k)) - v_{\max}\Delta t. \]

Therefore, using \( c(\mathbf{p}) = d(\mathbf{p}) - R \),

\[ c(\mathbf{p}(t)) \; = \; d(\mathbf{p}(t)) - R \; \ge \; d(\mathbf{p}(t_k)) - v_{\max}\Delta t - R \; = \; c(\mathbf{p}(t_k)) - v_{\max}\Delta t \; > \; 0, \]

which proves that \( c(\mathbf{p}(t)) \; > \; 0 \) for all \( t \in [t_k, t_{k+1}] \).

Problem 3 (Dynamic window derivation): Assume discrete-time acceleration bounds \( |v_{k+1}-v_k| \le a_{\max}\Delta t \) and \( |\omega_{k+1}-\omega_k| \le \alpha_{\max}\Delta t \) plus saturation bounds \( 0 \le v \le v_{\max} \), \( |\omega| \le \omega_{\max} \). Derive the feasible intervals for \( v_{k+1} \) and \( \omega_{k+1} \).

Solution: From acceleration bounds: \( v_{k+1} \in [v_k-a_{\max}\Delta t,\; v_k+a_{\max}\Delta t] \), \( \omega_{k+1} \in [\omega_k-\alpha_{\max}\Delta t,\; \omega_k+\alpha_{\max}\Delta t] \). Intersect with saturation constraints to obtain:

\[ v_{k+1} \in \Big[\max(0, v_k-a_{\max}\Delta t),\; \min(v_{\max}, v_k+a_{\max}\Delta t)\Big], \\ \omega_{k+1} \in \Big[\max(-\omega_{\max}, \omega_k-\alpha_{\max}\Delta t),\; \min(\omega_{\max}, \omega_k+\alpha_{\max}\Delta t)\Big]. \]

11. Summary

You built a controlled benchmark to compare two mobile-specific local planning families in dense obstacles. With identical limits, maps, and termination criteria, you can attribute differences in success/collision rate, time, path length, and minimum clearance to the algorithmic family: velocity-space sampling (DWA-style) versus trajectory-band optimization (TEB-style).

12. References

  1. Fox, D., Burgard, W., & Thrun, S. (1997). The dynamic window approach to collision avoidance. IEEE Robotics & Automation Magazine, 4(1), 23–33.
  2. Rösmann, C., Feiten, W., Wösch, T., Hoffmann, F., & Bertram, T. (2017). Efficient trajectory optimization using a sparse model. IEEE International Conference on Robotics and Automation (ICRA), 1383–1390.
  3. Rösmann, C., Hoffmann, F., & Bertram, T. (2017). Integrated online trajectory planning and optimization in distinctive topologies. Robotics and Autonomous Systems, 88, 142–153.
  4. LaValle, S.M. (2006). Planning Algorithms. Cambridge University Press.
  5. Choset, H., Lynch, K.M., Hutchinson, S., Kantor, G., Burgard, W., Kavraki, L.E., & Thrun, S. (2005). Principles of Robot Motion. MIT Press.
  6. Latombe, J.-C. (1991). Robot Motion Planning. Kluwer Academic Publishers.