35 min

Trajectories: getting there smoothly rather than instantly

A controller never asks whether the setpoint was reasonable, so you have to feed it a sequence of reachable setpoints instead of one impossible jump.

Where you are. You can build a controller that drives a joint to a commanded angle. This lesson is about where that commanded angle comes from, which turns out to matter as much as the controller does.

The lift that does not throw you on the floor

You press 8. The doors close, and for about two seconds the floor firms up under your feet, as if you had gained a little weight. Then it goes quiet. Somewhere near the top the floor softens for a moment, your knees unload slightly, and the doors open.

None of that is the motor being weak. A lift motor could reach full speed in a tenth of a second. It would also put every passenger on the floor and spill every coffee in the car, so nobody builds one that does. The ride you actually get was designed: ease in, hold a steady speed, ease out. The only time you consciously notice a lift is when one of those three parts is done badly.

Your robot has no passengers. It has something worse: a gearbox with backlash in it, a motor with a hard torque limit, a link that flexes, and a block sitting loose in its gripper. It gets the same treatment, for less sentimental reasons.

The idea in one paragraph

A controller answers exactly one question: given the error right now, how hard do I push? It never asks whether the setpoint it was handed was a reasonable thing to want. Hand it a step - be at 90 degrees, now - and you have implicitly asked for infinite speed and infinite acceleration, so the controller does the only thing available to it: maximum torque, saturation, and whatever overshoot follows from that. A trajectory is the missing piece. Instead of one setpoint you feed the controller a sequence of setpoints, one per control tick, that walks from where the arm is to where you want it at a speed and an acceleration the machine can actually deliver. Building one takes two decisions: which space you interpolate in, joint angles or tool position, because those give genuinely different paths through the room; and what velocity profile you follow along the way.

A step is a wish, not a command

Take the PID controller you just wrote, with the gains from the last lesson, and command a 90 degree step. Then command the same 90 degrees spread over 1.5 seconds. Same controller, same plant, same gains; only the shape of the setpoint changes.

90° the angle you command step: all of it, at once profile: spread over 1.5 s 0 s 1 s 2 s 3 s 0°/s 180°/s 360°/s the speed that results peak 335°/s peak 102°/s time
Above, two commands for the same 90 degree move, an instant step and a 1.5 second smooth profile. Below, the joint speed each one produces: 335 degrees per second for the step against 102 for the profile

Wider than the screen; scroll it sideways.

step1.5 s profile
torque the controller asks for12.6 N·m on the first ticknever above 1.99 N·m
against a motor that can give3 N·m3 N·m
ticks spent saturated3.6%none
peak joint speed335°/s102°/s

The step asks for four times what the motor has, gets three quarters of it thrown away by the clip, and drives the joint through the target at 335 degrees per second. The profile never asks for anything the motor cannot deliver, so the controller stays in the regime where its gains actually mean something.

Which space do you move through?

You want to move the tool from A to B. There are two obvious ways to fill in the middle, and they produce different motions.

Joint space. Interpolate each joint angle independently from its start value to its end value. It is trivially cheap, no inverse kinematics is needed anywhere in the middle, and if both endpoints are reachable then every configuration in between is too, because you never left the space of valid joint angles.

Cartesian space. Interpolate the tool pose, then solve inverse kinematics at every point to find the joint angles that produce it. You get the straight line, which is what welding, wiping a surface, inserting a peg and carrying a full glass all require.

They are not close.

base 1.14 m apart at the middle of the move interpolate the joint angles the shoulder sweeps; the tool traces an arc interpolate the tool position a straight line, whatever the joints must do start end
The same two tool positions joined two ways: interpolating the joint angles sweeps the tool along a circular arc that bulges 1.14 metres above the straight line joining the same two points

Wider than the screen; scroll it sideways.

On the two-link arm with links of 1.00 and 0.70, take the tool from (1.45, 0.35) to (-1.45, 0.35). The two configurations have the same elbow angle, −58.33°, because both points are the same distance from the base. So interpolating joint angles means sweeping the shoulder through 152.86° with the elbow held still, and the tool traces a circle of radius 1.4916.

