Robot Motion
Chapter 18PART VDynamics, Trajectories, and ConstraintsDifficulty: AdvancedEstimated reading time: 70 min

Trajectory Planning

A path is a curve; a trajectory is a curve with a clock. Path-velocity decomposition turns an n-joint arm into a one-dimensional system in the path parameter, the time-optimal clock is a bang-bang curve in the (s, ṡ) phase plane that hugs a velocity-limit curve, zero-inertia points make it slide, TOPP-RA replaces switch hunting with reachable intervals, and Choset's GRID SEARCH plans in (q, q̇) when the path is not fixed.

This kind of trajectory is called a “bang-bang” trajectory, and at least one of the actuators is always saturated. The heart of the time-scaling problem is to find the switching points between maximum and minimum acceleration.
Howie Choset, Kevin Lynch, Seth Hutchinson, George Kantor, Wolfram Burgard, Lydia Kavraki, and Sebastian ThrunPrinciples of Robot Motion (2005), §11.2

In this chapter

Every planner in Parts II and III handed Reach a curve and walked away. The curve had no clock. This chapter attaches one, and the first thing it discovers is that the best clock is almost never uniform. Run a path at constant speed and some motor pegs where the configuration is heavy and idles where it is light; run it on the clock this chapter computes and exactly one motor is at its limit at every instant, in turn.

The idea that makes this tractable fits in one sentence. Once the path is fixed, an nn-joint arm with nn torque limits is a one-dimensional system in the path parameter ss, and its time-optimal execution is a curve in the (s,s˙)(s, \dot s) phase plane that hugs a velocity-limit curve and switches between maximum and minimum acceleration a finite number of times. A reader who can read that phase plane can read every time-scaling result since 1985, including the 2018 reformulation that replaced switch-point hunting with interval propagation.

The second half of the chapter asks what happens when the path is not fixed, and meets the honest ancestor of every state-lattice planner: Choset's grid search over (q,q˙)(q, \dot q), run here on Chapter 6's A*. It is time-optimal for its own discretization, complete in a precise ϵ\epsilon-sense, and exponential in the number of joints — which is exactly why Chapter 19 lets the path move by optimization instead.

The problem: one path, two clocks

Take the spline below — three waypoints on Reach's torus, the arm hanging below its shoulder on a vertical Workbench — and execute it twice in the same 0.620.62 s. The left arm runs a uniform clock: accelerate briefly, cruise at constant s˙\dot s, brake briefly. The right arm runs the clock this chapter computes.

t=0
Figure The same path (purple) on the same arm with the same motors, both finishing in 0.618 s. The uniform clock spends its acceleration where the clock says and pegs a motor; the time-scaled clock spends it where the configuration can afford it, and at every instant exactly one gauge is full and neither exceeds its rail.

Nothing about the curve differs between the two lanes. What differs is s(t)s(t): the map from time to position along the path. Choset's §11.1 makes the distinction a definition. A path is a twice-differentiable curve q:[0,1]→Qq : [0, 1] \to \Q. A time scaling is a twice-differentiable, monotone map s:[0,tf]→[0,1]s : [0, t_f] \to [0, 1] with s˙>0\dot s > 0 on (0,tf)(0, t_f). A trajectory is their composition q(s(t))q(s(t)). Uniform time scalings s(t)=kts(t) = kt are a thin subset of the ones allowed, and the question of this chapter is: of all admissible clocks, which is fastest?

Building intuition

The phase plane is a speed-limit sign that changes every meter

The whole problem lives in one picture. Put ss on the horizontal axis and s˙\dot s on the vertical. A time scaling is a curve from (0,s˙0)(0, \dot s_0) to (1,s˙f)(1, \dot s_f) that never dips below s˙=0\dot s = 0. At every state (s,s˙)(s, \dot s) the torque limits allow a range of path accelerations L≤s¨≤UL \le \ddot s \le U — a cone of directions the curve may take. Above a certain speed the cone closes: there is no torque vector that keeps the arm on the path. That locus is the velocity-limit curve, and the region above it is inadmissible, hatched amber in the widget exactly as a configuration-space obstacle would be — it is an obstacle in (s,s˙)(s, \dot s).

Three things to notice, each of which becomes a theorem in the next section.

The limit is coupled, not per-joint. Drag a waypoint so the elbow folds sharply mid-path and the amber region dips to a waist. No single joint is near its own speed limit there; what has happened is that the centrifugal torque one joint needs, hq˙12h\dot q_1^2, is a torque the other joint's motor must supply, and above some s˙\dot s it cannot while the first joint is also braking at full torque. The dashed grey ghost is a constant-speed clock; wherever it turns amber, that clock is infeasible, and no controller that only looks at the current joint speeds will know why.

The optimal curve is made of two kinds of arc. Solid blue curves are integrated forward at the maximum acceleration UU; the dashed blue curve FF is integrated backward from the goal at the minimum acceleration LL. The orange profile is a concatenation of pieces of them, switching at the ticked points. The fastest trajectory is the one that is highest in the plane while staying out of the amber: tf=∫01ds/s˙t_f = \int_0^1 ds/\dot s, so area under the curve is time saved.

Zero-inertia points are where the picture changes shape. The vertical dashed amber lines mark ss values where some ai(s)=(Mq′)ia_i(s) = (M q')_i changes sign: that actuator momentarily cannot change s¨\ddot s at all, and instead bounds s˙\dot s directly. The limit curve is continuous through such a point but its slope is not, and the optimal profile may have to slide along it rather than bounce off. Raise both torque limits by a factor of 1.51.5 on the default spline and a slide appears near s=0.57s = 0.57.

One motor at a time

The design question most readers bring to this chapter is: "time-optimal means every motor at full power, right?" The Saturation Scope answers it with the torque histories of the solution above.

At every instant one strip rides its rail and the other does whatever the coupling demands; the switch is the instant the saturated joint changes. Scale the limits to 1.5×1.5\times and the amber stretch is a singular arc: the profile slides along the velocity-limit curve with an acceleration strictly between LL and UU, and neither motor is saturated. Choset's §11.2.1 derives exactly when that happens and what acceleration to use.

Notation used in this chapter
SymbolMeaningNote
s∈[0,1],  s(t),  s˙,  s¨s \in [0, 1],\; s(t),\; \dot s,\; \ddot sPath parameter; the time scaling s : [0, t_f] → [0, 1] and its rates.Choset §11.1
q(s),  q′=dq/ds,  q′′q(s),\; q' = dq/ds,\; q''The path and its derivatives in s. q'' must be continuous — b(s) contains it.
a(s),  b(s),  c(s)a(s),\; b(s),\; c(s)Inertial, velocity-product and gravity vectors of the path-constrained dynamics a s̈ + b ṡ² + c = u (eq. 11.6).
uimin⁡≤ui≤uimax⁡u_i^{\min} \le u_i \le u_i^{\max}Actuator limits, possibly state dependent (eq. 11.1). Symmetric constant bounds |u_i| ≤ u_i^max are the default here.
αi,βi;  Li,Ui;  L(s,s˙),U(s,s˙)\alpha_i, \beta_i;\; L_i, U_i;\; L(s,\dot s), U(s,\dot s)Per-actuator acceleration bounds (eq. 11.8), their assignment by the sign of a_i, and the max/min over i (eq. 11.9).
v(s),  s˙max⁡(s),  s˙zipmax⁡(s)v(s),\; \dot s^{\max}(s),\; \dot s^{\max}_{zip}(s)Velocity-limit curve where L = U (eq. 11.10); its generalization through zero-inertia points; the direct bound from a zero-inertia row (eq. 11.11).
F,  Ai,  S={s1,s2,… }F,\; A_i,\; \mathcal{S} = \{s_1, s_2, \dots\}The backward minimum-acceleration curve; the forward curves of the construction; the switch list.
(slim,s˙lim),  (stan,s˙tan),  s¨tangent±(s_{lim}, \dot s_{lim}),\; (s_{tan}, \dot s_{tan}),\; \ddot s^{\pm}_{tangent}Where A_i penetrates the limit curve; the tangent point found from it; the left and right tangent accelerations at a singular point.
x=s˙2x = \dot s^2Squared path speed — the variable in which every actuator limit is linear (TOPP-RA).Pham & Pham 2018
h,  A,  A(q,q˙),  A^h,\; \mathcal{A},\; \mathcal{A}(q, \dot q),\; \hat{\mathcal{A}}Lattice timestep; the discretized control set {−a_max, 0, a_max}ⁿ; the feasible-acceleration parallelepiped of a manipulator and its one-step conservative subset.Choset §11.3.3
δv(c0,c1),  ϵ,  Topt\delta_v(c_0, c_1),\; \epsilon,\; T_{opt}Speed-dependent safety margin c₀ + c₁‖q̇‖; the approximation parameter of Theorem 11.3.2; the optimal time.

The mathematics

Definitions

Path, time scaling, trajectory are as in the hook: q:[0,1]→Qq : [0,1] \to \Q twice differentiable; s:[0,tf]→[0,1]s : [0, t_f] \to [0,1] twice differentiable and monotone with s˙>0\dot s > 0 on the open interval; the trajectory is q(s(t))q(s(t)). Twice differentiability of ss is what makes q¨\ddot q exist and be bounded, which is what makes torque finite.

The motion cone at (s,s˙)(s, \dot s) is the set of tangent directions of curves through that state whose path acceleration satisfies L(s,s˙)≤s¨≤U(s,s˙)L(s, \dot s) \le \ddot s \le U(s, \dot s). The state is inadmissible when L>UL > U: the cone is empty and the robot is doomed to leave the path immediately. At an admissible state the robot may still be doomed eventually, if every curve inside the cones from there reaches the inadmissible region (Choset's figure 11.2).

The velocity-limit curve v(s)v(s) is the locus where L=UL = U — the cone collapses to one vector. It is computed by equating Li=UjL_i = U_j for every pair of actuators, solving each for s˙\dot s, and keeping the minimum.

A bang-bang time scaling has s¨∈{L,U}\ddot s \in \{L, U\} at every instant except possibly on singular arcs. At least one actuator is saturated at all times.

Zero-inertia, critical, singular. A point where ai(s)=0a_i(s) = 0 for some ii is a zero-inertia point: actuator ii cannot affect s¨\ddot s and instead bounds s˙\dot s directly. A point of the limit curve s˙max⁡\dot s^{\max} set by such a bound is critical (L<UL < U there, unlike on vv). A critical point from which integrating UU forward or LL backward penetrates the limit curve immediately is singular.

A δv(c0,c1)\delta_v(c_0, c_1)-safe trajectory keeps clearance at least c0+c1∥q˙∥c_0 + c_1\|\dot q\| from every obstacle at every instant — the faster, the wider the berth.

Path-constrained dynamics

DerivationSubstituting the path into the Chapter 17 equations

Step 1 — the chain rule. With q=q(s(t))q = q(s(t)), q˙=q′s˙\dot q = q'\dot s and q¨=q′′s˙2+q′s¨\ddot q = q''\dot s^2 + q'\ddot s (eqs. 11.2–11.3). The first is linear in s˙\dot s; the second has a term in s˙2\dot s^2 and a term in s¨\ddot s, and nothing else.

Step 2 — substitute. Into M(q)q¨+q˙TΓ(q)q˙+g(q)=uM(q)\ddot q + \dot q\T\Gamma(q)\dot q + g(q) = u:

M (q′′s˙2+q′s¨)+(q′s˙)T Γ (q′s˙)+g=u.M\,(q''\dot s^2 + q'\ddot s) + (q'\dot s)\T\,\Gamma\,(q'\dot s) + g = u .

Step 3 — group by powers of the clock. The velocity-product term is bilinear in q˙\dot q, so the two factors of s˙\dot s come out as s˙2\dot s^2:

Mq′⏟a(s) s¨+(Mq′′+q′TΓq′)⏟b(s) s˙2+g⏟c(s)=u.\underbrace{M q'}_{a(s)}\,\ddot s + \underbrace{\big(M q'' + q'\T\Gamma q'\big)}_{b(s)}\,\dot s^2 + \underbrace{g}_{c(s)} = u .

Every coefficient depends on ss alone. The nn equations now constrain one scalar unknown s¨\ddot s at each state (s,s˙)(s, \dot s) — that is the whole content of path-velocity decomposition.

The velocity-product piece of bb is exactly Chapter 17's velocityProduct(q, q′), the vector C(q,q˙)q˙C(q, \dot q)\dot q evaluated at q˙=q′\dot q = q'. No new physics is computed here; the library function PathDynamics::abc calls the Chapter 17 model and does the two matrix–vector products.

Why bib_i can vanish when aia_i does not, and vice versa (Choset Problem 11.5). ai=(Mq′)ia_i = (Mq')_i is zero when the path's tangent is orthogonal, in the MM-metric's ii-th row, to joint ii — the RP arm's prismatic joint at the midpoint of a straight Cartesian line, where the radial velocity is momentarily zero. bib_i collects Mq′′M q'' and the centrifugal/Coriolis terms, which can vanish on a straight segment (q′′=0q'' = 0) with no Coriolis coupling even where ai≠0a_i \ne 0. The micro-example below has b1=0b_1 = 0 and a1=πa_1 = \pi at every ss; Choset's RP arm has a2(12)=0a_2(\tfrac12) = 0 and b2≡0b_2 \equiv 0.

The micro-example. Reach from the Chapter 17 micro-example (Li=1L_i = 1, ri=12r_i = \tfrac12, m1=2m_1 = 2, m2=1m_2 = 1, I1=16I_1 = \tfrac16, I2=112I_2 = \tfrac1{12}) on a horizontal Workbench (ag=0a_g = 0), joint 2 held at π/2\pi/2 by its own motor while joint 1 sweeps a quarter turn: q(s)=(πs/2, π/2)q(s) = (\pi s/2,\ \pi/2), so q′=(π/2,0)q' = (\pi/2, 0), q′′=0q'' = 0, and MM is constant along the path with M=[21/31/31/3]M = \big[\begin{smallmatrix} 2 & 1/3 \\ 1/3 & 1/3 \end{smallmatrix}\big] from Chapter 17. Then

a(s)=Mq′=(π, π6)=(3.1416, 0.5236),b(s)=(Γ111, Γ112) π24=(0, 0.5⋅2.4674)=(0, 1.2337),c(s)=0.a(s) = M q' = \big(\pi,\ \tfrac{\pi}{6}\big) = (3.1416,\ 0.5236),\qquad b(s) = \big(\Gamma^1_{11},\ \Gamma^2_{11}\big)\,\tfrac{\pi^2}{4} = (0,\ 0.5 \cdot 2.4674) = (0,\ 1.2337),\qquad c(s) = 0 .

Joint 2's b2=h (q1′)2b_2 = h\,(q_1')^2 is the centrifugal torque the elbow motor must supply to keep the elbow at π/2\pi/2 while the shoulder swings — the coupling that will set the speed limit.

Acceleration bounds and the velocity-limit curve

DerivationFrom torque limits to a cone, and from the cone to the curve

Step 1 — isolate s¨\ddot s in each row. Row ii of umin⁡≤as¨+bs˙2+c≤umax⁡u^{\min} \le a\ddot s + b\dot s^2 + c \le u^{\max} reads uimin⁡−bis˙2−ci≤ais¨≤uimax⁡−bis˙2−ciu_i^{\min} - b_i\dot s^2 - c_i \le a_i\ddot s \le u_i^{\max} - b_i\dot s^2 - c_i.

Step 2 — divide, and flip if negative. Dividing by ai>0a_i > 0 gives βi≤s¨≤αi\beta_i \le \ddot s \le \alpha_i. Dividing by ai<0a_i < 0 reverses the inequality: αi≤s¨≤βi\alpha_i \le \ddot s \le \beta_i. That swap is the whole content of eq. 11.8, and it is why the sign of aia_i matters: a joint whose coordinate decreases along the path has its "maximum torque" bound on the low side of s¨\ddot s.

Step 3 — intersect the intervals. s¨\ddot s must satisfy all nn rows, so it lies in [max⁡iLi,min⁡iUi]=[L,U][\max_i L_i, \min_i U_i] = [L, U]. Each LiL_i and UiU_i is an affine function of x=s˙2x = \dot s^2 at fixed ss: Li=pi+mixL_i = p_i + m_i x with mi=−bi/aim_i = -b_i/a_i. The max of affine functions is convex and piecewise affine; the min is concave. Neither is smooth, so neither is vv.

Step 4 — the boundary of admissibility. The cone is empty when L>UL > U, i.e. when some Li>UjL_i > U_j. At fixed ss, as s˙\dot s grows from zero, the first value at which some pair crosses, Li(s˙)=Uj(s˙)L_i(\dot s) = U_j(\dot s), is where admissibility is lost. Because each side is affine in xx, the crossing is in closed form, xij=(pjU−piL)/(miL−mjU)x_{ij} = (p^U_j - p^L_i)/(m^L_i - m^U_j), and v(s)=min⁡v(s) = \min over pairs of xij\sqrt{x_{ij}} taken over positive roots. The minimum rule is exact rather than heuristic: if any Li=UjL_i = U_j then L≥Li=Uj≥UL \ge L_i = U_j \ge U, so the state is already on or past the boundary.

What the closed form assumes (Choset footnote 2). That s˙=0\dot s = 0 is admissible for every ss — the robot can hold any configuration statically — and that admissibility is lost at most once as s˙\dot s grows. Friction, weak actuators, or velocity-dependent limits can violate this and create inadmissible islands in the plane. The algorithm below ignores islands; the library reports the first assumption's failure as an error rather than guessing.

A check that is not a frozen number. velocityLimit is compared against a brute-force scan of L≤UL \le U with bisection on 150150 seeded random (a,b,c)(a, b, c) rows in two and three dimensions; the worst disagreement is 4×10−144 \times 10^{-14}.

Back to the micro-example, limits ∣τ1∣≤20|\tau_1| \le 20, ∣τ2∣≤12|\tau_2| \le 12 N·m. Joint 1: U1=20/π=6.366U_1 = 20/\pi = 6.366, L1=−6.366L_1 = -6.366, independent of s˙\dot s because b1=c1=0b_1 = c_1 = 0. Joint 2: U2=(12−1.2337s˙2)/0.5236=22.918−2.3562s˙2U_2 = (12 - 1.2337\dot s^2)/0.5236 = 22.918 - 2.3562\dot s^2 and L2=−22.918−2.3562s˙2L_2 = -22.918 - 2.3562\dot s^2. The only pair that can collapse is L1=U2L_1 = U_2: −6.366=22.918−2.3562s˙2-6.366 = 22.918 - 2.3562\dot s^2, so s˙2=12.429\dot s^2 = 12.429 and

v(s)=3.5254for every sv(s) = 3.5254 \quad\text{for every } s

— a joint-1 speed of 5.545.54 rad/s. Faster than this, the elbow motor cannot supply the centrifugal torque hq˙12h\dot q_1^2 while the shoulder brakes at full torque. Lower τ2max⁡\tau_2^{\max} to 1010 N·m and vv drops to 3.28753.2875; the library pins both.

Time optimality is bang-bang

DerivationWhy the optimum rides the cone boundary

Step 1 — higher is faster. For two admissible curves s˙a(s)≥s˙b(s)\dot s_a(s) \ge \dot s_b(s) on [0,1][0, 1], ∫ds/s˙a≤∫ds/s˙b\int ds/\dot s_a \le \int ds/\dot s_b. Time is monotone in the curve's height, so the optimum is the pointwise-highest admissible curve.

Step 2 — a tangent strictly inside the cone is wasteful. If at some state the curve's slope ds˙/ds=s¨/s˙d\dot s/ds = \ddot s/\dot s lies strictly between L/s˙L/\dot s and U/s˙U/\dot s, one can raise the curve locally — accelerate harder before, brake harder after — without leaving admissibility, and the new curve is higher. So on an optimal curve the tangent is on the boundary almost everywhere.

Step 3 — the reachable set is bounded by two integral curves. From any state, the set of all curves with tangents inside the cones is bounded above by the UU-integral curve and below by the LL-integral curve (Choset's figure 11.2). The optimum is therefore a concatenation of pieces of UU- and LL-curves.

Step 4 — where the switches are. The pieces meet where a UU-curve crosses an LL-curve, or where the limit curve forces a change: a UU-curve that would penetrate the amber must have been abandoned earlier for an LL-curve that grazes the limit tangentially. Finding those points is the algorithm.

The Pontryagin reading. Chapter 19 derives the same result from the minimum principle: for a minimum-time problem the Hamiltonian is affine in the control, ∂H/∂u\partial H/\partial u contains no uu, and HH is minimized at a bound of the control set. Choset's §11.3.1 makes the connection explicit for the double integrator, where the switch is at ts=d/umax⁡t_s = \sqrt{d/u_{\max}} — the same number the lattice reproduces below.

The micro-example never touches its limit curve. With constant U1=6.366U_1 = 6.366 and L1=−6.366L_1 = -6.366 the A0A_0 curve is x=s˙2=2⋅6.366 sx = \dot s^2 = 2 \cdot 6.366\,s and FF is x=2⋅6.366 (1−s)x = 2 \cdot 6.366\,(1 - s); they cross at s=12s = \tfrac12 with s˙=6.366=2.5231<3.5254\dot s = \sqrt{6.366} = 2.5231 < 3.5254. One switch, at

ts=2⋅0.5/6.366=0.3963 s,tf=2ts=0.7927 s.t_s = \sqrt{2 \cdot 0.5 / 6.366} = 0.3963\ \text{s},\qquad t_f = 2 t_s = 0.7927\ \text{s}.

Joint 2 is honest at the peak: during acceleration τ2=a2U1+b2s˙2=0.5236⋅6.366+1.2337⋅6.366=11.19<12\tau_2 = a_2 U_1 + b_2\dot s^2 = 0.5236 \cdot 6.366 + 1.2337 \cdot 6.366 = 11.19 < 12 N·m. That is Choset's Example 11.2.1 situation — the limit curve never reached, one switch — on this book's arm. Turn gravity on (ag=9.81a_g = 9.81) and at s=12s = \tfrac12 the gravity vector is c=(10.405,−3.468)c = (10.405, -3.468), giving U1=3.054U_1 = 3.054, L1=−9.678L_1 = -9.678 and v(12)=4.080v(\tfrac12) = 4.080; at s=0s = 0 the shoulder's gravity torque is c1=19.62c_1 = 19.62 N·m against a 2020 N·m limit, so U1(0)=0.1210U_1(0) = 0.1210 — the arm can barely lift, and the whole motion takes 2.32762.3276 s with its single switch pushed out to s=0.7342s = 0.7342.

Zero-inertia points and singular arcs

DerivationSliding along the limit curve

Step 1 — the row loses its s¨\ddot s. With ai=0a_i = 0, row ii of eq. 11.7 reads uimin⁡≤bis˙2+ci≤uimax⁡u_i^{\min} \le b_i\dot s^2 + c_i \le u_i^{\max}. Nothing the actuator does changes s¨\ddot s; the constraint is on s˙\dot s alone. Solving for s˙\dot s gives s˙zipmax⁡\dot s^{\max}_{zip} (eq. 11.11). If bi=0b_i = 0 too, the row constrains nothing — provided the robot can hold the configuration, which is Choset's Problem 11.6 and the standing assumption.

Step 2 — a critical point is not a collapsed cone. On v(s)v(s) the cone is a single vector, L=UL = U. At a point of s˙max⁡\dot s^{\max} set by a zero-inertia bound, L<UL < U: the cone is open, but it may point out of the admissible region.

Step 3 — clip to the tangent. Following UU forward from a singular point penetrates the limit curve; the fastest admissible acceleration is the one that keeps the state on the curve, which is the curve's tangent in the (s,s˙)(s, \dot s) plane: ds˙/dt=s¨=s˙ ds˙max⁡/dsd\dot s/dt = \ddot s = \dot s\,d\dot s^{\max}/ds. Hence s¨max⁡=min⁡(s¨tangent+,U)\ddot s_{\max} = \min(\ddot s^{+}_{tangent}, U), with the right-hand derivative where the curve has a corner, and symmetrically s¨min⁡=max⁡(s¨tangent−,L)\ddot s_{\min} = \max(\ddot s^{-}_{tangent}, L) for the backward integration.

Step 4 — singular arcs. Critical points lie where M(q)M(q) loses rank along the path's tangent, a lower-dimensional set; a path crossing it transversally has isolated critical points, a path moving along it has a critical arc. Integrating s¨max⁡\ddot s_{\max} and s¨min⁡\ddot s_{\min} instead of UU and LL lets the algorithm slide along such an arc with an acceleration strictly between LL and UU, rather than chattering between them — and on a slide neither actuator is saturated.

In x=s˙2x = \dot s^2 the rule is a one-liner. The library integrates in xx, where a stage from sks_k to sk+1s_{k+1} with constant s¨\ddot s moves xx by 2s¨ Δ2\ddot s\,\Delta. When the free step would land above xmax⁡(sk+1)x_{\max}(s_{k+1}), the stage acceleration that lands exactly on the curve is u∗=(xmax⁡−xk)/(2Δ)u^* = (x_{\max} - x_k)/(2\Delta); if u∗u^* is inside the cone the stage slides, otherwise it penetrates. At a critical point that is Choset's rule; at an ordinary point of vv the collapsed cone makes the test pass only when the integral curve is tangent to the limit — which is the tangent-point condition of step 4 of the algorithm, found for free.

Choset's RP arm, honestly. Example 11.2.1 runs the RP arm of Chapter 17's Example 10.1.2 (m1=5m_1 = 5, I1=0.1I_1 = 0.1, r1=0.2r_1 = 0.2, m2=3m_2 = 3, I2=0.05I_2 = 0.05, ag=9.8a_g = 9.8) along the straight Cartesian line x(s)=(2s−1,1)x(s) = (2s - 1, 1) with limits ±20\pm 20 N·m and ±40\pm 40 N. The path has a2(12)=0a_2(\tfrac12) = 0 with b2≡0b_2 \equiv 0 — a zero-inertia point that imposes no velocity bound, the case of Problem 11.5. Choset reports a minimum time of approximately 0.8880.888 s with one switch. With the printed parameters, however, the gravity torque on joint 1 at either end of the path is ag(m1r1+m22)cos⁡(π/4)=36.33a_g(m_1 r_1 + m_2\sqrt 2)\cos(\pi/4) = 36.33 N·m against a 2020 N·m limit, so U(0,0)=−2.572U(0, 0) = -2.572 and L(1,0)=+2.572L(1, 0) = +2.572: the arm can neither start forward from rest nor come to rest at the end, and both algorithms in this chapter report that the rest-to-rest problem has no solution — Choset's Problems 11.3 and 11.4 in the flesh. The 0.8880.888 s figure is not reproducible from the printed parameters. The library's cross-check therefore asserts the diagnosis, and reports the gravity-free execution: one switch at s=12s = \tfrac12 and tf=1.1445t_f = 1.1445 s, with TOPP-RA agreeing to the grid.

GRID SEARCH is optimal for its discretization

DerivationWhy the tree is a grid, and what the theorem buys

Step 1 — closure under the control set. Integrating q¨i=c amax⁡\ddot q_i = c\,a_{\max}, c∈{−1,0,1}c \in \{-1, 0, 1\}, for time hh from (qi,q˙i)(q_i, \dot q_i) gives q˙i+c amax⁡h\dot q_i + c\,a_{\max}h and qi+q˙ih+12c amax⁡h2q_i + \dot q_i h + \tfrac12 c\,a_{\max}h^2. Write q˙i=m amax⁡h\dot q_i = m\,a_{\max}h and qi=p⋅12amax⁡h2q_i = p\cdot\tfrac12 a_{\max}h^2 with integers m,pm, p; then the new indices are m+cm + c and p+2m+cp + 2m + c — integers again. Every node of Choset's figure 11.8 lies on the grid of figure 11.9, and the tree is really a graph on that grid.

Step 2 — level is time. Each edge takes exactly hh, so breadth-first expansion is uniform-cost search with unit edges, and the first level at which a node enters the goal region is the minimum number of steps: time-optimal for the chosen discretization of time and controls. A* with an admissible heuristic pops the same level with fewer expansions. The per-axis continuous bang-bang time max⁡iTi\max_i T_i is admissible because it ignores the other axes, the obstacles, the velocity limit and the discretization — every one of which can only make the lattice slower.

Step 3 — safety absorbs the discretization. Any continuous δv\delta_v-safe trajectory can be tracked by lattice moves within an error that shrinks with hh; the margin c0+c1∥q˙∥c_0 + c_1\|\dot q\| is spent on that error, leaving (1−ϵ)δv(1 - \epsilon)\delta_v. The two bounds on hh are what make the bookkeeping close — the first keeps vmax⁡v_{\max} an integer multiple of amax⁡ha_{\max}h, the second ties the tracking error to the margin.

Step 4 — the grid is finite, so the time is bounded. The configuration-space diameter, the velocity bound and hh fix the number of lattice points, which is O((1/ϵ)3n)O((1/\epsilon)^{3n}) once hh is expressed through ϵ\epsilon. Polynomial in ϵ\epsilon, exponential in nn: nobody has run this beyond a few degrees of freedom, and Choset says so.

What the theorem does not say. The (1−ϵ)δv(1 - \epsilon)\delta_v-safe trajectory the algorithm finds may look nothing like the true time-optimal δv\delta_v-safe one; the only promise is about its duration. And the proof — Donald and Xavier's — is beyond this chapter as it is beyond Choset's.

The manipulator generalization (Choset pp. 396–399). For M(q)q¨+Cq˙+g=uM(q)\ddot q + C\dot q + g = u with box torque limits, the feasible accelerations at a state form a parallelepiped A(q,q˙)\mathcal{A}(q, \dot q) that changes during the step and collapses where M−1M^{-1} loses rank. A bound on the largest eigenvalue of MM keeps it from collapsing; a bound on M˙\dot M gives a conservative subset A^⊂A\hat{\mathcal{A}} \subset \mathcal{A} feasible over the whole step; the controls are the q¨\ddot q grid points inside A^\hat{\mathcal{A}}. The completeness result survives with (1+ϵ)Topt(1 + \epsilon)T_{opt} in place of ToptT_{opt}; the running time stays exponential in nn.

The hand trace Choset's figure 11.9 invites: amax⁡=1a_{\max} = 1, h=12h = \tfrac12, from rest at 00 to rest at 11. The lattice has Δq˙=0.5\Delta\dot q = 0.5 and Δq=0.125\Delta q = 0.125; the goal is p=8p = 8. The search returns the four-level sequence (+1,+1,−1,−1)(+1, +1, -1, -1) — velocities 0.5,1.0,0.5,00.5, 1.0, 0.5, 0 and positions 0.125,0.5,0.875,1.00.125, 0.5, 0.875, 1.0 — in tf=2t_f = 2 s, which is the continuous 2d/amax⁡2\sqrt{d/a_{\max}} exactly, and the 8181 control sequences of length four collapse to 4949 distinct lattice states. For the widget's default d=2d = 2 the continuous bound is 22=2.8282\sqrt 2 = 2.828 s; the lattice returns 33 s at h∈{1,12,14}h \in \{1, \tfrac12, \tfrac14\} and 2.8752.875 s at h=18h = \tfrac18 (23 levels). Finer is never slower, and never faster than the bound.

TOPP-RA: reachability on squared speed

DerivationInterval propagation instead of switch hunting

Step 1 — the substitution. ddss˙2=2s˙ ds˙ds=2s¨\tfrac{d}{ds}\dot s^2 = 2\dot s\,\tfrac{d\dot s}{ds} = 2\ddot s. In xx the kinematics are linear and a constant-s¨\ddot s stage is a straight line; the micro-example's switch at s=12s = \tfrac12 lands on the grid exactly, and s˙=0\dot s = 0 is no longer a singularity of the integration.

Step 2 — convexity of the stage constraint set. At fixed sks_k the 2n2n inequalities in (xk,uk)(x_k, u_k) are half-planes, and xk+1=xk+2ukΔx_{k+1} = x_k + 2u_k\Delta is affine. The feasible set is a convex polygon; its projection onto the xkx_k axis is an interval.

Step 3 — propagate backward. KN=[xf,xf]K_N = [x_f, x_f]. Given Kk+1=[ℓ,h]K_{k+1} = [\ell, h], add the two half-planes ℓ≤xk+2ukΔ≤h\ell \le x_k + 2u_k\Delta \le h and take the minimum and maximum of xkx_k over the polygon — two two-variable LPs, solved exactly by vertex enumeration since there are at most 2n+62n + 6 half-planes. The result is KkK_k, the set of states at sks_k from which the goal is still reachable: the complement of Choset's "doomed" states, computed without ever drawing a curve.

Step 4 — greedy forward is optimal. A larger xkx_k never shrinks the reachable set downstream (controllability is monotone in xx under these constraints), so choosing the largest admissible xk+1∈Kk+1x_{k+1} \in K_{k+1} at each stage dominates every other choice pointwise, and pointwise-highest is fastest by the first derivation.

What it sidesteps. No tangent points, no root finding, no footnote-3 bisection, no special case for zero-inertia rows — a row with ai=0a_i = 0 is just a half-plane with no uu in it. The price is a grid: the answer is optimal for the discretization, and the stage constraints are enforced at sks_k only. On the chapter's seeded gravity-loaded splines at N=800N = 800 the two constructions agree on tft_f to 0.38%0.38\% at worst, and they agree on which problems have no solution at all.

The algorithm

Choset's five steps, with the §11.2.1 modification and the footnote-3 bisection that this chapter's implementation uses in step 4. The construction is drawn live in w18.1: FF dashed, each AiA_i solid, the hit points in amber.

Algorithmtime_scale(a, b, c, u_min, u_max, ṡ₀, ṡ_f)CostO(N) stages per curve; O(N log(1/tol)) per switch for the bisection
In
path-constrained dynamics on a grid s_k = k/N, actuator limits, boundary speeds
Out
switch list 𝒮, the profile (s_k, ṡ_k), t_f — or StallsAtZero / NoSolution / LimitIsland
  1. S←{}\mathcal{S} \leftarrow \{\}, i←0i \leftarrow 0, (si,s˙i)←(0,s˙0)(s_i, \dot s_i) \leftarrow (0, \dot s_0). Tabulate s˙max⁡(sk)=min⁡(v,s˙zipmax⁡,s˙ratemax⁡)\dot s^{\max}(s_k) = \min(v, \dot s^{\max}_{zip}, \dot s^{\max}_{rate}) and fail with NoSolution if any s˙max⁡(sk)≤0\dot s^{\max}(s_k) \le 0 (the robot cannot hold that configuration).
  2. Backward curve FF. Integrate s¨=L\ddot s = L backward from (1,s˙f)(1, \dot s_f) until the limit curve is penetrated (record the hit), s=0s = 0, or s˙=0\dot s = 0 at s<1s < 1 — in which case return StallsAtZero (Problem 11.3). A stage that would penetrate but can land on the curve with s¨min⁡=max⁡(s¨tangent−,L)\ddot s_{\min} = \max(\ddot s^{-}_{tangent}, L) slides instead.
  3. Forward curve AiA_i. Integrate s¨=U\ddot s = U forward from (si,s˙i)(s_i, \dot s_i), sliding with s¨max⁡=min⁡(s¨tangent+,U)\ddot s_{\max} = \min(\ddot s^{+}_{tangent}, U) where allowed, until it crosses FF — append the crossing ss to S\mathcal{S} and return — or penetrates the limit curve at (slim,s˙lim)(s_{lim}, \dot s_{lim}). If it reaches s=1s = 1 below s˙f\dot s_f or reaches s˙=0\dot s = 0, return NoSolution (Problem 11.4).
  4. Tangent point (footnote 3). On the line s=slims = s_{lim} bisect on s˙′∈[0,s˙lim]\dot s' \in [0, \dot s_{lim}] for the highest LL-curve from (slim,s˙′)(s_{lim}, \dot s') that does not penetrate the limit curve; its point of closest approach is (stan,s˙tan)(s_{tan}, \dot s_{tan}) — a tangent point, or a critical point. Integrate s¨=L\ddot s = L backward from (slim,s˙′)(s_{lim}, \dot s') until it rises above AiA_i; the crossing si+1s_{i+1} is a max→min switch — append it. If it reaches s˙=0\dot s = 0 or passes below the start, return NoSolution: the start state is doomed (figure 11.2).
  5. (si+2,s˙i+2)←(stan,s˙tan)(s_{i+2}, \dot s_{i+2}) \leftarrow (s_{tan}, \dot s_{tan}) is a min→max switch — append it, set i←i+2i \leftarrow i + 2, go to 3. A min→max switch immediately followed by a max→min at the same node is a slide, not a switch, and is removed from S\mathcal{S}.

Two implementation choices matter. The curves are integrated in x=s˙2x = \dot s^2 by RK4 on the uniform grid, with the stage states clamped to the admissible region at the half-grid nodes — next to a zero-inertia point a stage that lands a hair above the limit would otherwise read a bound ni/ain_i/a_i with ai≈0a_i \approx 0 and drive xx negative. And the bisection in step 4 replaces the slope test dv/ds=U/vdv/ds = U/v of Choset's main text: it never selects a tangent point whose own deceleration curve is doomed, which the slope test can, and it costs about 4848 extra LL-curve integrations per switch.

Algorithmtoppra(a, b, c, u_min, u_max, grid)CostO(N) two-variable LPs backward, O(N) forward
In
path-constrained dynamics tabulated at s_0 < … < s_N, limits, ṡ₀, ṡ_f
Out
controllable sets K_k, the greedy profile x_k, t_f — or the first k at which K_k is empty
  1. KN←[s˙f2,s˙f2]K_N \leftarrow [\dot s_f^2, \dot s_f^2].
  2. for k=N−1k = N-1 down to 00: half-planes uimin⁡≤aiu+bix+ci≤uimax⁡u_i^{\min} \le a_i u + b_i x + c_i \le u_i^{\max} at sks_k, plus x≥0x \ge 0 and Kk+1lo≤x+2uΔk≤Kk+1hiK_{k+1}^{lo} \le x + 2u\Delta_k \le K_{k+1}^{hi}; Kk←[min⁡x,max⁡x]K_k \leftarrow [\min x, \max x] over the polygon's vertices. If empty, report no solution from sks_k.
  3. if s˙02∉K0\dot s_0^2 \notin K_0: no solution (the start is doomed).
  4. for k=0k = 0 to N−1N-1: uk←u_k \leftarrow the largest uu feasible at (sk,xk)(s_k, x_k) with xk+2uΔk∈Kk+1x_k + 2u\Delta_k \in K_{k+1}; xk+1←xk+2ukΔkx_{k+1} \leftarrow x_k + 2u_k\Delta_k.
  5. tf←∑k2Δk/(xk+xk+1)t_f \leftarrow \sum_k 2\Delta_k/(\sqrt{x_k} + \sqrt{x_{k+1}}).
AlgorithmGRID SEARCH((q*_start, q̇*_start), G, 𝒜, h) — Choset Algorithm 21CostO((1/ε)^{3n}) — Theorem 11.3.2
In
lattice start state, goal region, control set {−a_max, 0, a_max}ⁿ, timestep
Out
a piecewise-constant-acceleration trajectory to G, or FAILURE
  1. place the start at the root of TT (level 0); level←0level \leftarrow 0, solved←solved \leftarrow false
  2. while not solvedsolved: if level levellevel is empty return FAILURE
  3. for each node in level levellevel, for each control in A\mathcal{A}: integrate the control for time hh, getting newnodenewnode
  4. if newnodenewnode is new, and the arc neither passes within c0+c1∥q˙∥c_0 + c_1\|\dot q\| of an obstacle nor exceeds vmax⁡v_{\max}: add it as a child
  5. if the arc enters GG: solved←solved \leftarrow true, store newnodenewnode
  6. level←level+1level \leftarrow level + 1
  7. return the stored trajectory that reaches GG first
Algorithmlattice_search(start, goal, a_max, v_max, h, safe) — oursCostsame worst case; in practice 10–700× fewer expansions than breadth-first on the chapter's instances
In
the same, as integer lattice coordinates (p, m) with q = p·½a_max h², q̇ = m·a_max h
Out
the same trajectory, with a count of expansions
  1. graph: node (p,m)(p, m); neighbors (p+2m+c, m+c)(p + 2m + c,\ m + c) for c∈{−1,0,1}nc \in \{-1, 0, 1\}^n, kept if the quadratic arc is safe and ∣(m+c) amax⁡h∣≤vmax⁡|(m + c)\,a_{\max}h| \le v_{\max}; every node inside GG shares one key
  2. heuristic h(p,m)←max⁡iTih(p, m) \leftarrow \max_i T_i, the per-axis continuous bang-bang time to the goal — admissible and consistent
  3. Chapter 6's BestFirst with f=g+hf = g + h; breadth-first is the same engine with f=f = link length
  4. return the back-pointer path; controls are Δm\Delta m between consecutive states

Implementation in Rust

The trajectory crate is introduced here and imported by Chapters 19, 21 and 23. Its surface is a Path trait, the path-constrained dynamics, the limits, two time scalers, the lattice, and one type — Trajectory — whose whole job is to be a different type from Path.

crates/trajectory/src/path.rs
use nalgebra::SVector;

/// A twice-differentiable curve q : [0, 1] → Q. `ddq` must be *continuous*: the
/// velocity-product term b(s) contains M q'', and a C¹ spline would make it jump
/// at every knot — the phase plane would grow a fence of fake zero-inertia points.
pub trait Path<const N: usize> {
    fn q(&self, s: f64) -> SVector<f64, N>;
    fn dq(&self, s: f64) -> SVector<f64, N>;
    fn ddq(&self, s: f64) -> SVector<f64, N>;
}

/// A straight line in the chart; ddq ≡ 0. The micro-example's path.
pub struct Segment<const N: usize> { pub a: SVector<f64, N>, pub b: SVector<f64, N> }

impl<const N: usize> Path<N> for Segment<N> {
    fn q(&self, s: f64) -> SVector<f64, N> { self.a + s * (self.b - self.a) }
    fn dq(&self, _s: f64) -> SVector<f64, N> { self.b - self.a }
    fn ddq(&self, _s: f64) -> SVector<f64, N> { SVector::zeros() }
}

/// Natural cubic spline through m ≥ 2 waypoints at uniform knots s_k = k/(m − 1).
/// Natural ends (q'' = 0) are the right choice for a rest-to-rest motion: b(s)
/// then vanishes at both ends and the plane's edges are governed by a and c alone.
pub struct CubicSpline<const N: usize> {
    knots: Vec<f64>,
    pts: Vec<SVector<f64, N>>,
    /// Second derivatives at the knots, from one tridiagonal solve per coordinate.
    m2: Vec<SVector<f64, N>>,
}

impl<const N: usize> CubicSpline<N> {
    /// Waypoints on Tⁿ are unwrapped first: consecutive angles differ by the
    /// shortest arc, so a path through the seam is fitted in the chart lift
    /// rather than interpolated across a 2π jump (Chapter 5).
    pub fn on_torus(waypoints: &[SVector<f64, N>]) -> Self {
        let mut pts = vec![waypoints[0]];
        for w in &waypoints[1..] {
            let prev = *pts.last().unwrap();
            pts.push(prev + (w - prev).map(|d| (d + PI).rem_euclid(2.0 * PI) - PI));
        }
        Self::fit(pts)
    }

    fn fit(pts: Vec<SVector<f64, N>>) -> Self {
        let m = pts.len();
        let knots: Vec<f64> = (0..m).map(|k| k as f64 / (m - 1) as f64).collect();
        // Interior rows: h M_{k-1} + 4h M_k + h M_{k+1} = 6 (d_k − d_{k-1}); M_0 = M_{m-1} = 0.
        let m2 = natural_second_derivatives(&knots, &pts);
        Self { knots, pts, m2 }
    }
}
crates/trajectory/src/constrained.rs + limits.rs
use dynamics::Dynamics;

/// a(s) s̈ + b(s) ṡ² + c(s) = u — Chapter 17's model restricted to a path (C eq. 11.6).
pub struct PathDynamics<'a, D: Dynamics<N>, P: Path<N>, const N: usize> {
    pub dynamics: &'a D,
    pub path: &'a P,
}

impl<D: Dynamics<N>, P: Path<N>, const N: usize> PathDynamics<'_, D, P, N> {
    /// b uses the velocity-product *vector* at q̇ = q'(s): q'ᵀ Γ q' = C(q, q') q' exactly.
    pub fn abc(&self, s: f64) -> (SVector<f64, N>, SVector<f64, N>, SVector<f64, N>) {
        let (q, dq, ddq) = (self.path.q(s), self.path.dq(s), self.path.ddq(s));
        let m = self.dynamics.mass(&q);
        (m * dq, m * ddq + self.dynamics.velocity_product(&q, &dq), self.dynamics.gravity(&q))
    }

    /// The torque that executes the path at phase state (s, ṡ) with path acceleration s̈.
    pub fn torque(&self, s: f64, sdot: f64, sddot: f64) -> SVector<f64, N> {
        let (a, b, c) = self.abc(s);
        a * sddot + b * sdot * sdot + c
    }
}

pub struct TorqueLimits<const N: usize> { pub min: SVector<f64, N>, pub max: SVector<f64, N> }

/// Below this |a_i| a row is zero-inertia: it bounds ṡ, not s̈.
pub const ZERO_INERTIA_EPS: f64 = 1e-9;

/// (L, U) at a phase-plane state — eqs. 11.8–11.9. Dividing by a negative a_i
/// flips the inequality; that swap is the whole content of 11.8.
pub fn accel_bounds<const N: usize>(abc: &Abc<N>, lim: &TorqueLimits<N>, sdot: f64) -> (f64, f64) {
    let x = sdot * sdot;
    let (mut lo, mut hi) = (f64::NEG_INFINITY, f64::INFINITY);
    for i in 0..N {
        let ai = abc.0[i];
        if ai.abs() < ZERO_INERTIA_EPS { continue; }
        let alpha = (lim.max[i] - abc.1[i] * x - abc.2[i]) / ai;
        let beta  = (lim.min[i] - abc.1[i] * x - abc.2[i]) / ai;
        let (li, ui) = if ai > 0.0 { (beta, alpha) } else { (alpha, beta) };
        lo = lo.max(li);
        hi = hi.min(ui);
    }
    (lo, hi)
}

/// v(s) from the pairwise crossings L_i = U_j (eq. 11.10). Each bound is affine
/// in x = ṡ², p + m x, so the crossing is x = (p_U − p_L)/(m_L − m_U); keep the
/// smallest positive root over all pairs. +∞ when no pair can collapse the cone.
pub fn velocity_limit<const N: usize>(abc: &Abc<N>, lim: &TorqueLimits<N>) -> f64 {
    let rows: Vec<Row> = (0..N).filter(|&i| abc.0[i].abs() >= ZERO_INERTIA_EPS).map(|i| Row::new(abc, lim, i)).collect();
    let mut best = f64::INFINITY;
    for li in &rows { for uj in &rows {
        let den = li.m_l - uj.m_u;
        if den.abs() < 1e-14 { continue; }
        let x = (uj.p_u - li.p_l) / den;
        if x > 0.0 && x < best { best = x; }
    } }
    best.sqrt()
}

/// ṡ_max(s) = min(v, ṡ_zip, ṡ_rate): the true limit curve of §11.2.1, with
/// joint-rate limits |q̇_i| ≤ v_i folded in as rows ṡ ≤ v_i / |q'_i| (Exercise 5).
pub fn sdot_max<const N: usize>(abc: &Abc<N>, dq: &SVector<f64, N>, lim: &TorqueLimits<N>, rate: Option<&[f64; N]>) -> LimitPoint {
    let v = velocity_limit(abc, lim);
    let zip = zero_inertia_limit(abc, lim);        // u_min ≤ b_i ṡ² + c_i ≤ u_max on rows with a_i = 0
    let rate = joint_rate_limit(dq, rate);
    LimitPoint::min_of(v, zip, rate)               // remembers which bound is active: cone, zip or rate
}
crates/trajectory/src/time_scale.rs
pub enum TimeScaleError {
    /// F reached ṡ = 0 before s = 0: full braking cannot bring the arm to rest (Problem 11.3).
    StallsAtZero { s: f64 },
    /// A_i reached ṡ = 0 or s = 1, or the start is doomed (Problem 11.4, figure 11.2).
    NoSolution { s: f64, detail: String },
    /// More switches than the admissible region can honestly have (footnote 2).
    LimitIsland { s: f64 },
}

pub struct TimeScaling {
    pub switches: Vec<f64>,          // Choset's list 𝒮
    pub sdot: Vec<f64>,              // the profile on s_k = k/N
    pub stage: Vec<f64>,             // s̈ on each stage: x(s) is piecewise linear, so a replay applies exactly this
    pub mode: Vec<PhaseMode>,        // Max | Min | Slide — Slide is a singular arc
    pub t: Vec<f64>,                 // t[N] = t_f
}

/// One grid stage in x = ṡ². When the free step leaves the admissible region,
/// try the stage acceleration that lands exactly on the limit curve; it is
/// feasible iff it lies in the cone — Choset's s̈_max = min(s̈⁺_tangent, U) at a
/// critical point, and the tangency test for free on an ordinary point of v(s).
fn step_clipped(t: &Tables, k: usize, x: f64, mode: Mode, dir: i32) -> StepOut {
    let raw = rk4_in_x(t, k, x, mode, dir);              // stage states clamped to x ≤ x_max at the half grid
    let kn = (k as i32 + dir) as usize;
    if !above(raw, t.xmax[kn]) { return StepOut::free(raw); }
    let u_star = dir as f64 * (t.xmax[kn] - x) / (2.0 * t.ds);
    let (l0, u0) = accel_bounds(&t.abc[k], &t.lim, x.sqrt());
    let (l1, u1) = accel_bounds(&t.abc[kn], &t.lim, t.xmax[kn].sqrt());
    if u_star >= l0.min(l1) && u_star <= u0.max(u1) { StepOut::slide(t.xmax[kn]) } else { StepOut::penetrated(raw) }
}

pub fn time_scale<const N: usize>(pd: &PathDynamics<'_, impl Dynamics<N>, impl Path<N>, N>, lim: &TorqueLimits<N>,
                                  sdot0: f64, sdot_f: f64, grid: usize) -> Result<TimeScaling, TimeScaleError> {
    let t = Tables::tabulate(pd, lim, grid);
    // Step 2: F backward from (1, ṡ_f).
    let f = integrate_backward(&t, grid, sdot_f * sdot_f, Mode::Min)?;    // StallsAtZero if x ≤ 0 before s = 0
    let (mut ki, mut xi, mut switches, mut profile) = (0, sdot0 * sdot0, vec![], Profile::new(grid));
    loop {
        // Step 3: A_i forward with U; crossing F is tested on the free curve before penetration.
        match integrate_forward(&t, ki, xi, Mode::Max, &f)? {
            Forward::CrossedF { node, frac, a } => { profile.write(ki, node, &a, Mode::Max); profile.write_tail(&f);
                                                   switches.push_crossing(node, frac, grid); break; }
            Forward::Penetrated { k_lim, a, .. } => {
                // Step 4, footnote 3: bisect on ṡ′ at s_lim for the highest admissible L-curve.
                let (x_prime, lc) = bisect_highest_admissible_l_curve(&t, k_lim, a[k_lim], &f);
                let k_tan = lc.closest_approach_to_limit();                        // tangent or critical point
                let meet = meet_backward(&t, &a, ki, k_lim, x_prime)?;            // where L rises above A_i
                profile.write(ki, meet.node, &a, Mode::Max);
                profile.write(meet.node + 1, k_tan, &lc, Mode::Min);
                switches.push_max_to_min(meet); switches.push_min_to_max(k_tan);  // Step 5
                (ki, xi) = (k_tan, lc[k_tan]);
            }
        }
    }
    Ok(profile.finish(switches))     // t_f = Σ 2Δ/(ṡ_k + ṡ_{k+1}); slides reported as Mode::Slide
}
crates/trajectory/src/toppra.rs
/// TOPP-RA (Pham & Pham 2018): controllable intervals in x = ṡ² backward, greedy forward.
pub fn toppra<const N: usize>(pd: &PathDynamics<'_, impl Dynamics<N>, impl Path<N>, N>, lim: &TorqueLimits<N>,
                              sdot0: f64, sdot_f: f64, grid: usize) -> Result<TimeScaling, TimeScaleError> {
    let ds = 1.0 / grid as f64;
    let abc: Vec<Abc<N>> = (0..=grid).map(|k| pd.abc(k as f64 * ds)).collect();
    let mut k_set = vec![(sdot_f * sdot_f, sdot_f * sdot_f); grid + 1];
    for k in (0..grid).rev() {
        // Half-planes in (x, u): the 2N actuator rows at s_k, x ≥ 0, and ℓ ≤ x + 2uΔ ≤ h for K_{k+1}.
        let mut hp = actuator_half_planes(&abc[k], lim);     // a row with a_i = 0 is a half-plane with no u in it
        hp.push(HalfPlane { p: 1.0, r: 2.0 * ds, q: k_set[k + 1].1 });
        hp.push(HalfPlane { p: -1.0, r: -2.0 * ds, q: -k_set[k + 1].0 });
        k_set[k] = extreme_x(&hp).ok_or(TimeScaleError::NoSolution { s: k as f64 * ds, detail: "controllable set empty".into() })?;
    }
    if sdot0 * sdot0 < k_set[0].0 || sdot0 * sdot0 > k_set[0].1 { return Err(TimeScaleError::NoSolution { s: 0.0, detail: "ṡ₀ not controllable".into() }); }
    let mut x = vec![sdot0 * sdot0; grid + 1];
    for k in 0..grid {
        let (lo, hi) = feasible_u(&abc[k], lim, x[k]);                        // the cone at (s_k, x_k)
        let u = hi.min((k_set[k + 1].1 - x[k]) / (2.0 * ds)).max(lo.max((k_set[k + 1].0 - x[k]) / (2.0 * ds)));
        x[k + 1] = (x[k] + 2.0 * u * ds).max(0.0);                            // greedy: largest x_{k+1} ∈ K_{k+1}
    }
    Ok(TimeScaling::from_x(x, ds))
}
crates/trajectory/src/lattice.rs
use search::{astar, Graph};

/// Integer lattice coordinates: q_i = p_i · ½ a_max h², q̇_i = m_i · a_max h.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct LatticeState<const N: usize> { pub p: [i64; N], pub m: [i64; N] }

pub struct LatticeGraph<'a, const N: usize> {
    pub a_max: f64, pub h: f64, pub v_max: f64,
    pub c0: f64, pub c1: f64,
    /// Clearance at a configuration; the lattice samples it along every arc.
    pub clearance: &'a dyn Fn(&SVector<f64, N>) -> f64,
    pub goal: LatticeState<N>,
}

impl<const N: usize> Graph<LatticeState<N>> for LatticeGraph<'_, N> {
    /// Neighbors are (p + 2m + c, m + c) for c ∈ {−1, 0, 1}ᴺ, kept when the
    /// quadratic arc is δ_v(c₀, c₁)-safe and the end velocity is within v_max.
    fn neighbors(&self, s: &LatticeState<N>) -> Vec<(LatticeState<N>, f64)> {
        controls::<N>().filter_map(|c| {
            let next = LatticeState { p: array_fn(|i| s.p[i] + 2 * s.m[i] + c[i]), m: array_fn(|i| s.m[i] + c[i]) };
            self.arc_safe(s, &c).then_some((next, self.h))
        }).collect()
    }
}

/// Minimum time of a 1-D double integrator from (q₀, v₀) to (q₁, v₁): one switch,
/// via the peak velocity v_p with (v_p² − v₀²)/2a + (v_p² − v₁²)/2a = Δq. This is
/// also Chapter 19's min-time double integrator read off the minimum principle.
pub fn bang_bang_time_1d(q0: f64, v0: f64, q1: f64, v1: f64, a: f64) -> f64 { /* two candidate families */ }

/// Algorithm 21 with A* (h = max_i per-axis bang-bang time, admissible) or Choset's literal breadth-first.
pub fn lattice_search<const N: usize>(start: LatticeState<N>, goal: LatticeState<N>, g: &LatticeGraph<'_, N>) -> Option<Vec<[i64; N]>> {
    let h = |n: &LatticeState<N>| (0..N).map(|i| bang_bang_time_1d(q_of(n.p[i], g), v_of(n.m[i], g), q_of(goal.p[i], g), v_of(goal.m[i], g), g.a_max))
                                        .fold(0.0, f64::max);
    let path = astar(g, start, goal, h)?;
    Some(path.windows(2).map(|w| array_fn(|i| w[1].m[i] - w[0].m[i])).collect())   // controls are Δm
}

/// A time-parameterized curve — Choset §1.3's distinction made a type. Trackers
/// accept a `Trajectory`, never a `Path`: handing a geometric curve to a
/// controller is a compile error, not a runtime surprise.
pub struct Trajectory<M: Manifold> { pub samples: Vec<(f64, M)> }

The worked example, printed

cargo run --example reach_sweep -p trajectory builds the micro-example and prints

a(0.5) = [3.1416, 0.5236]    b(0.5) = [0.0000, 1.2337]    c(0.5) = [0.0000, 0.0000]
v(s)   = 3.5254  (constant; the pair L₁ = U₂)
switches = [0.5000]    t_s = 0.3963 s    t_f = 0.7927 s    peak ṡ = 2.5231
τ₂ at the switch = 11.19 N·m  (< 12: honest)
TOPP-RA, 200-point grid: t_f = 0.7927 s
gravity on:  c(0.5) = [10.4051, -3.4684]   U₁(0.5) = 3.0542   L₁(0.5) = -9.6782   v(0.5) = 4.0799
             U₁(0) = 0.1210   t_f = 2.3276 s   switches = [0.7342]
τ₂ = 10 N·m: v = 3.2875   switches = [0.5071]   t_f = 0.7928 s
RP arm (Choset Ex. 11.2.1, printed parameters): |c₁(0)| = 36.33 > 20  →  U(0,0) = -2.572, L(1,0) = +2.572
             time_scale: StallsAtZero   toppra: controllable set empty at s = 0.999
             a_g = 0:  one switch at 0.5000, t_f = 1.1445 s  (toppra 1.1445)
lattice 1-DOF, a = 1, h = 0.5, 0 → 1:  controls [+1, +1, -1, -1]  t_f = 2.000 = 2√(d/a)
             81 sequences → 49 states at level 4;  expansions A* 5 vs breadth-first 43
lattice 2-DOF around a disc, h = 0.5:  6 levels;  expansions A* 17 / Alg. 21 4769 / heap BFS 12866
             c₁ = 0.5: 8 levels;  h = 0.25: 12 → 14 levels (3.0 → 3.5 s), A* 494 expansions

#[test] fn reproduces_micro_example() asserts each line to 10−310^{-3} (the integrator's grid sets the tolerance) and ∣tfTOPP-RA−tf∣<5×10−3|t_f^{\text{TOPP-RA}} - t_f| < 5 \times 10^{-3} on a 200-point grid. #[test] fn lattice_finds_bang_bang() is the hand trace above. #[test] fn replay_tracks_path() closes the loop with Chapter 17: it time-scales the widgets' default spline (tf=0.6176t_f = 0.6176 s, switches at 0.3980.398, 0.5710.571, 0.6680.668), computes u(t)=as¨+bs˙2+cu(t) = a\ddot s + b\dot s^2 + c from the profile, integrates Reach2R with Chapter 17's RK4 at h=0.25h = 0.25 ms, and asserts that the integrated q(t)q(t) stays within 10−310^{-3} rad of q(s(t))q(s(t)) — the measured worst error is 4.5×10−44.5 \times 10^{-4} rad — with every sampled torque inside its limit to within the one-percent slop of the stage-constant s¨\ddot s. The arm hangs below the shoulder in that test on purpose: an open-loop replay through the inverted configuration diverges exponentially whatever the planner did, which is Chapter 19's cue for feedback.

The widgets on this page run the TypeScript port in lib/trajectory/, a line-for-line translation of the Rust above. Its thirteen self-checks reproduce every number in the printout and add the invariants the mathematics guarantees: the closed-form v(s)v(s) against a brute-force scan, an actuator saturated at every node of every solved profile, the profile never above s˙max⁡\dot s^{\max} and touching it only at switches or slides, x(s)x(s) continuous across every switch (the bug that check was written to catch jumped the profile by 1.21.2 in xx), TOPP-RA within 0.38%0.38\% of the switching construction on seeded gravity-loaded splines and agreeing with it on which problems are unsolvable, and tft_f non-increasing as the motors get stronger.

Putting it together

The integration lab is the pipeline Chapter 23 ships: a Chapter 12 RRT path for Reach, shortcut by Chapter 13 into a polyline, smoothed into a CubicSpline on T2T^2, time-scaled here, and replayed by the Chapter 17 integrator with the computed torques. On the Workbench with gravity the gauge grazes its rail at every instant and never crosses it — that is what bang-bang looks like from the motor's side.

Two honesty items the lab makes visible. First, the shortcutting step matters more than it looks: a polyline has corners, a spline through a polyline's vertices has large q′′q'' near sharp ones, and large q′′q'' is large b(s)b(s), which lowers the velocity-limit curve exactly where the path turns. The time scaler is only as fast as the path is smooth. Second, decoupled planning is honest about time only locally: it finds the fastest clock for the path it was handed, and a slightly different path through the same homotopy class may be much faster. Choset's §11.2.2 sketches Shiller and Dubowsky's answer — enumerate grid paths, bound their times from below with a speed cap, time-scale the best, prune the rest, and polish locally with the time scaler as the objective. Modern practice replaces that search with the optimizers of the next chapter, which move the path and the clock together. The lattice of §11.3.3 is the other direct route, and its exponential grid is the reason Chapter 21's state lattices and hybrid A* prune it with motion primitives and a heuristic rather than enumerate it.

What survives into Chapter 19 unchanged: the time-scaled trajectory is the warm start CHOMP and iLQR polish; the bang-bang structure reappears from Pontryagin's minimum principle; and the Trajectory type is what every controller there accepts.

Exercises

  1. Foundation exerciseDifficulty 1 of 3When is there no limit curve at all?

    Show that a single actuator with constant symmetric bounds ∣u∣≤umax⁡|u| \le u_{\max} and a(s)≠0a(s) \ne 0 has L<UL < U at every state, so no velocity-limit curve exists and the time-optimal clock is pure bang-bang with one switch. Then replace the bound by the DC-motor law umax⁡(s,s˙)=u0−ks˙u^{\max}(s, \dot s) = u_0 - k\dot s (torque falling with speed) and show that a limit curve appears. Where is it?

  2. Foundation exerciseDifficulty 2 of 3Gravity on the micro-example

    For the micro-example path with gravity on, derive c(s)c(s) in closed form from Chapter 17's g(q)g(q) and show that U1(0)=(20−19.62)/πU_1(0) = (20 - 19.62)/\pi. What torque limit on joint 1 would make s=0s = 0 inadmissible at rest, and what does that mean physically?

    U₁ at s = 0 with gravity on, in units of 1/s²

  3. Conceptual exerciseDifficulty 2 of 3Predict the switch
    Predict first

    In the Phase-Plane Racer switch on the micro-example preset (gravity off) and lower τ₂ from 12 to 10 N·m. Where does the single switch go, and does the trajectory now touch v(s)?

  4. Conceptual exerciseDifficulty 2 of 3Levels and detours on the lattice

    In the Lattice Hopper (1-DOF, amax⁡=1a_{\max} = 1, d=2d = 2) predict the level count and the lattice tft_f at h=14h = \tfrac14 before running it, then at h=18h = \tfrac18. Then switch to the 2-DOF room, set c1=0.5c_1 = 0.5 and explain the longer, slower trajectory in terms of Choset's figure 11.11.

    Lattice t_f at h = ¼ for d = 2, a_max = 1, in seconds

    s
  5. Practical exerciseDifficulty 2 of 3Joint-rate limits as rows of the limit curve

    Add joint-velocity limits ∣q˙i∣≤vmax⁡,i|\dot q_i| \le v_{\max,i} to limits.rs as additional rows of the velocity-limit computation. Since q˙i=qi′s˙\dot q_i = q'_i\dot s, each is the bound s˙≤vmax⁡,i/∣qi′(s)∣\dot s \le v_{\max,i}/|q'_i(s)|, a critical-type point (L<UL < U there). Show in a test that s˙max⁡(s)\dot s^{\max}(s) becomes min⁡(v(s),s˙zip,min⁡ivmax⁡,i/∣qi′(s)∣)\min(v(s), \dot s_{zip}, \min_i v_{\max,i}/|q_i'(s)|), and that the time scaler slides along the rate limit where it is active rather than bouncing. The TypeScript port's jointRateLimit and the 'rate' kind of LimitPoint are the reference.

  6. Practical exerciseDifficulty 3 of 3TOPP-RA versus the switching construction (stretch)

    Run toppra and time_scale on 5050 seeded natural cubic splines for Chapter 17's Reach3R with gravity, limits drawn from a fixed range, N=800N = 800. Report, honestly, the fraction on which both report no solution, the fraction on which exactly one does, and the distribution of ∣tfTOPP-RA−tf∣/tf|t_f^{\text{TOPP-RA}} - t_f|/t_f on the rest. Explain every disagreement larger than the grid resolution: a zero-inertia point the slope test mishandles, a stage constraint TOPP-RA enforces only at sks_k, or an inadmissible island (footnote 2) that neither algorithm is built for. The 2-DOF version of this experiment is the chapter's check: six solvable splines of eight, no disagreement on solvability, worst relative difference 3.8×10−33.8 \times 10^{-3}.

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 11 is the source: the path-constrained dynamics and the phase plane (§11.2), the Time-Scaling Algorithm and its footnote-3 bisection, zero-inertia points (§11.2.1), Example 11.2.1 (whose printed parameters this chapter re-examines), the Shiller–Dubowsky sketch (§11.2.2), and GRID SEARCH with Theorem 11.3.2 (§11.3.3). Choset's bibliography is where Shin & McKay [385], Pfeiffer & Johanni [348], Slotine & Yang [388] and Donald & Xavier [135] are cited.

  2. Bobrow, J. E., Dubowsky, S., and Gibson, J. S. (1985) Time-Optimal Control of Robotic Manipulators Along Specified Paths. International Journal of Robotics Research 4(3).doi:10.1177/027836498500400301 (opens in a new tab)

    One of the two 1985 papers that solved the decoupled problem; the phase-plane construction with its switching curves is theirs (Shin & McKay's companion paper in IEEE Transactions on Automatic Control 30(6) reached the same result independently).

  3. Pham, H. and Pham, Q.-C. (2018) A New Approach to Time-Optimal Path Parameterization Based on Reachability Analysis. IEEE Transactions on Robotics 34(3).doi:10.1109/TRO.2018.2819195 (opens in a new tab)

    TOPP-RA: controllable sets in squared speed propagated by small linear programs, then a greedy forward pass. The reformulation that replaces switch-point hunting, implemented here with exact two-variable LPs and checked against the switching construction.

  4. Pivtoraiko, M., Knepper, R. A., and Kelly, A. (2009) Differentially Constrained Mobile Robot Motion Planning in State Lattices. Journal of Field Robotics 26(3).doi:10.1002/rob.20285 (opens in a new tab)

    The state lattice as the descendant of GRID SEARCH: motion primitives on a regular lattice of states, searched with a heuristic. Chapter 21 develops the lineage.

  5. Dolgov, D., Thrun, S., Montemerlo, M., and Diebel, J. (2010) Path Planning for Autonomous Vehicles in Unknown Semi-Structured Environments. International Journal of Robotics Research 29(5).doi:10.1177/0278364909359210 (opens in a new tab)

    Hybrid A*: the practical end of the GRID SEARCH → state lattice line, with continuous states attached to discrete cells. Cited here for the lineage; Chapter 21 implements it.

  6. LaValle, S. M. and Kuffner, J. J. (2001) Randomized Kinodynamic Planning. International Journal of Robotics Research 20(5).doi:10.1177/02783640122067453 (opens in a new tab)

    Choset's 'third approach' to direct trajectory planning: trade the lattice's optimality for probabilistic completeness in (q, q̇). Chapter 21's kinodynamic trees use the same state.

  7. Lynch, K. M. and Park, F. C. (2017) Modern Robotics: Mechanics, Planning, and Control. Cambridge University Press.link to Modern Robotics: Mechanics, Planning, and Control (opens in a new tab)

    Section 9.4 presents the same time-scaling algorithm, by one of Choset's co-authors, with the (s, ṡ) phase plane drawn the way w18.1 draws it.