Where you are. You can build a single 4x4 pose, apply it to a point, and invert it with the closed form. This lesson strings several of them together, which is what a robot arm actually is.
Where the spare key is
A neighbour tells you where the spare key lives.
Second building on the left. In through the side door. Third room on the right. The desk facing the window. Top-right drawer. At the back, under the manual.
Six instructions, and not one of them means anything alone. “Third room on the right” is undefined until you are through the side door, facing whatever way that door left you facing. “Top-right drawer” needs the desk, and which desk needs the room. Every step is measured from where the previous step put you, in the directions the previous step left you pointing.
Now shuffle them. Read the list backwards and you are standing in the street holding a drawer. Swap two in the middle and you get a set of instructions that still sounds perfectly reasonable, still terminates, and puts you in the wrong building. That last case is the dangerous one, and it is the one this lesson is about.
A robot arm is that list. The hand’s position in the room is never stored anywhere. It is computed, every control cycle, by walking a chain of local facts and multiplying them together.
The idea in one paragraph
Frames come in chains: the world holds the robot’s base, the base holds the shoulder, the shoulder holds the elbow, the elbow holds the hand. Each link of the chain is a 4x4 pose describing one thing relative to the thing before it, and each of those is a purely local fact you can measure once with a ruler or read off a joint encoder. The pose of the hand in the world is the product of the whole chain, written in the order you walked it: . The notation checks itself, because touching subscripts have to match and then cancel. Reversing a chain is not one big inverse: it is the same factors in reverse order, each one individually inverted, exactly like taking off shoes before socks.
An arm is a list of local facts
Wider than the screen; scroll it sideways.
Look at what each factor actually contains.
is where the robot is bolted down. Measured once, when the robot was installed, and constant until somebody moves the table.
, , are each one link. The fixed part is geometry: how long the link is, which way the next joint’s axis points. The moving part is a single joint angle, read from an encoder a few hundred times a second. So a factor like is really , a small function of one number.
That is the property worth noticing. Nothing in this chain requires anybody to know anything global. The person who designed the elbow bracket did not need to know where the robot would be installed, and the installer did not need to know the bracket. Each measurement is local, and multiplication assembles them.
Subscripts that cancel
Here is the mechanical check that removes most order bugs before you run anything.
Write the outer frame first and the inner frame second, so means “the pose of B, expressed in A”, which is also “the function from B-coordinates to A-coordinates”. Now line the factors up:
Adjacent subscripts must match. When they do, they cancel and leave the outer pair. When they do not, the expression has no meaning, and your head should reject it the way a type checker rejects passing a string where a socket goes.
Carry the check into your variable names and you get it for free at the call site:
cup_W = apply_T(compose(T_WB, T_BC), cup_C) # W-B, B-C, C. reads correctly
Read that line left to right: W-B, B-C, C. Each pair touches. The answer is in W. If somebody later inserts a factor in the wrong place, the reading breaks visibly in the source, before it breaks silently in the robot.
Two directions, both correct
This is the part that reliably confuses people, so it gets its own section.
Matrix multiplication applies the rightmost factor first. Yet the chain reads left to right as a walk outward from the world. Both statements are true at once, and they are describing different things.
Wider than the screen; scroll it sideways.
The left-to-right reading is about frames: start at the world, step into the base, into the shoulder, into the elbow, into the hand. That is how you build the expression.
The right-to-left reading is about the point: it starts life in hand coordinates, so the hand’s transform touches it first, then the elbow’s, and the base’s last. That is how the arithmetic runs.
They agree because they have to. Composing four random transforms into one matrix and applying it to a point gave the same answer as applying the four transforms one at a time from the right, to within 2.2e-16.
Worked chain: the camera saw a cup
Concrete numbers, all metres and all reproducible.
The robot base sits 0.40 m in front of the world origin and 0.10 m to the left, yawed 30 degrees. A camera is mounted on a post 0.15 m behind the base and 0.70 m above it, pitched down so that it looks forward and 45 degrees below horizontal. The camera reports a cup 0.90 m down its optical axis, a few centimetres off centre.
import numpy as np
from scipy.spatial.transform import Rotation
from transforms_starter import make_T, apply_T, compose, inv_T
T_WB = make_T(Rotation.from_euler("z", 30, degrees=True).as_matrix(), [ 0.40, 0.10, 0.00])
T_BC = make_T(Rotation.from_euler("y", 135, degrees=True).as_matrix(), [-0.15, 0.00, 0.70])
cup_C = np.array([0.03, -0.04, 0.90])
cup_B = apply_T(T_BC, cup_C)
cup_W = apply_T(compose(T_WB, T_BC), cup_C)
print("cup in base coordinates ", cup_B.round(4)) # [ 0.4652 -0.04 0.0424]
print("cup in world coordinates", cup_W.round(4)) # [0.8229 0.298 0.0424]
Two sanity checks before trusting any of it. The cup in base coordinates comes out at , about four centimetres above the base’s own plane, which is the table. A cup on a table should be a few centimetres up, so that passes. And it is 0.4652 m in front of the base, which is inside the reach of a small arm. Numbers that survive a physical sniff test are the cheapest bug filter you have.
| Quantity | Value (m) | What it is for |
|---|---|---|
cup_C | (0.03, -0.04, 0.90) | what the camera actually measured |
cup_B | (0.4652, -0.0400, 0.0424) | what the arm’s controller needs |
cup_W | (0.8229, 0.2980, 0.0424) | what a second robot, or a human, would need |
Notice that cup_B is the one the robot uses. The world frame is convenient for people and for talking to other machines, and the arm never asks for it.
Turning a chain around
You will want the other direction constantly, and there are two moves worth having in your fingers.
Reverse the whole chain. The inverse of a product is the product of the inverses in reverse order:
Check it with the subscripts: H-B, B-W, so the Bs touch and it leaves H-W. Written the other way round, is , which reads B-W then H-B: W and H never touch, so nothing cancels. Both forms were checked numerically on random poses; reverse order came back true, same order came back false.
Wider than the screen; scroll it sideways.
Splice two chains that share a frame. This is how you turn measurements into the transform you actually want. If you know where the base is in the world and where the camera is in the world, the camera’s pose relative to the base is:
B-W, W-C, so the Ws cancel and it leaves B-C. Verified: composing and into , then recovering it with , returned the original exactly.
The same splice answers the question a controller really asks, which is not “where is the cup” but “where is the cup, relative to my hand, right now”:
T_BH = make_T(Rotation.from_euler("z", -20, degrees=True).as_matrix(), [0.35, 0.05, 0.15])
cup_H = apply_T(compose(inv_T(T_BH), T_BC), cup_C)
print("cup, seen from the gripper", cup_H.round(4)) # [ 0.139 -0.0452 -0.1076]
print("distance to close", round(float(np.linalg.norm(cup_H)), 4)) # 0.1815
H-B, B-C, C: the subscripts chain, so the answer is in H. The gripper has 18.15 cm to travel: 13.9 cm forward, 10.8 cm down and a 4.5 cm sideways correction. That vector, refreshed every cycle, is the input to everything in the rest of this module.
Check yourself
1. You have (the base in the world) and (the camera in the world). Write and prove the order with subscripts.
, which is . Reading the subscripts: B-W then W-C, so the Ws touch and cancel, leaving B-C. Written the other way, gives W-C then B-W, where C and B never touch, so it is not an expression about anything. Numerically the first form recovers the original exactly.
2. Matrix multiplication applies the rightmost factor first, but the chain reads left to right as a walk from the world outward. Both are true. Explain how.
They describe different objects. Left to right is the order you traverse the frames while building the expression: world, base, shoulder, elbow, hand. Right to left is the order the factors touch a point, and a point in this chain starts out in hand coordinates, so the hand’s transform is the one nearest to it and acts first. The rightmost factor is the innermost frame. Composing four transforms into one matrix and applying it, versus applying them one at a time from the right, agreed to 2.2e-16.
3. Why is a wrongly ordered chain more dangerous than a chain with a wrong number in it?
Because it still runs and still returns a plausible answer. A wrong number usually produces something obviously out of range, or fails a sanity check on the table height. A reversed product returns three well-scaled floats that are simply about a different situation. In the worked example the swap moved the cup 0.75 m and put it below the table; across 2000 random four-link chains the median disagreement was 2.8 length units against link offsets of at most 1, and nothing landed within a centimetre of the truth. Nothing throws. The subscript check catches it in the source, before the robot does.
4. To go from the hand back to the world, why can you not just invert each factor and leave the order alone?
Because inverting a product reverses it: . Leaving the order alone gives , whose subscripts do not chain, and which is numerically false in general; both were checked. The intuition is that the factors were applied in order, so undoing them has to start with the last thing applied. Socks then shoes going on, shoes then socks coming off.
5. Nothing in the chain stores the hand’s pose in the world. Why is that a feature rather than an oversight?
Because every factor is a local fact somebody can actually measure, and none of them requires global knowledge. The link geometry is fixed by the mechanism and measured once. The joint variables come from encoders. The base pose comes from the installation. Store the hand’s world pose instead and you have to update it whenever any joint moves, any link is swapped, or the robot is repositioned, and the moment one of those updates is missed the stored value is silently stale. A derived quantity cannot go stale.
6. A colleague reports that their grasps are consistently off by the same few centimetres in the same direction, regardless of where the object is. Which factor would you look at first?
The one calibrated rather than measured or read from an encoder, which is usually the camera-to-base transform . A constant offset in one factor of a chain shows up as a constant offset in the result, independent of the object, and it survives every test that only ever looks at relative motion. Joint-angle errors and link-length errors change with configuration, so they produce errors that vary as the arm moves. Errors that do not vary point at the constant factors.
Do this
Finish code/transforms_starter.py. You wrote make_T, apply_T and inv_T last lesson; now fill in compose(*Ts) and the two quaternion helpers, from_quat_trans and to_quat_trans.
compose is a fold with matrix multiplication, and the only decision in it is the order. Get that decision right by writing down what compose(T_AB, T_BC, T_CD) has to return and reading the subscripts.
Then run the property tests:
python transforms_starter.py --test
It builds 1,000 random poses and checks four things that between them pin down the whole library: that composing a transform with its own inverse gives the identity in both directions, that composition is associative, that applying a composed transform equals applying the factors one at a time, and that the rotation block stays orthonormal through a three-factor chain. It finishes with the concrete case from the last lesson.
Two numbers to expect, both measured against solutions/transforms.py on this exact suite. Over 10,000 random poses the worst deviation of from the identity was 1.8e-15, and of was 1.1e-15. If your implementation is correct you should see the same order of magnitude, around 1e-15, which is floating-point noise and not error. If you see 1e-8, you have a real bug wearing a small number.
When it is green, run the worked chain from this lesson and confirm you get the cup at (0.4652, -0.04, 0.0424) in the base frame. That file is now a dependency of every remaining lesson in the module.
What you can now do
You can write a transform chain in walking order, from the world down to the hand, and check the order mechanically by making the subscripts touch and cancel. You can explain why the same product reads left to right for frames and right to left for points without either reading being wrong. You can reverse a chain by reversing the factors and inverting each one, splice two chains that share a frame to recover a transform you did not measure directly, and recognise the signature of a constant error in one calibrated factor.