The planner hands you waypoints. The motors need a smooth, timed, dynamically-feasible path — a position and a velocity at every instant. This lesson builds that bridge with polynomials and the elegant trick of differential flatness.
In Lesson 3 you watched A* and RRT search a map and hand back a route. But look closely at what they return: a list of waypoints — corner, corner, corner — connected by straight line segments. A jagged polyline. Geometrically it threads between the obstacles. But try to drive it.
At the first corner the robot would have to stop dead, spin to face the next segment, and accelerate again — an instantaneous change of direction. Real wheels can't do that. The path has infinite curvature at every corner: to turn a sharp angle at any nonzero speed you'd need infinite sideways acceleration, which means infinite force, which the motors do not have.
And there's a second, quieter problem. The polyline tells the robot where to go but not when, or how fast. It is pure geometry — a shape on the floor with no clock attached. A robot doesn't consume geometry. Its controller (Lesson 6) needs to know, at every tick: "right now you should be at this point, moving at this velocity, turning at this rate." That is a trajectory — a timed, smooth function of the clock — not a polyline.
Watch the difference. Below, the same waypoints are followed two ways. The jagged path forces hard stops at every corner (the speed dial slams to zero); the smooth trajectory glides through, keeping speed up and turning gently. Press Drive and compare.
Same four waypoints. Toggle the mode and press Drive. Watch the speed dial: the polyline must brake to zero at each corner (infinite curvature); the smooth trajectory keeps speed and turns gradually.
The jagged path is kinematically infeasible at speed: no real robot survives a corner with zero turning radius. The smooth one is what we'll learn to build — and the surprising news is that for wheeled robots it's not hard. The trick is to pick the right variables to be smooth, and let everything else follow.
Notice where we are on the autonomy stack. Trajectory generation lives inside the Motion Planning module — it is the bridge between the discrete planner above it and the continuous controller below. It produces the reference the controller will later chase.
The two words sound interchangeable in everyday speech, but in robotics they are precisely different, and the difference is the whole point of this lesson. Let's pin both down.
A path is a geometric object: a curve in space, a set of positions with no notion of time. "Drive along this arc, then this line." It answers where, never when. Formally it's a function of a path parameter — often arc length s — written ξ(s). Slide s from 0 to its end and you sweep out the shape, but nothing tells you how fast.
A trajectory is a path with a clock bolted on: a function of time that says where the robot is at every instant. We write it
Read this as: "ξ is a function that takes a time t (a real number) and returns the robot's full configuration at that time — here a point in ℝ3." Here n is the dimension of the configuration (3 for our wheeled robot: x, y, heading θ). Because ξ(t) is a function of time, we can differentiate it — and that derivative is the velocity, exactly what the motors need.
To specify a trajectory we don't list every point — that's infinitely many numbers. Instead we pin down its boundary conditions: the values it must take at the start and end. For a wheeled robot a complete boundary spec is:
That's it. A handful of numbers at the two ends. The job of trajectory generation is to interpolate a smooth curve through them — smooth enough that the robot's wheels can produce it. Notice the velocity boundary conditions: a path never mentions velocity, but a trajectory must, because a controller hand-off ("start tracking at speed 0.5 m/s, heading south") only makes sense if the trajectory was built to honor it.
Play with the boundary conditions below. Each handle sets a start/goal position; the heading arrows set θ at the ends. The curve redraws to honor all of them — that's the interpolation problem we'll solve by hand in the next chapter.
Drag the orange start and green goal handles. Drag the small dots on each to set the start/goal heading. The smooth curve honoring all four conditions redraws live. (This is exactly the cubic we derive next chapter.)
We need a smooth function of time that hits our boundary conditions. The simplest, most useful choice is a polynomial in t. Each coordinate of the robot gets its own polynomial; we'll do one coordinate now and the rest are identical.
A polynomial is smooth (infinitely differentiable), easy to differentiate to get velocity, and — crucially — linear in its coefficients, which turns "find the curve" into "solve a linear system." We parameterize a coordinate x(t) as a weighted sum of basis functions ψi(t):
The textbook's choice of basis is the monomials: ψ0=1, ψ1=t, ψ2=t², ψ3=t³. So
The unknowns are the four coefficients a0, a1, a2, a3. Why exactly four? Because we have four boundary conditions to honor on this one coordinate: position and velocity at both ends. Four conditions, four unknowns — a square system. The velocity is the time derivative, which for a polynomial is trivial:
Let's design one coordinate concretely. Suppose we want, on a horizon T = 2 seconds:
"Start at 0 moving at speed 1, end at 4 having coasted to a stop." Plug each condition into the polynomial. This is the entire method — watch every step.
Condition 1 — x(0)=0. Set t=0: every term with a t vanishes, leaving
Condition 2 — &xdot;(0)=1. Set t=0 in the velocity: again everything with a t dies, leaving
So the first two coefficients fall out for free — the t=0 conditions always read off a0 and a1 directly. The remaining two come from the t=T end.
Condition 3 — x(2)=4. Plug t=2 (and the known a0=0, a1=1):
Condition 4 — &xdot;(2)=0. Plug t=2 into the velocity (a1=1):
Solve the 2×2 that remains. Subtract the first from the second to cancel a2:
Back-substitute into 4a2 + 8a3 = 2:
The trajectory. Assembling all four coefficients:
Sanity check the goal: x(2) = 2 + 8 − 6 = 4 ✓, and &xdot;(2) = 1 + 8 − 9 = 0 ✓. It starts at 0 with speed 1, ends at 4 at rest. We just designed a motion with arithmetic.
Doing it by elimination is fine for two unknowns, but the general recipe writes all four conditions as one matrix equation M·a = b. Each row evaluates a basis (or its derivative) at an endpoint. With basis [1, t, t², t³] and its derivative [0, 1, 2t, 3t²] at t=0 and t=T:
With T=2 and the right-hand side b = [0, 1, 4, 0]⊤, solving this 4×4 gives back exactly a = [0, 1, 2, −0.75]⊤ — the same coefficients we found by hand. The top-left 2×2 being the identity is why a0 and a1 read off directly.
The hand derivation maps line-for-line onto code. Build M and b, then solve.
python import numpy as np def cubic_coeffs(x0, xdot0, xf, xdotf, T): # Solve for [a0,a1,a2,a3] so that x(t)=a0+a1 t+a2 t^2+a3 t^3 # matches position+velocity at t=0 and t=T. M = np.array([ [1, 0, 0, 0 ], # x(0) = a0 [0, 1, 0, 0 ], # xdot(0)= a1 [1, T, T**2, T**3 ], # x(T) [0, 1, 2*T, 3*T**2 ], # xdot(T) ], dtype=float) b = np.array([x0, xdot0, xf, xdotf], dtype=float) return np.linalg.solve(M, b) a = cubic_coeffs(0, 1, 4, 0, T=2) print(a) # [ 0. 1. 2. -0.75] -- matches the hand solve def eval_poly(a, t): return a[0] + a[1]*t + a[2]*t**2 + a[3]*t**3 print(eval_poly(a, 2)) # 4.0
And the idiomatic one-liner once you trust the pattern — NumPy can evaluate the polynomial and its derivative for you:
python # a is in increasing-power order [a0,a1,a2,a3]; np.polyval wants decreasing power p = np.polynomial.Polynomial(a) # increasing-power, clean pos = p(2.0) # 4.0 vel = p.deriv()(2.0) # 0.0 (xdot at T)
A cubic has four coefficients, so it pins down four conditions: position and velocity at each end. If you also care about acceleration at the ends (smooth start/stop with no jerk in the force), you need two more conditions, hence two more coefficients — a degree-5 polynomial, the quintic:
Same recipe, bigger matrix: six conditions (position, velocity, acceleration at both ends) → a 6×6 solve. The rule is mechanical: each derivative you constrain at each endpoint costs one more coefficient. 2 ends × (pos, vel) = 4 → cubic. 2 ends × (pos, vel, accel) = 6 → quintic.
Set the four boundary conditions and the horizon T. The widget builds M and b, solves for the coefficients, and plots x(t) (orange) with its velocity &xdot;(t) (teal). Watch the coefficient readout update — it's the matrix solve, live.
We can now fit a beautiful smooth cubic through any boundary conditions. So we're done — just fit a curve and drive it? No. There's a subtler trap, and it's the deepest idea in wheeled-robot motion. The curve might be smooth and still be physically impossible for the robot to follow.
Recall the unicycle model from Lesson 2 — the simplest wheeled robot. Its motion obeys
where V is the forward speed and ω the turning rate — the two things you actually command. Stare at the first two equations. They say the velocity vector (&xdot;, &ydot;) always points along the heading θ. The robot can only ever move in the direction it faces. It cannot slide sideways. Ever.
This is the nonholonomic constraint — "rolling without side-slip." A wheel can roll forward or pivot, but it cannot translate along its own axle. Written as an equation, the sideways velocity is forced to zero:
So an arbitrary smooth (x(t), y(t)) is not automatically drivable. We need the heading θ(t) and the turn rate ω(t) it implies to be ones the robot can actually produce, in step with where it's pointing. The naive approach — fit x and y independently and hope — can produce a curve no wheeled robot on Earth can track.
Try it below: drag the curve into a shape and watch the required heading arrows. When the curve doubles back or makes a hairpin, the heading has to swing violently — demanding a turn rate ω the robot can't hit. The red flag means "infeasible: this needs a turn faster than ωmax."
Drag the control points to reshape the curve. The arrows show the heading the unicycle would need at each point. Segments needing a turn rate above ωmax flash red — infeasible. Gentle curves stay green.
So the question becomes: how do we generate a smooth curve that is guaranteed to be consistent with the robot's dynamics? If we could pick a few variables to be smooth and have the heading, speed, and turn rate fall out automatically — always feasible by construction — we'd be done. That is exactly what differential flatness gives us. It's the hero of this lesson, and it's next.
Here is the beautiful idea, due to Fliess and made practical for robots by Van Nieuwstadt and Murray. For a special class of systems, there exists a magic choice of outputs — the flat outputs — such that every state and every input of the system can be written as an algebraic function of those outputs and a finite number of their time derivatives. No integration. No solving differential equations. Just plug in derivatives.
Why does this matter so much? Because if the position (x, y) is a valid flat output, then any smooth curve we draw for (x(t), y(t)) — like the cubics from Chapter 2 — automatically defines a heading, speed, and turn rate that satisfy the dynamics exactly. The infeasibility worry from Chapter 3 evaporates: feasibility is guaranteed by construction (as long as we avoid one singularity, Chapter 6). We get to design in the easy space (just draw x and y) and the hard variables follow.
Claim: the unicycle's flat output is its position z = (x, y). We'll recover θ, V, and ω from x, y and their derivatives, using nothing but algebra. Start from the model:
Step 1 — recover the speed V. Square (i) and (ii) and add them. Using cos²θ + sin²θ = 1:
The speed is just the magnitude of the velocity vector — obvious in hindsight, but now it's derived purely from the flat output's first derivatives.
Step 2 — recover the heading θ. Divide (ii) by (i): the V's cancel, leaving the slope of the velocity vector.
(We use atan2, the two-argument arctangent, instead of plain arctan(&ydot;/&xdot;) so the heading lands in the correct quadrant for all directions, not just the right half-plane.) The robot's heading is simply the direction its position is currently moving — which makes total sense for a robot that can only go where it points.
Step 3 — recover the turn rate ω. This is the only one needing a second derivative. We want ω = &thetadot;. Since θ = atan2(&ydot;, &xdot;), differentiate. The derivative of atan2(g, f) with respect to time is (f·ġ − g·&fdot;)/(f² + g²). Here f = &xdot; so &fdot; = &xddot;, and g = &ydot; so ġ = &yddot;:
Some books write the numerator as &xddot;&ydot; − &yddot;&xdot; with the opposite sign convention — it's the same magnitude, just a sign choice for turn direction. The denominator is V² again. So the turn rate is the cross product of velocity and acceleration, normalized by speed squared — precisely the signed curvature times speed, which is what a turn rate is.
python import numpy as np def flat_to_state_input(xd, yd, xdd, ydd): # xd,yd = first derivs of flat output; xdd,ydd = second derivs. # Returns (theta, V, omega) for the unicycle, all algebraic. V2 = xd*xd + yd*yd V = np.sqrt(V2) theta = np.arctan2(yd, xd) # quadrant-correct heading omega = (xd*ydd - yd*xdd) / V2 # cross(vel, acc) / V^2 return theta, V, omega
Three lines turn a position curve into a fully feasible robot trajectory. This little function is, quite literally, the heart of the entire homework problem for this lecture — and of countless real trajectory generators.
Below, draw a position curve and watch θ, V, and ω pop out at any point you hover.
A figure-flowing position curve is drawn. Move the slider to pick a time along it; the readout shows the recovered heading θ, speed V, and turn rate ω computed from the derivatives at that point — the heading arrow and a curvature circle update live.
Now we assemble the whole pipeline. The recipe (straight from the lecture notes) is three steps:
The boundary spec is given in robot terms: start/end position, heading, and speed. We must convert these into conditions on x, y and their derivatives, because that's what the polynomial fit consumes. The bridge is the flatness map run backwards: from &xdot; = V cosθ and &ydot; = V sinθ,
So a start heading and speed become a start velocity vector for the flat output. Let's use this homework-style spec:
Convert the ends. At θ0 = −π/2 (pointing straight down, in −y): cos(−π/2)=0, sin(−π/2)=−1. So
And identically at the end (same heading and speed): &xdot;(T)=0, &ydot;(T)=−0.5. Now we have four conditions per coordinate — exactly a cubic each.
x-coordinate. Conditions: x(0)=0, &xdot;(0)=0, x(15)=5, &xdot;(15)=0. From t=0: a0=0, a1=0. From t=15:
From the velocity equation a2 = −22.5 a3. Substitute: 225(−22.5 a3) + 3375 a3 = 5 → −5062.5 a3 + 3375 a3 = 5 → −1687.5 a3 = 5 → a3 ≈ −0.002963, and a2 ≈ 0.06667. So
y-coordinate. Conditions: y(0)=0, &ydot;(0)=−0.5, y(15)=5, &ydot;(15)=−0.5. From t=0: b0=0, b1=−0.5. From t=15:
Velocity gives b2 = −22.5 b3; substituting as before: −1687.5 b3 = 12.5 → b3 ≈ −0.007407, b2 ≈ 0.16667. So
Let's evaluate everything at t = 7.5 (the midpoint). First the derivatives. For x: &xdot; = 0.13333 t − 0.008889 t², &xddot; = 0.13333 − 0.017778 t.
For y: &ydot; = −0.5 + 0.33333 t − 0.022222 t², &yddot; = 0.33333 − 0.044444 t.
Now the flatness formulas at t=7.5:
So at the midpoint the robot is moving at about 0.90 m/s, heading up-and-right at 56°, and (because both accelerations happen to vanish there) momentarily not turning. Every number came from evaluating polynomials and plugging into three formulas — no differential equation was ever solved. That is differential flatness doing its job.
The whole pipeline, end to end, in code:
python import numpy as np def cubic_coeffs(p0, v0, pf, vf, T): M = np.array([[1,0,0,0],[0,1,0,0], [1,T,T**2,T**3],[0,1,2*T,3*T**2]], float) return np.linalg.solve(M, np.array([p0,v0,pf,vf], float)) T = 15.0; V0 = Vf = 0.5; th0 = thf = -np.pi/2 # Step 1: pose+speed -> flat-output velocity boundary conditions ax = cubic_coeffs(0, V0*np.cos(th0), 5, Vf*np.cos(thf), T) # x(t) ay = cubic_coeffs(0, V0*np.sin(th0), 5, Vf*np.sin(thf), T) # y(t) def deriv(a, t, k): # k-th derivative of the cubic at t c = np.polynomial.Polynomial(a) for _ in range(k): c = c.deriv() return c(t) t = 7.5 xd, yd = deriv(ax,t,1), deriv(ay,t,1) xdd, ydd = deriv(ax,t,2), deriv(ay,t,2) V = np.hypot(xd, yd) theta = np.arctan2(yd, xd) omega = (xd*ydd - yd*xdd) / (xd*xd + yd*yd) print(round(V,3), round(theta,3), round(omega,3)) # 0.901 0.983 0.0
Differential flatness looked too good. There's a catch, and it lives right in the denominators. Look again at the recovery formulas:
What happens when the robot stops — when V = √(&xdot;²+&ydot;²) = 0? The denominator of ω becomes zero. ω is undefined. And θ = atan2(0, 0) is also undefined — if you're not moving, your direction of motion is meaningless. This is the flatness singularity at V = 0.
The fix in practice: keep V > 0 throughout the trajectory — never plan a stop in the middle of a flat segment, and give the endpoints a small nonzero speed. If you genuinely must stop, you stitch trajectories: stop is its own segment handled by a different controller, then a fresh flat segment launches from a small speed.
Even away from the singularity, the recovered inputs must respect the robot's physical limits. Two show up constantly:
The trouble is that polynomial coefficients don't obviously control these — you fit for position, and speed/turn-rate are whatever derivatives fall out. So you generate the trajectory, then check. If a limit is violated, the cleanest cure is the elegant idea of time scaling.
Here is the key separation. A trajectory bundles two things: a geometric path (the shape on the floor) and a timing law (how fast you move along it). Time scaling says: keep the shape, change only the clock. If you traverse the identical curve but take longer, every velocity shrinks — without changing where the robot goes.
The lecture's clean demonstration: take a 1D path x(s) = x0 + s(xf − x0) for s ∈ [0,1], and drive the parameter s with a smooth cubic timing law that starts and ends at rest:
This satisfies s(0)=0, s(T)=1, ⋅(0)=⋅(T)=0. The maximum speed along the path comes out to
Read that: doubling T halves the peak speed. The geometry is untouched — same start, same end, same shape — but the whole motion is gentler. So if your candidate trajectory's peak speed is, say, 0.9 m/s and your limit is 0.5, you scale time by the ratio (here ~1.8× longer) and the speed drops under the cap while the path stays exactly the same.
Below, watch a candidate trajectory's speed profile against the limit, and stretch T with the slider. When the peak speed dips under Vmax, the trajectory turns feasible — same path, slower clock.
Top: the fixed geometric path (unchanged by T). Bottom: the speed profile V(t) vs. the dashed limit Vmax. Drag T larger — the speed curve flattens under the limit and the readout flips to feasible. The path never moves.
Flatness gives us a feasible trajectory quickly. But often we want the best one: smoothest, least energy, fastest. That's the realm of trajectory optimization (also called optimal control). Instead of just interpolating boundary conditions, we minimize a cost.
The general optimal control problem, straight from the lecture, looks like this. We choose the state trajectory x(t) and inputs u(t) to minimize a cost while obeying the dynamics and limits:
Decoding the symbols: g is the stage cost (the price you pay at each instant — e.g. control effort), h is the terminal cost (a price on where you end up), and the integral sums the stage cost over the whole motion. The constraint &xdot; = a(x,u) forces the trajectory to obey physics; the set memberships enforce limits.
The cost is the engineering decision — it encodes what "good" means:
The problem above is infinite-dimensional — the unknown is a whole continuous function. Computers need finitely many numbers. The dominant approach is the direct method: chop time into N points, treat the values xi, ui at those points as the unknowns, and approximate the dynamics with a discrete update (e.g. forward Euler xi+1 = xi + Δt·a(xi, ui)). The cost integral becomes a sum. This turns the calculus problem into a nonlinear program (NLP) — minimize-a-function-over-vectors — that solvers like IPOPT, SNOPT, or CasADi crunch.
Here's where flatness and optimization shake hands. If you parameterize the flat output as a polynomial (as in Chapter 2), the trajectory is linear in the coefficients. A smoothness cost like "integral of squared acceleration" is then a quadratic function of those coefficients, and the boundary conditions are linear equality constraints. Minimizing a quadratic subject to linear constraints is a quadratic program (QP) — convex, fast, with a unique global minimum:
where a stacks all polynomial coefficients, Q is built from integrals of products of basis derivatives, and M a = b is exactly the boundary-condition system from Chapter 2 (plus rows for any interior waypoints). When you have more coefficients than constraints, the QP picks the smoothest feasible trajectory among the infinitely many that fit. This is the backbone of minimum-snap planning.
A 1D point goes 0→1 over horizon T. Slide the effort weight: high weight buys a gentle, low-acceleration glide (small peak force); low weight lets it rush. Watch the position, velocity, and the cost readout trade off — the optimizer's dial, in your hand.
Time for the payoff. Everything we've built — cubic fitting, flatness recovery, time scaling — lives in one interactive tool. Drag the start and goal poses; the lesson fits a flat cubic trajectory for x(t) and y(t), then drives a robot along it while plotting the recovered speed V(t) and turn rate ω(t) live. A slider for the total time T shows time scaling in action: slower T lowers the required V and ω.
Three things to look for as you play:
Drag the orange start and green goal handles (and the small heading dots) to set the boundary poses. Press Drive to animate the robot along the fitted flat trajectory. The right panels plot V(t) and ω(t) with their limits. Stretch T to time-scale; toggle the waypoint to bend the path through a middle point.
This is the whole lesson in one widget: a planner-style endpoint spec goes in; a smooth, timed, dynamically-feasible trajectory comes out, complete with the exact velocity and turn-rate commands the controller in Lesson 6 will track. You designed it by dragging two dots.
Our trajectory is computed open-loop: a function of time and the initial condition only, with no feedback. The lecture is blunt about the consequence: "open-loop control laws suffer from robustness issues since they do not make corrections based on real-time observations." If a gust of wind, a wheel slip, or a modeling error nudges the robot off the plan, an open-loop trajectory has no idea and no recourse. It walks to the chair with its eyes closed.
Two practical patterns fix this.
This is the dominant architecture, and it's exactly the bridge to Lesson 6. Split control into two layers:
The open-loop trajectory provides the nominal motion — what should happen in a perfect world. The closed-loop tracker measures the actual pose, compares it to the reference, and applies corrections to kill the tracking error. The reference says "be here, going this fast"; the tracker makes sure you actually are. Our whole lesson built the reference half. Lesson 6 builds the tracker.
Model Predictive Control (MPC) takes the idea further: don't compute one long trajectory once. Instead, re-plan continuously. At each instant, solve a short trajectory-optimization problem over a finite look-ahead window (the horizon), execute only the first slice of the resulting plan, then — one tick later, with fresh sensor data — re-solve from the new actual state. The horizon slides forward with time, hence "receding."
So the trajectory you generate in this lesson is never the end of the story — it's the reference that a closed-loop layer (a tracker, or a re-planning MPC loop) consumes to stay robust. Open-loop generation and closed-loop tracking are partners, not rivals.
A disturbance pushes the robot off plan each step. Open-loop commits to one trajectory and drifts away; receding-horizon re-fits a fresh flat trajectory from the current measured pose each tick and stays locked on the goal. Toggle and press Run.
You now own one of the most useful tricks in robotics: turning a planner's waypoints into a smooth, timed, dynamically-feasible trajectory, almost for free, via differential flatness. Let's consolidate.
| Concept | What it is | The key formula / fact |
|---|---|---|
| Path | geometry only — a shape, no clock | ξ(s), function of path parameter s |
| Trajectory | path + timing — position & velocity at every instant | ξ(t), function of time t |
| Cubic fit | smooth coord from 4 boundary conditions | x(t)=a₀+a₁t+a₂t²+a₃t³, solve M·a=b |
| Flat output | variables that algebraically yield all states/inputs | unicycle: z = (x, y) |
| Recover speed | from flat-output derivatives | V = √(&xdot;² + &ydot;²) |
| Recover heading | direction of motion | θ = atan2(&ydot;, &xdot;) |
| Recover turn rate | cross(vel,acc)/V² | ω = (&xdot;&yddot; − &ydot;&xddot;)/(&xdot;²+&ydot;²) |
| Singularity | recovery breaks when stopped | V = 0 ⇒ θ, ω undefined — keep V > 0 |
| Time scaling | respect speed limits without changing the path | larger T ⇒ lower peak speed; &xdot;max ∝ 1/T |
| Optimization | best trajectory, not just feasible | min cost s.t. dynamics+limits; polynomial ⇒ QP |
Trajectory generation is the lower half of the Motion Planning & Control module. It consumes the discrete plan from the search algorithms above it and produces the continuous reference the controller below it tracks: