4. Thinking of rotations in terms of reflections
The relationship between rotation and reflections is not immediately obvious, but easy to understand and extremely useful. We will explore this relationship algebraically first and then geometrically.
First we need to understand the concept of a reflection matrix.
Reflection is also linear
It can be shown that reflection of sum of two vectors is the sum of reflections of those vectors. The scaling argument also holds.
4.1 Reflection matrices
One can think of reflection around a point, a line or a hyperplane. We start with reflection around a line and then derive some properties for reflection around a hyperplane.
4.1.1 Reflection around a line
import math
pie.clear()
pie.scalar(70, out='unit')
GRAY, BLUE, PINK, GREEN, GOLD = '#71717a', '#1d4ed8', '#be185d', '#047857', '#ca8a04'
phi = pie.scalar(0.7, out='phi')
VX, VY, LLEN = 2, 0.7, 2.6
# One part of reflecting v = (VX, VY) around the line along u = (cos φ, sin φ), as (x, y)
def reflect_line_point(phi, part):
ux, uy = math.cos(phi), math.sin(phi)
dot = ux * VX + uy * VY
par = (dot * ux, dot * uy)
parts = {
'par': par,
'perp': (VX - par[0], VY - par[1]),
'neg_perp': (par[0] - VX, par[1] - VY),
'out': (2 * par[0] - VX, 2 * par[1] - VY),
'end': (LLEN * ux, LLEN * uy),
'neg_end': (-LLEN * ux, -LLEN * uy),
'hyp_end': (-LLEN * uy, LLEN * ux),
'hyp_neg_end': (LLEN * uy, -LLEN * ux),
}
return parts[part]
line_pt = pie.lift(reflect_line_point, pie.Point)
part_x = pie.lift(lambda phi, part: reflect_line_point(phi, part)[0], pie.Scalar)
part_y = pie.lift(lambda phi, part: reflect_line_point(phi, part)[1], pie.Scalar)
mirror = pie.line(line_pt(phi, 'neg_end', out='nep', hidden=True), line_pt(phi, 'end', out='ep', hidden=True), out='mirror', stroke=GRAY, hidden=True)
pie.la_vec2d(VX, VY, out='vV', stroke=BLUE)
This vector denotes the point we want to reflect.
This is the line of reflection. It is represented by a unit vector .
pie.show(mirror)
- It keeps the component of along the same.
- The component parallel to :
par_x, par_y = part_x(phi, 'par'), part_y(phi, 'par')
pie.la_vec2d(par_x, par_y, out='vPar', stroke=GREEN)
- The component of perpendicular to is negated.
- The component perpendicular to :
par_x, par_y = part_x(phi, 'par'), part_y(phi, 'par')
pie.la_vec2d(part_x(phi, 'perp'), part_y(phi, 'perp'), par_x, par_y, out='vPerp', stroke=PINK)
pie.la_vec2d(part_x(phi, 'neg_perp'), part_y(phi, 'neg_perp'), par_x, par_y, out='neg_vPerp', stroke=PINK)
pie.la_vec2d(part_x(phi, 'out'), part_y(phi, 'out'), out='vOut', stroke=GOLD)
The sum of these two components - the parallel and the negative of perpendicular - gives the reflected vector.
- The original vector:
- The reflected vector:
Animate the line of reflection
pie.translate_scalar_to(phi, 3.1415, animate=True)
The matrix reflects any vector around the line given by a unit vector .
This equation, and the entire line of reasoning, holds for any higher dimensional space as well.
4.1.2 Reflection around a hyperplane
Now we define reflection around a hyperplane.
What is a hyperplane?
It means a line in 2D, a plane in 3D and a dimensional space in a -dimensional space.
We represent this hyperplane using a unit vector that is orthogonal to this space. This reflection flips the component of the vector parallel to the vector while keeping the rest of the components the same.
pie.line(line_pt(phi, 'hyp_end', out='eep', hidden=True), line_pt(phi, 'hyp_neg_end', out='neep', hidden=True), out='hyperplane', stroke=GRAY)
The reflection we just saw can be seen as a reflection around this hyperplane
Using the same logic as above, we have:
- The reflected vector:
Thus, the reflection matrix is:
4.2 Algebraic approach
Given a unit vector and the corresponding (hyperplane) reflection matrix , it is easy to see that
- : it is symmetric.
- : its transpose is its inverse.
Proof
since for a unit vector .
- A reflection matrix has eigenvalues equal to and one eigenvalue equal to .
Outline of proof
This matrix maps to and keeps the rest of the orthogonal vectors the same. Both and have the same length.
To summarize:
| Reflection matrix | Rotation matrix | |
|---|---|---|
| Orthogonality | ||
| Determinant |
Now it is easy to see that if are two reflection matrices,
Thus is a rotation matrix. In other words:
A composition of two reflections is a rotation.
4.3 Geometric approach
We visualize a rotation as a composition of two reflections around two axes, working entirely in the 2D plane first where each axis is a line through the origin.
4.3.1 Visualizing in 2D
import math
pie.clear()
pie.scalar(70, out='unit')
GRAY, BLUE, PINK, GOLD, GREEN = '#71717a', '#1d4ed8', '#be185d', '#ca8a04', '#047857'
origin = pie.point(0, 0, out='origin', hidden=True)
circ = pie.circle(origin, 2, out='circ', stroke=GRAY)
theta = pie.scalar(0.6, out='theta')
BETA, RR, MLEN = 0.35, 2, 2.6
V0 = (RR * math.cos(BETA), RR * math.sin(BETA))
# A point of the figure, as (x, y), for the angle θ between the mirrors L1 (fixed, horizontal) and L2
def two_mirrors_point(theta, key):
u2 = (math.cos(theta), math.sin(theta))
def reflect(u, v):
dot = u[0] * v[0] + u[1] * v[1]
return (2 * dot * u[0] - v[0], 2 * dot * u[1] - v[1])
v1 = reflect((1, 0), V0)
v2 = reflect(u2, v1)
c2, s2 = math.cos(2 * theta), math.sin(2 * theta)
points = {
'v1': v1,
'v2': v2,
'vr': (c2 * V0[0] - s2 * V0[1], s2 * V0[0] + c2 * V0[1]),
'lv1': (1.18 * v1[0], 1.18 * v1[1]),
'lv2': (1.28 * v2[0], 1.28 * v2[1]),
'e2': (MLEN * u2[0], MLEN * u2[1]),
'ne2': (-MLEN * u2[0], -MLEN * u2[1]),
'l2': (1.08 * MLEN * u2[0], 1.08 * MLEN * u2[1]),
'thl': (0.95 * math.cos(theta / 2), 0.95 * math.sin(theta / 2)),
't2l': (1.7 * math.cos(BETA + theta), 1.7 * math.sin(BETA + theta)),
}
return points[key]
fig_pt = pie.lift(two_mirrors_point, pie.Point)
fig_x = pie.lift(lambda theta, key: two_mirrors_point(theta, key)[0], pie.Scalar)
fig_y = pie.lift(lambda theta, key: two_mirrors_point(theta, key)[1], pie.Scalar)
mirror1 = pie.line(pie.point(-MLEN, 0, out='ne1p', hidden=True), pie.point(MLEN, 0, out='e1p', hidden=True), out='mirror1', stroke=GRAY)
mirror2 = pie.line(fig_pt(theta, 'ne2', out='ne2p', hidden=True), fig_pt(theta, 'e2', out='e2p', hidden=True), out='mirror2', stroke=GRAY)
# The input v
pie.la_vec2d(V0[0], V0[1], out='vV', stroke=BLUE)
pie.text('v', 1.18 * V0[0], 1.18 * V0[1], 16, out='vlab', stroke=BLUE)
# The mirror labels and the angle θ between them
pie.text('L1', 1.08 * MLEN, 0, 15, out='l1lab', stroke=GRAY)
pie.text('L2', fig_x(theta, 'l2'), fig_y(theta, 'l2'), 15, out='l2lab', stroke=GRAY)
pie.arc(origin, 0.75, 0, pie.lift(math.degrees, pie.Scalar)(theta), out='arc_theta', stroke=GRAY)
pie.text('θ', fig_x(theta, 'thl'), fig_y(theta, 'thl'), 16, out='thlab', stroke=GRAY)
# v reflected across L1, then across L2
pie.la_vec2d(fig_x(theta, 'v1'), fig_y(theta, 'v1'), out='vV1', stroke=PINK)
pie.text('v1', fig_x(theta, 'lv1'), fig_y(theta, 'lv1'), 16, out='v1lab', stroke=PINK)
pie.la_vec2d(fig_x(theta, 'v2'), fig_y(theta, 'v2'), out='vV2', stroke=GOLD)
pie.text('v2', fig_x(theta, 'lv2'), fig_y(theta, 'lv2'), 16, out='v2lab', stroke=GOLD)
# v rotated directly by 2θ
pie.la_vec2d(fig_x(theta, 'vr'), fig_y(theta, 'vr'), out='vRot', stroke=GREEN)
end_deg = pie.lift(lambda theta: math.degrees(BETA + 2 * theta), pie.Scalar)
pie.arc(origin, 1.5, math.degrees(BETA), end_deg(theta), out='arc_two', stroke=GREEN)
pie.text('2θ', fig_x(theta, 't2l'), fig_y(theta, 't2l'), 16, out='t2lab', stroke=GREEN)
We start with the vector we want to rotate. Here is the input .
Now place two mirror lines through the origin. The first, axis , is fixed along the horizontal.
The second, axis , is tilted away from it.
The angle between the two axes is marked by the arc.
- Reflecting across gives .
- Reflecting across gives , which lands back on the guide circle of radius , since a reflection preserves length.
Now check the claim. Rotate the original directly by : the green vector lands exactly on top of , and its arc is twice the arc between the mirrors.
Two reflections equal one rotation by twice the inter-mirror angle.
Widen the angle between the axes and watch , , both arcs and the green comparison vector move together, staying locked to the identity as changes.
pie.translate_scalar_to(theta, 1, animate=True)
We will use this insight - that two reflections compose into a rotation - to describe a rotation using geometric algebra next.
4.3.2 Visualizing in 3D
In 3D each hyperplane is a plane and this is genuinely hard to picture, so we reduce it to 2D without loss of generality and let the flat picture, where the angles are honest, carry the argument.
An observation we use as as trick
Consider a 3D reflection around a hyperplane represented by a unit vector . If lies in the XY plane, it will only negate the component of an input vector that lies in this place. It will leave the z-value of untouched. In other words:
Thus, two or more reflections whose vectors lie in the XY plane when composed will all leave the z-coordinate unchanged:
pie.clear()
pie.scalar(32, out='unit')
pie.scalar(0, out='gridOpacity')
GRAY, GROUND, GROUND_EDGE, DARK = '#71717a', '#d4d4d8', '#a1a1aa', '#27272a'
corners = [
pie.point(0, 3, out='gnd_a', hidden=True),
pie.point(5.1961524, 0, out='gnd_b', hidden=True),
pie.point(0, -3, out='gnd_c', hidden=True),
pie.point(-5.1961524, 0, out='gnd_d', hidden=True),
]
pie.polygon(corners, out='ground', fill=GROUND, stroke=GROUND_EDGE)
pie.line(pie.point(0, -3, out='z_bot', hidden=True), pie.point(0, 3.4, out='z_top', hidden=True), out='z_axis', stroke=DARK)
pie.la_vec2d(2.5980762, -1.5, out='scaffold_x', stroke=GRAY)
pie.la_vec2d(-2.5980762, -1.5, out='scaffold_y', stroke=GRAY)
# Labels for the axis, the triad and the plane
pie.text('z : rotation axis (fixed line)', 0.3, 3.3, 13, out='tbox_0')
pie.text('x', 2.7980762, -1.8, 13, out='tbox_1')
pie.text('y', -3.0980762, -1.8, 13, out='tbox_2')
pie.text('xy-plane : every reflection stays inside here', -2, -3.6, 13, out='tbox_3')
pie.text("The two mirror normals span a plane; take it as the xy-plane, so z is the shared rotation axis. Any vector's z-component is left untouched by both reflections, so only its in-plane part moves. Tracking that part in this flat plane loses nothing.", 0, -7, 12, 9.2, -1, out='tbox_4')
# A vector v, its image rotated about z, and a level line joining their tips
GREEN, TEAL, LEVEL = '#047857', '#0d9488', '#a1a1aa'
pie.la_vec2d(1.348063, 0.252281, out='vec_v', stroke=GREEN)
pie.la_vec2d(-1.348063, 0.252281, out='vec_vr', stroke=TEAL)
pie.line(pie.point(1.348063, 0.252281, out='vtip', hidden=True), pie.point(-1.348063, 0.252281, out='vrtip', hidden=True), out='level', stroke=LEVEL)
pie.text('same height, so same z', 0, 1, 12, out='tbox_z')
Choose coordinates and look at the xy-plane as a faint ground. Perpendicular to it stands the z-axis, which we take as the fixed rotation axis. The in-plane x and y directions span that ground.
Labelling the axis, the triad and the plane fixes the frame.
Here is the argument without loss of generality: the two mirror normals span a plane, which we take as the xy-plane, so is the fixed rotation axis.
A general need not lie in this plane, but each normal has no -component, so both reflections leave 's -component untouched. Only its in-plane part ever moves, and it stays in the plane. Tracking here loses no generality; the axial part just rides along fixed.
To see this on the scaffold itself, add a vector and its rotated image. Here is a vector , lifted off the ground to height .
Rotating it about the -axis gives its image : the in-plane part has turned, but the tip does not rise or fall.
The level connector shows both tips sit at one height, so is unchanged.
Thus we can use the same visualization as for the 2D case to illustrate the connection between rotations and composition of two reflections in the general case.