Skip to content

Newton-Raphson

Follow the tangent; it knows the way.

Beginner / root-finding / iteration / calculus

Newton's method replaces the function with its tangent line and takes the line's root as the next guess: quadratic convergence when the start is kind, divergence when it isn't. Click anywhere to choose a start and watch the tangents walk - x-squared minus two lands on the square root of two to ten decimals in six steps.

Tagged: tangent line, quadratic convergence, divergence, basin

Live simulation

Click to choose a starting point; bad starts really do diverge.

Speed

Click to choose a starting point; bad starts really do diverge.

Mathematics

Newton's method linearizes the function at the current guess and solves the linear problem: x_{n+1} = x_n - f(x_n)/f'(x_n). Near a simple root the error squares each step - quadratic convergence - but a horizontal tangent (f' near 0) or an unlucky start can throw the iterate across the plot.

x_{n+1} = x_n - f(x_n) / f'(x_n)

quadratic convergence: e_{n+1} ~= C e_n^2

divergence signals: f'(x) -> 0, or iteration count blows past the cap

The tangent lines are drawn from every recent iterate, so the geometric meaning - follow the tangent to the x-axis - is the animation. For x^2 - 2 the iteration reaches the square root of 2 to machine precision in about six steps from a start that is off by one; the convergence-iteration count in the metrics shows the quadratic squeeze.

How it works

Clicking the plot sets the starting x; each tick performs a fixed number of steps, recording each tangent (position, value, slope) before applying the update. Convergence is declared when |f| drops below 1e-10; divergence when the derivative vanishes or the step cap is exceeded - both honest outcomes, both displayed.

For sin(x) - 0.4 there are infinitely many roots: the start point chooses which one you land in.

Implementation

src/engine/experiments/newton-raphson.ts · 195 lines · the module running above

TypeScript · fixed-timestep engine

1import { BaseExperiment } from "../core/base-experiment";
2import type { ParameterDef, PointerState, ThemeColors } from "../core/types";
3
4type Fn = { label: string; f: (x: number) => number; df: (x: number) => number; domain: [number, number]; start: number };
5
6const FUNCTIONS: Record<string, Fn> = {
7 "x^2 - 2": {
8 label: "x² - 2",
9 f: (x) => x * x - 2,
10 df: (x) => 2 * x,
11 domain: [-3.4, 3.4],
12 start: 2.2,
13 },
14 "x^3 - x - 2": {
15 label: "x³ - x - 2",
16 f: (x) => x ** 3 - x - 2,
17 df: (x) => 3 * x * x - 1,
18 domain: [-2.6, 2.6],
19 start: 0.5,
20 },
21 "sin(x) - 0.4": {
22 label: "sin(x) - 0.4",
23 f: (x) => Math.sin(x) - 0.4,
24 df: (x) => Math.cos(x),
25 domain: [-8, 8],
26 start: 1.2,
27 },
28};
29
30/**
31 * Newton-Raphson root finding, one tangent per step: x <- x - f(x)/f'(x),
32 * iterates drawn live on the function's plot. Convergence is quadratic when
33 * it works; the divergent starts are part of the lesson.
34 */
35export class NewtonRaphsonExperiment extends BaseExperiment {
36 readonly id = "newton-raphson";
37
38 private fn = FUNCTIONS["x^2 - 2"];
39 private x = 1.5;
40 private iterations = 0;
41 private history: { x: number; tangentFrom: number; slope: number }[] = [];
42 private status: "idle" | "running" | "converged" | "diverged" = "idle";
43 private pointer = { x: 0, y: 0, down: false, inside: false };
44
45 protected params(): ParameterDef[] {
46 return [
47 {
48 key: "fn",
49 label: "Function",
50 options: [
51 { value: "x^2 - 2", label: "x² - 2" },
52 { value: "x^3 - x - 2", label: "x³ - x - 2" },
53 { value: "sin(x) - 0.4", label: "sin(x) - 0.4" },
54 ],
55 defaultValue: "x^2 - 2",
56 },
57 { key: "steps", label: "Steps per tick", min: 1, max: 10, step: 1, defaultValue: 1 },
58 ];
59 }
60
61 protected onReset(): void {
62 this.fn = FUNCTIONS[this.str("fn")];
63 this.x = this.fn.start;
64 this.iterations = 0;
65 this.history = [];
66 this.status = "running";
67 }
68
69 protected onParameterChange(key: string): void {
70 if (key === "fn") this.reset();
71 }
72
73 onPointer(state: PointerState): void {
74 this.pointer = state;
75 if (state.inside && state.down) {
76 const [d0, d1] = this.fn.domain;
77 this.x = d0 + (state.x / Math.max(1, this.width)) * (d1 - d0);
78 this.iterations = 0;
79 this.history = [];
80 this.status = "running";
81 }
82 }
83
84 protected onUpdate(): void {
85 if (this.status !== "running") return;
86 const steps = this.num("steps");
87 for (let i = 0; i < steps; i++) {
88 const fx = this.fn.f(this.x);
89 const dfx = this.fn.df(this.x);
90 if (Math.abs(dfx) < 1e-12) {
91 this.status = "diverged";
92 return;
93 }
94 this.history.push({ x: this.x, tangentFrom: fx, slope: dfx });
95 if (this.history.length > 40) this.history.shift();
96 this.x -= fx / dfx;
97 this.iterations += 1;
98 if (Math.abs(this.fn.f(this.x)) < 1e-10) {
99 this.status = "converged";
100 return;
101 }
102 if (this.iterations > 60) {
103 this.status = "diverged";
104 return;
105 }
106 }
107 }
108
109 render(ctx: CanvasRenderingContext2D, theme: ThemeColors): void {
110 ctx.fillStyle = theme.bg;
111 ctx.fillRect(0, 0, this.width, this.height);
112 const [d0, d1] = this.fn.domain;
113 let fMin = -4;
114 let fMax = 4;
115 if (this.fn.label === "x³ - x - 2") [fMin, fMax] = [-3, 3];
116 const toX = (x: number) => ((x - d0) / (d1 - d0)) * this.width;
117 const toY = (y: number) => this.height / 2 - (y / (fMax - fMin)) * this.height * 0.9;
118 ctx.strokeStyle = theme.line;
119 ctx.beginPath();
120 ctx.moveTo(0, toY(0));
121 ctx.lineTo(this.width, toY(0));
122 ctx.stroke();
123 // Function
124 ctx.strokeStyle = theme.fgSecondary;
125 ctx.lineWidth = 1.6;
126 ctx.beginPath();
127 let pen = false;
128 for (let px = 0; px <= this.width; px += 1) {
129 const xv = d0 + (px / this.width) * (d1 - d0);
130 const yv = this.fn.f(xv);
131 if (Math.abs(yv) > 12) {
132 pen = false;
133 continue;
134 }
135 const py = toY(yv);
136 if (pen) ctx.lineTo(px, py);
137 else ctx.moveTo(px, py);
138 pen = true;
139 }
140 ctx.stroke();
141 // Tangent lines from recent iterates
142 this.history.forEach((entry, index) => {
143 const alpha = 0.25 + (0.6 * index) / Math.max(1, this.history.length);
144 ctx.strokeStyle = theme.viz[1];
145 ctx.globalAlpha = index === this.history.length - 1 ? 0.9 : alpha * 0.5;
146 ctx.beginPath();
147 const xa = entry.x - 1.2;
148 const xb = entry.x + 1.2;
149 ctx.moveTo(toX(xa), toY(entry.tangentFrom + entry.slope * (xa - entry.x)));
150 ctx.lineTo(toX(xb), toY(entry.tangentFrom + entry.slope * (xb - entry.x)));
151 ctx.stroke();
152 // Drop to x-axis: the new iterate
153 ctx.globalAlpha = 0.9;
154 ctx.strokeStyle = theme.accent;
155 ctx.setLineDash([3, 3]);
156 ctx.beginPath();
157 ctx.moveTo(toX(entry.x), toY(entry.tangentFrom));
158 ctx.lineTo(toX(entry.x), toY(0));
159 ctx.stroke();
160 ctx.setLineDash([]);
161 ctx.globalAlpha = 1;
162 });
163 ctx.lineWidth = 1;
164 // Current iterate on the axis
165 ctx.fillStyle = theme.accent;
166 ctx.beginPath();
167 ctx.arc(toX(this.x), toY(0), 4.5, 0, Math.PI * 2);
168 ctx.fill();
169 ctx.font = "11px ui-monospace, monospace";
170 ctx.fillStyle = theme.fgTertiary;
171 ctx.fillText(`${this.fn.label} - click to choose a starting point`, 10, 16);
172 }
173
174 getMetrics() {
175 return {
176 iteration: this.iterations,
177 x: this.x.toFixed(6),
178 f_x: this.fn.f(this.x).toExponential(2),
179 status: this.status,
180 };
181 }
182
183 describe(): string {
184 if (this.status === "converged") {
185 return `Newton-Raphson converged to x = ${this.x.toFixed(6)} in ${this.iterations} iterations (f(x) = ${this.fn.f(this.x).toExponential(1)}).`;
186 }
187 if (this.status === "diverged") return "The iteration diverged: this start runs away from every root.";
188 return `Newton-Raphson at x = ${this.x.toFixed(4)} after ${this.iterations} tangent steps.`;
189 }
190
191 entities(): number {
192 return this.iterations;
193 }
194}
195

Performance

Time per tick
O(1) per iteration (one f and one f' evaluation)
Space
A short tangent-history buffer
Notes
Start exactly at a maximum and f' = 0: the method is undefined - the plot shows the flat tangent that cannot proceed.

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.