Linear Transformations

Concept

A matrix is a machine that moves vectors. Feed it a vector, and it hands back another one β€” stretched, spun, flipped, or skewed. Two rules make this a linear transformation: it keeps the origin fixed, and it sends straight lines to straight lines. The punchline of the chapter: every matrix is such a machine, and every such machine is a matrix β€” and the matrix itself tells you exactly what the machine does, just by reading its columns.

Here is the whole chapter in one concrete example. The matrix

$$ \mathbf{S} = \begin{pmatrix} 2 & 0 \\ 0 & 3 \end{pmatrix} $$

doubles the $x$-coordinate and triples the $y$-coordinate of any vector it touches: the point $(1, 2)$ becomes $(2, 6)$, the point $(4, 5)$ becomes $(8, 15)$. By the end of this chapter you'll be able to look at any $2 \times 2$ matrix and picture what it does to the whole plane β€” no arithmetic required.

Why This Matters

A matrix doesn't just sit there β€” it does something. Multiplying a matrix by a vector transforms that vector into a new one: it can stretch it, shrink it, rotate it, flip it, or skew it. Understanding what a matrix does geometrically β€” not just how to multiply it β€” is the bridge from algebra to intuition. Later this will let you understand why certain weight matrices in neural networks work the way they do, but for now the goal is simpler: when you see a matrix, you should be able to picture the transformation it represents. And the picture is built from just two vectors: the basis vectors.


Mathematical Notation

Standard basis vectors: the axes, given names

In $\mathbb{R}^2$ the standard basis vectors are the two unit steps along the axes:

$$ \mathbf{e}_1 = \begin{pmatrix} 1 \\ 0 \end{pmatrix}, \qquad \mathbf{e}_2 = \begin{pmatrix} 0 \\ 1 \end{pmatrix} $$

$\mathbf{e}_1$ means "one unit to the right", $\mathbf{e}_2$ means "one unit up". (In $\mathbb{R}^3$ there is a third one, $\mathbf{e}_3 = (0, 0, 1)$ β€” one unit out of the page.) They are the simplest arrows the plane has, and every other vector is built from them:

$$ \begin{pmatrix} 3 \\ 4 \end{pmatrix} = 3 \begin{pmatrix} 1 \\ 0 \end{pmatrix} + 4 \begin{pmatrix} 0 \\ 1 \end{pmatrix} = 3\mathbf{e}_1 + 4\mathbf{e}_2 $$

This sum of scaled vectors is a linear combination: "scale each basis vector, then add the results." The numbers $3$ and $4$ are the coordinates of the point β€” they say "walk 3 steps right, then 4 steps up."

Why do we care? Because a linear transformation is completely determined by what it does to these two vectors. If you know where $\mathbf{e}_1$ and $\mathbf{e}_2$ land, you know where every vector lands:

$$ T\begin{pmatrix} 3 \\ 4 \end{pmatrix} = 3\,T(\mathbf{e}_1) + 4\,T(\mathbf{e}_2) $$

The transformation is a machine; the basis vectors are the test inputs that reveal how the machine works.

Linear Transformation Definition

A function $T: \mathbb{R}^n \to \mathbb{R}^m$ is a linear transformation if it satisfies two properties:

  1. Additivity: $T(\mathbf{u} + \mathbf{v}) = T(\mathbf{u}) + T(\mathbf{v})$
  2. Homogeneity: $T(\alpha \mathbf{v}) = \alpha T(\mathbf{v})$

Reading the notation $T: \mathbb{R}^n \to \mathbb{R}^m$ β€” yes: $\mathbb{R}^n$ is the domain (where inputs come from), $\mathbb{R}^m$ the codomain (where outputs land). The order is the arrow's β€” "from β†’ to", the same convention as every function, $f: X \to Y$: source first, target second. The letters are just names: $n$ = input dimension, $m$ = output dimension, which is why $T$'s matrix is $m \times n$ (one row per output, one column per input). Nothing forces the letter $n$ before $m$ β€” if the inputs were $m$-dimensional you'd write $T: \mathbb{R}^m \to \mathbb{R}^n$ and the matrix would be $n \times m$. The arrow's direction decides; the letters just name the sizes.