You can check the gap by hand. The circle has radius 1.4916 and the straight line sits at height 0.35, so the arc bulges above it by 1.4916 − 0.35 = 1.1416 metres. The path is also 3.98 metres long against the straight line’s 2.90, thirty-seven percent further travelled for the same two endpoints.

joint spaceCartesian space
what you interpolateeach joint anglethe tool pose
IK neededat the two ends onlyat every tick
tool pathwhatever falls outexactly the straight line
reachability of the middleguaranteed if the ends arenot guaranteed at all
joint speedsconstant, by constructionwhatever the geometry demands
what it is forfree-space transitcontact, straight-line insertions, carrying

When the straight line is not available

The last two rows of that table are where real systems break.

base no reach closer in than 0.30 m the straight line 55% of it has no joint solution the joint-space path arcs around the outside of the hole; every point on it is reachable start end
Both end points are reachable but the straight line between them crosses the hole the arm cannot reach into, so 55 percent of that path has no joint solution, while the joint-space path arcs around the outside of the hole

Wider than the screen; scroll it sideways.

The same arm has a hole in the middle of its workspace: it cannot reach anything closer than L1L2=0.30|L_1 - L_2| = 0.30 to its own base, because the elbow cannot fold tighter than fully folded. Now take two points that are both comfortably reachable, (0.25, 0.30) and (0.25, -0.30), each 0.39 from the base. Interpolate between them in Cartesian space and 277 of 501 sampled points on that straight line, fifty-five percent of the path, have no joint solution whatsoever. The line passes 0.25 from the base, and there is nothing the solver can do about it.

The joint-space route between the same two points is fine. The elbow angle happens to be the same at both ends again, so sweeping the shoulder carries the tool round an arc of radius 0.39, safely outside the hole, and every point on it is reachable by construction.

There is a second, quieter version of the same problem. A straight line can stay inside the workspace and still pass close to a singularity, where the Jacobian loses a direction. Near one, a perfectly modest tool speed demands enormous joint speed, so the trajectory is geometrically fine and physically impossible. A path validator that checks reachability but not conditioning will pass it, and the arm will hit a velocity limit halfway through and fall behind the trajectory it was given.

How fast along the way

Having chosen the path, you choose the timing. The workhorse is the trapezoidal velocity profile: accelerate at the acceleration limit, cruise at the speed limit, decelerate at the acceleration limit.

0.0 s 0.5 s 1.0 s 1.5 s 0 rad/s 0.75 rad/s 1.5 rad/s speed limit, 1.5 rad/s speed up at the accel limit cruise 0.55 s slow down at the accel limit area = 1.571 rad = 90° the whole move 1.547 s in total 0.50 s speeding up 0.55 s cruising 0.50 s slowing down
A trapezoidal velocity profile for a 90 degree move: accelerate at the limit for half a second, cruise at the speed limit for 0.55 seconds, decelerate at the limit for half a second, with the area under the curve equal to the distance travelled

Wider than the screen; scroll it sideways.

Two lines of arithmetic size it. The ramps take vmax/amaxv_{\max}/a_{\max} seconds each, and the total is

T=dvmax+vmaxamaxT = \frac{d}{v_{\max}} + \frac{v_{\max}}{a_{\max}}

which says the move takes as long as cruising the whole distance would, plus one ramp’s worth of penalty for having to start and stop. For a 90 degree move with vmax=1.5v_{\max} = 1.5 rad/s and amax=3a_{\max} = 3 rad/s², that is 1.5708/1.5 + 1.5/3 = 1.5472 s: half a second speeding up, 0.55 s at speed, half a second slowing down.

If the move is short enough, there is no cruise at all. When damax<vmax2d\,a_{\max} < v_{\max}^{2} the joint is still accelerating when it has to start braking, the profile is a triangle, and T=2d/amaxT = 2\sqrt{d/a_{\max}}. With the same 90 degrees and a 3 rad/s speed limit the profile never gets near that limit: it peaks at 2.17 rad/s and the move takes 1.4472 s.

