30 min

Rotations in 2D: a table of where the axes land

A 2D rotation matrix is nothing but the two places the unit axes end up, written side by side as columns.

Where you are. You can describe one physical point in two different frames and keep the labels straight. This lesson opens up the RR that appeared in pW=R(θ)pB+t\mathbf{p}_W = R(\theta)\,\mathbf{p}_B + \mathbf{t} and shows you it is only a small table with a very literal meaning.

Turn a sticky note

Put a sticky note flat on the desk. In one corner draw a dot, and from that dot draw two short arrows: one along the bottom edge, one up the side edge. Then scribble whatever you like across the rest of the note. Your initials, a smiley, a wiring diagram.

Now hold a finger on the dot and turn the note about forty degrees.

Everything moved. Every point of the scribble is somewhere new. But you did not move any of them; you moved the note, and they were stuck to it.

Here is the part worth stopping on. Imagine a friend who watched only the two arrows and never saw the scribble. They could still tell you where every single scribbled point ended up. Not approximately. Exactly. Because every point on that note was some amount along the first arrow plus some amount along the second, and turning the note did not change those two amounts; it only moved the two arrows.

Two arrows carry the whole thing. That is this lesson.

The idea in one paragraph

Turning things in a plane is a single operation applied to every point at once, and the operation is fully described by where the two unit axes end up. Write those two landing places side by side as columns and you have a 2×2 table. Multiplying a point by that table is exactly the instruction “rebuild the point out of the moved axes.” The table is called a rotation matrix, and the most useful habit in this whole module is reading one column by column: column one is where x went, column two is where y went. Two properties follow immediately, and both are free debugging tools. The columns stay unit length and perpendicular, and the determinant stays exactly +1. When either one drifts, the matrix has quietly stopped being a rotation, and your robot’s poses have quietly started lying to you.

Where the two axes land

Start from the definition of sine and cosine, because that is all you need.

A point sitting on the unit circle at angle ϕ\phi, measured counterclockwise from the +x direction, has coordinates (cosϕ,sinϕ)(\cos\phi, \sin\phi). That is not a theorem; it is what those two functions mean.

The x-axis unit vector, x^=(1,0)\hat{\mathbf{x}} = (1, 0), sits on that circle at ϕ=0\phi = 0. Turn the plane by θ\theta and it is now at angle θ\theta. So it lands at:

x^    (cosθ,sinθ)\hat{\mathbf{x}} \;\rightarrow\; (\cos\theta, \sin\theta)

The y-axis unit vector, y^=(0,1)\hat{\mathbf{y}} = (0, 1), sits on the same circle at ϕ=90\phi = 90^{\circ}. Turn by θ\theta and it is at 90+θ90^{\circ} + \theta:

y^    (cos(90+θ),sin(90+θ))=(sinθ,cosθ)\hat{\mathbf{y}} \;\rightarrow\; (\cos(90^{\circ} + \theta), \sin(90^{\circ} + \theta)) = (-\sin\theta, \cos\theta)

That last step is the quarter-turn shift between sine and cosine. You can read it off the two graphs, or take the picture at face value: a quarter turn sends “pointing right” to “pointing up,” so the two components swap and one picks up a minus sign. Check it with numbers if you like. At θ=40\theta = 40^{\circ}, (cos40,sin40)=(0.766,0.643)(\cos 40^{\circ}, \sin 40^{\circ}) = (0.766, 0.643), and turning that a further quarter turn gives (0.643,0.766)(-0.643, 0.766), which is (sin40,cos40)(-\sin 40^{\circ}, \cos 40^{\circ}).

Why writing them as columns is the same as multiplying

Every point in the plane has a recipe. A point p=(a,b)\mathbf{p} = (a, b) means “go aa along x^\hat{\mathbf{x}}, then bb along y^\hat{\mathbf{y}}”:

p=ax^+by^\mathbf{p} = a\,\hat{\mathbf{x}} + b\,\hat{\mathbf{y}}

