Chapter 9: Dot products and duality

1. What Are Coordinates Really?

Standard Basis

When we write \(v = \begin{bmatrix} 3 \\ 2 \end{bmatrix}\), we mean:

\[\begin{split} v = 3\hat{i} + 2\hat{j} = 3\begin{bmatrix} 1 \\ 0 \end{bmatrix} + 2\begin{bmatrix} 0 \\ 1 \end{bmatrix} \end{split}\]

The numbers [3, 2] are scalars that tell us:

  • How much to scale \(\hat{i}\) (rightward unit vector)

  • How much to scale \(\hat{j}\) (upward unit vector)

Alternative Basis

But what if someone uses different basis vectors?

Say Jennifer uses:

  • \(b_1 = \begin{bmatrix} 2 \\ 1 \end{bmatrix}\) (from our perspective)

  • \(b_2 = \begin{bmatrix} -1 \\ 1 \end{bmatrix}\) (from our perspective)

To her, these are just [1, 0] and [0, 1]!

The same physical vector has different coordinates in her system.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
import seaborn as sns

sns.set_style("whitegrid")
plt.rcParams['figure.figsize'] = (12, 8)
def draw_vector(ax, origin, vector, color='blue', label=None, width=2.5):
    """Draw a 2D vector."""
    arrow = FancyArrowPatch(
        origin, origin + vector,
        mutation_scale=20, lw=width, arrowstyle='-|>', color=color, label=label
    )
    ax.add_patch(arrow)

def draw_grid(ax, basis1, basis2, extent=3, color='gray', alpha=0.3, label_prefix=''):
    """Draw grid lines based on basis vectors."""
    for i in range(-extent, extent+1):
        # Lines parallel to basis1
        start = i * basis2 - extent * basis1
        end = i * basis2 + extent * basis1
        ax.plot([start[0], end[0]], [start[1], end[1]], color=color, alpha=alpha, linewidth=0.5)
        
        # Lines parallel to basis2  
        start = i * basis1 - extent * basis2
        end = i * basis1 + extent * basis2
        ax.plot([start[0], end[0]], [start[1], end[1]], color=color, alpha=alpha, linewidth=0.5)

def visualize_different_bases():
    """Show the same vector in different coordinate systems."""
    # Our standard basis
    i_hat = np.array([1, 0])
    j_hat = np.array([0, 1])
    
    # Jennifer's basis (in our coordinates)
    b1 = np.array([2, 1])
    b2 = np.array([-1, 1])
    
    # A vector in our coordinates
    v_our = np.array([3, 2])
    
    # Same vector in Jennifer's coordinates: [5/3, 1/3]
    # Meaning: v = (5/3)*b1 + (1/3)*b2
    v_jennifer = np.array([5/3, 1/3])
    
    # Verify
    v_reconstructed = v_jennifer[0] * b1 + v_jennifer[1] * b2
    
    fig, axes = plt.subplots(1, 2, figsize=(16, 7))
    
    # Left: Our coordinate system
    ax1 = axes[0]
    ax1.set_xlim(-2, 5)
    ax1.set_ylim(-2, 4)
    ax1.set_aspect('equal')
    ax1.grid(True, alpha=0.3)
    ax1.axhline(y=0, color='k', linewidth=1)
    ax1.axvline(x=0, color='k', linewidth=1)
    
    # Our grid
    draw_grid(ax1, i_hat, j_hat, extent=5, color='blue', alpha=0.2)
    
    # Our basis vectors
    draw_vector(ax1, np.array([0, 0]), i_hat, color='blue', label='î (our basis)', width=2)
    draw_vector(ax1, np.array([0, 0]), j_hat, color='cyan', label='ĵ (our basis)', width=2)
    
    # The vector
    draw_vector(ax1, np.array([0, 0]), v_our, color='red', label='v = [3, 2]', width=3)
    
    ax1.set_title('Our Coordinate System\nv = 3î + 2ĵ = [3, 2]', fontsize=14, fontweight='bold')
    ax1.legend(fontsize=11)
    ax1.set_xlabel('x')
    ax1.set_ylabel('y')
    
    # Right: Jennifer's coordinate system
    ax2 = axes[1]
    ax2.set_xlim(-2, 5)
    ax2.set_ylim(-2, 4)
    ax2.set_aspect('equal')
    ax2.axhline(y=0, color='k', linewidth=1)
    ax2.axvline(x=0, color='k', linewidth=1)
    
    # Jennifer's grid
    draw_grid(ax2, b1, b2, extent=3, color='green', alpha=0.2)
    
    # Jennifer's basis vectors
    draw_vector(ax2, np.array([0, 0]), b1, color='green', label='b₁ (Jennifer)', width=2)
    draw_vector(ax2, np.array([0, 0]), b2, color='lime', label='b₂ (Jennifer)', width=2)
    
    # Same physical vector
    draw_vector(ax2, np.array([0, 0]), v_our, color='red', 
               label=f'v = (5/3)b₁ + (1/3)b₂ = [{v_jennifer[0]:.2f}, {v_jennifer[1]:.2f}]', width=3)
    
    ax2.set_title('Jennifer\'s Coordinate System\nSame vector, different coordinates!', 
                 fontsize=14, fontweight='bold')
    ax2.legend(fontsize=11)
    ax2.set_xlabel('x')
    ax2.set_ylabel('y')
    
    plt.tight_layout()
    plt.show()
    
    print("The Same Vector in Two Languages:")
    print(f"\nOur coordinates: {v_our}")
    print(f"Jennifer's coordinates: [{v_jennifer[0]:.3f}, {v_jennifer[1]:.3f}]")
    print(f"\nVerification: {v_jennifer[0]:.3f}×{b1} + {v_jennifer[1]:.3f}×{b2} = {v_reconstructed}")
    print(f"Matches our vector: {np.allclose(v_reconstructed, v_our)}")