Concretely, take $T(x, y) = (x,\, y,\, x + y)$, mapping $\mathbb{R}^2$ to $\mathbb{R}^3$: the 2D point $(1, 2)$ goes in and the 3D point $(1, 2, 3)$ comes out, through the $3 \times 2$ matrix $\begin{pmatrix} 1 & 0 \\ 0 & 1 \\ 1 & 1 \end{pmatrix}$.

New to multiplying a matrix by a vector? It's the $p = 1$ case of matrix multiplication β€” see ch03, Multiplying a Matrix by a Vector. Each entry of the result is the dot product of one row of the matrix with the vector: row $[0\ 1]$ of the matrix above, dotted with $(1, 2)$, gives $0\cdot1 + 1\cdot2 = 2$ β€” the middle entry of $(1, 2, 3)$.

Together these give linearity:

$$ T(\alpha \mathbf{u} + \beta \mathbf{v}) = \alpha T(\mathbf{u}) + \beta T(\mathbf{v}) $$

These rules are not abstract β€” they say "you may add first and transform later, or transform first and add later; the answer is the same." Check with the scaling machine $\mathbf{S} = \begin{pmatrix} 2 & 0 \\ 0 & 3 \end{pmatrix}$ using $\mathbf{u} = (1, 2)$ and $\mathbf{v} = (3, 4)$:

$$ T(\mathbf{u} + \mathbf{v}) = T(4, 6) = (8, 18), \qquad T(\mathbf{u}) + T(\mathbf{v}) = (2, 6) + (6, 12) = (8, 18) \quad \checkmark $$
$$ T(2\mathbf{v}) = T(6, 8) = (12, 24), \qquad 2\,T(\mathbf{v}) = 2\,(6, 12) = (12, 24) \quad \checkmark $$

Matrix of a Transformation

Every linear transformation $T: \mathbb{R}^n \to \mathbb{R}^m$ is represented by an $m \times n$ matrix $\mathbf{A}$ whose columns are the images of the standard basis vectors:

$$ \mathbf{A} = \begin{pmatrix} | & | & & | \\ T(\mathbf{e}_1) & T(\mathbf{e}_2) & \dots & T(\mathbf{e}_n) \\ | & | & & | \end{pmatrix} $$

Concretely: our scaling machine sends $\mathbf{e}_1 = (1, 0)$ to $(2, 0)$ and $\mathbf{e}_2 = (0, 1)$ to $(0, 3)$, so its matrix is

$$ \begin{pmatrix} 2 & 0 \\ 0 & 3 \end{pmatrix} \qquad \text{first column } (2, 0), \text{ second column } (0, 3) $$

Read the columns and you've read the transformation.

Applying the transformation to a vector $\mathbf{x}$ is matrix-vector multiplication β€” a linear combination of the columns with weights $x_1, \dots, x_n$:

$$ T(\mathbf{x}) = \mathbf{A}\mathbf{x} = x_1 T(\mathbf{e}_1) + x_2 T(\mathbf{e}_2) + \dots + x_n T(\mathbf{e}_n) $$

Concretely:

$$ \mathbf{S}\begin{pmatrix} 3 \\ 4 \end{pmatrix} = 3 \begin{pmatrix} 2 \\ 0 \end{pmatrix} + 4 \begin{pmatrix} 0 \\ 3 \end{pmatrix} = \begin{pmatrix} 6 \\ 0 \end{pmatrix} + \begin{pmatrix} 0 \\ 12 \end{pmatrix} = \begin{pmatrix} 6 \\ 12 \end{pmatrix} $$

which is just "scale the coordinates": $(3, 4) \to (6, 12)$, as promised.

Composition

If $S: \mathbb{R}^n \to \mathbb{R}^m$ has matrix $\mathbf{A}$ and $T: \mathbb{R}^m \to \mathbb{R}^p$ has matrix $\mathbf{B}$, then the composition $T \circ S: \mathbb{R}^n \to \mathbb{R}^p$ (apply $S$ first, then $T$) has matrix $\mathbf{B}\mathbf{A}$:

$$ (T \circ S)(\mathbf{x}) = T(S(\mathbf{x})) = \mathbf{B}(\mathbf{A}\mathbf{x}) = (\mathbf{B}\mathbf{A})\mathbf{x} $$

Composition is matrix multiplication β€” this is why matrix multiplication is defined the way it is.

Concretely, rotate by $90Β°$ and then reflect across the $x$-axis. The point $(1, 0)$ rotates to $(0, 1)$, then reflects to $(0, -1)$. The composed matrix is

$$ \mathbf{B}\mathbf{A} = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} \begin{pmatrix} 0 & -1 \\ 1 & 0 \end{pmatrix} = \begin{pmatrix} 0 & -1 \\ -1 & 0 \end{pmatrix} $$

and indeed $\begin{pmatrix} 0 & -1 \\ -1 & 0 \end{pmatrix} \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \begin{pmatrix} 0 \\ -1 \end{pmatrix}$, matching the two-step trip.


Intuition

The unit square as a test object

The unit square β€” the $1 \times 1$ square with corners $(0,0)$, $(1,0)$, $(1,1)$, $(0,1)$ β€” is the best way to see a transformation. Watch where its four corners go and you've watched the whole plane move, because the square is just the basis vectors stretched into a shape:

y
|
(0,1) ●────────● (1,1)
      |         |
      |         |
(0,0) ●────────● (1,0)  β†’ x

The bottom-left corner is the origin (which never moves), the bottom-right corner is $\mathbf{e}_1 = (1, 0)$, and the top-left corner is $\mathbf{e}_2 = (0, 1)$. Every transformation below is shown by what it does to this square.

Scaling β€” stretch the grid

The matrix $\mathbf{S} = \begin{pmatrix} 2 & 0 \\ 0 & 3 \end{pmatrix}$ multiplies $x$ by 2 and $y$ by 3. The basis vectors become $\mathbf{e}_1 \to (2, 0)$ and $\mathbf{e}_2 \to (0, 3)$ β€” the square stretches into a $2 \times 3$ rectangle:

y                              y
|                              |
(0,3) ●────────● (2,3)         |
      |         |              |
      |         |              |
(0,0) ●────────● (2,0)  β†’ x    |

Every point follows the same rule: $(4, 5) \to (8, 15)$. Scale by a negative number and the grid flips as it stretches: $\begin{pmatrix} -1 & 0 \\ 0 & 1 \end{pmatrix}$ sends $(x, y) \to (-x, y)$ β€” a mirror flip through the $y$-axis.

Rotation β€” spin the grid

Rotating counter-clockwise by $90Β°$ is the matrix $\mathbf{R} = \begin{pmatrix} 0 & -1 \\ 1 & 0 \end{pmatrix}$. Watch what happens to the basis vectors: $\mathbf{e}_1 = (1, 0)$ becomes $(0, 1)$ β€” it is $\mathbf{e}_2$ now β€” and $\mathbf{e}_2 = (0, 1)$ becomes $(-1, 0)$:

before                    after (90Β° CCW)
y                         y
|                         |
(0,1)●────●(1,1)    (-1,1)●────●(0,1)
     |    |               |    |
     |    |               |    |
(0,0)●────●(1,0)    (-1,0)●────●(0,0)  β†’ x

The square is still a square β€” rotation is a rigid motion, it changes no distances or angles β€” it's just spun. A point like $(3, 4)$ lands at $(-4, 3)$. Rotating by $45Β°$ instead gives $\begin{pmatrix} \sqrt{2}/2 & -\sqrt{2}/2 \\ \sqrt{2}/2 & \sqrt{2}/2 \end{pmatrix}$, and $\mathbf{e}_1 \to (\sqrt{2}/2, \sqrt{2}/2) \approx (0.7071, 0.7071)$ β€” one unit along the diagonal.

Reflection β€” flip the grid

Reflecting across the $x$-axis is $\begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}$: the $x$-axis is the mirror line, so $\mathbf{e}_1$ stays put while $\mathbf{e}_2 \to (0, -1)$:

y
(0,1)●────●(1,1)
     |    |
     |    |          ← mirror line: the x-axis
(0,0)●────●(1,0)
     |    |
     |    |
(0,-1)●────●(1,-1)

Every point's height flips sign: $(3, 4) \to (3, -4)$. Reflecting twice returns the original square β€” flipping a pancake twice puts it back.

Shear β€” skew the grid

A horizontal shear, $\begin{pmatrix} 1 & 2 \\ 0 & 1 \end{pmatrix}$, slides each row to the right by an amount proportional to its height. The bottom edge stays glued to the $x$-axis ($\mathbf{e}_1 \to (1, 0)$), while the top edge slides: $\mathbf{e}_2 \to (2, 1)$:

before                          after
y                               y
(0,1)●────●(1,1)          (2,1)●────●(3,1)
     \    \                   /    /
      \    \                 /    /
(0,0)●────●(1,0)      (0,0)●────●(1,0)

It's the motion of pushing the top of a deck of cards sideways: the square leans into a parallelogram with the same area (nothing was stretched, just slid). A point $(3, 4)$ becomes $(3 + 2 \cdot 4, 4) = (11, 4)$.

Visual summary

Each transformation is fully described by where it sends the basis vectors β€” those two arrows are the columns of the matrix:

e1 = (1,0)  ->  (2, 0)     (0, 1)      (1, 0)      (1, 0)
e2 = (0,1)  ->  (0, 3)     (-1, 0)     (0, -1)     (2, 1)
                Scale(2,3) Rotate(90Β°) Reflect x   Shear x(2)

Key insight: reading down each column tells you the entire transformation. No need to visualize the whole grid β€” just look at where the two basis vectors land. This is exactly what transform(&[1.0, 0.0]) and transform(&[0.0, 1.0]) compute in code.


Worked Examples

Example 1: Scale(2,3) β€” the unit square becomes a rectangle

Take $\mathbf{S} = \begin{pmatrix} 2 & 0 \\ 0 & 3 \end{pmatrix}$ and trace all four corners of the unit square:

CornerImageWhat happened
$(0, 0)$$(0, 0)$origin stays put
$(1, 0)$$(2, 0)$$\mathbf{e}_1$ doubled
$(1, 1)$$(2, 3)$both coordinates scaled
$(0, 1)$$(0, 3)$$\mathbf{e}_2$ tripled

The square became a $2 \times 3$ rectangle, so its area went from $1$ to $2 \cdot 3 = 6$. That factor β€” "by how much does the transformation stretch area?" β€” is the determinant of the matrix, the subject of ch07.

Example 2: Rotate 90Β° counter-clockwise β€” e1 becomes e2

$\mathbf{R} = \begin{pmatrix} 0 & -1 \\ 1 & 0 \end{pmatrix}$. Follow the point $(3, 4)$ step by step, column by column:

$$ \mathbf{R}\begin{pmatrix} 3 \\ 4 \end{pmatrix} = 3\,\mathbf{R}(\mathbf{e}_1) + 4\,\mathbf{R}(\mathbf{e}_2) = 3\begin{pmatrix} 0 \\ 1 \end{pmatrix} + 4\begin{pmatrix} -1 \\ 0 \end{pmatrix} = \begin{pmatrix} 0 \\ 3 \end{pmatrix} + \begin{pmatrix} -4 \\ 0 \end{pmatrix} = \begin{pmatrix} -4 \\ 3 \end{pmatrix} $$

So $(3, 4)$ β€” "3 right, 4 up" β€” spins to $(-4, 3)$ β€” "4 left, 3 up". A quarter-turn of the whole plane. The same number appears in the code test test_rotation_90_degrees: $\mathbf{e}_1 = (1, 0)$ becomes $(0, 1)$.

Example 3: Reflect across the x-axis β€” a triangle flips

$\mathbf{F} = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix}$ flips each point over the $x$-axis: $(x, y) \to (x, -y)$. Take the triangle with vertices $(1, 1)$, $(3, 2)$, $(2, 4)$:

VertexImage
$(1, 1)$$(1, -1)$
$(3, 2)$$(3, -2)$
$(2, 4)$$(2, -4)$

The triangle appears upside-down below the $x$-axis β€” the mirror image. The code test test_reflect_x checks the same rule on $(3, 4) \to (3, -4)$.

Example 4: Compose rotate-then-reflect β€” order matters

Apply rotation by $90Β°$ first, then reflection across the $x$-axis, to the point $(1, 0)$:

  1. Rotate: $(1, 0) \to (0, 1)$
  2. Reflect: $(0, 1) \to (0, -1)$

The composed matrix is $\mathbf{F}\mathbf{R} = \begin{pmatrix} 1 & 0 \\ 0 & -1 \end{pmatrix} \begin{pmatrix} 0 & -1 \\ 1 & 0 \end{pmatrix} = \begin{pmatrix} 0 & -1 \\ -1 & 0 \end{pmatrix}$, and indeed $\begin{pmatrix} 0 & -1 \\ -1 & 0 \end{pmatrix}(1, 0) = (0, -1)$ β€” the two-step trip in one multiplication.

Now reverse the order β€” reflect first, then rotate. Reflect $(1, 0)$ leaves it alone, and rotating gives $(0, 1)$. Different answer! The matrices don't commute: $\mathbf{R}\mathbf{F} = \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} \neq \mathbf{F}\mathbf{R}$. This is why composition order matters everywhere in linear algebra β€” rightmost matrix is applied first. The code test test_composition_rotate_then_reflect verifies both the composed matrix and the two-step application agree.


Rust Implementation

Add a new crate to your workspace:

cd code && cargo new --lib --edition 2024 ch05-linear-transformations

This crate builds on the Matrix struct from ch03. Open code/ch05-linear-transformations/src/lib.rs and start by copying the full Matrix impl from ch03-linear-algebra-matrices/src/lib.rs. Then add the following methods and free functions:

/// ── Linear transformations ──

impl Matrix {
    /// Apply this transformation to a vector (as a column matrix).
    ///
    /// If this matrix is mΓ—n, the input vector must have n components,
    /// and the result has m components.
    ///
    /// Mathematically: T(v) = A * v  where A is this matrix.
    pub fn transform(&self, v: &[f64]) -> Vec<f64> {
        assert_eq!(
            self.cols,
            v.len(),
            "Matrix cols {} doesn't match vector length {}",
            self.cols,
            v.len()
        );

        let mut result = vec![0.0; self.rows];
        for i in 0..self.rows {
            let mut sum = 0.0;
            for j in 0..self.cols {
                sum += self.get(i, j) * v[j];
            }
            result[i] = sum;
        }
        result
    }
}

/// ── 2D transformation factory functions ──

/// Create a 2D scaling matrix: Scale(sx, sy).
///
/// [ sx   0 ]
/// [ 0   sy ]
pub fn scale_2d(sx: f64, sy: f64) -> Matrix {
    Matrix::new(vec![sx, 0.0, 0.0, sy], 2, 2)
}

/// Create a 2D rotation matrix (counter-clockwise by `angle` radians).
///
/// [ cosΞΈ  -sinΞΈ ]
/// [ sinΞΈ   cosΞΈ ]
pub fn rotate_2d(angle: f64) -> Matrix {
    let c = angle.cos();
    let s = angle.sin();
    Matrix::new(vec![c, -s, s, c], 2, 2)
}

/// Create a 2D reflection matrix across the x-axis.
///
/// [ 1   0 ]
/// [ 0  -1 ]
pub fn reflect_x_2d() -> Matrix {
    Matrix::new(vec![1.0, 0.0, 0.0, -1.0], 2, 2)
}

/// Create a 2D reflection matrix across the y-axis.
///
/// [ -1   0 ]
/// [  0   1 ]
pub fn reflect_y_2d() -> Matrix {
    Matrix::new(vec![-1.0, 0.0, 0.0, 1.0], 2, 2)
}

