Chapter 4: Matrix multiplication as compositionยถ

1. The Geometric Meaningยถ

Key Questionยถ

When a transformation is applied, by what factor does it scale areas?

The Determinantยถ

For a matrix \(\begin{bmatrix} a & b \\ c & d \end{bmatrix}\), the determinant is: $\(\text{det}\begin{pmatrix}\begin{bmatrix} a & b \\ c & d \end{bmatrix}\end{pmatrix} = ad - bc\)$

Interpretationยถ

  • det = 2: Areas are doubled

  • det = 0.5: Areas are halved

  • det = 1: Areas preserved (rotation, reflection)

  • det = 0: Space is squished to lower dimension

  • det < 0: Orientation is flipped

Exampleยถ

A unit square (area = 1) transforms to a parallelogram. The determinant = area of that parallelogram!

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, Polygon, Circle
from matplotlib.collections import PatchCollection
import seaborn as sns

sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (12, 10)
np.set_printoptions(precision=3, suppress=True)
def draw_grid(ax, transform=None, color='lightgray', alpha=0.3):
    """Draw coordinate grid"""
    for x in range(-3, 4):
        points = np.array([[x, y] for y in np.linspace(-3, 3, 50)])
        if transform is not None:
            points = points @ transform.T
        ax.plot(points[:, 0], points[:, 1], color=color, alpha=alpha, linewidth=0.5)
    
    for y in range(-3, 4):
        points = np.array([[x, y] for x in np.linspace(-3, 3, 50)])
        if transform is not None:
            points = points @ transform.T
        ax.plot(points[:, 0], points[:, 1], color=color, alpha=alpha, linewidth=0.5)

def draw_unit_square(ax, transform=None, **kwargs):
    """Draw unit square"""
    square = np.array([[0, 0], [1, 0], [1, 1], [0, 1]])
    if transform is not None:
        square = square @ transform.T
    patch = Polygon(square, closed=True, **kwargs)
    ax.add_patch(patch)
    return square

def setup_ax(ax, xlim=(-3, 3), ylim=(-3, 3), title=''):
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)
    ax.set_aspect('equal')
    ax.axhline(y=0, color='k', linewidth=0.8)
    ax.axvline(x=0, color='k', linewidth=0.8)
    ax.grid(True, alpha=0.3)
    if title:
        ax.set_title(title, fontsize=14, fontweight='bold')

# Examples with different determinants
fig, axes = plt.subplots(2, 3, figsize=(18, 12))

transformations = [
    ('Stretch (det = 6)', np.array([[3, 0], [0, 2]])),
    ('Shear (det = 1)', np.array([[1, 1], [0, 1]])),
    ('Shrink (det = 0.25)', np.array([[0.5, 0], [0, 0.5]])),
    ('Rotation (det = 1)', np.array([[0, -1], [1, 0]])),
    ('Reflection (det = -1)', np.array([[-1, 0], [0, 1]])),
    ('Projection (det = 0)', np.array([[1, 0], [0, 0]]))
]

