Chapter 33
28 min read
Section 279 of 353

Physical Interpretation and Applications

Poisson's Equation

Introduction

Section 1 introduced Poisson's equation 2ϕ=f\nabla^2 \phi = f as the master equation of potential fields. That tells us what the equation says. This section answers the harder question: why does the equation say it? What does the Laplacian 2\nabla^2 really measure? Why is a charge a "source" of potential? Why does a heavy weight create a tent-shaped dip in an elastic string? And how does the same equation power electric fields, gravity, soap films, fluid pressure, and modern image-processing tricks?

One sentence to remember the whole section: 2ϕ\nabla^2 \phi at a point measures how much the potential dips below the average of its surroundings. A positive source pushes the centre up above its neighbours; a negative source pulls it down. Poisson's equation is nothing more than "the deviation from the local average is set by the source density".

The Big Idea: Sources Warp Potentials

Picture a long, taut elastic string fixed at both ends. With no weights hanging from it, the string sits perfectly straight — that's the boundary value problem with zero source, namely Laplace's equation 2ϕ=0\nabla^2 \phi = 0. Now hang a small weight on it. The string sags into a single triangular dent right under the weight. Hang another weight further along, and the string's shape becomes the sum of two triangular dents. The string "remembers" every weight you place on it — and the remembrance is local, additive, and shaped exactly by the geometry of the string itself.

That story is the physical content of Poisson's equation in 1D: u(x)=f(x)-u''(x) = f(x). In 2D the string becomes an elastic membrane (a soap film, a drumhead), and the weights become localised pressures. In 3D the membrane becomes invisible — but you can still ask "what is the gravitational potential created by this mass distribution?" or "what is the electric potential from this charge distribution?" The mathematics is identical: 2ϕ=f\nabla^2 \phi = f.

Universal pattern: in every application, the right-hand side ff is a source density (charges, masses, heat injectors, pressure on a membrane) and the left-hand side 2ϕ\nabla^2 \phi is a geometric defect in the potential — its failure to equal its neighbours' average.

The Laplacian as Deviation from the Average

The cleanest way to feel what 2\nabla^2 actually does is to look at its discrete cousin on a regular grid. In 2D with spacing hh the standard 5-point stencil is

2ϕ(x,y)    ϕE+ϕW+ϕN+ϕS4ϕCh2  =  4h2(ϕE+ϕW+ϕN+ϕS4average of neighboursϕC).\nabla^2 \phi(x, y) \;\approx\; \frac{\phi_E + \phi_W + \phi_N + \phi_S - 4\phi_C}{h^2} \;=\; \frac{4}{h^2}\Big(\underbrace{\tfrac{\phi_E+\phi_W+\phi_N+\phi_S}{4}}_{\text{average of neighbours}} - \phi_C\Big).

Read the right-hand side as a comparison: take the average of the four neighbours of the cell, subtract the cell's own value, scale by 4/h24/h^2. That single quantity is the discrete Laplacian — it is a measurement of by how much the centre is below its neighbours' mean.

Sign of ∇²φ at a pointMeaningPhysical example
PositiveCentre is below neighbours' average — a valleyInside a positive charge cloud (potential dips toward extremum from outside)
ZeroCentre equals neighbours' average — harmonicVacuum / source-free region; mean value property
NegativeCentre is above neighbours' average — a peakLocal maximum of an electric potential, e.g. near a point charge

With this lens, Poisson's equation is a sentence: the source density at a point equals how much the potential there deviates from its surroundings, in units of 1/h21/h^2. The mean-value property of harmonic functions falls out as the special case where the deviation is zero everywhere.

Interactive: The Laplacian Detector

The Laplacian Detector

Click anywhere inside the heatmap to drop a probe. The probe (red square) compares the field value at its center to the average of its four neighbours (amber squares). That comparison is exactly what the discrete Laplacian computes: 2ϕϕE+ϕW+ϕN+ϕS4ϕCh2.\nabla^2 \phi \approx \frac{\phi_E + \phi_W + \phi_N + \phi_S - 4\phi_C}{h^2}.

