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.
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 -joint arm with torque limits is a one-dimensional system in the path parameter , and its time-optimal execution is a curve in the 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 , run here on Chapter 6's A*. It is time-optimal for its own discretization, complete in a precise -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 s. The left arm runs a uniform clock: accelerate briefly, cruise at constant , brake briefly. The right arm runs the clock this chapter computes.
Nothing about the curve differs between the two lanes. What differs is : 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 . A time scaling is a twice-differentiable, monotone map with on . A trajectory is their composition . Uniform time scalings 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 on the horizontal axis and on the vertical. A time scaling is a curve from to that never dips below . At every state the torque limits allow a range of path accelerations — 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 .
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, , is a torque the other joint's motor must supply, and above some 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 ; the dashed blue curve is integrated backward from the goal at the minimum acceleration . 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: , so area under the curve is time saved.
Zero-inertia points are where the picture changes shape. The vertical dashed amber lines mark values where some changes sign: that actuator momentarily cannot change at all, and instead bounds 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 on the default spline and a slide appears near .
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 and the amber stretch is a singular arc: the profile slides along the velocity-limit curve with an acceleration strictly between and , and neither motor is saturated. Choset's §11.2.1 derives exactly when that happens and what acceleration to use.
| Symbol | Meaning | Note |
|---|---|---|
| Path parameter; the time scaling s : [0, t_f] → [0, 1] and its rates. | Choset §11.1 | |
| The path and its derivatives in s. q'' must be continuous — b(s) contains it. | ||
| Inertial, velocity-product and gravity vectors of the path-constrained dynamics a s̈ + b ṡ² + c = u (eq. 11.6). | ||
| Actuator limits, possibly state dependent (eq. 11.1). Symmetric constant bounds |u_i| ≤ u_i^max are the default here. | ||
| Per-actuator acceleration bounds (eq. 11.8), their assignment by the sign of a_i, and the max/min over i (eq. 11.9). | ||
| 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). | ||
| The backward minimum-acceleration curve; the forward curves of the construction; the switch list. | ||
| Where A_i penetrates the limit curve; the tangent point found from it; the left and right tangent accelerations at a singular point. | ||
| Squared path speed — the variable in which every actuator limit is linear (TOPP-RA). | Pham & Pham 2018 | |
| 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 | |
| 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: twice differentiable; twice differentiable and monotone with on the open interval; the trajectory is . Twice differentiability of is what makes exist and be bounded, which is what makes torque finite.
The motion cone at is the set of tangent directions of curves through that state whose path acceleration satisfies . The state is inadmissible when : 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 is the locus where — the cone collapses to one vector. It is computed by equating for every pair of actuators, solving each for , and keeping the minimum.
A bang-bang time scaling has at every instant except possibly on singular arcs. At least one actuator is saturated at all times.
Zero-inertia, critical, singular. A point where for some is a zero-inertia point: actuator cannot affect and instead bounds directly. A point of the limit curve set by such a bound is critical ( there, unlike on ). A critical point from which integrating forward or backward penetrates the limit curve immediately is singular.
A -safe trajectory keeps clearance at least 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 , and (eqs. 11.2–11.3). The first is linear in ; the second has a term in and a term in , and nothing else.
Step 2 — substitute. Into :
Step 3 — group by powers of the clock. The velocity-product term is bilinear in , so the two factors of come out as :
Every coefficient depends on alone. The equations now constrain one scalar unknown at each state — that is the whole content of path-velocity decomposition.
The velocity-product piece of is exactly Chapter 17's velocityProduct(q, q′), the vector
evaluated at . No new physics is computed here; the library
function PathDynamics::abc calls the Chapter 17 model and does the two matrix–vector products.
Why can vanish when does not, and vice versa (Choset Problem 11.5). is zero when the path's tangent is orthogonal, in the -metric's -th row, to joint — the RP arm's prismatic joint at the midpoint of a straight Cartesian line, where the radial velocity is momentarily zero. collects and the centrifugal/Coriolis terms, which can vanish on a straight segment () with no Coriolis coupling even where . The micro-example below has and at every ; Choset's RP arm has and .
The micro-example. Reach from the Chapter 17 micro-example (, , , , , ) on a horizontal Workbench (), joint 2 held at by its own motor while joint 1 sweeps a quarter turn: , so , , and is constant along the path with from Chapter 17. Then
Joint 2's is the centrifugal torque the elbow motor must supply to keep the elbow at 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 in each row. Row of reads .
Step 2 — divide, and flip if negative. Dividing by gives . Dividing by reverses the inequality: . That swap is the whole content of eq. 11.8, and it is why the sign of matters: a joint whose coordinate decreases along the path has its "maximum torque" bound on the low side of .
Step 3 — intersect the intervals. must satisfy all rows, so it lies in . Each and is an affine function of at fixed : with . The max of affine functions is convex and piecewise affine; the min is concave. Neither is smooth, so neither is .
Step 4 — the boundary of admissibility. The cone is empty when , i.e. when some . At fixed , as grows from zero, the first value at which some pair crosses, , is where admissibility is lost. Because each side is affine in , the crossing is in closed form, , and over pairs of taken over positive roots. The minimum rule is exact rather than heuristic: if any then , so the state is already on or past the boundary.
What the closed form assumes (Choset footnote 2). That is admissible for every — the robot can hold any configuration statically — and that admissibility is lost at most once as 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
with bisection on seeded random rows in two and three dimensions; the
worst disagreement is .
Back to the micro-example, limits , N·m. Joint 1: , , independent of because . Joint 2: and . The only pair that can collapse is : , so and
— a joint-1 speed of rad/s. Faster than this, the elbow motor cannot supply the centrifugal torque while the shoulder brakes at full torque. Lower to N·m and drops to ; 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 on , . 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 lies strictly between and , 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 -integral curve and below by the -integral curve (Choset's figure 11.2). The optimum is therefore a concatenation of pieces of - and -curves.
Step 4 — where the switches are. The pieces meet where a -curve crosses an -curve, or where the limit curve forces a change: a -curve that would penetrate the amber must have been abandoned earlier for an -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, contains no , and 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 — the same number the lattice reproduces below.
The micro-example never touches its limit curve. With constant and the curve is and is ; they cross at with . One switch, at
Joint 2 is honest at the peak: during acceleration 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 () and at the gravity vector is , giving , and ; at the shoulder's gravity torque is N·m against a N·m limit, so — the arm can barely lift, and the whole motion takes s with its single switch pushed out to .
Zero-inertia points and singular arcs
DerivationSliding along the limit curve
Step 1 — the row loses its . With , row of eq. 11.7 reads . Nothing the actuator does changes ; the constraint is on alone. Solving for gives (eq. 11.11). If 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 the cone is a single vector, . At a point of set by a zero-inertia bound, : the cone is open, but it may point out of the admissible region.
Step 3 — clip to the tangent. Following 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 plane: . Hence , with the right-hand derivative where the curve has a corner, and symmetrically for the backward integration.
Step 4 — singular arcs. Critical points lie where 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 and instead of and lets the algorithm slide along such an arc with an acceleration strictly between and , rather than chattering between them — and on a slide neither actuator is saturated.
In the rule is a one-liner. The library integrates in , where a stage from to with constant moves by . When the free step would land above , the stage acceleration that lands exactly on the curve is ; if is inside the cone the stage slides, otherwise it penetrates. At a critical point that is Choset's rule; at an ordinary point of 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 (, , , , , ) along the straight Cartesian line with limits N·m and N. The path has with — a zero-inertia point that imposes no velocity bound, the case of Problem 11.5. Choset reports a minimum time of approximately s with one switch. With the printed parameters, however, the gravity torque on joint 1 at either end of the path is N·m against a N·m limit, so and : 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 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 and 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 , , for time from gives and . Write and with integers ; then the new indices are and — 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 , 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 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 -safe trajectory can be tracked by lattice moves within an error that shrinks with ; the margin is spent on that error, leaving . The two bounds on are what make the bookkeeping close — the first keeps an integer multiple of , 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 fix the number of lattice points, which is once is expressed through . Polynomial in , exponential in : nobody has run this beyond a few degrees of freedom, and Choset says so.
What the theorem does not say. The -safe trajectory the algorithm finds may look nothing like the true time-optimal -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 with box torque limits, the feasible accelerations at a state form a parallelepiped that changes during the step and collapses where loses rank. A bound on the largest eigenvalue of keeps it from collapsing; a bound on gives a conservative subset feasible over the whole step; the controls are the grid points inside . The completeness result survives with in place of ; the running time stays exponential in .
The hand trace Choset's figure 11.9 invites: , , from rest at to rest at . The lattice has and ; the goal is . The search returns the four-level sequence — velocities and positions — in s, which is the continuous exactly, and the control sequences of length four collapse to distinct lattice states. For the widget's default the continuous bound is s; the lattice returns s at and s at (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. . In the kinematics are linear and a constant- stage is a straight line; the micro-example's switch at lands on the grid exactly, and is no longer a singularity of the integration.
Step 2 — convexity of the stage constraint set. At fixed the inequalities in are half-planes, and is affine. The feasible set is a convex polygon; its projection onto the axis is an interval.
Step 3 — propagate backward. . Given , add the two half-planes and take the minimum and maximum of over the polygon — two two-variable LPs, solved exactly by vertex enumeration since there are at most half-planes. The result is , the set of states at 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 never shrinks the reachable set downstream (controllability is monotone in under these constraints), so choosing the largest admissible 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 is just a half-plane with no in it. The price is a grid: the answer is optimal for the discretization, and the stage constraints are enforced at only. On the chapter's seeded gravity-loaded splines at the two constructions agree on to 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: dashed, each solid, the hit points in amber.
- 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
- , , . Tabulate and fail with NoSolution if any (the robot cannot hold that configuration).
- Backward curve . Integrate backward from until the limit curve is penetrated (record the hit), , or at — in which case return StallsAtZero (Problem 11.3). A stage that would penetrate but can land on the curve with slides instead.
- Forward curve . Integrate forward from , sliding with where allowed, until it crosses — append the crossing to and return — or penetrates the limit curve at . If it reaches below or reaches , return NoSolution (Problem 11.4).
- Tangent point (footnote 3). On the line bisect on for the highest -curve from that does not penetrate the limit curve; its point of closest approach is — a tangent point, or a critical point. Integrate backward from until it rises above ; the crossing is a max→min switch — append it. If it reaches or passes below the start, return NoSolution: the start state is doomed (figure 11.2).
- is a min→max switch — append it, set , 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 .
Two implementation choices matter. The curves are integrated in 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 with and drive negative. And the bisection in step 4 replaces the slope test 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 extra -curve integrations per switch.
- 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
- .
- for down to : half-planes at , plus and ; over the polygon's vertices. If empty, report no solution from .
- if : no solution (the start is doomed).
- for to : the largest feasible at with ; .
- .
- In
- lattice start state, goal region, control set {−a_max, 0, a_max}ⁿ, timestep
- Out
- a piecewise-constant-acceleration trajectory to G, or FAILURE
- place the start at the root of (level 0); , false
- while not : if level is empty return FAILURE
- for each node in level , for each control in : integrate the control for time , getting
- if is new, and the arc neither passes within of an obstacle nor exceeds : add it as a child
- if the arc enters : true, store
- return the stored trajectory that reaches first
- 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
- graph: node ; neighbors for , kept if the quadratic arc is safe and ; every node inside shares one key
- heuristic , the per-axis continuous bang-bang time to the goal — admissible and consistent
- Chapter 6's
BestFirstwith ; breadth-first is the same engine with link length - return the back-pointer path; controls are 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.
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 }
}
}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
}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
}/// 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))
}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 (the integrator's grid sets the
tolerance) and 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 ( s,
switches at , , ), computes from the
profile, integrates Reach2R with Chapter 17's RK4 at ms, and asserts that the
integrated stays within rad of — the measured worst error is
rad — with every sampled torque inside its limit to within the one-percent slop
of the stage-constant . 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 against a brute-force scan, an actuator
saturated at every node of every solved profile, the profile never above and touching
it only at switches or slides, continuous across every switch (the bug that check was written
to catch jumped the profile by in ), TOPP-RA within of the switching construction
on seeded gravity-loaded splines and agreeing with it on which problems are unsolvable, and
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 ,
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 near sharp ones, and large is large , 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
- Foundation exerciseDifficulty 1 of 3When is there no limit curve at all?
Show that a single actuator with constant symmetric bounds and has 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 (torque falling with speed) and show that a limit curve appears. Where is it?
- Foundation exerciseDifficulty 2 of 3Gravity on the micro-example
For the micro-example path with gravity on, derive in closed form from Chapter 17's and show that . What torque limit on joint 1 would make inadmissible at rest, and what does that mean physically?
U₁ at s = 0 with gravity on, in units of 1/s²
- Conceptual exerciseDifficulty 2 of 3Predict the switchPredict 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)?
- Conceptual exerciseDifficulty 2 of 3Levels and detours on the lattice
In the Lattice Hopper (1-DOF, , ) predict the level count and the lattice at before running it, then at . Then switch to the 2-DOF room, set 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 - Practical exerciseDifficulty 2 of 3Joint-rate limits as rows of the limit curve
Add joint-velocity limits to
limits.rsas additional rows of the velocity-limit computation. Since , each is the bound , a critical-type point ( there). Show in a test that becomes , and that the time scaler slides along the rate limit where it is active rather than bouncing. The TypeScript port'sjointRateLimitand the'rate'kind ofLimitPointare the reference. - Practical exerciseDifficulty 3 of 3TOPP-RA versus the switching construction (stretch)
Run
toppraandtime_scaleon seeded natural cubic splines for Chapter 17'sReach3Rwith gravity, limits drawn from a fixed range, . Report, honestly, the fraction on which both report no solution, the fraction on which exactly one does, and the distribution of 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 , 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 .
References
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.