visualize_different_bases()

2. Change of Basis Matrix

From Jennifer’s Language to Ours

To translate Jennifer’s coordinates to our coordinates, use a change of basis matrix:

\[\begin{split} P = \begin{bmatrix} | & | \\ b_1 & b_2 \\ | & | \end{bmatrix} \end{split}\]

Columns = Jennifer’s basis vectors in our coordinates

Then:

\[ v_{\text{our}} = P \cdot v_{\text{Jennifer}} \]

Example

\[\begin{split} P = \begin{bmatrix} 2 & -1 \\ 1 & 1 \end{bmatrix}, \quad v_{\text{Jennifer}} = \begin{bmatrix} 5/3 \\ 1/3 \end{bmatrix} \end{split}\]
\[\begin{split} v_{\text{our}} = \begin{bmatrix} 2 & -1 \\ 1 & 1 \end{bmatrix} \begin{bmatrix} 5/3 \\ 1/3 \end{bmatrix} = \begin{bmatrix} 3 \\ 2 \end{bmatrix} \end{split}\]
def demonstrate_change_of_basis_matrix():
    """Show how change of basis matrix works."""
    # Jennifer's basis vectors (in our coordinates)
    b1 = np.array([2, 1])
    b2 = np.array([-1, 1])
    
    # Change of basis matrix
    P = np.column_stack([b1, b2])
    
    print("Change of Basis Matrix P:")
    print(P)
    print("\nColumns = Jennifer's basis vectors in our coordinates")
    print(f"Column 1 = b₁ = {b1}")
    print(f"Column 2 = b₂ = {b2}")
    
    # Test: Jennifer says [-1, 2]
    v_jennifer = np.array([-1, 2])
    
    # Convert to our coordinates
    v_our = P @ v_jennifer
    
    print(f"\n{'='*50}")
    print("Translation Example:")
    print(f"Jennifer says: v = {v_jennifer}")
    print(f"Meaning: v = {v_jennifer[0]}×b₁ + {v_jennifer[1]}×b₂")
    print(f"\nApply P:")
    print(f"v_our = P × v_Jennifer = {P} × {v_jennifer} = {v_our}")
    print(f"\nIn our system: v = {v_our}")
    
    # Verify manually
    v_manual = v_jennifer[0] * b1 + v_jennifer[1] * b2
    print(f"\nManual check: {v_jennifer[0]}×{b1} + {v_jennifer[1]}×{b2} = {v_manual}")
    print(f"Matches: {np.allclose(v_our, v_manual)} ✓")

demonstrate_change_of_basis_matrix()

3. Going the Other Way: Inverse

From Our Language to Jennifer’s

To go from our coordinates to Jennifer’s, use the inverse:

\[ v_{\text{Jennifer}} = P^{-1} \cdot v_{\text{our}} \]

Why?

  • \(P\) transforms: Jennifer → us

  • \(P^{-1}\) transforms: us → Jennifer

  • \(P^{-1}P = I\) (does nothing)