for idx, (name, matrix) in enumerate(transformations):
    ax = axes[idx // 3, idx % 3]
    det = np.linalg.det(matrix)
    
    setup_ax(ax, xlim=(-2, 4), ylim=(-2, 4), title=name)
    
    # Original unit square (light)
    draw_unit_square(ax, fill=False, edgecolor='blue', 
                    linestyle='--', linewidth=2, alpha=0.5)
    ax.text(0.5, 0.5, '1', fontsize=16, ha='center', va='center',
           color='blue', alpha=0.5, fontweight='bold')
    
    # Transformed square
    transformed_square = draw_unit_square(ax, matrix, fill=True, 
                                         facecolor='orange', 
                                         edgecolor='red', 
                                         alpha=0.6, linewidth=2)
    
    # Calculate center and area
    center = transformed_square.mean(axis=0)
    area = abs(det)
    
    # Show area
    ax.text(center[0], center[1], f'{area:.2f}', 
           fontsize=16, ha='center', va='center',
           color='darkred', fontweight='bold')
    
    # Show determinant
    det_text = f'det = {det:.2f}'
    color = 'green' if det > 0 else ('red' if det < 0 else 'purple')
    ax.text(0.98, 0.02, det_text, transform=ax.transAxes,
           fontsize=13, ha='right', va='bottom',
           bbox=dict(boxstyle='round', facecolor=color, alpha=0.3),
           fontweight='bold')

plt.tight_layout()
plt.show()

print("Key Insight: The determinant tells you how much areas change!")
print("\nโ€ข det > 1: Areas expand")
print("โ€ข det = 1: Areas preserved (rotation/reflection)")
print("โ€ข 0 < det < 1: Areas shrink")
print("โ€ข det = 0: Squished to lower dimension")
print("โ€ข det < 0: Orientation flipped")

2. Negative Determinants and Orientationยถ

What Does Negative Mean?ยถ

A negative determinant means the transformation flips orientation.

Orientation Testยถ

Imagine \(\hat{i}\) as your right hand and \(\hat{j}\) as your left hand:

  • det > 0: \(\hat{i}\) is still to the right of \(\hat{j}\) (same orientation)

  • det < 0: \(\hat{i}\) is now to the left of \(\hat{j}\) (flipped orientation)

The Signยถ

  • Magnitude: How much area scales

  • Sign: Whether orientation flips

# Visualize orientation flipping
fig, axes = plt.subplots(1, 3, figsize=(18, 6))

def draw_basis_with_labels(ax, matrix, title):
    setup_ax(ax, xlim=(-2, 3), ylim=(-2, 3), title=title)
    
    # Original basis (faint)
    i_orig = np.array([1, 0])
    j_orig = np.array([0, 1])
    ax.arrow(0, 0, i_orig[0], i_orig[1], head_width=0.1, 
            fc='lightgreen', ec='lightgreen', alpha=0.3, width=0.03)
    ax.arrow(0, 0, j_orig[0], j_orig[1], head_width=0.1, 
            fc='lightcoral', ec='lightcoral', alpha=0.3, width=0.03)
    
    # Transformed basis
    i_new = matrix @ i_orig
    j_new = matrix @ j_orig
    
    ax.arrow(0, 0, i_new[0], i_new[1], head_width=0.15, head_length=0.15,
            fc='green', ec='green', width=0.05, label='รฎ')
    ax.arrow(0, 0, j_new[0], j_new[1], head_width=0.15, head_length=0.15,
            fc='red', ec='red', width=0.05, label='ฤต')
    
    # Label them
    ax.text(i_new[0]*1.2, i_new[1]*1.2, 'รฎ', fontsize=18, 
           color='green', fontweight='bold')
    ax.text(j_new[0]*1.2, j_new[1]*1.2, 'ฤต', fontsize=18, 
           color='red', fontweight='bold')
    
    # Determinant
    det = np.linalg.det(matrix)
    sign_text = "Same orientation" if det > 0 else "FLIPPED orientation"
    color = 'green' if det > 0 else 'red'
    
    ax.text(0.5, 0.95, f'det = {det:.1f}', transform=ax.transAxes,
           fontsize=14, ha='center', va='top', fontweight='bold',
           bbox=dict(boxstyle='round', facecolor=color, alpha=0.3))
    ax.text(0.5, 0.05, sign_text, transform=ax.transAxes,
           fontsize=12, ha='center', va='bottom',
           bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.5))
    
    # Draw transformed unit square
    draw_unit_square(ax, matrix, fill=True, facecolor=color, 
                    edgecolor='black', alpha=0.2, linewidth=1.5)

# Positive determinant
draw_basis_with_labels(axes[0], np.array([[2, 1], [0, 1]]), 
                      'Positive det: No flip')

# Negative determinant (reflection)
draw_basis_with_labels(axes[1], np.array([[-1, 0], [0, 1]]), 
                      'Negative det: Flipped!')

# Negative determinant (different)
draw_basis_with_labels(axes[2], np.array([[1, 2], [2, 1]]), 
                      'Negative det: Flipped!')

plt.tight_layout()
plt.show()

print("Orientation Check:")
print("Stand at the origin. Point right hand along รฎ, left hand along ฤต.")
print("โ€ข If det > 0: Hands are still in the right/left configuration")
print("โ€ข If det < 0: Your hands have crossed! Orientation flipped.")

3. Zero Determinant = Squishing Dimensionsยถ

Critical Conceptยถ

\[\text{det}(A) = 0 \iff \text{columns of } A \text{ are linearly dependent}\]

Geometric Meaningยถ

  • det = 0: The transformation squishes all of space onto a line (or point)

  • All 2D area becomes 0

  • The transformation is not invertible

Why?ยถ

If columns are linearly dependent, they point in the same direction (or one is zero). The parallelogram they form has zero area!

# Demonstrate zero determinant
fig, axes = plt.subplots(1, 3, figsize=(18, 6))

# Case 1: Collinear columns
M1 = np.array([[2, 4], [1, 2]])  # Second column is 2ร— first
setup_ax(axes[0], title='Collinear Columns\n(det = 0)')
draw_grid(axes[0], M1, color='orange', alpha=0.5)

i_hat = M1[:, 0]
j_hat = M1[:, 1]
axes[0].arrow(0, 0, i_hat[0], i_hat[1], head_width=0.2, 
             fc='green', ec='green', width=0.08, alpha=0.7)
axes[0].arrow(0, 0, j_hat[0], j_hat[1], head_width=0.2, 
             fc='red', ec='red', width=0.08, alpha=0.7)
axes[0].text(0.5, 0.9, f'det = {np.linalg.det(M1):.3f}', 
            transform=axes[0].transAxes, fontsize=14, 
            bbox=dict(boxstyle='round', facecolor='red', alpha=0.3))
axes[0].text(0.5, 0.1, 'ฤต = 2รฎ\n(on same line!)', 
            transform=axes[0].transAxes, fontsize=11, ha='center',
            bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.5))

# Case 2: Projection onto x-axis
M2 = np.array([[1, 2], [0, 0]])  # Squishes to x-axis
setup_ax(axes[1], title='Projection to Line\n(det = 0)')

# Show many points collapsing to a line
n_points = 100
original_points = np.random.randn(2, n_points) * 1.5
transformed_points = M2 @ original_points

axes[1].scatter(original_points[0], original_points[1], 
               c='blue', alpha=0.3, s=20, label='Before')
axes[1].scatter(transformed_points[0], transformed_points[1], 
               c='red', alpha=0.6, s=30, label='After', marker='x')

# Draw the line everything collapses to
x_line = np.linspace(-4, 4, 100)
axes[1].plot(x_line, np.zeros_like(x_line), 'r-', linewidth=3, 
            alpha=0.5, label='All 2D โ†’ this line')

axes[1].legend(loc='upper right')
axes[1].text(0.5, 0.9, f'det = {np.linalg.det(M2):.3f}', 
            transform=axes[1].transAxes, fontsize=14,
            bbox=dict(boxstyle='round', facecolor='red', alpha=0.3))

# Case 3: Zero column
M3 = np.array([[1, 0], [0, 0]])  # One column is zero
setup_ax(axes[2], title='Zero Column\n(det = 0)')
draw_grid(axes[2], M3, color='purple', alpha=0.5)

axes[2].arrow(0, 0, 1, 0, head_width=0.2, 
             fc='green', ec='green', width=0.08, alpha=0.7)
axes[2].plot(0, 0, 'ro', markersize=15, label='ฤต = [0, 0]')
axes[2].text(0.5, 0.9, f'det = {np.linalg.det(M3):.3f}', 
            transform=axes[2].transAxes, fontsize=14,
            bbox=dict(boxstyle='round', facecolor='red', alpha=0.3))
axes[2].legend()

plt.tight_layout()
plt.show()

print("Zero Determinant Means:")
print("โ€ข Columns are linearly dependent")
print("โ€ข Transformation squishes space to lower dimension")
print("โ€ข Matrix is NOT invertible (no way to undo it)")
print("โ€ข You lose information!")

4. Computing Determinantsยถ

2ร—2 Formulaยถ

\[\begin{split}\det\begin{pmatrix}\begin{bmatrix} a & b \\ c & d \end{bmatrix}\end{pmatrix} = ad - bc\end{split}\]

Why This Formula?ยถ

The parallelogram formed by \(\begin{bmatrix} a \\ c \end{bmatrix}\) and \(\begin{bmatrix} b \\ d \end{bmatrix}\) has area \(|ad - bc|\).

Geometric Derivationยถ

  1. Parallelogram area = base ร— height

  2. Can also compute as: (bounding box) - (triangular corners)

  3. Works out to \(ad - bc\)!

3ร—3 and Higherยถ

Use cofactor expansion or row reduction (covered in advanced notebooks)

# Visualize the 2x2 formula geometrically
fig, axes = plt.subplots(1, 2, figsize=(16, 7))

# Example matrix
a, b, c, d = 3, 1, 1, 2
M = np.array([[a, c], [b, d]])

# Left: Show the parallelogram
setup_ax(axes[0], xlim=(-0.5, 4), ylim=(-0.5, 3), 
        title=f'Parallelogram Area\nMatrix = [[{a}, {c}], [{b}, {d}]]')

# Draw parallelogram
parallelogram = np.array([[0, 0], [a, b], [a+c, b+d], [c, d]])
patch = Polygon(parallelogram, closed=True, fill=True, 
               facecolor='cyan', edgecolor='blue', 
               alpha=0.5, linewidth=2)
axes[0].add_patch(patch)

# Draw vectors
axes[0].arrow(0, 0, a, b, head_width=0.15, head_length=0.15,
             fc='green', ec='green', width=0.06, label=f'รฎ โ†’ [{a}, {b}]')
axes[0].arrow(0, 0, c, d, head_width=0.15, head_length=0.15,
             fc='red', ec='red', width=0.06, label=f'ฤต โ†’ [{c}, {d}]')

# Show the formula
det = a*d - b*c
axes[0].text(0.5, 0.95, f'Area = ad - bc\n= {a}ร—{d} - {b}ร—{c}\n= {det}', 
            transform=axes[0].transAxes, fontsize=13, ha='center', va='top',
            bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7),
            family='monospace')
axes[0].legend(loc='lower right')

# Right: Show the bounding box decomposition
setup_ax(axes[1], xlim=(-0.5, 4), ylim=(-0.5, 3), 
        title='Bounding Box Method')

# Draw bounding box
box = plt.Rectangle((0, 0), a+c, b+d, fill=False, 
                    edgecolor='gray', linestyle='--', linewidth=2)
axes[1].add_patch(box)

# Draw parallelogram
patch2 = Polygon(parallelogram, closed=True, fill=True, 
                facecolor='cyan', edgecolor='blue', 
                alpha=0.5, linewidth=2)
axes[1].add_patch(patch2)

# Label corners that get subtracted
# Top-left triangle
tri1 = np.array([[0, b+d], [c, d], [c, b+d]])
axes[1].add_patch(Polygon(tri1, fill=True, facecolor='red', alpha=0.3))

# Bottom-right triangle
tri2 = np.array([[a, 0], [a+c, 0], [a, b]])
axes[1].add_patch(Polygon(tri2, fill=True, facecolor='red', alpha=0.3))

axes[1].text(0.5, 0.95, 
            f'Box area = (a+c)(b+d) = {(a+c)*(b+d)}\n'
            f'Subtract corners: {b*c} + {b*c}\n'
            f'Parallelogram = {(a+c)*(b+d)} - 2ร—{b*c}\n'
            f'            = {(a+c)*(b+d) - 2*b*c} = ad - bc',
            transform=axes[1].transAxes, fontsize=11, ha='center', va='top',
            bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7),
            family='monospace')

plt.tight_layout()
plt.show()

print("\nDeterminant Formula (2ร—2):")
print(f"det([[{a}, {c}], [{b}, {d}]]) = {a}ร—{d} - {b}ร—{c} = {det}")
print(f"\nThis equals the AREA of the parallelogram!")