The bill for that speed is at the corners. Acceleration jumps instantly from zero to amaxa_{\max} at the start, and instantly to zero at the end of the ramp. That instantaneous change in acceleration is jerk, and infinite jerk is what excites a real arm’s structural flex and makes it ring after it stops. The fixes are to round the corners, giving an S-curve, or to use a polynomial whose acceleration starts and ends at zero.

Here is what those choices actually cost, for the same distance in the same total time:

profilepeak speedpeak accelerationat the ends
stepunboundedunboundedundefined
constant velocity1.000d/T1.000\,d/Tunboundedspeed jumps
trapezoid, ⅓ blend1.500d/T1.500\,d/T4.500d/T24.500\,d/T^{2}acceleration jumps
cubic polynomial1.500d/T1.500\,d/T6.000d/T26.000\,d/T^{2}acceleration jumps
quintic polynomial1.875d/T1.875\,d/T5.774d/T25.774\,d/T^{2}smooth

Read it the other way round and it becomes a decision. Hold the limits fixed instead of the time, at vmax=1.5v_{\max} = 1.5 and amax=3a_{\max} = 3, and ask how long each profile needs for the 90 degree move: the trapezoid takes 1.547 s, the cubic 1.773 s, and the quintic 1.964 s. Smoothness is not free. It costs 15% and 27% of your cycle time respectively, which on a machine doing one move every two seconds all day is the whole argument.

Stopping at every corner is slow

Real tasks are lists of waypoints, not single moves. Run the profile above between each consecutive pair and the arm comes to a complete stop at every waypoint, which is correct, safe, and slow: each stop costs you a full deceleration and acceleration ramp.

The standard remedy is blending. Instead of passing exactly through an intermediate waypoint, the trajectory cuts the corner within a tolerance you specify, and the arm carries its speed through. You trade geometric precision at the waypoint, which usually does not matter because the waypoint was just somewhere on the way, for cycle time, which usually does.

Which gives the shape the milestone project needs, and it is worth writing down now:

  • Transit above the workspace: joint space, blended, fast. Nobody cares what path the tool takes through empty air.
  • Descend onto the object: Cartesian, straight down, slow. Here the path is the point, because approaching a grasp from the side knocks the object over.
  • Lift: Cartesian, straight up, slow, for the same reason in reverse.
  • Carry to the bin: joint space again, blended.
  • Every waypoint on that list is validated before anything moves.

Check yourself

1. Your controller is tuned and stable, but a 90 degree step command still overshoots and saturates the motor. Why is retuning the wrong response?

Because the problem is in the command, not the controller. A step asks for infinite velocity, so the error is at its maximum on tick one, the controller demands 12.6 N·m from a 3 N·m motor, and everything after that happens in saturation, where the gains no longer describe the system’s behaviour at all. Detuning to survive the step makes every other move sluggish. Feeding the same controller a 1.5 second profile keeps the demand under 2 N·m, never saturates, and cuts peak joint speed from 335 to 102 degrees per second, with no change to the gains.

2. Both the start and the end of a Cartesian move are reachable. Is the move safe to execute?

No, and this is one of the most common ways a working demo breaks on new inputs. The reachable workspace is not convex: this arm has a hole of radius L1L2=0.30|L_1 - L_2| = 0.30 around its base, and joint limits cut further pieces out. Two points either side of the hole are individually fine while 55% of the straight line between them has no joint solution. A Cartesian move needs the whole path validated. A separate check is also needed for near-singular stretches, where the path is reachable but demands joint speeds the arm does not have.

3. Why does interpolating joint angles between two poses at the same distance from the base trace a circular arc?

Because the distance from base to tool depends only on the elbow angle. If both poses have the same tool radius, they have the same elbow angle, so interpolating the joints holds the elbow fixed and sweeps only the shoulder. A fixed elbow with a rotating shoulder is exactly the definition of a circle centred on the base. In the worked example the radius is 1.4916 and the straight line sits at 0.35, so the arc bulges above it by 1.4916 − 0.35 = 1.1416 metres, which is the number the simulation reports.