def demonstrate_inverse_transform():
    """Show using inverse to go from our coords to Jennifer's."""
    # Change of basis matrix
    b1 = np.array([2, 1])
    b2 = np.array([-1, 1])
    P = np.column_stack([b1, b2])
    
    # Compute inverse
    P_inv = np.linalg.inv(P)
    
    print("Change of Basis Matrix P:")
    print(P)
    print("\nInverse P⁻¹:")
    print(P_inv)
    
    # Our vector
    v_our = np.array([3, 2])
    
    # Convert to Jennifer's coordinates
    v_jennifer = P_inv @ v_our
    
    print(f"\n{'='*50}")
    print("Translation Example (reverse direction):")
    print(f"We say: v = {v_our}")
    print(f"\nApply P⁻¹:")
    print(f"v_Jennifer = P⁻¹ × v_our = {v_jennifer}")
    print(f"\nJennifer would say: v = [{v_jennifer[0]:.4f}, {v_jennifer[1]:.4f}]")
    
    # Verify by going back
    v_back = P @ v_jennifer
    print(f"\nVerification: P × v_Jennifer = {v_back}")
    print(f"Matches original: {np.allclose(v_our, v_back)} ✓")
    
    # Check P⁻¹P = I
    identity = P_inv @ P
    print(f"\nP⁻¹ × P = \n{identity}")
    print(f"Is identity? {np.allclose(identity, np.eye(2))} ✓")

demonstrate_inverse_transform()

4. Transformations in Different Bases

The Challenge

Suppose you have a transformation matrix \(A\) in our coordinate system.

What does this same transformation look like in Jennifer’s coordinate system?

The Formula

\[ A_{\text{Jennifer}} = P^{-1} A P \]

Why This Works

  1. Start with Jennifer’s coordinates: \(v_J\)

  2. Convert to our system: \(P v_J\)

  3. Apply transformation \(A\): \(A(Pv_J)\)

  4. Convert back to Jennifer’s system: \(P^{-1}(APv_J)\)

So the entire operation is: \(P^{-1}AP\)

This is called similarity transformation or conjugation.

def demonstrate_transformation_in_different_basis():
    """Show how transformations look in different coordinate systems."""
    # A 90-degree rotation (in our coordinates)
    A_our = np.array([[0, -1],
                      [1,  0]])
    
    # Jennifer's basis
    b1 = np.array([2, 1])
    b2 = np.array([-1, 1])
    P = np.column_stack([b1, b2])
    P_inv = np.linalg.inv(P)
    
    # Transform A to Jennifer's coordinate system
    A_jennifer = P_inv @ A_our @ P
    
    print("Transformation in Our System:")
    print(f"A (90° rotation) = \n{A_our}\n")
    
    print("Same Transformation in Jennifer's System:")
    print(f"A_Jennifer = P⁻¹AP = \n{A_jennifer}\n")
    
    # Test with a vector
    v_our = np.array([1, 0])
    v_jennifer = P_inv @ v_our
    
    print(f"{'='*60}")
    print("Test: Apply rotation to î = [1, 0]")
    print(f"\nIn our system:")
    print(f"  v = {v_our}")
    print(f"  Av = {A_our @ v_our}")
    
    print(f"\nIn Jennifer's system:")
    print(f"  v = {v_jennifer}")
    print(f"  A_Jennifer × v = {A_jennifer @ v_jennifer}")
    
    # Verify they represent the same transformation
    result_our = A_our @ v_our
    result_jennifer = A_jennifer @ v_jennifer
    result_back = P @ result_jennifer
    
    print(f"\nVerification:")
    print(f"Our result converted to Jennifer's: {P_inv @ result_our}")
    print(f"Jennifer's result: {result_jennifer}")
    print(f"Match: {np.allclose(P_inv @ result_our, result_jennifer)} ✓")

demonstrate_transformation_in_different_basis()

5. Why This Matters: Eigenvalue Problems

The Power of Changing Basis

Sometimes a transformation looks complicated in one basis but simple in another!

Eigenvalue decomposition finds a basis where the transformation is diagonal:

\[ A = PDP^{-1} \]

where \(D\) is diagonal (very simple!).

Example: Powers of Matrices

Computing \(A^{100}\) directly is hard, but:

\[ A^{100} = (PDP^{-1})^{100} = PD^{100}P^{-1} \]

\(D^{100}\) is easy - just raise diagonal entries to 100!