Turning the plane is rigid, so the recipe survives the turn untouched. The turned point is aa along the turned x^\hat{\mathbf{x}}, plus bb along the turned y^\hat{\mathbf{y}}. That is the sticky-note observation written as algebra:

Rp=a(cosθ,sinθ)+b(sinθ,cosθ)=(acosθbsinθ,  asinθ+bcosθ)\begin{aligned} R\,\mathbf{p} &= a\,(\cos\theta, \sin\theta) + b\,(-\sin\theta, \cos\theta) \\ &= (a\cos\theta - b\sin\theta,\; a\sin\theta + b\cos\theta) \end{aligned}

the recipe never changes · only the two ingredients turn x y 1.2 1.6 p (1.60, 1.20) R(40°) R x R y 1.2 1.6 R p (0.45, 1.95) p = 1.6 x + 1.2 y R p = 1.6 (R x) + 1.2 (R y) |p| = |R p| = 2.00 · turning cannot change a length
The point p is 1.6 along x plus 1.2 along y. After a 40 degree turn the same two coefficients rebuild the rotated point from the two rotated axes, and the length is unchanged at 2.00.

Wider than the screen; scroll it sideways.

Now look at what a matrix-vector product actually computes. Stack the two landing places as columns:

R(θ)=[cosθsinθsinθcosθ]R(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix}

Multiply that by (a,b)(a, b) and you get (acosθbsinθ,  asinθ+bcosθ)(a\cos\theta - b\sin\theta,\; a\sin\theta + b\cos\theta). The same thing, character for character. Matrix multiplication is not a separate mechanism you have to learn on top of the geometry; it is the geometry, written down so a computer can execute it.

Reading a matrix without multiplying anything

This is the payoff, and it is worth practising until it is automatic. Someone hands you:

R=[0110]R = \begin{bmatrix} 0 & -1 \\ 1 & 0 \end{bmatrix}

Do not multiply. Read the columns. Column one is (0,1)(0, 1), so x landed on +y. Column two is (1,0)(-1, 0), so y landed on −x. Right went to up, up went to left. That is a quarter turn counterclockwise, and you can confirm it by setting θ=90\theta = 90^{\circ} in the formula: cos90=0\cos 90^{\circ} = 0, sin90=1\sin 90^{\circ} = 1.

a rotation turns a vector · it never changes the vector's length x y 1 1 θ = 40° R · x R · y v (1.70, 0.35) R(θ) · v (1.08, 1.36) R is a table of where the axes land R(40°) = 0.77 0.64 −0.64 0.77 column 1: where x lands = (cos θ, sin θ) column 2: where y lands = (−sin θ, cos θ) the arc has one radius: |v| = |R · v| = 1.74
A vector v turned by 40 degrees. Both tips sit on the same arc, so the length is untouched and only the direction changed. The two thin arrows show where the unit x and y axes land, and those two landings are exactly the columns of R.

Wider than the screen; scroll it sideways.

Two checks you get for free

Both properties fall out of the columns being the turned axes, and both are cheap assertions you should be writing in real code.

The columns are unit length and perpendicular. Column one has squared length cos2θ+sin2θ\cos^2\theta + \sin^2\theta, which is 1, so its length is 1. So is column two’s. Their dot product is (cosθ)(sinθ)+(sinθ)(cosθ)=0(\cos\theta)(-\sin\theta) + (\sin\theta)(\cos\theta) = 0. Of course: they are the original axes, which were unit length and perpendicular, and turning changed neither. A matrix whose columns behave this way is called orthonormal, and the compact way to say all of it at once is RR=IR^{\top}R = I.

That identity hands you something practical. If RR=IR^{\top}R = I, then RR^{\top} is the inverse. Undoing a rotation costs you a transpose, not a linear solve. And R(θ)R(\theta)^{\top} is exactly R(θ)R(-\theta), which is what “undo the turn” should mean.

