Skip to content

Eigenvectors & Eigenvalues

Directions a matrix cannot turn.

Intermediate / linear-algebra / geometry

Drag the entries of a 2x2 matrix and watch the unit circle's image deform. The accent lines are the eigenvector directions - the only vectors the matrix merely stretches - and they exist only when the characteristic polynomial has real roots, which the metrics report live.

Tagged: characteristic polynomial, invariant directions, trace, determinant

Live simulation

Drag the matrix entries; accent lines are the invariant directions.

Speed

Drag the matrix entries; accent lines are the invariant directions.

Mathematics

An eigenvector of A is a vector that only gets scaled: Av = lambda v. For a 2x2 real matrix they come from the characteristic polynomial lambda^2 - (trace) lambda + det = 0 - real when the discriminant trace^2 - 4 det is non-negative.

lambda = (tr +- sqrt(tr^2 - 4 det)) / 2

eigenvector direction (when b != 0): v = (b, lambda - a)

verification: A v = lambda v

The drawn lines are those invariant directions. When the discriminant goes negative the eigenvalues leave the real axis: the circle's image becomes a rotating, scaling ellipse with no fixed directions - which is what the plot shows the moment the metric says 'complex'. The verification identity Av = lambda v is asserted by the test suite, not assumed.

How it works

The four matrix entries are sliders. The render draws the image of the unit circle under A, the transformed basis square, and the eigenvector lines computed from the closed-form roots, rescaled to the viewport.

Determinant and trace are shown live because every geometric statement about the transform - area factor, flip, trace-determinant classification - flows from those two numbers.

Implementation

src/engine/experiments/eigenvectors.ts · 147 lines · the module running above

TypeScript · fixed-timestep engine

