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. Keyboard: Space runs or pauses, . steps, r resets.
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
| 1 | import { BaseExperiment } from "../core/base-experiment"; |
| 2 | import type { ParameterDef, PointerState, ThemeColors } from "../core/types"; |
| 3 | |
| 4 | type Fn = { label: string; f: (x: number) => number; df: (x: number) => number; domain: [number, number]; start: number }; |
| 5 | |
| 6 | const 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 | */ |
| 35 | export 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.