def demonstrate_diagonal_simplification():
    """Show power of diagonal basis."""
    # A transformation (Fibonacci matrix)
    A = np.array([[1, 1],
                  [1, 0]])
    
    # Eigenvalue decomposition
    eigenvalues, eigenvectors = np.linalg.eig(A)
    P = eigenvectors
    D = np.diag(eigenvalues)
    P_inv = np.linalg.inv(P)
    
    print("Original Matrix A (Fibonacci):")
    print(A)
    print("\nEigenvalue Decomposition: A = PDP⁻¹")
    print(f"\nP (eigenvector basis):\n{P}")
    print(f"\nD (diagonal!):\n{D}")
    
    # Compute A^10 two ways
    print(f"\n{'='*60}")
    print("Computing A¹⁰:")
    
    # Direct way (slow)
    A_10_direct = np.linalg.matrix_power(A, 10)
    
    # Smart way using diagonal
    D_10 = np.diag(eigenvalues ** 10)
    A_10_smart = P @ D_10 @ P_inv
    
    print(f"\nDirect computation A¹⁰:\n{A_10_direct.astype(int)}")
    print(f"\nUsing eigenvalues D¹⁰:\n{A_10_smart.real.astype(int)}")
    print(f"\nMatch: {np.allclose(A_10_direct, A_10_smart.real)} ✓")
    
    print(f"\nD¹⁰ = \n{D_10.real}")
    print("\nJust raise each diagonal entry to power 10!")
    print("This is why eigenvalues are so powerful.")

demonstrate_diagonal_simplification()

6. Summary and Key Takeaways

Main Ideas

  1. Coordinates are not absolute

    • Depend on choice of basis vectors

    • Same vector, different numbers

  2. Change of basis matrix

    • Columns = new basis vectors in old coordinates

    • \(P\): Jennifer → us

    • \(P^{-1}\): us → Jennifer

  3. Transformations change too

    • \(A_{\text{new}} = P^{-1}AP\)

    • Similarity transformation

  4. Why it matters

    • Find simpler representations

    • Diagonal matrices (eigenvalues)

    • Computational efficiency

Formulas

\[\begin{split} \begin{align} v_{\text{old}} &= P \cdot v_{\text{new}} \\ v_{\text{new}} &= P^{-1} \cdot v_{\text{old}} \\ A_{\text{new}} &= P^{-1} A_{\text{old}} P \end{align} \end{split}\]

Applications

  • 🔢 Eigenvalue decomposition: Find natural axes

  • 📊 PCA: Change to principal component basis

  • 🎮 Computer graphics: Object-local vs world coordinates

  • 🔬 Quantum mechanics: Different measurement bases

  • 🤖 Robotics: Joint vs Cartesian coordinates

The Big Picture

There’s no “correct” coordinate system - just different perspectives on the same mathematical reality. Choosing the right basis can:

  • Simplify calculations

  • Reveal structure

  • Enable efficient algorithms

This is one of the most powerful ideas in linear algebra!

7. Practice Exercises

Exercise 1: Change of Basis

Given basis vectors \(b_1 = [1, 1]\) and \(b_2 = [-1, 1]\):

a) What is the change of basis matrix \(P\)?

b) Convert \(v = [4, 2]\) (our coords) to the new basis

c) Verify by reconstructing \(v\) from new coordinates

# Your code here
b1 = np.array([1, 1])
b2 = np.array([-1, 1])
P = np.column_stack([b1, b2])

print(f"a) Change of basis matrix P:\n{P}\n")

v_old = np.array([4, 2])
P_inv = np.linalg.inv(P)
v_new = P_inv @ v_old

print(f"b) v_new = P⁻¹ × v_old = {v_new}\n")

v_reconstructed = P @ v_new
print(f"c) Reconstruct: P × v_new = {v_reconstructed}")
print(f"Matches original: {np.allclose(v_old, v_reconstructed)} ✓")

Exercise 2: Transform a Transformation

The matrix \(A = \begin{bmatrix} 2 & 0 \\ 0 & 3 \end{bmatrix}\) represents scaling in the standard basis.

What does it look like in the basis \(b_1 = [1, 1], b_2 = [1, -1]\)?

# Your code here
A_old = np.array([[2, 0],
                  [0, 3]])

b1 = np.array([1, 1])
b2 = np.array([1, -1])
P = np.column_stack([b1, b2])
P_inv = np.linalg.inv(P)

A_new = P_inv @ A_old @ P

print(f"A in standard basis:\n{A_old}\n")
print(f"A in new basis:\n{A_new}")