If a system is linear — meaning it doesn't mix or warp inputs — you can solve it in pieces and stack the answers. That's the whole thing.
An operator L is linear if it satisfies exactly two rules. Call them homogeneity and additivity:
Think of it geometrically. A linear operator is a flat transformation of space — it can stretch, rotate, reflect. But it can't fold, warp, or curve. Lines through the origin stay lines. The origin stays fixed. No mixing of coordinates in nonlinear ways.
| Operation | Linear? | Why |
|---|---|---|
| d/dt [ f(t) ] | ✓ Yes | Derivative distributes over sums, pulls out constants |
| ∫ f(t) dt | ✓ Yes | Integration distributes, constants come out |
| Matrix multiply Ax | ✓ Yes | A(u+v) = Au + Av by matrix algebra |
| y² or y·y' | ✗ No | Products of the unknown — breaks additivity |
| sin(y) or eʸ | ✗ No | Nonlinear functions of y — can't factor out |
Watch what L does to a vector. Linear transforms keep grid lines straight and origin fixed. Toggle nonlinear mode to see what breaks.
A linear system Ax = b has a beautiful decomposition. The complete solution is always a particular solution plus the null space:
Geometrically: x_particular is one arrow that points at b. The null space is a flat subspace through the origin that A crushes to zero. You can shift x_p anywhere within that flat subspace and still land on b.
Also: if you have multiple targets, split them. If b = b₁ + b₂, solve Ax₁ = b₁ and Ax₂ = b₂ separately, then add x₁ + x₂. That's superposition of sources.
Drag the two source vectors b₁ (gold) and b₂ (teal). Their particular solutions x₁ and x₂ appear. The full solution x₁+x₂ (rose) always satisfies A(x₁+x₂) = b₁+b₂.
// Superposition in JavaScript — Ax = b function solve2x2(A, b) { // Cramer's rule for 2x2 const det = A[0][0] * A[1][1] - A[0][1] * A[1][0]; return [ (b[0] * A[1][1] - b[1] * A[0][1]) / det, (A[0][0] * b[1] - A[1][0] * b[0]) / det ]; } const A = [[2, 1], [1, 3]]; const b1 = [3, 0]; // first source const b2 = [0, 4]; // second source const x1 = solve2x2(A, b1); // particular for b1 const x2 = solve2x2(A, b2); // particular for b2 // Superposition: just add them const x_total = [x1[0] + x2[0], x1[1] + x2[1]]; // Verify: A·x_total should equal b1 + b2 const b_total = [b1[0]+b2[0], b1[1]+b2[1]]; // ✓ same result as solving A·x = b_total directly
The exact same structure appears. A linear ODE uses L as a differential operator:
y_h captures what the system "wants to do" on its own — its natural frequencies. y_p captures how the system responds to the external forcing g(t). They're independent, and linearity lets you add them freely.
If the forcing has multiple pieces, split and conquer:
Two particular solutions to y'' + ω²y = Asin(ωt) at different frequencies. Watch how they add. Toggle components on/off to see each piece.
// ODE superposition — animated wave demo const t_vals = linspace(0, 4 * Math.PI, 500); // Two forcing frequencies const ω1 = 1.0, A1 = 1.0; const ω2 = 2.3, A2 = 0.6; // Particular solution to y'' + ω²y = A·sin(Ωt) // with Ω ≠ ω → y_p = A/(ω²-Ω²) · sin(Ωt) function yp(t, ω_nat, Ω_drive, A) { const denom = ω_nat ** 2 - Ω_drive ** 2; return (A / denom) * Math.sin(Ω_drive * t); } // Superposed solution — just add const y_sum = t_vals.map(t => yp(t, 3.0, ω1, A1) + yp(t, 3.0, ω2, A2) );
For y'' + 2γy' + ω₀²y = cos(ωt). The natural response decays (damped oscillator). The particular (steady-state) persists. Their sum is the full solution.
Here's the visual intuition that ties it all together. In 2D vector space, A transforms the plane. The solution to Ax = b is a point in input space that maps to b in output space. The null space is the flat direction A is blind to.
Left: input space (x). Right: output space (b = Ax). The particular solution x_p maps exactly to b. Any null space shift stays invisible — A crushes it to 0.
// Universal superposition pattern — works everywhere function solveLinearSystem(L, sources) { // sources = [g1, g2, g3, ...] (right-hand side terms) // Step 1: solve homogeneous const y_h = solveHomogeneous(L); // Step 2: solve particular for EACH source independently const y_particulars = sources.map(g => solveParticular(L, g)); // Step 3: superpose — just add everything const y_p = y_particulars.reduce((sum, ypi) => add(sum, ypi)); // General solution return add(y_h, y_p); // This works whether L is a matrix, d/dt, or any linear operator. // Linearity is the only requirement. }