30 min

Composing transform chains: reading a product without sign errors

Stringing poses together from world to base to joint to hand, and using the subscripts as a type check on the order.

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: TWH=TWBTBSTSETEHT_{WH} = T_{WB}\,T_{BS}\,T_{SE}\,T_{EH}. 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

read T_AB as 'the pose of frame B, expressed in frame A' {W} the room {B} robot base {S} shoulder joint {E} elbow joint {H} hand / gripper T_WB T_BS T_SE T_EH T_WH = T_WB · T_BS · T_SE · T_EH B S E touching subscripts must match, then cancel · W←B, B←S, S←E, E←H leaves W←H The classic error T_EH · T_SE · T_BS · T_WB the same four factors reversed · H and S never touch, so nothing cancels
The frame chain from world to base to shoulder to elbow to hand. Each arrow is one transform, the arrows multiply out in the same left-to-right order you walked them, touching subscripts match and cancel to leave T_WH, and the same four factors written backwards cancel nothing.

Wider than the screen; scroll it sideways.

Look at what each factor actually contains.

TWBT_{WB} is where the robot is bolted down. Measured once, when the robot was installed, and constant until somebody moves the table.

TBST_{BS}, TSET_{SE}, TEHT_{EH} 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 TSET_{SE} is really TSE(θelbow)T_{SE}(\theta_{\text{elbow}}), 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 TABT_{AB} means “the pose of B, expressed in A”, which is also “the function from B-coordinates to A-coordinates”. Now line the factors up:

TWC=TWBTBCW-B, B-CW-Cthe B’s touch, so they cancelTWC=TBCTWBB-C, W-B?C and W never touch: meaningless\begin{aligned} T_{WC} &= T_{WB}\,T_{BC} && \text{W-B, B-C} \rightarrow \text{W-C} && \text{the B's touch, so they cancel} \\ T_{WC} &= T_{BC}\,T_{WB} && \text{B-C, W-B} \rightarrow \text{?} && \text{C and W never touch: meaningless} \end{aligned}

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.

one product, two ways to read it the walk: W to B to S to E to H T_WB · T_BS · T_SE · T_EH · p_H the point: T_EH touches p first, T_WB last
The product T_WB times T_BS times T_SE times T_EH applied to a point in the hand frame. Read left to right it is the walk from world out to hand. Read right to left it is the order in which the factors touch the point.

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 z=0.0424z = 0.0424, about four centimetres above the base’s own z=0z = 0 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.

QuantityValue (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:

(TWBTBH)1=TBH1TWB1=THBTBW\left(T_{WB}\,T_{BH}\right)^{-1} = T_{BH}^{-1}\,T_{WB}^{-1} = T_{HB}\,T_{BW}

Check it with the subscripts: H-B, B-W, so the Bs touch and it leaves H-W. Written the other way round, TWB1TBH1T_{WB}^{-1}\,T_{BH}^{-1} is TBWTHBT_{BW}\,T_{HB}, 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.

to go back, reverse the order and invert every factor T_WH = T_WB · T_BH W B H T_HW = T_HB · T_BW W B H socks then shoes going on; shoes then socks coming off
Going out from world to base to hand multiplies T_WB by T_BH. Coming back multiplies T_HB by T_BW, the same factors in reversed order with each one inverted.

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:

TBC=TWB1TWC=TBWTWCT_{BC} = T_{WB}^{-1}\,T_{WC} = T_{BW}\,T_{WC}

B-W, W-C, so the Ws cancel and it leaves B-C. Verified: composing TWBT_{WB} and TBCT_{BC} into TWCT_{WC}, then recovering it with TWB1TWCT_{WB}^{-1}\,T_{WC}, returned the original TBCT_{BC} 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.

Drag the joint angles of a four-frame chain and watch each factor and the running product change. The static version: with all joints at zero the product collapses to the sum of the link offsets, and rotating any joint moves every frame downstream of it while leaving every frame upstream untouched.

Check yourself

1. You have TWBT_{WB} (the base in the world) and TWCT_{WC} (the camera in the world). Write TBCT_{BC} and prove the order with subscripts.

TBC=TWB1TWCT_{BC} = T_{WB}^{-1}\,T_{WC}, which is TBWTWCT_{BW}\,T_{WC}. Reading the subscripts: B-W then W-C, so the Ws touch and cancel, leaving B-C. Written the other way, TWCTBWT_{WC}\,T_{BW} 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 TBCT_{BC} 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: (AB)1=B1A1(A\,B)^{-1} = B^{-1}A^{-1}. Leaving the order alone gives A1B1A^{-1}B^{-1}, 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 TBCT_{BC}. 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 Rt-R^{\top}\mathbf{t} 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 TT1T\,T^{-1} from the identity was 1.8e-15, and of T1TT^{-1}T 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.

What you can now do

You can write a transform chain in the correct order, prove it with subscript cancellation, and reverse one without inverting the whole product.