/// Create a 2D shear matrix (horizontal shear).
///
/// [ 1   shx ]
/// [ 0    1  ]
pub fn shear_x_2d(shx: f64) -> Matrix {
    Matrix::new(vec![1.0, shx, 0.0, 1.0], 2, 2)
}

/// Create a 2D shear matrix (vertical shear).
///
/// [ 1    0  ]
/// [ shy  1  ]
pub fn shear_y_2d(shy: f64) -> Matrix {
    Matrix::new(vec![1.0, 0.0, shy, 1.0], 2, 2)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn approx_eq(a: f64, b: f64) -> bool {
        (a - b).abs() < 1e-10
    }

    #[test]
    fn test_scale_basis_vectors() {
        let s = scale_2d(2.0, 3.0);
        // e1 = (1, 0) β†’ (2, 0)
        let r1 = s.transform(&[1.0, 0.0]);
        assert!(approx_eq(r1[0], 2.0) && approx_eq(r1[1], 0.0));
        // e2 = (0, 1) β†’ (0, 3)
        let r2 = s.transform(&[0.0, 1.0]);
        assert!(approx_eq(r2[0], 0.0) && approx_eq(r2[1], 3.0));
    }

    #[test]
    fn test_scale_point() {
        let s = scale_2d(2.0, 3.0);
        let r = s.transform(&[4.0, 5.0]);
        // (4, 5) β†’ (8, 15)
        assert!(approx_eq(r[0], 8.0) && approx_eq(r[1], 15.0));
    }

    #[test]
    fn test_rotation_90_degrees() {
        // Rotating (1, 0) by 90Β° CCW should give (0, 1)
        let r = rotate_2d(std::f64::consts::FRAC_PI_2);
        let v = r.transform(&[1.0, 0.0]);
        assert!(approx_eq(v[0], 0.0) && approx_eq(v[1], 1.0));
    }

    #[test]
    fn test_rotation_180_degrees() {
        // Rotating (1, 0) by 180Β° should give (-1, 0)
        let r = rotate_2d(std::f64::consts::PI);
        let v = r.transform(&[1.0, 0.0]);
        assert!(approx_eq(v[0], -1.0) && approx_eq(v[1], 0.0));
    }

    #[test]
    fn test_reflect_x() {
        let r = reflect_x_2d();
        let v = r.transform(&[3.0, 4.0]);
        // (3, 4) β†’ (3, -4)
        assert!(approx_eq(v[0], 3.0) && approx_eq(v[1], -4.0));
    }

    #[test]
    fn test_reflect_y() {
        let r = reflect_y_2d();
        let v = r.transform(&[3.0, 4.0]);
        // (3, 4) β†’ (-3, 4)
        assert!(approx_eq(v[0], -3.0) && approx_eq(v[1], 4.0));
    }

    #[test]
    fn test_composition_rotate_then_reflect() {
        let rotate = rotate_2d(std::f64::consts::FRAC_PI_2);  // 90Β° CCW
        let reflect = reflect_x_2d();

        // Compose: reflect ∘ rotate  (rotate first, then reflect)
        // Composition matrix = reflect * rotate
        let composed = reflect.multiply(&rotate);

        // Apply to (1, 0):
        //   rotate(1, 0) = (0, 1)
        //   reflect(0, 1) = (0, -1)
        let v = composed.transform(&[1.0, 0.0]);
        assert!(approx_eq(v[0], 0.0) && approx_eq(v[1], -1.0));

        // Apply sequentially to verify
        let step1 = rotate.transform(&[1.0, 0.0]);
        let step2 = reflect.transform(&step1);
        assert!(approx_eq(step2[0], 0.0) && approx_eq(step2[1], -1.0));
    }

    #[test]
    fn test_composition_scale_then_rotate() {
        // Scale by (2, 1) then rotate 45Β°
        let s = scale_2d(2.0, 1.0);
        let r = rotate_2d(std::f64::consts::FRAC_PI_4);

        let composed = r.multiply(&s);

        // Apply to (1, 0):
        //   scale(1, 0) = (2, 0)
        //   rotate(2, 0) = (2cos45Β°, 2sin45Β°) = (√2, √2) β‰ˆ (1.4142, 1.4142)
        let v = composed.transform(&[1.0, 0.0]);
        let sqrt2 = std::f64::consts::FRAC_1_SQRT_2 * 2.0; // = 2/√2 = √2
        assert!(approx_eq(v[0], sqrt2) && approx_eq(v[1], sqrt2));
    }

    #[test]
    fn test_shear_x() {
        let sh = shear_x_2d(2.0);
        let v = sh.transform(&[3.0, 4.0]);
        // (3, 4) β†’ (3 + 2*4, 4) = (11, 4)
        assert!(approx_eq(v[0], 11.0) && approx_eq(v[1], 4.0));
    }

    #[test]
    fn test_identity_transformation() {
        let i = Matrix::identity(3);
        let v = i.transform(&[5.0, 6.0, 7.0]);
        assert!(approx_eq(v[0], 5.0) && approx_eq(v[1], 6.0) && approx_eq(v[2], 7.0));
    }

    #[test]
    fn test_double_reflection_is_identity() {
        // Reflect across x-axis twice β†’ back to original
        let r = reflect_x_2d();
        let double = r.multiply(&r); // (reflect ∘ reflect) = identity
        let v = double.transform(&[123.0, -456.0]);
        assert!(approx_eq(v[0], 123.0) && approx_eq(v[1], -456.0));
    }

    #[test]
    fn test_rotation_then_inverse() {
        // Rotating by ΞΈ then by -ΞΈ gives identity
        let theta = 0.7;
        let r = rotate_2d(theta);
        let r_inv = rotate_2d(-theta);
        // Compose: r_inv ∘ r = identity
        // (since rotation matrices satisfy R(-ΞΈ) = R(ΞΈ)^T = R(ΞΈ)^{-1})
        let composed = r_inv.multiply(&r);
        let v = composed.transform(&[3.0, 4.0]);
        assert!(approx_eq(v[0], 3.0) && approx_eq(v[1], 4.0));
    }
}