Positive source in the middle
center φC = 0.9983
east φE = 0.9914
west φW = 0.9983
north φN = 0.9914
south φS = 0.9983
neighbour avg = 0.9948
deviation (φC − avg) = 0.0034
∇²φ ≈ -47.75
Sign of ∇²φ tells you whether the centre is a valley (positive) or a peak (negative) relative to its neighbours.
Try this: switch to the "Saddle" or "Linear ramp" field and click anywhere. You will see the centre always equals the average of the four neighbours, so 2ϕ=0\nabla^2 \phi = 0. These are harmonic functions — solutions of Laplace's equation. Switch to the Gaussian bump and click on its summit: now the centre is well above its neighbours, so 2ϕ\nabla^2 \phi is strongly negative. The bump "costs" a negative source to sustain its peak.

The Rubber-Membrane Analogy

Here is the most useful mental picture in the whole subject. Imagine an elastic string stretched horizontally between two pegs at x=0x = 0 and x=1x = 1. The string lives in a vertical plane. Now hang weights on it. Each weight pulls down with a force proportional to its mass; the string's tension fights back. In equilibrium the shape u(x)u(x) satisfies

Tu(x)=f(x),u(0)=u(1)=0,-T\,u''(x) = f(x), \quad u(0) = u(1) = 0,