The determinant is exactly +1. For a 2×2 matrix the determinant is adbcad - bc, which here is cosθcosθ(sinθ)(sinθ)=cos2θ+sin2θ=1\cos\theta \cdot \cos\theta - (-\sin\theta)(\sin\theta) = \cos^2\theta + \sin^2\theta = 1. Geometrically the determinant is what the matrix does to the area of the unit square, with a sign that records whether the corners still go round the same way.

dashed = the unit square before · filled = where each matrix sends it · arrows 1 and 2 are its two columns 1 2 det = +1 area kept · sweep kept 1 2 det = −1 same square · sweep reversed 1 2 det = 0.64 every length × 0.8 · area × 0.64
Three matrices applied to the same dashed unit square. A rotation keeps both area and sweep direction, determinant plus one. Swapping the two axes lands on the very same square but sweeps the other way, determinant minus one. A rotation scaled by 0.8 shrinks the square, determinant 0.64.

Wider than the screen; scroll it sideways.

What you checkA real rotation givesWhat another value means
RRR^{\top}Rthe identitythe columns are no longer unit length or no longer perpendicular; the matrix now stretches or shears
det(R)\det(R)exactly +1+11-1 is a mirror; anything else is a scale, so every pose it touches is growing or shrinking

What drift actually looks like

Checks that never fire are decoration. This one fires.

Take a small rotation, R(0.001rad)R(0.001\,\text{rad}), and compose it with itself a million times, which a controller running at 500 Hz gets through in about half an hour. In float64 the result is still a rotation to ten decimal places: its determinant differs from 1 by about 1.6e-11. Nothing to do.

Do the same arithmetic in float32 and the determinant comes out at 1.0465. The columns are no longer unit length; they measure about 1.023. That matrix is not turning things any more, it is turning them and inflating them by 2.3%, and it will keep inflating them for as long as the process runs.

Composing turns, and one thing that will stop being true

Multiply two rotations together and you get R(a)R(b)=R(a+b)R(a)\,R(b) = R(a + b), agreeing to about a part in 10¹⁶, which is as exact as floating point gets. Turning by 37° and then by 58° is turning by 95°, which is the least surprising sentence in this module and still worth writing down, because two things follow.

First, in 2D the order does not matter: a+ba + b and b+ab + a are the same number, so R(a)R(b)=R(b)R(a)R(a)\,R(b) = R(b)\,R(a). Enjoy that. It is the last time it will be true.

Second, if you actually carry out the multiplication and look at the top-left entry, you get cosacosbsinasinb\cos a \cos b - \sin a \sin b, and it has to equal cos(a+b)\cos(a + b). The angle-addition formulas are not extra trigonometry to memorise; they are what matrix multiplication does to two rotations.

Check yourself

1. Read this matrix without multiplying anything: R = [[0.5, −0.866], [0.866, 0.5]]. Which way, and by how much?

Column one is (0.5,0.866)(0.5, 0.866), so the x-axis landed pointing up and to the right at a fairly steep angle. Column two is (0.866,0.5)(-0.866, 0.5), the same direction turned a further quarter turn. Since column one must be (cosθ,sinθ)(\cos\theta, \sin\theta) and cosθ=0.5\cos\theta = 0.5 with sinθ\sin\theta positive, θ=60\theta = 60^{\circ} counterclockwise. The whole read is: “where did x go, and what angle is that?”

2. A colleague hands you [[0, 1], [1, 0]] and calls it a rotation. Disprove it in one line, and say what it actually does.

det=0011=1\det = 0 \cdot 0 - 1 \cdot 1 = -1, so it is not a rotation. It passes the orthonormality check, which is exactly why the determinant check is not redundant. What it does is swap x and y: a mirror across the 45° line. Handedness is reversed, and no angle θ\theta produces it.

3. After an hour of composing small updates your matrix has det = 0.94. What is physically happening, and what are the two fixes?