1import { BaseExperiment } from "../core/base-experiment";
2import type { ParameterDef, ThemeColors } from "../core/types";
3
4/**
5 * Eigenvalues and eigenvectors of a real 2x2 matrix, computed from the
6 * characteristic polynomial and verified: each drawn eigenvector v satisfies
7 * A v = lambda v to machine precision (the test suite checks it too).
8 */
9export class EigenvectorsExperiment extends BaseExperiment {
10 readonly id = "eigen-basis";
11
12 private a = 2;
13 private b = 1;
14 private c = 0.5;
15 private d = 1;
16
17 protected params(): ParameterDef[] {
18 return [
19 { key: "a", label: "Matrix a (row 1, col 1)", min: -3, max: 3, step: 0.1, defaultValue: 2 },
20 { key: "b", label: "Matrix b (row 1, col 2)", min: -2, max: 2, step: 0.1, defaultValue: 1 },
21 { key: "c", label: "Matrix c (row 2, col 1)", min: -2, max: 2, step: 0.1, defaultValue: 0.5 },
22 { key: "d", label: "Matrix d (row 2, col 2)", min: -3, max: 3, step: 0.1, defaultValue: 1 },
23 ];
24 }
25
26 protected onReset(): void {
27 this.a = this.num("a");
28 this.b = this.num("b");
29 this.c = this.num("c");
30 this.d = this.num("d");
31 }
32
33 protected onParameterChange(): void {
34 this.reset();
35 }
36
37 protected onUpdate(): void {}
38
39 private apply(x: number, y: number): [number, number] {
40 return [this.a * x + this.b * y, this.c * x + this.d * y];
41 }
42
43 /** Real eigenpairs from the characteristic polynomial when they exist. */
44 private eigenPairs(): { lambda: number; vx: number; vy: number }[] {
45 const trace = this.a + this.d;
46 const det = this.a * this.d - this.b * this.c;
47 const disc = trace * trace - 4 * det;
48 if (disc < 0) return [];
49 const sqrt = Math.sqrt(disc);
50 const l1 = (trace + sqrt) / 2;
51 const l2 = (trace - sqrt) / 2;
52 const vectorFor = (lambda: number): { vx: number; vy: number } => {
53 // (A - lambda I) v = 0: rows are [a-l, b] and [c, d-l].
54 if (Math.abs(this.b) > 1e-9) return { vx: this.b, vy: lambda - this.a };
55 if (Math.abs(this.c) > 1e-9) return { vx: lambda - this.d, vy: this.c };
56 if (Math.abs(this.a - lambda) <= Math.abs(this.d - lambda)) return { vx: 1, vy: 0 };
57 return { vx: 0, vy: 1 };
58 };
59 const pairs = [{ lambda: l1, ...vectorFor(l1) }];
60 if (Math.abs(sqrt) > 1e-9) pairs.push({ lambda: l2, ...vectorFor(l2) });
61 return pairs;
62 }
63
64 render(ctx: CanvasRenderingContext2D, theme: ThemeColors): void {
65 ctx.fillStyle = theme.bg;
66 ctx.fillRect(0, 0, this.width, this.height);
67 const scale = Math.min(this.width, this.height) / 6.4;
68 const cx = this.width / 2;
69 const cy = this.height / 2;
70 ctx.strokeStyle = theme.line;
71 ctx.beginPath();
72 ctx.moveTo(0, cy);
73 ctx.lineTo(this.width, cy);
74 ctx.moveTo(cx, 0);
75 ctx.lineTo(cx, this.height);
76 ctx.stroke();
77 // Transformed unit circle: the image of the circle under A.
78 ctx.strokeStyle = theme.viz[1];
79 ctx.lineWidth = 1.5;
80 ctx.beginPath();
81 for (let deg = 0; deg <= 360; deg += 2) {
82 const rad = (deg * Math.PI) / 180;
83 const [px, py] = this.apply(Math.cos(rad), Math.sin(rad));
84 const sx = cx + px * scale;
85 const sy = cy - py * scale;
86 if (deg === 0) ctx.moveTo(sx, sy);
87 else ctx.lineTo(sx, sy);
88 }
89 ctx.stroke();
90 // Transformed basis square (light)
91 ctx.strokeStyle = theme.line;
92 ctx.beginPath();
93 const corners: [number, number][] = [[1, 1], [-1, 1], [-1, -1], [1, -1]];
94 corners.forEach(([x, y], index) => {
95 const [px, py] = this.apply(x, y);
96 const sx = cx + px * scale;
97 const sy = cy - py * scale;
98 if (index === 0) ctx.moveTo(sx, sy);
99 else ctx.lineTo(sx, sy);
100 });
101 ctx.closePath();
102 ctx.stroke();
103 // Eigenvector directions: invariant lines through the origin.
104 let pairIndex = 0;
105 for (const pair of this.eigenPairs()) {
106 const norm = Math.hypot(pair.vx, pair.vy) || 1;
107 const ex = (pair.vx / norm) * 2.8 * scale;
108 const ey = (pair.vy / norm) * 2.8 * scale;
109 ctx.strokeStyle = pairIndex === 0 ? theme.accent : theme.accentStrong;
110 ctx.lineWidth = 2;
111 ctx.beginPath();
112 ctx.moveTo(cx - ex, cy + ey);
113 ctx.lineTo(cx + ex, cy - ey);
114 ctx.stroke();
115 pairIndex += 1;
116 }
117 ctx.font = "11px ui-monospace, monospace";
118 ctx.fillStyle = theme.fgTertiary;
119 ctx.fillText(`A = [[${this.a.toFixed(1)}, ${this.b.toFixed(1)}], [${this.c.toFixed(1)}, ${this.d.toFixed(1)}]]`, 10, 16);
120 }
121
122 getMetrics() {
123 const pairs = this.eigenPairs();
124 const trace = this.a + this.d;
125 const det = this.a * this.d - this.b * this.c;
126 return {
127 lambda1: pairs[0] ? pairs[0].lambda.toFixed(3) : "complex",
128 lambda2: pairs[1] ? pairs[1].lambda.toFixed(3) : "complex",
129 trace: trace.toFixed(2),
130 determinant: det.toFixed(2),
131 realEigenvectors: pairs.length,
132 };
133 }
134
135 describe(): string {
136 const pairs = this.eigenPairs();
137 if (pairs.length === 0) {
138 return "This matrix has complex eigenvalues: the unit circle's image rotates and scales without invariant directions.";
139 }
140 return `Two real eigenvector directions drawn in accent; along them the matrix acts as pure scaling by ${pairs[0].lambda.toFixed(2)} (and ${pairs[1]?.lambda.toFixed(2) ?? "n/a"}).`;
141 }
142
143 entities(): number {
144 return 2;
145 }
146}
147

Performance

Time per tick
O(1): a handful of arithmetic operations
Space
None
Notes
Set b = c = 0 and the matrix is diagonal: the eigenvectors snap to the axes, exactly as the algebra predicts.

Live numbers (FPS, frame time, steps per second, microseconds per step, entity count) are in the performance overlay on the simulation above: the pulse icon in its top-right corner.