Robot Motion
Chapter 07PART IIClassical Planners — Search, Potentials, Roadmaps, CellsDifficulty: IntermediateEstimated reading time: 60 min

Potential Functions

Attractive and repulsive potentials and gradient descent; distance from a range scan and from a grid; brushfire and wave-front as breadth-first search; the local-minimum disease of additive potentials and the navigation functions that cure it; and lifting workspace forces to Reach's joint torques through the transposed Jacobian.

The potential function approach directs a robot as if it were a particle moving in a gradient vector field.
Howie Choset, Kevin Lynch, Seth Hutchinson, George Kantor, Wolfram Burgard, Lydia Kavraki, and Sebastian ThrunPrinciples of Robot Motion (2005), Chapter 4

In this chapter

Chapter 6 planned on a graph someone had already built. This chapter asks what a robot can do with nothing but a goal and a sense of distance: treat the goal as a valley, the obstacles as hills, and roll downhill. The construction is beautifully local. The gradient of the potential at qq needs only the goal and the nearest obstacle, so Rusty can compute it from a range scan and Reach from a few points on its links — no configuration space is ever built.

The "aha" comes in two halves. The first is a failure: a sum of one valley and some hills can have a second valley that is not the goal, and gradient descent rolls into it and stops. The widget that opens the chapter shows a bead doing exactly that in the mouth of a horseshoe, and then — this is the part people get wrong — between two convex discs. The disease is in the sum, not in concavity. The second half is the cure, and it is not a tweak to the hills. A distance-to-goal field has exactly one minimum, and on a grid the breadth-first search the reader already owns computes it: wave-front is BFS rooted at the goal, brushfire is BFS rooted at the obstacles. Navigation functions show that the single minimum can even be had smoothly, on a restricted family of worlds, so the local-minimum problem is a fact about additive potentials, not about potentials.

Everything here is incomplete except the wave-front planner, which is resolution complete and exponential in dimension; the chapter says so each time. What survives is the field idea: a scalar function whose gradient the robot follows. Chapter 8 collides brushfire fronts on the generalized Voronoi diagram, Chapter 19's CHOMP descends a distance field, and Chapter 23's reactive layer is this chapter running at a hundred hertz.

The problem: roll downhill, and ask who built the hill

Put a bead at the left edge of a table and a goal at the right. Define a scalar field UU that is small at the goal and large near obstacles, and let the bead move with velocity −∇U-\nabla U. With a single disc between them, the bead swerves around the disc and arrives. Replace the disc by a horseshoe whose mouth faces the bead, and it rolls into the mouth and stops.

Nothing is broken. The badge under the canvas reports what the mathematics says: the gradient has vanished at a point that is not the goal, and both eigenvalues of the Hessian there are positive. The bead sits at the bottom of a genuine bowl. The goal's pull and the base's push cancel exactly on the axis of the horseshoe, and the two arms push back toward the axis whenever the bead drifts off it, so the point is stable in every direction. Choset's figure 4.10 draws this and his figure 4.11 makes the sharper point: switch the widget to two convex discs and the bead still stops, between them. Concavity has nothing to do with it.

Now switch on wave-front mode. The heightfield changes to the breadth-first cost-to-goal of Chapter 6, computed on a tenth-of-a-metre raster of the same obstacles, and the bead escapes along a grid-shortest path. That field has one minimum, provably, because it is distance to the goal. The rest of the chapter is the difference between those two heightfields.

Building intuition

Why a sum has extra valleys

The additive potential is U=Uatt+UrepU = U_{att} + U_{rep}: a bowl centred at the goal plus a spike on each obstacle. Far from any obstacle only the bowl matters and the bead heads for the goal. Near an obstacle the spike takes over and pushes the bead off. In between, the two gradients can cancel, and the only question is whether the cancellation point is stable. Between two obstacles symmetric about the line to the goal it is: the repulsive push is purely axial on the axis by symmetry, it grows without bound toward the gap while the attractive pull stays bounded, so somewhere on the axis the two cancel; and off the axis both spikes push back toward it. Derivation 3 writes this out. The Potential Well's Q⋆Q^\star slider changes where the cancellation happens but cannot make it go away; the ζ\zeta and η\eta sliders in the disclosure move it along the axis. Only the obstacle layout can remove a minimum, and the planner does not control the layout.

Fronts on a grid

The grid cure is older than the word "potential". Label every obstacle pixel 11, its free neighbours 22, their unlabelled neighbours 33, and so on: a fire spreading outward from every obstacle at once. When it is done, each pixel's label is one more than its link length to the nearest obstacle. That is brushfire, Choset §4.3.2, and it is a multi-source breadth-first search.

Two things to watch. First, the shape of a front: under 4-point connectivity the fronts are diamonds, because the label counts Manhattan steps; under 8-point they are squares, because the label counts chessboard steps. Neither is a circle. Brushfire computes a grid metric, and the hover readout compares it with the exact Euclidean distance transform ported from the sister book: the two agree only along the axes. Second, the solid pixels where two fronts meet. On the micro-grid that is column 4, equidistant from both obstacles; each of its three pixels was reached by both fronts and keeps two back pointers. Chapter 8 will call that column the grid generalized Voronoi diagram. Seed the same search from the goal pixel instead, labelled 22, and you have the wave-front planner of §4.5: a pixel's label is 2+2 + its grid distance to the goal, and "step to any neighbour labelled one less" is a shortest grid path.

One minimum, smoothly

Grids are exponential in dimension. Is the single minimum available without one? Rimon and Koditschek's answer is yes, on a sphere world — a disc with disjoint disc obstacles — and on anything diffeomorphic to one.

The field is φ=d2/[d2κ+β]1/κ\varphi = d^2 / [d^{2\kappa} + \beta]^{1/\kappa}, where dd is distance to the goal and β\beta is a product that vanishes on every obstacle boundary. At κ=2\kappa = 2 the Sculptor finds three minima; at κ=3\kappa = 3 two; from κ=7\kappa = 7 exactly one. Slide κ\kappa and watch the spurious minima slide toward their obstacles and turn into saddles. This is the point the widget exists to make: a potential with no spurious minima exists, and it is smooth. The price is the family of worlds it works on and a sharpening exponent whose threshold the theorem only promises exists. Star mode shows the trick that widens the family: a map hλh_\lambda that squashes each star-shaped obstacle onto a disc, with φ∘hλ\varphi \circ h_\lambda inheriting the single minimum because critical points correspond one-to-one under a diffeomorphism.

A body is not a point

Reach's configuration space is a torus; there is no "goal point" in the workspace to put a bowl at. Choset's §4.7 writes the potential in the workspace, at a few control points on the arm, and lifts each workspace force to a joint torque with the transposed Jacobian of that point.