It is no longer a pure rotation; it now shrinks. In 2D the determinant is the area factor, and drift from composing rotations is close to even across directions, so each length is scaled by roughly 0.940.9695\sqrt{0.94} \approx 0.9695, about 3% short. Every pose passing through it comes out slightly closer to the origin than the truth, and the error compounds. Fix one: re-orthonormalise, snapping the matrix back to the nearest true rotation. Fix two: stop composing raw matrices for long-lived state and keep orientation in a form you can cheaply renormalise. Also check your dtype, because float32 is usually the reason.

4. Why is R⁻¹ = Rᵀ, and why does anybody care?

Because the columns are unit length and mutually perpendicular, RR=IR^{\top}R = I by direct computation, which is the definition of an inverse. Caring is practical: inverting a frame transform is one of the most common operations in a robotics codebase, and this makes it a memory shuffle rather than a numerical solve. It is also exact, so it introduces no error of its own, unlike a general matrix inverse.

5. You rotate a robot’s footprint 90° and it ends up on the other side of the room instead of spinning in place. What did you forget?

That RR rotates about the origin, and the footprint is not at the origin. You applied RpR\,\mathbf{p}, which swings every point around the world origin at whatever radius it happened to sit at. What you wanted was R(pcentre)+centreR\,(\mathbf{p} - \mathbf{centre}) + \mathbf{centre}: move the pivot to the origin, turn, move it back.

6. R(a) R(b) = R(a + b) means 2D rotations can be applied in either order. Why should you refuse to carry that intuition into the next lesson?

Because the reason it holds is that a 2D rotation is described by a single number, and numbers add in either order. In 3D a rotation needs an axis as well as an angle, and two rotations about different axes are not one rotation about a shared axis, so there is no single number left to add. Order becomes load-bearing, and it stays load-bearing for the rest of the course.

Do this

Twenty minutes, and the first half needs no computer.

1. On paper. Read each of these by columns only. No multiplication, no calculator. Write down in words where x went, where y went, and what turn that is.

A=[1001]B=[0110]C=[1001]A = \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} \qquad B = \begin{bmatrix} 0 & 1 \\ -1 & 0 \end{bmatrix} \qquad C = \begin{bmatrix} -1 & 0 \\ 0 & -1 \end{bmatrix}

Then say which of them are their own inverse, using only what the columns told you. There is more than one, and the reason is an angle, not an accident.

2. In a REPL. Build the matrix yourself and make the two checks fire.

import numpy as np

def R(t):
    c, s = np.cos(t), np.sin(t)
    return np.array([[c, -s], [s, c]])

M = R(np.deg2rad(40))
print(M.T @ M)                    # the identity, to ~1e-16
print(np.linalg.det(M))           # 1.0
print(np.allclose(M.T, R(np.deg2rad(-40))))   # True: transpose is undo

Now break it on purpose, by composing a small turn a million times in single precision.

step = R(1e-3).astype(np.float32)
acc = np.eye(2, dtype=np.float32)
for _ in range(1_000_000):
    acc = (acc @ step).astype(np.float32)
print(np.linalg.det(acc.astype(np.float64)))           # ~1.0465
print(np.linalg.norm(acc.astype(np.float64), axis=0))  # ~1.023, not 1

Then change every np.float32 to np.float64 and run it again. Watching the same loop produce a rotation in one dtype and a slowly inflating not-quite-rotation in the other is the whole argument for asserting det\det in your own code, made once, cheaply, on a laptop.

What you can now do

You can derive the 2D rotation matrix from nothing but the definition of sine and cosine, read any rotation matrix by asking where each axis went, invert one for free with a transpose, and spot a matrix that has stopped being a rotation by checking RRR^{\top}R and det\det. You can also say why a rotation always turns about the origin, and what to do when that is not what you wanted.

What you can now do

You can derive the 2D rotation matrix from trigonometry, read any rotation matrix straight off its columns, and use orthonormality and det = +1 to catch one that has quietly stopped being a rotation.