5. Determinant of Matrix Productsยถ

Beautiful Propertyยถ

\[\det(AB) = \det(A) \cdot \det(B)\]

Intuitionยถ

If transformation \(B\) scales areas by factor 3, and transformation \(A\) scales areas by factor 2, then doing both scales areas by \(3 \times 2 = 6\).

Why It Mattersยถ

  • Makes it easy to compute determinants of compositions

  • Connects to eigenvalues

  • Fundamental for many proofs

# Demonstrate det(AB) = det(A) ร— det(B)
fig, axes = plt.subplots(1, 4, figsize=(20, 5))

# Two transformations
A = np.array([[2, 0], [0, 1.5]])  # Stretch
B = np.array([[1, 0.5], [0, 1]])  # Shear

det_A = np.linalg.det(A)
det_B = np.linalg.det(B)
det_AB = np.linalg.det(A @ B)

# Step 0: Original
setup_ax(axes[0], title='Original\n(area = 1)')
draw_unit_square(axes[0], fill=True, facecolor='lightblue', 
                edgecolor='blue', alpha=0.6, linewidth=2)
axes[0].text(0.5, 0.5, '1', fontsize=20, ha='center', va='center', fontweight='bold')

# Step 1: After B
setup_ax(axes[1], title=f'After B\n(area = {det_B:.2f})')
draw_unit_square(axes[1], B, fill=True, facecolor='lightgreen', 
                edgecolor='green', alpha=0.6, linewidth=2)
square_B = draw_unit_square(axes[1], B, fill=False, edgecolor='black', linewidth=0)
center_B = square_B.mean(axis=0)
axes[1].text(center_B[0], center_B[1], f'{det_B:.2f}', 
            fontsize=18, ha='center', va='center', fontweight='bold')

# Step 2: After A(B(ยท))
setup_ax(axes[2], title=f'After A, then B\n(area = {det_AB:.2f})')
draw_unit_square(axes[2], A @ B, fill=True, facecolor='lightyellow', 
                edgecolor='orange', alpha=0.6, linewidth=2)
square_AB = draw_unit_square(axes[2], A @ B, fill=False, edgecolor='black', linewidth=0)
center_AB = square_AB.mean(axis=0)
axes[2].text(center_AB[0], center_AB[1], f'{det_AB:.2f}', 
            fontsize=18, ha='center', va='center', fontweight='bold')

# Step 3: Verify the formula
axes[3].axis('off')
axes[3].text(0.5, 0.6, 
            'Determinant Product Rule\n\n'
            f'det(A) = {det_A:.2f}\n'
            f'det(B) = {det_B:.2f}\n'
            f'det(A) ร— det(B) = {det_A * det_B:.2f}\n\n'
            f'det(AB) = {det_AB:.2f}\n\n'
            'โœ“ They match!',
            transform=axes[3].transAxes, fontsize=14, ha='center', va='center',
            bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.8),
            family='monospace', fontweight='bold')

plt.tight_layout()
plt.show()

# Numerical verification
print("Verification:")
print(f"det(A) = {det_A}")
print(f"det(B) = {det_B}")
print(f"det(A) ร— det(B) = {det_A * det_B}")
print(f"\ndet(AB) = {det_AB}")
print(f"\nDifference: {abs(det_AB - det_A * det_B):.10f}")
print("\nโœ“ The formula works!")

6. Applications and Intuitionยถ

When to Use Determinantsยถ

  1. Check invertibility: \(\det(A) \neq 0 \iff A\) is invertible

  2. Compute areas/volumes: How much does transformation scale space?

  3. Solve linear systems: Cramerโ€™s rule (though not recommended for large systems)

  4. Eigenvalues: Characteristic equation uses determinant

  5. Change of variables: In integrals (Jacobian determinant)

Quick Testsยถ

  • \(\det(I) = 1\) (identity preserves area)

  • \(\det(A^T) = \det(A)\) (transpose doesnโ€™t change determinant)

  • \(\det(A^{-1}) = 1/\det(A)\) (inverse scales by reciprocal)

  • \(\det(cA) = c^n \det(A)\) for \(n \times n\) matrix