where TT is tension (we'll set it to 1 for clarity) and f(x)f(x) is force per unit length. This is literally Poisson's equation in 1D. The negative sign means: where the load pushes down (f > 0), the string's shape curls downward (u'' < 0, peaks). And because u=2uu'' = \nabla^2 u in 1D, the "deviation from average" story applies word-for-word: the string's height at a point is below the average of the heights right next to it whenever there is a downward load there.

Even better: the same picture works in 2D for an elastic membrane stretched over a frame. Push down on it with a pressure f(x,y)f(x, y) and the membrane's height satisfies 2u=f-\nabla^2 u = f. A concentrated pressure (a heavy bead resting on the soap film) creates a cone-shaped dip; a smooth pressure creates a smooth bowl. The membrane is a physical analog computer for Poisson's equation.

Interactive: 1D Membrane Under Loads

1D Membrane: How Sources Sag the String

Imagine an elastic string fixed at x=0x=0 and x=1x=1. Hang weights on it. The shape that minimises elastic energy obeys u(x)=iwiδ(xsi)-u''(x) = \sum_i w_i\,\delta(x - s_i). Each weight pulls a single tent-shaped dent of height si(1si)wis_i(1-s_i)w_i. Move the sliders, watch how adding loads is exactly superposition of tents.

00.250.50.751xu(x)w=1.0
load #1

Notice every individual dashed tent has its peak at its own source. The cyan solution is the sum of those tents. This is the Green's-function representation of the 1D Poisson equation, made visible.

Two things to notice in the playground above:

  1. Each individual load creates a triangular tent. The peak of the tent is exactly under the load, and its height is s(1s)ws(1-s) \cdot w for a load of weight ww at position ss. So a unit load right in the middle (s=0.5s = 0.5) dips the string by exactly 0.250.25.
  2. Multiple loads simply add. The cyan curve is the pointwise sum of the dashed tents. This is linearity — Poisson's equation is a linear equation, so the response to a sum of sources is the sum of the responses.

Superposition and the Green's Function

The tent shape is not random. It is the Green's function of the 1D Poisson operator with grounded ends — the response to a single point source of strength 1. Once you know how the string reacts to a single point load, you know how it reacts to any load distribution, by summing tents (in the discrete case) or integrating (in the continuum case):

u(x)=01G(x,s)f(s)ds,G(x,s)={(1s)xxss(1x)x>s.u(x) = \int_0^1 G(x, s)\, f(s)\, ds, \qquad G(x, s) = \begin{cases} (1-s)\,x & x \le s \\ s\,(1-x) & x > s \end{cases}.

That is the whole content of the Green's-function method: convert a differential equation into an integral by knowing the response to a single Dirac kick. In higher dimensions the tent gets fancier — it becomes 1/(4πr)1/(4\pi r) in 3D, the Coulomb potential — but the idea is identical.

Big takeaway: a Green's function answers "how does this physical system respond to a single, sharp poke?" Once you have it, you have solved the equation for every possible right-hand side — just superpose pokes.

Electrostatic Intuition: Charges as Heaters of Potential

In electrostatics the same equation reads 2V=ρ/ε0\nabla^2 V = -\rho / \varepsilon_0. A positive charge density acts like a localised heater pushing the potential up; a negative density acts like a refrigerator pulling it down. In regions of empty space the potential is harmonic — it is the average of its surroundings — which is why field lines don't suddenly start or stop in vacuum and why a conductor placed in an electric field gets a uniform potential on its surface (any peak inside would be an electrostatic impossibility, by the maximum principle).

QuantitySymbolRole in Poisson
PotentialV(\mathbf{r})Unknown φ — what we are solving for
Electric field\mathbf{E} = -\nabla VGradient of the potential
Charge density\rho(\mathbf{r})Source f, scaled by -1/\varepsilon_0
Vacuum permittivity\varepsilon_0Coupling constant

The intuition translates directly: a positive point charge sits in a "dent" of the potential pointing up — its local value is higher than the surrounding mean. The Coulomb potential V=q/(4πε0r)V = q / (4\pi \varepsilon_0 r) is the 3D Green's function, exactly analogous to the triangular tent of the loaded string.

Gravitational Intuition: Mass Wells

Newtonian gravity satisfies 2Φ=4πGρm\nabla^2 \Phi = 4\pi G \rho_m with ρm\rho_m the mass density. The sign convention is flipped from electrostatics, which matches our intuition: mass is always positive, and a mass sits in a well of the gravitational potential, not a hill. Falling toward a planet is rolling down the well.

Why the same equation? Both electrostatics and Newtonian gravity are described by an inverse-square force law from a point source. Inverse-square force means inverse-distance potential, and the Laplacian of 1/r1/r away from the origin is exactly zero (you can verify by hand). At the origin it produces a delta function of strength 4π-4\pi, which is the source. This is why Poisson's equation is the universal language of inverse-square forces.

Interactive: 2D Poisson Solver

2D Poisson Solver (Jacobi Iteration)

Paint sources onto a grounded square ( ϕ=0\phi = 0 on every boundary). The animation runs the discrete Poisson update ϕi,j14(ϕE+ϕW+ϕN+ϕS+h2fi,j)\phi_{i,j} \leftarrow \tfrac{1}{4}(\phi_E + \phi_W + \phi_N + \phi_S + h^2 f_{i,j}) 30× per frame. Positive sources (white) build red bumps; negative sources (cyan) dig blue wells. The boundary value 0 forces the potential to flatten back to zero at the edges.

Jacobi iterations: 0

Paint a single positive source somewhere off-centre. The red bump that grows under it is the 2D analog of the triangular tent — its precise shape is the 2D Green's function (a logarithm, in fact). Paint a negative source next to it. Two blobs of opposite sign create a dipole pattern, with field lines flowing from + to − through the surrounding medium. The solver is literally computing the electrostatic potential of any charge distribution you draw, with grounded conducting walls.


Fluid Pressure in Incompressible Flow

For an incompressible fluid, the Navier–Stokes equations give a Poisson equation for the pressure:

2p=ρf(uu).\nabla^2 p = -\rho_f\, \nabla \cdot (\mathbf{u} \cdot \nabla \mathbf{u}).

The right-hand side is a divergence of advection — physically, regions where fluid parcels are getting pushed together or pulled apart. The pressure field adjusts itself so that the flow stays divergence-free. In computational fluid dynamics this is the celebrated pressure-Poisson equation, solved every time step inside every incompressible solver. The intuition is unchanged: pressure rises where the flow tries to compress, falls where it tries to expand, exactly as much as needed to keep the velocity field divergence-free.

Steady Heat with Sources

For steady heat conduction with internal heat generation q(r)q(\mathbf{r}) in a material of conductivity kk, the temperature obeys k2T=q-k \nabla^2 T = q. Same equation, again. The intuition this time: temperature at a point is the average of its surroundings — unless there is a heater there, in which case it pokes above. The maximum principle says the hottest point in a steady-state slab with no internal heat source must be on the boundary. Internal heaters break this and create temperature peaks in the interior.


Worked Example: Uniformly Loaded String

Let's do the cleanest analytical problem by hand. Solve

u(x)=1on (0,1),u(0)=u(1)=0.-u''(x) = 1 \quad \text{on } (0, 1), \qquad u(0) = u(1) = 0.

This is a string under uniform downward pressure. Try to predict the shape before scrolling — by symmetry it has to be a parabola peaked at x=1/2x = 1/2.

▶ Step-by-step solution (click to expand)
Step 1. Integrate once. From u(x)=1u''(x) = -1 we get u(x)=x+C1u'(x) = -x + C_1.
Step 2. Integrate again. u(x)=x2/2+C1x+C2u(x) = -x^2/2 + C_1 x + C_2.
Step 3. Apply u(0)=0u(0) = 0: C2=0C_2 = 0.
Step 4. Apply u(1)=0u(1) = 0: 1/2+C1=0    C1=1/2-1/2 + C_1 = 0 \;\Rightarrow\; C_1 = 1/2.
Step 5. Assemble: u(x)=12x(1x)u(x) = \tfrac{1}{2}\,x(1 - x). This is the famous parabola. The peak sits at x=1/2x = 1/2 with height u(1/2)=1/8=0.125u(1/2) = 1/8 = 0.125.
Sanity check from the discrete view: at any interior grid point xix_i, the discrete Poisson equation is ui+12ui+ui1=h2u_{i+1} - 2u_i + u_{i-1} = -h^2. On the parabola, the second difference of a quadratic is exact: u(x+h)2u(x)+u(xh)=u(x)h2=h2u(x+h) - 2u(x) + u(x-h) = u''(x) h^2 = -h^2. Match! So the parabola is not only the analytical solution but also the exact discrete solution — finite differences nail this problem on the first try.
Geometric reading: a uniform downward force bends the string into a parabola. The deeper the load (here unit force per length), the deeper the dip (proportional). Double the force and the dip doubles — linearity again. Move to a string twice as long and the dip grows by a factor of four (because L2L^2 appears in the formula umax=L2/8u_{\max} = L^2/8).

Building Intuition with Plain Python

Time to make the picture computational. We'll solve the same 1D problem numerically using Jacobi iteration — the simplest possible iterative scheme, perfect for building intuition because every step is just "replace each cell with the average of its neighbours plus a source term". The same deviation from average story we've been telling, now as an algorithm.

The idea: rearrange the discrete Poisson equation (ui12ui+ui+1)/h2=fi-(u_{i-1} - 2u_i + u_{i+1})/h^2 = f_i to solve for uiu_i:

ui  =  12(ui1+ui+1+h2fi).u_i \;=\; \tfrac{1}{2}\big(u_{i-1} + u_{i+1} + h^2\, f_i\big).

Now start with any guess (say zeros) and apply this rule everywhere, over and over. Each pass shaves a little error off. Eventually the rule is satisfied at every node and we have the solution.

Jacobi solver for the 1D Poisson equation
🐍solve_poisson_1d.py
1Import NumPy

We need vectorised arrays and basic math (linspace, ones_like, max, abs). NumPy is the right tool here because finite-difference grids are 1D / 2D arrays, and we want clean slice arithmetic, not Python loops over scalars.

EXAMPLE
import numpy as np  → np is now an alias for the NumPy module.
3Function signature: what the solver promises

The function takes a Python callable f, a grid resolution N (default 51 nodes including the two boundary points), a safety cap max_iter on the Jacobi loop, and a convergence tolerance tol. It returns the grid x, the discrete solution u, and the iteration count that was actually used.

EXAMPLE
Calling solve_poisson_1d(lambda x: np.ones_like(x)) means: solve -u''(x) = 1 with u(0)=u(1)=0. The exact solution is u(x) = x(1-x)/2, peaking at u(0.5) = 0.125.
EXECUTION STATE
f = callable: x → f(x)
N = 51 (grid nodes)
max_iter = 5000
tol = 1e-7
8Compute the grid spacing h

With N nodes evenly spaced on [0, 1] there are N − 1 intervals, each of length 1 / (N − 1). This h appears squared in the discrete Laplacian, so getting it right is critical.

EXAMPLE
If N = 51, then h = 1 / 50 = 0.02 and h² = 0.0004.
EXECUTION STATE
h = 0.02
9Build the x grid

np.linspace(0, 1, N) gives the array of node positions, including the two endpoints 0 and 1. These are the fixed boundary nodes for the Dirichlet condition.

EXAMPLE
x = [0.00, 0.02, 0.04, ..., 0.98, 1.00] (length 51).
EXECUTION STATE
x = array of 51 floats from 0 to 1
10Sample the source f at every node

We evaluate the source function once, vectorised. From now on f_vals[i] is the source at node i. Pre-computing avoids re-calling f inside the inner loop.

EXAMPLE
For f(x) = 1, f_vals = [1.0, 1.0, ..., 1.0] (length 51).
EXECUTION STATE
f_vals = array of 51 ones
12Initial guess u = 0

Jacobi needs an initial guess. Zeros are fine because (a) the boundary condition u(0)=u(1)=0 is already satisfied at the endpoints, and (b) the iteration is linear so the choice only affects convergence speed, not the limit.

EXAMPLE
u = [0.0, 0.0, ..., 0.0] (length 51).
EXECUTION STATE
u = zeros(51)
13Allocate u_new buffer

Jacobi computes new values using only old values; we cannot overwrite u in place during a sweep. So we keep a second buffer u_new and copy it back at the end of each sweep.

EXAMPLE
u_new is also zeros(51) initially.
EXECUTION STATE
u_new = zeros(51)
15Outer iteration loop

Each pass of this loop is one Jacobi sweep over all interior nodes. We stop early when the change is below tol, or hard-stop at max_iter as a safety net.

EXAMPLE
Typical run reaches diff < 1e-7 around iteration 3000 for N = 51.
16Inner loop over interior nodes (i = 1, …, N − 2)

We skip i = 0 and i = N − 1 because those are Dirichlet boundary nodes and stay pinned at 0. Updating every interior node from old neighbours is the defining property of Jacobi vs Gauss–Seidel.

LOOP TRACE · 2 iterations
First sweep, i = 25 (middle node), N = 51
u[24] = 0.0
u[26] = 0.0
h*h*f_vals[25] = 0.0004 * 1 = 0.0004
u_new[25] = 0.5 * (0 + 0 + 0.0004) = 0.0002
After ~500 sweeps, i = 25
u[24] = ≈ 0.1248
u[26] = ≈ 0.1248
h*h*f_vals[25] = 0.0004
u_new[25] = 0.5*(0.1248 + 0.1248 + 0.0004) = 0.1250
17The Jacobi update rule itself

This is the discrete Poisson equation rearranged for u_i. Starting from (u_{i+1} − 2u_i + u_{i-1}) / h² = −f_i, multiply by h², group, and divide by 2 to obtain u_i = ½(u_{i-1} + u_{i+1} + h²·f_i). The factor h²·f_i is the only term that knows about the source; the rest is plain averaging of neighbours.

EXAMPLE
Hand check at i = 25 in steady state: 0.5*(0.1248 + 0.1248 + 0.0004) = 0.1250 ✓
19Convergence measure: max change across the grid

np.max(np.abs(u_new − u)) is the L∞ norm of the update. When it drops below tol, every node has stopped moving by more than tol, so the iteration has reached a fixed point (which is the solution).

EXAMPLE
Typical sequence of diff values: 0.02 → 0.012 → 0.008 → ... → 1.1e-7 → stop.
EXECUTION STATE
diff = decreasing positive number
20Copy u_new back into u for the next sweep

We need a clean copy because u_new will be overwritten cell-by-cell in the next sweep, and the Jacobi rule requires us to read the old u values. .copy() makes a fresh allocation; assigning u = u_new without copy would alias the two buffers and break Jacobi.

EXAMPLE
After this line u and u_new share the same numerical values but live in different memory.
21Early termination

If we have already met the tolerance, no point doing more work. We break out and return the count so the caller can report convergence speed.

EXAMPLE
For N = 51, f = 1, this triggers around iter ≈ 3200.
24Return the answer

We hand back the grid, the converged solution, and how many iterations it took. The caller plots or compares to the analytical answer.

EXAMPLE
Returns (array, array, int) — a tuple of NumPy arrays plus a Python int.
26Use the solver: uniform load f = 1

Source f(x) = 1 everywhere — the famous textbook case. Exact solution is u(x) = x(1 − x)/2, a smooth parabola with maximum at x = 0.5 of value 0.125.

EXAMPLE
lambda x: np.ones_like(x) is a vectorised constant-1 function.
27Report convergence

Prints how many Jacobi sweeps we needed. Jacobi for 1D Poisson on N nodes scales like O(N²) sweeps for ε accuracy, which is why production code uses Gauss–Seidel, SOR or multigrid instead.

EXAMPLE
Typical output: 'converged in 3214 iterations'.
28Spot-check against the analytical solution

u(0.5) should be x(1 − x)/2 evaluated at 0.5, i.e. 0.125. The discrete solver matches to the floating-point limit because for the f = 1 case the discrete Laplacian is actually exact on a parabola (second differences of a quadratic are exact).

EXAMPLE
Expected printout: 'u(0.5) = 0.12500  (exact = 0.125)'.
EXECUTION STATE
u[len(u)//2] = 0.12500
exact = 0.125
10 lines without explanation
1import numpy as np
2
3def solve_poisson_1d(f, N=51, max_iter=5000, tol=1e-7):
4    """
5    Solve -u''(x) = f(x) on (0, 1) with u(0) = u(1) = 0
6    using Jacobi iteration on a uniform grid.
7    """
8    h = 1.0 / (N - 1)
9    x = np.linspace(0.0, 1.0, N)
10    f_vals = f(x)
11
12    u = np.zeros(N)            # initial guess; boundary stays 0
13    u_new = np.zeros(N)
14
15    for it in range(max_iter):
16        for i in range(1, N - 1):
17            u_new[i] = 0.5 * (u[i - 1] + u[i + 1] + h * h * f_vals[i])
18        diff = np.max(np.abs(u_new - u))
19        u = u_new.copy()
20        if diff < tol:
21            break
22
23    return x, u, it + 1
24
25x, u, iters = solve_poisson_1d(lambda x: np.ones_like(x))
26print(f"converged in {iters} iterations")
27print(f"u(0.5) = {u[len(u)//2]:.5f}  (exact = 0.125)")
Why Jacobi converges: the update rule is an averaging operation, and averaging is a smoothing operation. Errors that are short-wavelength (jagged) get smoothed out very fast; errors that are long-wavelength (smooth) take more sweeps to decay. This is exactly the observation that powers multigrid methods — solve the long-wavelength part on a coarser grid where each Jacobi sweep covers more ground.

Vectorized PyTorch Solver

The Python loops above are educational but slow — every interior cell touch costs a Python attribute lookup. Real solvers vectorise: replace the inner loop with a slice operation that hits every cell simultaneously. PyTorch is a natural choice because the same code runs unchanged on a GPU.

Moving up to 2D, the discrete Poisson equation becomes

ui,j  =  14(ui+1,j+ui1,j+ui,j+1+ui,j1+h2fi,j),u_{i,j} \;=\; \tfrac{1}{4}\big(u_{i+1,j} + u_{i-1,j} + u_{i,j+1} + u_{i,j-1} + h^2 f_{i,j}\big),

which is "the cell equals the average of its four neighbours plus the local source" — the deviation-from-average story in two dimensions.

Vectorised 2D Poisson solver in PyTorch
🐍solve_poisson_2d_torch.py
1Import PyTorch

PyTorch gives us GPU tensors and vectorised slice arithmetic. We do not need autograd here — Jacobi is a fixed-point iteration, not a learning loop — but the same code style scales naturally to differentiable PDE solvers if we ever want to learn boundary conditions or sources.

EXAMPLE
import torch  → tensors live on CPU by default, or on 'cuda' if specified.
32D solver signature

Same idea as the 1D Python version but two-dimensional. f is a callable f(X, Y) over coordinate meshgrids. device='cpu' lets you flip to 'cuda' on a GPU without changing anything else.

EXAMPLE
solve_poisson_2d_torch(f, N=64) gives a 64×64 grid (4096 nodes).
EXECUTION STATE
N = 64 (grid is N × N)
max_iter = 3000
tol = 1e-6
8Grid spacing in 2D

Square grid on [0, 1] × [0, 1] with N points per side gives the same h = 1 / (N − 1). The discrete 2D Laplacian uses h² once because the grid is isotropic.

EXAMPLE
For N = 64, h = 1/63 ≈ 0.01587, h² ≈ 0.000252.
EXECUTION STATE
h2 = 0.000252
11Build the meshgrid

torch.meshgrid(xs, ys, indexing='xy') gives X, Y tensors of shape (N, N) where X varies along the column axis and Y along the row axis. Together they label every grid point with its (x, y) coordinates. This is the standard NumPy-compatible orientation.

EXAMPLE
X[0, 5] = 5/63 ≈ 0.079 and Y[3, 0] = 3/63 ≈ 0.048.
EXECUTION STATE
X =
shape (64, 64), x-coordinate at each cell
Y =
shape (64, 64), y-coordinate at each cell
13Sample the source field once

We evaluate the source on the whole grid in one vectorised call. For the Gaussian source used below, f_vals is a bump centred at (0.5, 0.5) with peak height 50.

EXAMPLE
f_vals[32, 32] = 50 · exp(0) = 50.0 (centre); f_vals[0, 0] ≈ 50 · exp(-30·0.5) ≈ 1.5e-6 (corner).
EXECUTION STATE
f_vals =
shape (64, 64), Gaussian bump
14Initialise u to zero everywhere

Zeros satisfy the Dirichlet boundary condition u = 0 on all four sides immediately. The interior nodes will grow during iteration; the boundary nodes will stay pinned because we never write to them.

EXAMPLE
u is a (64, 64) tensor of float32 zeros.
EXECUTION STATE
u =
zeros((64,64))
16Main Jacobi loop

Same outer loop as the 1D version: each iteration is one sweep. The inner work is now a few tensor slice operations instead of a Python double loop, so each sweep is ~100× faster.

18The four neighbour averages — fully vectorised

Each slice picks out one neighbour of every interior cell at once. u[2:, 1:-1] is 'every cell shifted up by 1' (east neighbour), u[:-2, 1:-1] is 'every cell shifted down by 1' (west), and similarly for north and south. All four have the same shape (N − 2, N − 2). Summing them plus the source term gives the new interior values in one shot.

EXAMPLE
Centre cell at (32, 32) gets averaged with (33, 32), (31, 32), (32, 33), (32, 31) plus h²·50 ≈ 0.0126.
EXECUTION STATE
avg =
shape (62, 62), proposed new interior
21Convergence test

We compare the proposed update to the current interior. .abs().max() is the L∞ norm — when this drops below tol every interior cell has stopped changing significantly, so we are at the fixed point of the discrete Poisson equation.

EXAMPLE
Typical diff schedule: 0.01 → 1e-3 → 1e-4 → ... → 9e-7 (stop).
EXECUTION STATE
diff = scalar tensor, decreasing
22Write the update back

u[1:-1, 1:-1] = avg copies the new interior into u in place. The boundary slice u[0, :], u[-1, :], u[:, 0], u[:, -1] is never touched, so the Dirichlet boundary remains exactly 0.

EXAMPLE
After this, u[32, 32] is the new centre value; u[0, 32] is still 0.
23Early termination

If diff < tol, the solver has converged. Breaking out saves the rest of max_iter. For the Gaussian source on N = 64 this typically fires around iter ≈ 1800.

26Return everything the caller needs

X and Y for plotting, u for the answer, it + 1 for diagnostics. Returning (X, Y, u) instead of just u makes downstream plotting one line.

29Pick a Gaussian source

50 · exp(−30·((X−0.5)² + (Y−0.5)²)) is a sharp bump centred at the middle. The integral of f over the square is finite, so the resulting potential has a smooth dome shape that decays to zero at the grounded boundary.

EXAMPLE
f at the centre is 50.0; at distance 0.3 from the centre f ≈ 50·exp(-2.7) ≈ 3.4.
30Run the solver

One call. Note that increasing N would refine the grid and slow convergence (Jacobi sweeps grow like O(N²) for fixed accuracy); switching device='cuda' would speed up the inner sweep dramatically because every Jacobi step is just five elementwise tensor ops.

EXECUTION STATE
u.max() = ≈ 0.40 (depends on source strength)
iters = ≈ 1800 for N=64
31Report

Prints iteration count and peak potential. A useful sanity check: doubling the source amplitude should double the peak (Poisson is linear); halving h should leave the peak almost unchanged (the answer is a continuum property).

EXAMPLE
Expected stdout: 'converged in ~1800 iters, peak u = 0.4031'.
18 lines without explanation
1import torch
2
3def solve_poisson_2d_torch(f, N=64, max_iter=3000, tol=1e-6, device="cpu"):
4    """
5    Solve -∇²u = f on (0,1)² with u = 0 on the boundary
6    using vectorised Jacobi on a (N, N) tensor.
7    """
8    h = 1.0 / (N - 1)
9    h2 = h * h
10
11    xs = torch.linspace(0, 1, N, device=device)
12    ys = torch.linspace(0, 1, N, device=device)
13    X, Y = torch.meshgrid(xs, ys, indexing="xy")
14    f_vals = f(X, Y)
15
16    u = torch.zeros(N, N, device=device)
17
18    for it in range(max_iter):
19        # Average of four neighbours plus source term, vectorised
20        avg = 0.25 * (u[2:, 1:-1] + u[:-2, 1:-1]
21                    + u[1:-1, 2:] + u[1:-1, :-2]
22                    + h2 * f_vals[1:-1, 1:-1])
23        diff = (avg - u[1:-1, 1:-1]).abs().max()
24        u[1:-1, 1:-1] = avg
25        if diff < tol:
26            break
27
28    return X, Y, u, it + 1
29
30# Gaussian source centred at (0.5, 0.5)
31f = lambda X, Y: 50.0 * torch.exp(-30.0 * ((X - 0.5)**2 + (Y - 0.5)**2))
32X, Y, u, iters = solve_poisson_2d_torch(f, N=64)
33print(f"converged in {iters} iters, peak u = {u.max().item():.4f}")
Performance intuition: the inner loop is now five tensor ops, each of which the GPU or CPU executes as a single vectorised kernel. On a 256 × 256 grid this is roughly 100× faster than the nested Python loop, and another 30–50× faster again on a GPU. The mathematical content is identical — only the bookkeeping changed.

Modern Applications

Once you can solve Poisson's equation efficiently, an astonishing range of problems opens up. The applications go far beyond physics.

ApplicationWhat plays the role of φWhat plays the role of f
ElectrostaticsElectric potential V−ρ / ε₀
GravitationGravitational potential Φ4πG ρ_m
Steady-state heatTemperature THeat source / k
Pressure-Poisson (CFD)Pressure p−ρ_f ∇·(u·∇u)
Soap film / membraneHeight u(x, y)Local downward force
Poisson image blendingPixel intensity over a regionLaplacian of the source patch
Poisson surface reconstructionIndicator function of a 3D solidDivergence of the oriented normals
Score-based diffusion modelsLog-density of dataDivergence of the score field

Poisson Image Editing

The seminal SIGGRAPH 2003 paper by Pérez, Gangnet, and Blake uses Poisson's equation to seamlessly paste a region of one image into another. Instead of copying pixel intensities, copy the Laplacian of the source patch and solve a Poisson equation inside the target region with the surrounding pixels as Dirichlet boundary conditions. The result is a clone whose colours match the destination's lighting and tone — because Poisson's equation literally propagates boundary information into the interior in the smoothest possible way.

Poisson Surface Reconstruction

Kazhdan, Bolitho, and Hoppe's 2006 algorithm reconstructs a watertight 3D surface from a point cloud with oriented normals. They solve a Poisson equation 2χ=V\nabla^2 \chi = \nabla \cdot \mathbf{V} where the source is the divergence of the smoothed normal field. The level set χ=0.5\chi = 0.5 of the solution is the reconstructed surface. Every LiDAR-to-mesh pipeline you see in robotics or VR likely passes through this Poisson solve.

Generative AI: Score-Based Diffusion

Modern diffusion models (Score-SDE, EDM, Stable Diffusion) learn a vector field s(x)=xlogp(x)\mathbf{s}(\mathbf{x}) = \nabla_{\mathbf{x}} \log p(\mathbf{x}) — the gradient of the data's log-density. Recovering the density from the score is, formally, a Poisson problem on the divergence of the score field. The training objective (denoising score matching) is therefore mathematically a way of fitting sources to the Poisson equation governing the data manifold — a startling, modern appearance of the same 1820s equation we've been studying.


Summary

  • The Laplacian 2ϕ\nabla^2 \phi at a point measures how much the potential deviates from the average of its surroundings. Poisson's equation is just "deviation = source".
  • A loaded elastic string is a physical analog computer for the 1D Poisson equation. Each point load creates a triangular tent; multiple loads superpose. The tent is the Green's function of the operator.
  • The same equation governs electric potential, gravitational potential, steady-state temperature, and fluid pressure. The signs and constants differ; the geometry is identical.
  • Numerically, Poisson's equation reduces to repeated local averaging plus a source term. Jacobi iteration is the simplest such scheme and the cleanest incarnation of the "deviation from average" intuition.
  • The same operator powers modern applications: Poisson image editing, surface reconstruction from point clouds, and the recovery of data density in score-based diffusion models.
Looking ahead: in the next section we will formalise the Green's function approach properly, deriving the fundamental solution in 1D, 2D and 3D, and showing how any source distribution can be expressed as a superposition of point-source responses. The tent we played with in this section will become the tip of a beautiful analytical iceberg.
Loading comments...