Run the tests:

cargo test -p ch05-linear-transformations

You should see:

running 12 tests
test tests::test_composition_rotate_then_reflect ... ok
test tests::test_composition_scale_then_rotate ... ok
test tests::test_double_reflection_is_identity ... ok
test tests::test_identity_transformation ... ok
test tests::test_reflect_x ... ok
test tests::test_reflect_y ... ok
test tests::test_rotation_180_degrees ... ok
test tests::test_rotation_90_degrees ... ok
test tests::test_rotation_then_inverse ... ok
test tests::test_scale_basis_vectors ... ok
test tests::test_scale_point ... ok
test tests::test_shear_x ... ok

test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered

Walkthrough


Verification

TestWhat it checksInvariant
test_scale_basis_vectorsColumns of scale matrix = scaled basis vectors$A\mathbf{e}_i = i\text{th column}$
test_scale_pointScale transforms a point correctly$(4, 5) \to (8, 15)$
test_rotation_90_degrees90Β° rotation of (1, 0) β†’ (0, 1)$\mathbf{e}_1 \to \mathbf{e}_2$
test_rotation_180_degrees180Β° rotation of (1, 0) β†’ (-1, 0)$\mathbf{e}_1 \to -\mathbf{e}_1$
test_reflect_xReflection across x-axis flips y$(3, 4) \to (3, -4)$
test_reflect_yReflection across y-axis flips x$(3, 4) \to (-3, 4)$
test_shear_xHorizontal shear shifts by height$(3, 4) \to (11, 4)$
test_composition_rotate_then_reflectComposed matrix = sequential application$\mathbf{BAx} = \mathbf{B}(\mathbf{Ax})$
test_composition_scale_then_rotateTwo-step composition with mixed types$\mathbf{BAx} = \mathbf{B}(\mathbf{Ax})$
test_double_reflection_is_identityReflect twice = do nothing$R_x \circ R_x = I$
test_rotation_then_inverseRotate by ΞΈ then -ΞΈ = identity$R_{-\theta} R_{\theta} = I$

Key Takeaways