# Practical examples
print("Determinant Applications\n" + "="*50)

# Example 1: Testing invertibility
matrices = {
    'Invertible': np.array([[2, 1], [1, 3]]),
    'Singular (not invertible)': np.array([[2, 4], [1, 2]]),
    'Nearly singular': np.array([[1, 1], [1, 1.0001]])
}

print("\n1. Testing Invertibility:")
for name, M in matrices.items():
    det = np.linalg.det(M)
    invertible = "YES" if abs(det) > 1e-10 else "NO"
    print(f"  {name:30s}: det = {det:10.6f}  โ†’ Invertible? {invertible}")

# Example 2: Area scaling
print("\n2. Area Scaling:")
transformations = {
    'Double in both directions': np.array([[2, 0], [0, 2]]),
    'Rotation 45ยฐ': np.array([[np.cos(np.pi/4), -np.sin(np.pi/4)],
                              [np.sin(np.pi/4), np.cos(np.pi/4)]]),
    'Shear': np.array([[1, 2], [0, 1]])
}

for name, M in transformations.items():
    det = np.linalg.det(M)
    print(f"  {name:30s}: Areas scale by {det:.3f}")

# Example 3: Product rule
print("\n3. Product Rule:")
A = np.random.randn(3, 3)
B = np.random.randn(3, 3)
C = A @ B

det_A = np.linalg.det(A)
det_B = np.linalg.det(B)
det_C = np.linalg.det(C)
det_product = det_A * det_B

print(f"  det(A) = {det_A:.6f}")
print(f"  det(B) = {det_B:.6f}")
print(f"  det(A) ร— det(B) = {det_product:.6f}")
print(f"  det(AB) = {det_C:.6f}")
print(f"  Difference: {abs(det_C - det_product):.10f}")

# Example 4: Special matrices
print("\n4. Special Matrices:")
I = np.eye(3)
print(f"  det(Identity) = {np.linalg.det(I)}")

M = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(f"  det(Rank-deficient) = {np.linalg.det(M):.10f} (should be 0)")

theta = np.pi/3
rotation = np.array([[np.cos(theta), -np.sin(theta)],
                     [np.sin(theta), np.cos(theta)]])
print(f"  det(Rotation) = {np.linalg.det(rotation):.10f} (should be 1)")

Summaryยถ

The Essence of Determinantsยถ

  1. Geometric meaning: How much a transformation scales areas/volumes

  2. Sign: Whether orientation is flipped

  3. Zero determinant: Dimensions are squished (not invertible)

  4. Product rule: \(\det(AB) = \det(A) \cdot \det(B)\)

Mental Pictureยถ

When you see \(\det(A)\), think:

โ€œBy what factor does transformation \(A\) scale areas?โ€

  • \(|\det(A)| = 3\): Areas triple

  • \(\det(A) = 1\): Areas preserved (rotation)

  • \(\det(A) = 0\): Squished to lower dimension

  • \(\det(A) < 0\): Orientation flipped

Key Factsยถ

  • \(\det(I) = 1\)

  • \(\det(A^T) = \det(A)\)

  • \(\det(A^{-1}) = 1/\det(A)\)

  • \(\det(AB) = \det(A)\det(B)\)

  • \(A\) invertible \(\iff\) \(\det(A) \neq 0\)

Next Stepsยถ

Understanding determinants is crucial for:

  • Inverse matrices (next topic!)

  • Eigenvalues (characteristic equation)

  • Cross products (in 3D)

  • Change of variables in calculus

Exercisesยถ

  1. Compute \(\det\begin{pmatrix}\begin{bmatrix} 3 & 1 \\ 2 & 4 \end{bmatrix}\end{pmatrix}\) and interpret geometrically

  2. Show that det(rotation matrix) = 1 for any angle

  3. Find a matrix with det = -5

  4. Prove that if two rows of a matrix are identical, det = 0

  5. Verify \(\det(AB) = \det(A)\det(B)\) for random 3ร—3 matrices