4. When does a trapezoidal profile have no cruise phase, and what shape is it then?

When the move is too short to reach the speed limit before it has to start braking, which is the condition damax<vmax2d\,a_{\max} < v_{\max}^{2}. The profile becomes a triangle: accelerate to a peak of damax\sqrt{d\,a_{\max}} at the halfway point, then decelerate, taking 2d/amax2\sqrt{d/a_{\max}} in total. Most short moves on a real arm are triangular, which is why raising the speed limit often makes no difference to cycle time and raising the acceleration limit does.

5. A quintic profile is smoother than a trapezoid. Why not use it for everything?

Because smoothness costs time. For the same distance under the same speed and acceleration limits, the trapezoid takes 1.547 s, the cubic 1.773 s and the quintic 1.964 s, so the smoothest option is 27% slower. The trapezoid is fast precisely because it is always pressed against one limit or the other, and a polynomial that eases in and out spends much of the move well inside both. Use the smooth profile where jerk actually costs you something, such as a heavy payload, a flexible arm, or an object sitting loose in the gripper, and the trapezoid where it does not.

6. Where should a trajectory be generated relative to the control loop, and why not just generate it inside the controller?

Above it, and more slowly. The trajectory generator answers “where should the arm be at time t”, which needs the task, the waypoints and the limits, and it can comfortably run at 10 to 100 Hz. The controller answers “how hard do I push right now”, which needs only the latest error and must run at hundreds of hertz or more. Merging them forces the expensive planning work into the millisecond budget, and it destroys the separation that lets you swap a scripted waypoint list for a learned policy later without touching the controller at all.

Do this

Two experiments, about thirty minutes, both on code you already have.

1. Give your controller something reasonable to chase. In code/pid_pendulum.py, with your PID implementation in place, add a moving setpoint and use it:

def setpoint(t, T=1.5):
    s = min(t / T, 1.0)
    return TARGET * (10 * s ** 3 - 15 * s ** 4 + 6 * s ** 5)

then change tau = ctrl.update(TARGET - th) to tau = ctrl.update(setpoint(t) - th). Measure peak speed by adding print(np.degrees(np.abs(np.diff(ths))).max() / DT) after each run.

Predict the numbers before you run it. You should get 335.4°/s for the step and 102.3°/s for the profile, and the peak angle should stop exceeding the target entirely: 92.64° becomes 89.52°. Then try T = 1.0 and T = 2.5 and watch peak speed move to 160.9 and 60.8. One caution when reading the printed settling value: the profiled runs are still creeping up the last half degree at five seconds because the integral is catching up on the lag it built during the move, so set T_END = 8.0 before comparing final angles.

2. Measure the bow yourself. Using solutions/ik_2link.py, take A = (1.45, 0.35) and B = (-1.45, 0.35), solve both with ik_analytic, interpolate the joint angles in 200 steps, and run fk on each. Compare that path against the straight line between the two tool positions.

Confirm three things: the maximum gap is 1.1416, it occurs exactly at the midpoint, and it equals A0.35|A| - 0.35 to full precision. The third one is the point of the exercise. The deviation is not an empirical curiosity; it is the sagitta of a circular arc, and you could have predicted it before writing any code. Then repeat with A = (1.2, 0.45) and B = (1.2, -0.45) and check whether your closed-form prediction still holds, and if not, work out what is different about that pair.

What you can now do

You can explain why a step command is a violent thing to hand a controller, and quote what it costs in torque demand and peak speed. You can choose between joint-space and Cartesian interpolation on the merits, predict the arc that joint-space interpolation traces when both endpoints sit at the same radius, and say why validating two endpoints tells you nothing about the path between them. You can size a trapezoidal profile from a speed and an acceleration limit, recognise when it degenerates to a triangle, and put a number on what smoothing it costs you in cycle time.

What you can now do

You can generate a timed trajectory, choose between joint-space and Cartesian interpolation for a reason, and size a trapezoidal velocity profile from a speed and acceleration limit.