Two red control points on the end effector pull it to the goal pose; slate points on the links push away from the block, the post and the shelf. The bars under the canvases are the lifted torques Jj⊤fjJ_j^\top f_j, one row per control point, and their sum — the gradient the arm descends. Watch the default run twice. Without rfloatr_{float} link 1 drives straight through the post between its two repulsive points, which are far enough from the post that their push is weak; with rfloatr_{float} — a floating control point at whichever link point is nearest any obstacle — the same start reaches the goal. Repelling the vertices of a body does not protect its edges (Choset's figure 4.20); rfloatr_{float} reduces the risk and does not eliminate it, and the widget's alternate mode lets you find starts where even that is not enough.

The mathematics

Notation used in this chapter
SymbolMeaningNote
U,  Uatt,  Urep,  ∇UU,\; U_{att},\; U_{rep},\; \nabla Upotential, its attractive and repulsive parts, and the gradient the robot reads as a velocitybook-wide
ζ,  η\zeta,\; \etaattractive and repulsive gains
dgoal⋆d^\star_{goal}radius where the attractive potential switches from quadratic to conic
Q⋆,  Qi⋆Q^\star,\; Q^\star_irepulsive distance of influence — global, or per obstacle
di(q),  D(q),  cd_i(q),\; D(q),\; cdistance to QO_i, to the nearest obstacle, and the closest point on QO_i, so ∇d_i = (q − c)/d_ibook-wide
α(i),  ϵ\alpha(i),\; \epsilondescent step size at iteration i; the gradient tolerance in ‖∇U‖ < ε
∇2U\nabla^2 UHessian; nondegenerate critical point ⇔ nonsingular Hessian
βi(q),  β(q)\beta_i(q),\; \beta(q)obstacle functions (< 0 inside, 0 on ∂QO_i, > 0 outside); their product
γκ,  σλ,  ξκ\gamma_\kappa,\; \sigma_\lambda,\; \xi_\kappagoal term d^{2κ}; analytic switch x/(λ + x); sharpening x^{1/κ}
φ(q)\varphi(q)navigation function (Choset Def. 4.6.1), φ = ξ_κ ∘ σ_1 ∘ γ_κ/β
hλ,  Ti,  νi,  sih_\lambda,\; T_i,\; \nu_i,\; s_istar-to-sphere diffeomorphism; translated scaling maps; their scale factors; analytic switches
rj(q),  Jj(q),  rfloatr_j(q),\; J_j(q),\; r_{float}control point j and its Jacobian, u = J_jᵀ f; the floating control point nearest any obstacle

Potentials, critical points, and the Hessian

In the colours every figure on this page uses, the descent is

c˙(t)  =  −∇U(c(t)),U  =  12ζ d2(q,qgoal)  +  ∑i12η(1di(q)−1Qi⋆)2.\htmlClass{term-robot}{\dot c(t)} \;=\; -\nabla U(c(t)), \qquad U \;=\; \tfrac12 \zeta\, d^2(q, \htmlClass{term-goal}{\qgoal}) \;+\; \sum_i \tfrac12 \eta \Big(\tfrac{1}{\htmlClass{term-obstacle}{d_i(q)}} - \tfrac{1}{Q^\star_i}\Big)^2 .

The orange bead is c(t)c(t), the red goal sits at the bottom of the bowl, and every slate obstacle contributes one spike through its distance did_i. Point at a term and the matching layer in the Potential Well comes forward.

DerivationThe blended attractive potential has a continuous, bounded gradient

Statement (Choset eqs. 4.2–4.3). With

Uatt(q)={12ζ d2(q,qgoal),d≤dgoal⋆,dgoal⋆ ζ d(q,qgoal)−12ζ(dgoal⋆)2,d>dgoal⋆,U_{att}(q) = \begin{cases} \tfrac12 \zeta\, d^2(q, \qgoal), & d \le d^\star_{goal}, \\ d^\star_{goal}\, \zeta\, d(q, \qgoal) - \tfrac12 \zeta (d^\star_{goal})^2, & d > d^\star_{goal}, \end{cases}

∇Uatt\nabla U_{att} is continuous and its magnitude never exceeds ζdgoal⋆\zeta d^\star_{goal}.

Step 1 — inside. ∇12ζd2=ζ(q−qgoal)\nabla \tfrac12 \zeta d^2 = \zeta (q - \qgoal), of magnitude ζd≤ζdgoal⋆\zeta d \le \zeta d^\star_{goal}, vanishing linearly at the goal (eq. 4.1).

Step 2 — outside. ∇[dgoal⋆ζd]=dgoal⋆ζ (q−qgoal)/d\nabla [d^\star_{goal} \zeta d] = d^\star_{goal} \zeta\, (q - \qgoal)/d, of magnitude exactly ζdgoal⋆\zeta d^\star_{goal} everywhere.

Step 3 — the seam. At d=dgoal⋆d = d^\star_{goal} both expressions equal ζ(q−qgoal)\zeta (q - \qgoal); the constant −12ζ(dgoal⋆)2-\tfrac12 \zeta (d^\star_{goal})^2 makes the values agree too, so UattU_{att} is C1C^1. ■\blacksquare

Why blend at all. The pure conic potential ζd\zeta d has gradient of magnitude ζ\zeta everywhere and no direction at the goal — descent chatters around it. The pure quadratic vanishes nicely at the goal but asks for a velocity proportional to distance, which a far start turns into an absurd one. The blend takes each where it is good. The chapter's check pushes two hundred random seams and finds a gradient jump of 10−810^{-8} (the finite width of the test) and a bound excess of 10−1610^{-16}.

DerivationGradient of distance to a convex obstacle

Statement (Choset eq. 4.7). If cc is the unique closest point of a convex QOi\QO_i to q∉QOiq \notin \QO_i, then ∇di(q)=(q−c)/∥q−c∥\nabla d_i(q) = (q - c)/\|q - c\|, a unit vector pointing away from the obstacle.

Step 1 — uniqueness. ∥q−c∥\|q - c\| is strictly convex in cc and QOi\QO_i is convex and closed, so the minimiser cc is unique.

Step 2 — the envelope theorem. di(q)=min⁡c∥q−c∥d_i(q) = \min_c \|q - c\|; at a unique minimiser the derivative of the minimum with respect to qq is the partial derivative of the objective with cc held fixed.

Step 3 — differentiate. ∂q∥q−c∥=(q−c)/∥q−c∥\partial_q \|q - c\| = (q - c)/\|q - c\|.

Step 4 — where convexity enters. A nonconvex obstacle can have two closest points, and did_i is then not differentiable: the gradient jumps as qq crosses the set where they tie. Choset's §4.1 warns that a repulsive term written in D(q)D(q) oscillates where the nearest obstacle changes; a nonconvex obstacle does the same thing to did_i inside one term. ■\blacksquare

The two cures. Sum the repulsion per convex obstacle, Urep=∑iUrepiU_{rep} = \sum_i U_{rep_i}, and decompose nonconvex obstacles into convex pieces. The Potential Well's horseshoe is three rectangles for exactly this reason; the disclosure toggle gives you the single nonconvex polygon instead, and the bead chatters on its axis — the badge says "stalled" with a gradient of order one, because ∇d\nabla d flips sign every step. The check compares the analytic ∇Urep\nabla U_{rep} over discs, a polygon and a wall segment with central differences at 352 random points: worst relative error 6×10−106 \times 10^{-10}. Choset's §4.3.1 reading of did_i as a local minimum of ρ(q,θ)\rho(q, \theta) over the bearing is the sensor version of this derivation, and it is where the Apartment's thin walls expose a limit — see the algorithm section.

The local-minimum disease

DerivationAdditive potentials admit non-goal minima — two convex discs suffice

Statement (Choset §4.4, figures 4.10–4.11). For two disc obstacles placed symmetrically about the line from the start to the goal, there is a point on that line with ∇U=0\nabla U = 0 and positive-definite Hessian.

Step 1 — symmetry. On the axis of symmetry the two repulsive gradients are mirror images, so their sum is axial. The attractive gradient is axial too. So ∇U\nabla U is axial on the axis.

Step 2 — unbounded push, bounded pull. As the point on the axis moves toward the gap, each did_i falls toward its minimum and η(1/di−1/Q⋆)/di2\eta (1/d_i - 1/Q^\star)/d_i^2 grows without bound if the gap is narrower than 2Q⋆2Q^\star; the attractive pull is bounded by ζdgoal⋆\zeta d^\star_{goal} (Derivation 1). Far from the discs, beyond Q⋆Q^\star, the push is zero and the pull is toward the goal.

Step 3 — a zero. The axial component of ∇U\nabla U is continuous, negative (goalward) far from the gap and positive (away) close to it; the intermediate value theorem gives q⋆q^\star with ∇U(q⋆)=0\nabla U(q^\star) = 0.

Step 4 — transversal stability. Off the axis, the nearer disc pushes harder than the farther one and both push toward the axis; so the transverse second derivative is positive. The axial second derivative is positive because the push grows as the gap is approached. The Hessian is positive definite: q⋆q^\star is a minimum. ■\blacksquare

The numbers. Discs of radius 0.50.5 at (1,±0.8)(1, \pm 0.8), goal (4,0)(4, 0), ζ=η=1\zeta = \eta = 1, Q⋆=1.5Q^\star = 1.5, α=0.01\alpha = 0.01. From (−2,0.05)(-2, 0.05) the descent stops after 160 steps at q⋆=(0.2409,0.0000)q^\star = (0.2409, 0.0000) with Hessian eigenvalues 14.15 and 16.16, both positive. The chapter's check pins all of it. The horseshoe case is the same argument with the base supplying the push; Barraquand and Latombe's RPP escapes such minima with random walks and tries again, which is the honest fix of 1991 and the ancestor of Part III's sampling. The method is not complete.

DerivationBrushfire computes grid distance; wave-front computes grid cost-to-goal; both are BFS

Statement (Choset §4.3.2, §4.5; Chapter 6 Derivation 1). Labelling obstacle pixels 11, their free neighbours 22, theirs 33, … assigns each free pixel 1+1 + its link length to the nearest obstacle — Manhattan distance under 4-point connectivity, chessboard under 8-point. Seeding the goal pixel with 22 instead assigns 2+2 + link length to the goal, and "step to a neighbour labelled one less" is a shortest grid path.

Step 1 — it is a BFS. Brushfire is breadth-first search on Chapter 6's grid graph with every obstacle pixel in the initial queue at depth 00; wave-front is the same search with the goal alone at depth 00. Labels are depth plus the seed label.

Step 2 — depth is link length. Chapter 6, Derivation 1: a node's depth when first dequeued is its shortest link length from the seed set. On a unit-cost 4-connected grid that is the Manhattan distance; on an 8-connected one, the chessboard distance.

Step 3 — descent reaches the goal. On a bounded grid every reachable free pixel receives a label. A non-goal pixel labelled k>2k > 2 was labelled from a neighbour labelled k−1k - 1, so such a neighbour exists; stepping to it strictly decreases the label and the walk ends at the goal after exactly k−2k - 2 steps. The field has one minimum. ■\blacksquare

Two fronts meeting. A pixel reached by fronts from two different obstacles keeps two back pointers (Choset §5.2.5); the set of such pixels is the grid GVD of Chapter 8, and the micro-grid's column 4 is its smallest instance. On an even gap the collision lands on a pixel; on an odd gap it lands on the edge between two pixels, and the port flags both so the ridge has no holes. Grid versus Euclidean. On forty random grids totalling 7020 free cells the 4-point and 8-point labels equal the exact Manhattan and chessboard distances cell for cell, the exact Euclidean distance lies between them, 99.5% of cells are within one cell of it, and the worst gap is 2.789 cells — along a diagonal, where Manhattan overshoots by 2\sqrt 2. The exact Euclidean distance transform is a different algorithm, ported from the sister book's map chapter as mapping/edt, and Exercise 5 asks you to run the two side by side. Dimension. Pixels become voxels, 4-point becomes 6-point, 8-point becomes 26-point, and the cell count is exponential in the dimension of Q\Q — which is why Part III exists.

DerivationThe sphere-world navigation function

Statement (Choset §4.6.1, eqs. 4.8–4.13; Rimon and Koditschek). On a sphere world, φ=ξκ∘σ1∘(γκ/β)\varphi = \xi_\kappa \circ \sigma_1 \circ (\gamma_\kappa / \beta) is a navigation function for sufficiently large κ\kappa, where γκ=d2κ(q,qgoal)\gamma_\kappa = d^{2\kappa}(q, \qgoal), β=∏i=0nβi\beta = \prod_{i=0}^n \beta_i, σλ(x)=x/(λ+x)\sigma_\lambda(x) = x/(\lambda + x) and ξκ(x)=x1/κ\xi_\kappa(x) = x^{1/\kappa}.

Step 1 — the raw ratio. γκ/β\gamma_\kappa / \beta is zero only at the goal and +∞+\infty on every obstacle boundary, where some βi=0\beta_i = 0. It is positive in between.

Step 2 — large κ pushes the goal term ahead. Away from obstacles, ∂γκ/∂q\partial \gamma_\kappa / \partial q dominates ∂β/∂q\partial \beta / \partial q once κ\kappa is large, so the gradient of the ratio points goalward and no critical point survives there.

Step 3 — near an obstacle, only it matters. The ratio is then γκ/βi\gamma_\kappa / \beta_i times a slowly varying factor, and a critical point can lie only on the radial line from the obstacle to the goal; on that line the ratio decreases from the boundary toward the goal, so the Hessian cannot be positive definite. Spurious minima become saddles (Choset cites [239] for the proof).

Step 4 — squash. σ1\sigma_1 is strictly increasing, maps [0,∞)[0, \infty) onto [0,1)[0, 1), and so moves no critical point and changes no type; it bounds the function.

Step 5 — sharpen. ξκ\xi_\kappa is strictly increasing on (0,∞)(0, \infty) and makes the critical points nondegenerate (Morse); composing gives eq. (4.13). ■\blacksquare

Counting instead of eyeballing. Because σ1\sigma_1, ξκ\xi_\kappa and log⁡\log are strictly increasing, the critical points of φ\varphi off the goal are those of ψ=log⁡(γκ/β)=2κlog⁡d−∑ilog⁡βi\psi = \log(\gamma_\kappa / \beta) = 2\kappa \log d - \sum_i \log \beta_i, whose gradient is of order one where φ\varphi's is 10−810^{-8} — the plateau problem Choset's last paragraph of §4.6.1 admits: φ\varphi is flat near the goal and far from it and steep in between, which makes gradient descent on it numerically miserable. The Sculptor's beads descend the normalized gradient for that reason. The port finds critical points by damped Newton on ∇ψ\nabla \psi from a lattice of seeds and classifies them by the Hessian of ψ\psi. On the five-disc world the sweep reads:

κ\kappa23467810
minima3222111
saddles7666555

Choset's own five-disc figure 4.15 has three minima at κ=3\kappa = 3 and one at κ=10\kappa = 10; our discs sit elsewhere and lose their last spurious minimum at κ=7\kappa = 7. In every column minima−saddles=−4\text{minima} - \text{saddles} = -4: on a disc with five holes the Euler characteristic is 1−5=−41 - 5 = -4, and Morse theory makes that an invariant of the count — each spurious minimum that disappears takes exactly one saddle with it. The chapter's check pins the whole table. Nothing in the theorem says which κ\kappa suffices; it is existential, and in practice one sweeps.

DerivationNavigation functions pull back under diffeomorphisms; the star-to-sphere map

Statement (Choset §4.6.2, eqs. 4.14–4.19). If φ\varphi is a navigation function on MM and h:F→Mh : F \to M is a diffeomorphism — smooth, bijective, smooth inverse — then φ∘h\varphi \circ h is a navigation function on FF. For a star world FF the map

Ti(q)=νi(q) (q−qi)+pi,νi(q)=(1+βi(q))1/2 rid(q,qi),T_i(q) = \nu_i(q)\,(q - q_i) + p_i,\qquad \nu_i(q) = \frac{(1 + \beta_i(q))^{1/2}\, r_i}{d(q, q_i)},si(q,λ)=γκβˉiγκβˉi+λβi,βˉi=∏j≠iβj,sqgoal=1−∑i=0nsi,hλ(q)=sqgoal(q) q+∑i=0nsi(q) Ti(q)s_i(q, \lambda) = \frac{\gamma_\kappa \bar\beta_i}{\gamma_\kappa \bar\beta_i + \lambda \beta_i},\quad \bar\beta_i = \prod_{j \ne i} \beta_j,\quad s_{\qgoal} = 1 - \sum_{i=0}^{n} s_i,\quad h_\lambda(q) = s_{\qgoal}(q)\, q + \sum_{i=0}^{n} s_i(q)\, T_i(q)

is such a diffeomorphism onto the model sphere world for suitable λ\lambda.

Step 1 — critical points correspond. ∇(φ∘h)=Jh⊤ (∇φ∘h)\nabla (\varphi \circ h) = J_h^\top\, (\nabla \varphi \circ h), and JhJ_h is invertible, so ∇(φ∘h)(q)=0\nabla(\varphi \circ h)(q) = 0 iff ∇φ(h(q))=0\nabla \varphi(h(q)) = 0. The Hessians are congruent at critical points, so nondegeneracy and type survive. Minima map to minima: one on MM, one on FF.

Step 2 — each TiT_i maps its star boundary onto a circle. On ∂QOi\partial \QO_i, βi=0\beta_i = 0, so Ti(q)=ri(q−qi)/d(q,qi)+piT_i(q) = r_i (q - q_i)/d(q, q_i) + p_i: the point at bearing θ\theta from qiq_i lands at distance rir_i from pip_i at the same bearing. A star boundary of radius ρ(θ)\rho(\theta) is squashed radially by ri/ρ(θ)r_i / \rho(\theta).

Step 3 — the switches switch. On ∂QOi\partial \QO_i, βi=0\beta_i = 0 gives si=1s_i = 1 and every βˉj\bar\beta_j with j≠ij \ne i contains the factor βi=0\beta_i = 0, so sj=0s_j = 0 and sqgoal=0s_{\qgoal} = 0. Hence hλ=Tih_\lambda = T_i exactly on ∂QOi\partial \QO_i. At the goal γκ=0\gamma_\kappa = 0, every si=0s_i = 0, and hλ(qgoal)=qgoalh_\lambda(\qgoal) = \qgoal.

Step 4 — suitable λ. Smoothness and bijectivity for some λ\lambda beyond a threshold are quoted from Rimon and Koditschek. The port measures the local half: det⁡Jhλ>0\det J_{h_\lambda} > 0 on a 48×4848 \times 48 lattice of free points. ■\blacksquare

What the port had to learn. Three things about this construction are easy to get wrong, and the chapter's check caught all three. First, γκβˉi\gamma_\kappa \bar\beta_i is astronomically larger than λβi\lambda \beta_i for any λ\lambda of order one — on our world λ0≈2×1018\lambda_0 \approx 2 \times 10^{18} at κ=6\kappa = 6 — so a λ=1\lambda = 1 pins every switch at one and turns a rounding error of −10−16-10^{-16} in a boundary βi\beta_i into garbage for the other switches; the boundary is now handled exactly. Second, the model disc must be inscribed in its star: if a disc poked outside its star, Ti−idT_i - \mathrm{id} would point outward in the star's valleys while ∇si\nabla s_i points inward, and det⁡Jh\det J_h went negative in the switch collar for every large λ\lambda we tried. With inscribed discs, min⁡det⁡Jh=0.984\min \det J_h = 0.984 at λ0\lambda_0 over 1450 lattice points. Third, "suitable" is not decoration: at 10−6λ010^{-6} \lambda_0 the minimum determinant is −55-55 and the map folds. The Sculptor's λ\lambda slider spans both regimes. Virtual work, the other half of this derivation's collapsible, follows in §4.7 below.

Lifting forces to configuration space

DerivationVirtual work: a workspace force is a configuration-space force through Jᵀ

Statement (Choset §4.7.1, eq. 4.26). Power is coordinate-independent: f⊤x˙=u⊤q˙f^\top \dot x = u^\top \dot q for every q˙\dot q. With x˙=Jq˙\dot x = J \dot q, f⊤Jq˙=u⊤q˙f^\top J \dot q = u^\top \dot q for all q˙\dot q, so u=J⊤fu = J^\top f. The lifted forces of several control points are summed in configuration space, never in the workspace.

Example 4.7.1. A vertex a=(ax,ay)a = (a_x, a_y) of a planar rigid body at q=(x,y,θ)q = (x, y, \theta) sits at φ(q)=(x+axcos⁡θ−aysin⁡θ, y+axsin⁡θ+aycos⁡θ)\varphi(q) = (x + a_x \cos\theta - a_y \sin\theta,\ y + a_x \sin\theta + a_y \cos\theta) (eq. 4.20), with Jacobian

J=[10−axsin⁡θ−aycos⁡θ01−axcos⁡θ−aysin⁡θ]J = \begin{bmatrix} 1 & 0 & -a_x \sin\theta - a_y \cos\theta \\ 0 & 1 & \phantom{-}a_x \cos\theta - a_y \sin\theta \end{bmatrix}

(eq. 4.21), so u=J⊤f=(fx, fy, −fx(axsin⁡θ+aycos⁡θ)+fy(axcos⁡θ−aysin⁡θ))u = J^\top f = (f_x,\ f_y,\ -f_x (a_x \sin\theta + a_y \cos\theta) + f_y (a_x \cos\theta - a_y \sin\theta)) (eq. 4.23), and uθ=(r×f)zu_\theta = (r \times f)_z with rr the world vector from the body origin to the vertex. The check verifies this identity at a hundred random configurations to 10−1210^{-12} against Chapter 4's bodyPointJacobian.

Reach. A point at fraction tt along link kk is the tip of a shorter arm, so its Jacobian is Chapter 4's jacobian with the columns for joints beyond kk zero and the last link shortened to tLkt L_k; at t=1t = 1 on the last link it is jacobian, and the check says so. Then u(q)=∑jJj⊤(q) ∇Uj(rj(q))u(q) = \sum_j J_j^\top(q)\, \nabla U_j(r_j(q)) is the exact gradient of ∑jUj(rj(q))\sum_j U_j(r_j(q)) by the chain rule — including the floating point, by the envelope theorem of Derivation 2 — and the check compares it with central differences at 213 configurations: worst relative error 2×10−92 \times 10^{-9}. ■\blacksquare

Why summing in the workspace is wrong. Two control points have two Jacobians; J1⊤f1+J2⊤f2≠J⊤(f1+f2)J_1^\top f_1 + J_2^\top f_2 \ne J^\top (f_1 + f_2) for any single JJ. Control points approximate the body; they are the finite sample that stands in for an integral over its area, and figure 4.20's collision between them is the price. rfloatr_{float} reduces the price and does not eliminate it.

The algorithm

Choset's Algorithm 4 is the whole method; brushfire and wave-front are Chapter 6's breadth-first search with a different seed set; navigation functions are a formula; and lifting is a sum.

AlgorithmGRADIENT DESCENT — Choset Alg. 4, with the sign the text intendsCostO(number of potential terms) per step; the number of steps depends on α and the field
In
a means to compute ∇U(q) at a point q; a step schedule α(i); a tolerance ε
Out
the iterates {q(0), q(1), …, q(i)}, and why the loop stopped
  1. q(0)←qstartq(0) \leftarrow \qstart; i←0i \leftarrow 0
  2. while ∥∇U(q(i))∥≥ϵ\|\nabla U(q(i))\| \ge \epsilon do
  3.     q(i+1)←q(i)−α(i) ∇U(q(i))q(i + 1) \leftarrow q(i) - \alpha(i)\, \nabla U(q(i))
  4.     i←i+1i \leftarrow i + 1
  5. end while
  6. report: at the goal; or a local minimum (print the Hessian's eigenvalue signs); or a step limit

Honesty. Line 4 of Algorithm 4 as printed reads q(i+1)=q(i)+α(i)∇U(q(i))q(i+1) = q(i) + \alpha(i) \nabla U(q(i)), while the text says "take a small step in the direction opposite the gradient" and c˙=−∇U\dot c = -\nabla U. It is a sign typo; the code uses the minus. The port adds a cap on the step length (a repulsive spike must not fling the bead across the table), a collision test (so an α\alpha too large for the spike is reported, not ignored), and a stall detector for the chatter of Derivation 2's nonsmooth seam, where ∥∇U∥\|\nabla U\| never falls below ϵ\epsilon and the bead goes nowhere.

AlgorithmBRUSHFIRE — Choset §4.3.2 · WAVE-FRONT — Choset §4.5CostO(#cells): one multi-source breadth-first search
In
an occupancy grid (0 free, 1 obstacle); 4- or 8-point connectivity; for wave-front, the goal pixel
Out
a label per pixel (1 + grid distance to the nearest obstacle, or 2 + grid distance to the goal); back pointers; the pixels where two fronts met
  1. Brushfire: label every obstacle pixel 11; enqueue every free pixel adjacent to an obstacle with label 22 and the obstacle's component as its source
  2. Wave-front: enqueue the goal pixel with label 22
  3. while the queue is not empty: pop cc; for each free neighbour xx of cc (Chapter 6's GridGraph.neighbors):
  4.     if xx is unlabelled: label it label(c)+1\text{label}(c) + 1, point its back pointer at cc, inherit cc's source, enqueue it
  5.     else if (brushfire) xx's source differs from cc's and label(x)≥label(c)\text{label}(x) \ge \text{label}(c): record a second back pointer — two fronts met
  6. Grid gradient at a pixel: a neighbour with the lowest label (ties to the first in scan order E, W, N, S, then diagonals)
  7. Wave-front descent from qstart\qstart: step to a neighbour labelled one less until the label is 22

Reading distance off the grid: (label−1)⋅cellSize(\text{label} - 1) \cdot \text{cellSize} is the grid distance, and the gradient points from the back-pointer pixel to the robot's — four or eight possible directions, "just as the grid is an approximation of the workspace, so is the computed gradient an approximation of the actual distance gradient" (Choset). The micro-example's gradient at (5,3)(5, 3) points to (6,3)(6, 3): both (6,3)(6,3) and (5,2)(5,2) carry label 33 and east comes first.

AlgorithmSENSOR-BASED DISTANCE — Choset §4.3.1CostO(n)
In
a range scan ρ(q, θ_k) on n bearings (Chapter 3's rangeScan)
Out
one (d_i, ∇d_i) per visible obstacle: the local minima of ρ over θ, with ∇d_i = −(cos θ*, sin θ*)
  1. for each bearing kk with a finite reading: if ρk≤ρk−1\rho_k \le \rho_{k-1} and ρk≤ρk+1\rho_k \le \rho_{k+1} and at least one is strict, kk is a local minimum (a plateau counts once, at its first ray)
  2. emit di=ρkd_i = \rho_k and ∇di=−(cos⁡θk,sin⁡θk)\nabla d_i = -(\cos\theta_k, \sin\theta_k): the ray faces the closest point of its obstacle, so the gradient points back along it

What the Apartment taught the check. Each ray reads the exact distance along itself, so the smallest local minimum is never below D(q)D(q). Broadside — the foot of the perpendicular on the nearest wall at least Dtan⁡(π/n)D \tan(\pi/n) from both ends — the ray nearest the perpendicular hits within π/n\pi/n of it and overestimates DD by at most 1/cos⁡(π/n)−11/\cos(\pi/n) - 1, which is 3.8×10−53.8 \times 10^{-5} for 360 rays. But a zero-thickness wall seen end-on subtends no angle, and a fan of 360 rays can miss its end entirely; the nearest visible thing is then farther away. In 80 seeded poses, 70 were broadside and met the bound (the Apartment's axis-aligned walls are hit exactly perpendicularly by the fan, so the overestimate there is 10−1610^{-16}), and 10 were end-on with a worst overestimate of 8.3%. No sensor-only estimator can do better; Choset's remark that "an obstacle distance function may be incorrect if the obstacle is partially occluded by another" is the same limit from the other side.

AlgorithmNAVIGATION FUNCTION — Choset eq. (4.13), star worlds via h_λ (eq. 4.19)CostO(n) per evaluation for n obstacles, plus a 2 × 2 finite-difference Jacobian of h_λ
In
a sphere world (centre, r₀, discs) or a star world; q_goal; κ; for stars, λ
Out
φ(q) and ∇φ(q) by the quotient rule; for stars, (φ ∘ h_λ)(q) and J_hᵀ ∇φ by the chain rule
  1. A←d2(q,qgoal)A \leftarrow d^2(q, \qgoal); β←∏i=0nβi(q)\beta \leftarrow \prod_{i=0}^{n} \beta_i(q); B←Aκ+βB \leftarrow A^\kappa + \beta
  2. φ←A B−1/κ\varphi \leftarrow A\, B^{-1/\kappa}; ∇φ←∇A B−1/κ−(A/κ) B−1/κ−1 (κAκ−1∇A+∇β)\nabla \varphi \leftarrow \nabla A\, B^{-1/\kappa} - (A/\kappa)\, B^{-1/\kappa - 1}\, (\kappa A^{\kappa-1} \nabla A + \nabla \beta)
  3. critical points: damped Newton on ∇ψ\nabla \psi, ψ=κlog⁡A−∑ilog⁡βi\psi = \kappa \log A - \sum_i \log \beta_i, from a lattice of free seeds; polish, deduplicate, classify by the Hessian of ψ\psi; the goal is the minimum it always is
  4. star world: hλ(q)=sqgoalq+∑i=0nsiTi(q)h_\lambda(q) = s_{\qgoal} q + \sum_{i=0}^{n} s_i T_i(q) with a βi≤0\beta_i \le 0 sending qq to Ti(q)T_i(q) outright; (φ∘hλ)(q)(\varphi \circ h_\lambda)(q) and Jh⊤∇φ(hλ(q))J_h^\top \nabla\varphi(h_\lambda(q))
  5. suitable λ: λ0=max⁡\lambda_0 = \max over a lattice of free points, at least a quarter-radius clear of obstacle ii, of γκβˉi/βi\gamma_\kappa \bar\beta_i / \beta_i — so every switch is ≤12\le \tfrac12 outside that collar

The check verifies the quotient-rule gradient against central differences at 210 points (worst relative error 1.6×10−71.6 \times 10^{-7}) and that φ∈[0,1]\varphi \in [0, 1] on the free space.

AlgorithmCONTROL-POINT LIFTING — Choset eq. (4.26)CostO(#points · dim Q) per evaluation, plus one segment–scene distance per link for r_float
In
Reach and its scene; control points (link, fraction t, role); the goal configuration; whether to add r_float
Out
u(q) = Σ_j J_j(q)ᵀ ∇U_j(r_j(q)), the configuration-space gradient; per-term forces and torques for drawing
  1. for each control point jj: rj←r_j \leftarrow its world position from Chapter 2's FK; fj←∇Uj(rj)f_j \leftarrow \nabla U_j(r_j) with UjU_j attractive toward rj(qgoal)r_j(\qgoal) or repulsive from every obstacle within Q⋆Q^\star
  2.     Jj←J_j \leftarrow the control-point Jacobian (Chapter 4's jacobian for a shortened arm); u←u+Jj⊤fju \leftarrow u + J_j^\top f_j
  3. if floating: rfloat←r_{float} \leftarrow the link point nearest any obstacle (one segment–scene distance per link); treat it as one more repulsive control point at its (k,t)(k, t)
  4. descend q←q−α u(q)q \leftarrow q - \alpha\, u(q) with Algorithm 4, in joint space

Implementation in Rust

The potential crate is introduced here; brushfire is the owned grid distance transform that Chapters 8 and 10 import, and Potential is the interface Chapter 19's CHOMP reads a distance field through. Configurations are SVector<f64, N> so one Attractive serves a point in the plane and a control point on Reach.

crates/potential/src/lib.rs
use nalgebra::SVector;

/// A differentiable scalar field over an N-dimensional configuration space. The gradient is
/// read as a *velocity* for first-order robots (Choset Ch. 4's convention through §4.6) and
/// as a *force* once it is lifted through Jᵀ (§4.7).
pub trait Potential<const N: usize> {
    fn value(&self, q: &SVector<f64, N>) -> f64;
    fn gradient(&self, q: &SVector<f64, N>) -> SVector<f64, N>;
}

/// Choset eqs. (4.2)–(4.3): quadratic within `d_goal_star`, conic beyond. The two agree in value
/// and gradient at the seam (Derivation 1), so descent never sees it; `f64::INFINITY` gives the
/// pure quadratic of eq. (4.1).
pub struct Attractive<const N: usize> { pub goal: SVector<f64, N>, pub zeta: f64, pub d_goal_star: f64 }

impl<const N: usize> Potential<N> for Attractive<N> {
    fn value(&self, q: &SVector<f64, N>) -> f64 {
        let d = (q - self.goal).norm();
        if d <= self.d_goal_star { 0.5 * self.zeta * d * d }
        else { self.d_goal_star * self.zeta * d - 0.5 * self.zeta * self.d_goal_star * self.d_goal_star }
    }
    fn gradient(&self, q: &SVector<f64, N>) -> SVector<f64, N> {
        let diff = q - self.goal;
        let d = diff.norm();
        if d <= self.d_goal_star { self.zeta * diff }               // eq. (4.1): ζ(q − q_goal)
        else { (self.d_goal_star * self.zeta / d) * diff }          // eq. (4.3): magnitude ζ d*
    }
}

/// One repulsive term per obstacle (Choset §4.1, after eq. 4.7), so the gradient does not jump
/// when the nearest obstacle changes. `dist` is the Chapter 4 query: d_i(q) and its closest point.
pub struct Repulsive<'w, const N: usize> { pub dist: &'w dyn cspace::DistanceQuery<N>, pub eta: f64, pub q_star: f64 }

impl<const N: usize> Potential<N> for Repulsive<'_, N> {
    fn value(&self, q: &SVector<f64, N>) -> f64 {
        self.dist.distances(q).iter().filter(|o| o.d <= self.q_star)
            .map(|o| 0.5 * self.eta * (1.0 / o.d - 1.0 / self.q_star).powi(2))      // eq. (4.4)
            .sum()
    }
    fn gradient(&self, q: &SVector<f64, N>) -> SVector<f64, N> {
        let mut g = SVector::zeros();
        for o in self.dist.distances(q).iter().filter(|o| o.d <= self.q_star) {
            // eq. (4.5): η (1/Q* − 1/d) (1/d²) ∇d, with ∇d = (q − c)/d (Derivation 2).
            g += self.eta * (1.0 / self.q_star - 1.0 / o.d) / (o.d * o.d) * o.grad;
        }
        g
    }
}

/// U = Σ terms — the whole method and its local minima.
pub struct Additive<const N: usize>(pub Vec<Box<dyn Potential<N>>>);

Gradient descent returns the iterates and the reason it stopped, with the Hessian's eigenvalues when it stopped at a critical point that is not the goal — the stuck badge of the Potential Well.

crates/potential/src/descent.rs
use nalgebra::{SMatrix, SVector};

pub enum DescentOutcome { Goal, LocalMinimum { hessian_eigs: Vec<f64> }, StepLimit, Collision, Stalled }

/// Choset Algorithm 4 with the sign the text intends: q(i+1) = q(i) − α(i) ∇U(q(i)).
/// `alpha` is a schedule ("often made on an ad hoc or empirical basis", Choset); `eps` the
/// forgiving termination ‖∇U‖ < ε; `max_step` caps a repulsive spike's fling.
pub fn gradient_descent<const N: usize>(
    u: &dyn Potential<N>, q_start: SVector<f64, N>, alpha: impl Fn(usize) -> f64,
    eps: f64, max_steps: usize, goal: Option<(SVector<f64, N>, f64)>, max_step: f64,
) -> (Vec<SVector<f64, N>>, DescentOutcome) {
    let mut path = vec![q_start];
    for i in 0..max_steps {
        let q = *path.last().unwrap();
        if let Some((g, tol)) = goal { if (q - g).norm() <= tol { return (path, DescentOutcome::Goal); } }
        let grad = u.gradient(&q);
        if grad.norm() < eps {
            // Not at the goal, gradient gone: classify the critical point by its Hessian.
            let h = hessian(u, &q, 1e-4);
            let eigs = h.symmetric_eigenvalues().iter().copied().collect();
            return (path, DescentOutcome::LocalMinimum { hessian_eigs: eigs });
        }
        let mut step = -alpha(i) * grad;
        if step.norm() > max_step { step *= max_step / step.norm(); }
        path.push(q + step);
    }
    (path, DescentOutcome::StepLimit)
}

/// ∇²U by central differences of the gradient, symmetrized — Choset reads the type of a
/// critical point off its signature, and Chapter 8 reads the Morse index off the same matrix.
pub fn hessian<const N: usize>(u: &dyn Potential<N>, q: &SVector<f64, N>, h: f64) -> SMatrix<f64, N, N> {
    let mut m = SMatrix::<f64, N, N>::zeros();
    for j in 0..N {
        let (mut qp, mut qm) = (*q, *q);
        qp[j] += h; qm[j] -= h;
        m.set_column(j, &((u.gradient(&qp) - u.gradient(&qm)) / (2.0 * h)));
    }
    0.5 * (m + m.transpose())
}

Brushfire is the owned artifact. It is Chapter 6's breadth-first search with every obstacle pixel as a source, and it keeps the second back pointer that Chapter 8 reads as the grid GVD.

crates/potential/src/brushfire.rs
use std::collections::VecDeque;

#[derive(Clone, Copy)] pub enum Conn { Four, Eight }

/// Choset's labels: obstacle 1, free pixels 2, 3, …; `back2` is set where a second front from a
/// *different* obstacle arrived — §5.2.5's "two different back pointers", the grid GVD.
pub struct DistGrid { pub label: Vec<u32>, pub back: Vec<Option<usize>>, pub back2: Vec<Option<usize>>, pub source: Vec<i32>, pub w: usize, pub h: usize }

pub fn brushfire(occ: &cspace::Raster, conn: Conn) -> DistGrid {
    let (w, h) = (occ.width(), occ.height());
    let comp = obstacle_components(occ);                       // a front's source is an obstacle, not a pixel
    let mut g = DistGrid { label: vec![0; w * h], back: vec![None; w * h], back2: vec![None; w * h], source: vec![-1; w * h], w, h };
    let mut queue = VecDeque::new();
    for k in 0..w * h {
        if occ.is_occupied(k) { g.label[k] = 1; continue; }
        // Step 1 of the text: free pixels neighbouring an obstacle are labelled 2.
        if let Some(o) = occ.lattice_neighbors(k, conn).find(|&n| occ.is_occupied(n)) {
            g.label[k] = 2; g.back[k] = Some(o); g.source[k] = comp[o]; queue.push_back(k);
        }
    }
    while let Some(c) = queue.pop_front() {
        let (lc, sc) = (g.label[c], g.source[c]);
        for x in occ.free_neighbors(c, conn) {               // Star(c) on Chapter 6's grid graph
            if g.label[x] == 0 {
                g.label[x] = lc + 1; g.back[x] = Some(c); g.source[x] = sc; queue.push_back(x);
            } else if g.source[x] != sc && g.source[x] >= 0 && g.label[x] >= lc {
                // Two fronts meet. Even gap: x was labelled lc + 1 by the other front and now gets
                // its second back pointer. Odd gap: both pixels carry lc and the collision falls on
                // their shared edge — both are flagged so the ridge has no holes.
                if g.label[x] == lc + 1 && g.back2[x].is_none() { g.back2[x] = Some(c); }
                g.mark_ridge(x); if g.label[x] == lc { g.mark_ridge(c); }
            }
        }
    }
    g
}

impl DistGrid {
    /// Pixels two fronts from different obstacles reached — Chapter 8's grid GVD.
    pub fn ridge_cells(&self) -> Vec<usize> { (0..self.w * self.h).filter(|&k| self.is_ridge(k)).collect() }
    /// "Point to a lowest neighbor" (Choset §4.3.2); ties to the first in scan order.
    pub fn grid_gradient(&self, cell: usize, conn: Conn) -> Option<usize> { /* lowest-label neighbour */ todo_lowest(self, cell, conn) }
}

/// Wave-front (§4.5): the same propagation from the goal pixel alone, seeded with 2.
pub fn wavefront(occ: &cspace::Raster, goal: usize, conn: Conn) -> DistGrid { propagate(occ, &[(goal, 2, 0)], conn) }

The navigation function is eq. (4.13) with its analytic gradient, and the lifted potential for Reach is a sum of Jᵀ f terms.

crates/potential/src/navigation.rs · src/lifted.rs
/// Eq. (4.13) on a sphere world; `Potential<2>` via the quotient rule.
pub struct NavigationFunction<'w> { pub world: &'w SphereWorld, pub goal: Point2<f64>, pub kappa: u32 }

impl Potential<2> for NavigationFunction<'_> {
    fn value(&self, q: &Vector2<f64>) -> f64 {
        let a = (q - self.goal.coords).norm_squared();                 // γ_1 = d²
        let b = a.powi(self.kappa as i32) + self.world.beta(q);        // d^{2κ} + β
        if b <= 0.0 { 1.0 } else { a * b.powf(-1.0 / self.kappa as f64) }
    }
    fn gradient(&self, q: &Vector2<f64>) -> Vector2<f64> {
        let k = self.kappa as f64;
        let diff = q - self.goal.coords;
        let a = diff.norm_squared();
        let (beta, grad_beta) = self.world.beta_and_gradient(q);
        let b = a.powf(k) + beta;
        let bm = b.powf(-1.0 / k);
        let da = 2.0 * diff;
        let db = k * a.powf(k - 1.0) * da + grad_beta;
        da * bm - (a / k) * (bm / b) * db                              // quotient rule
    }
}

/// §4.7.2–4.7.3: control points on Reach, each with a workspace potential; `gradient` is
/// u(q) = Σ_j J_j(q)ᵀ ∇U_j(r_j(q)), summed in C-space (eq. 4.26) — never in the workspace.
pub struct LiftedPotential<'r> { pub arm: &'r robots::Reach, pub scene: &'r collide::Scene, pub points: Vec<ControlPoint>, pub goal: Tn<2>, pub floating: bool }

impl Potential<2> for LiftedPotential<'_> {
    fn value(&self, q: &Vector2<f64>) -> f64 { self.terms(q).iter().map(|t| t.value).sum() }
    fn gradient(&self, q: &Vector2<f64>) -> Vector2<f64> {
        let mut u = Vector2::zeros();
        for t in self.terms(q) {
            let j = cspace::control_point_jacobian(self.arm, q, t.link, t.t);   // Chapter 4's jacobian, shortened
            u += j.transpose() * t.force;                                          // u = Jᵀ f
        }
        u
    }
}

The worked example, and its printed output

crates/potential/examples/descent_step.rs · brushfire_7x3.rs · kappa_sweep.rs
fn main() {
    // Part B: one evaluation and one step, as in the text.
    let goal = Vector2::new(4.0, 3.0);
    let att = Attractive { goal, zeta: 1.0, d_goal_star: 10.0 };
    let disc = cspace::DiscObstacle { center: Point2::new(0.0, -2.0), radius: 1.0 };
    let rep = Repulsive { dist: &disc, eta: 1.0, q_star: 2.0 };
    let q = Vector2::zeros();
    println!("U_att = {}  grad = {:?}", att.value(&q), att.gradient(&q));
    println!("U_rep = {}  grad = {:?}", rep.value(&q), rep.gradient(&q));
    let u = Additive(vec![Box::new(att), Box::new(rep)]);
    println!("U = {}  grad = {:?}", u.value(&q), u.gradient(&q));
    println!("q1 = {:?}", q - 0.1 * u.gradient(&q));

    // Part A: the 7 × 3 micro-grid.
    let occ = cspace::Raster::from_ascii(&[".......", "#.....#", "......."]);  // first row is the top
    let d = brushfire(&occ, Conn::Four);
    for row in d.rows_top_first() { println!("{}", row.iter().map(u32::to_string).collect::<Vec<_>>().join(" ")); }
    println!("ridge: {}", d.ridge_cells().iter().map(|&k| d.col_row(k)).map(|(c, r)| format!("({c},{r})")).collect::<Vec<_>>().join(" "));

    // The κ sweep on the five-disc world.
    for kappa in [2, 3, 4, 6, 7, 8, 10] {
        let c = critical_points(&FIVE_DISC_WORLD, FIVE_DISC_GOAL, kappa);
        println!("kappa={kappa}: minima={} saddles={}", c.minima, c.saddles);
    }
}
cargo run -p potential --example descent_step · brushfire_7x3 · kappa_sweep
U_att = 12.5  grad = [-4.0, -3.0]
U_rep = 0.125  grad = [0.0, -0.5]
U = 12.625  grad = [-4.0, -3.5]
q1 = [0.4, 0.35]
2 3 4 5 4 3 2
1 2 3 4 3 2 1
2 3 4 5 4 3 2
ridge: (4,1) (4,2) (4,3)
kappa=2: minima=3 saddles=7
kappa=3: minima=2 saddles=6
kappa=4: minima=2 saddles=6
kappa=6: minima=2 saddles=6
kappa=7: minima=1 saddles=5
kappa=8: minima=1 saddles=5
kappa=10: minima=1 saddles=5

Read Part B against the text. Uatt=12⋅25=12.5U_{att} = \tfrac12 \cdot 25 = 12.5 with gradient q−qgoal=(−4,−3)q - \qgoal = (-4, -3). The disc's closest point is c=(0,−1)c = (0, -1), so d1=1d_1 = 1 and ∇d1=(0,1)\nabla d_1 = (0, 1); with η=1\eta = 1 and Q⋆=2Q^\star = 2, Urep=12(1−12)2=0.125U_{rep} = \tfrac12 (1 - \tfrac12)^2 = 0.125 and ∇Urep=(12−1)⋅1⋅(0,1)=(0,−0.5)\nabla U_{rep} = (\tfrac12 - 1) \cdot 1 \cdot (0, 1) = (0, -0.5). The total is U=12.625U = 12.625, ∇U=(−4,−3.5)\nabla U = (-4, -3.5), and one step with α=0.1\alpha = 0.1 lands at q(1)=(0.4,0.35)q(1) = (0.4, 0.35) — away from the obstacle and toward the goal. descent_step_matches_text pins these to 10−1210^{-12}; brushfire_7x3_labels asserts all 21 labels and the ridge set; brushfire_equals_bfs_depth_plus_one checks on seeded grids that brushfire equals search::bfs multi-source depth +1+ 1 and that the wave-front descent has the Dijkstra cost under unit weights; kappa_sweep pins exactly one minimum from κ=7\kappa = 7; reach_field lifts a unit force at Reach's tip and asserts uθ=(r×f)zu_\theta = (r \times f)_z. The TypeScript port in web/lib/potential/ runs the same thirteen checks, and every widget on this page is that port.

Two of the port's checks failed on first writing, and both failures were mathematics, not typos. The sensor-based distance check assumed the ray fan overestimates DD by at most the ray quantisation, and the Apartment's free-standing wall stubs, seen end-on, broke it by 8%: a zero-thickness wall subtends no angle, and the bound only holds broadside. The star-world check assumed hλh_\lambda maps every star boundary onto its circle for λ=1\lambda = 1, which is true in exact arithmetic and false by six units in floating point once a boundary βi\beta_i rounds to −10−16-10^{-16}, because γκβˉi\gamma_\kappa \bar\beta_i is 101710^{17} and the switches cannot see a λ\lambda of order one. Both are now stated as the theorems actually say them.

Putting it together: Rusty in a doorway, Reach at the bench

The integration lab runs Rusty in the Apartment under a sensor-based potential: the repulsive term is fed by the local minima of a 72-ray scan of radius 3 m (Algorithm SENSOR-BASED DISTANCE), the attractive term by the goal in the corridor, and the descent is Algorithm 4 at α=0.02\alpha = 0.02. From the middle of room A Rusty heads for the door, and in the alcove beside the jamb — where the goal pulls through the wall and the two wall faces push toward the centre of the alcove — it stops, with the stuck badge reading a positive-definite Hessian. The same raster the Chapter 6 planner used, run through wavefront from the goal cell at 4-point connectivity, gives a field with one minimum, and the grid descent walks Rusty out of the alcove and through the door — hugging the jamb at one cell's clearance, which is Choset's "dangerously close to obstacles" and the reason Chapter 8's Voronoi roadmap exists. The Potential Well's wave-front toggle is this lab on a table; the Apartment version is Exercise 4's.

Reach's half of the lab is the Arm in a Field widget run to the end: the same start descended twice, with the first collision logged. Without rfloatr_{float} link 1 meets the post after a dozen steps between its two fixed repulsive points; with it the arm reaches the goal pose in about five hundred. Move the goal into the block's shadow and you will find starts where it stalls in a local minimum of the lifted potential instead — the additive disease, now on the torus — and starts where even rfloatr_{float} fails, because a floating point repels the nearest link point and nothing else.

Three pointers close the chapter. Brushfire's double-back-pointer pixels are the grid generalized Voronoi diagram, and Chapter 8 proves the exact version is a roadmap, with the Morse vocabulary this chapter's "nondegenerate critical point" was borrowing from. Chapter 19's CHOMP descends a potential whose obstacle term is a distance field exactly like the brushfire one, over a whole trajectory at once, and inherits every local minimum on this page. And Part III's planners are the field's eventual answer to incompleteness: when the valley you roll into is not the goal's, sample.

Exercises

  1. Foundation exerciseDifficulty 2 of 3Convexity of d_i and the seams of U_rep

    Prove that di(q)=min⁡c∈QOi∥q−c∥d_i(q) = \min_{c \in \QO_i} \|q - c\| is a convex function of qq when QOi\QO_i is convex (Choset Ch. 5, Problem 19, brought forward). Conclude that with the per-obstacle sum Urep=∑iUrepiU_{rep} = \sum_i U_{rep_i}, the only places ∇Urep\nabla U_{rep} can fail to be continuous are the spheres di=Qi⋆d_i = Q^\star_i — and show that in fact it is continuous there too, so the seams that remain are those of Derivation 2's Step 4: where a nonconvex obstacle has two closest points.

  2. Foundation exerciseDifficulty 2 of 3Wave-front paths are L₁-shortest

    Show that the path produced by wave-front descent under 4-point connectivity is shortest in the L1L_1 (Manhattan) metric among all grid paths from the start to the goal (Choset Ch. 4, Problem 2). Then give a 3×33 \times 3 example where the 8-point descent path — shortest in link length — is not Euclidean-shortest among 8-connected paths, and say by how much.

    On an empty 3 × 3 grid from the bottom-left to the top-right cell under 8-point connectivity, the wave-front path has two links. What is the Euclidean length of a two-link path that goes diagonal, diagonal, divided by the length of one that goes east, east, then north, north? (Give the ratio as a decimal.)

  3. Conceptual exerciseDifficulty 1 of 3Predict the κ threshold, then move an obstacle
    Predict first

    In the Navigation Function Sculptor, read the critical-point count at κ = 6: two minima, six saddles. Without moving the slider: what is the smallest κ at which the count will read one minimum?

  4. Conceptual exerciseDifficulty 2 of 3A world of only convex discs, and the price of the escape

    In the Potential Well choose two convex discs, drag the goal and the discs until the bead gets stuck, and record the Hessian eigenvalues the badge prints. Then enable wave-front mode and estimate how close the escape path passes to the discs — Choset's "dangerously close to obstacles" in §4.5. Explain why the wave-front path must graze: among all grid paths of the same link length to the goal it has no reason to prefer clearance, and the one found first by "step to any neighbour labelled one less" is whichever the scan order produced.

  5. Practical exerciseDifficulty 2 of 3Brushfire as Dijkstra, within one cell of the EDT

    Implement an 8-point brushfire with diagonal cost 1.41.4 (Chapter 6's grid metric) as a Dijkstra rather than a BFS: labels become real-valued distances and the queue becomes a heap. Property-test it on 200 seeded grids against the exact Euclidean distance transform ported from the sister book (mapping/edt): assert that every cell's label is within one cell of the EDT, and record the worst gap. Then repeat with diagonal cost 2\sqrt 2 and report whether the worst gap improves — and why it cannot reach zero (the octile metric is not the Euclidean one).

  6. Practical exerciseDifficulty 3 of 3Wave-front on the torus

    Implement Choset Ch. 4 Problem 5: a wave-front planner for Reach on the T2T^2 raster of Chapter 4, with the goal seeded at the goal configuration and adjacency wrapping across the seam at ±π\pm\pi in both axes. Animate the arm on the Workbench along the descent path, and assert in a test that the path's link length equals the Chapter 6 breadth-first distance on the same torus graph — then compare the number of cells the wave-front labels with the number A* expands for the same query, and say which planner you would run twice.

References

  1. Choset, H., Lynch, K. M., Hutchinson, S., Kantor, G., Burgard, W., Kavraki, L. E., and Thrun, S. (2005) Principles of Robot Motion: Theory, Algorithms, and Implementations. MIT Press.link to Principles of Robot Motion: Theory, Algorithms, and Implementations (opens in a new tab)

    Chapter 4 is this chapter's source: the additive potential and its honesty items, Algorithm 4, sensor-based and brushfire distance, the local-minimum figures 4.10–4.11, the wave-front planner, Definition 4.6.1 and the sphere- and star-world constructions, and the virtual-work lifting of §4.7 with Example 4.7.1 and figure 4.20.

  2. Khatib, O. (1986) Real-Time Obstacle Avoidance for Manipulators and Mobile Robots. International Journal of Robotics Research 5(1), 90–98.doi:10.1177/027836498600500106 (opens in a new tab)

    The artificial potential field for manipulators: attractive goal, repulsive obstacles, and forces at points on the links lifted through the Jacobian — §4.7 in its original form, written for control rather than planning.

  3. Rimon, E. and Koditschek, D. E. (1992) Exact Robot Navigation Using Artificial Potential Functions. IEEE Transactions on Robotics and Automation 8(5), 501–518.doi:10.1109/70.163777 (opens in a new tab)

    Navigation functions: Definition 4.6.1, the sphere-world formula (4.13) with its existence-in-κ theorem, and the star-to-sphere diffeomorphism h_λ with its switches — Derivations 5 and 6.

  4. Barraquand, J. and Latombe, J.-C. (1991) Robot Motion Planning: A Distributed Representation Approach. International Journal of Robotics Research 10(6), 628–649.doi:10.1177/027836499101000604 (opens in a new tab)

    The Randomized Path Planner: gradient descent on a grid potential with random walks to escape local minima — Choset's reference [37], the honest 1991 fix and the ancestor of Part III's sampling.

  5. Latombe, J.-C. (1991) Robot Motion Planning. Kluwer Academic Publishers.link to Robot Motion Planning (opens in a new tab)

    Chapter 7 is the classical textbook treatment of potential-field methods, including the wave-front (numerical navigation function) planner and the local-minimum analysis this chapter condenses.

  6. Ratliff, N., Zucker, M., Bagnell, J. A., and Srinivasa, S. (2009) CHOMP: Gradient Optimization Techniques for Efficient Motion Planning. IEEE International Conference on Robotics and Automation (ICRA), 489–494.doi:10.1109/ROBOT.2009.5152817 (opens in a new tab)

    Forward pointer only: the obstacle term of CHOMP is a distance field like brushfire's, descended over a whole trajectory, and it inherits this chapter's local minima. Chapter 19 derives it.

  7. Thrun, S., Burgard, W., and Fox, D. (2005) Probabilistic Robotics. MIT Press.link to Probabilistic Robotics (opens in a new tab)

    The sister volume's map representations chapter ports the exact Euclidean distance transform this chapter compares brushfire against; the two are different algorithms computing different metrics.