Chapter 28
20 min read
Section 242 of 353

Applications: Steady-State Heat

Equilibrium and Potential

Learning Objectives

By the end of this section, you will be able to:

  1. Derive Laplace's equation 2u=0\nabla^2 u = 0 as the long-time limit of the heat equation.
  2. Explain the relaxation rule ui,j14(ui1,j+ui+1,j+ui,j1+ui,j+1)u_{i,j} \leftarrow \tfrac{1}{4}\bigl(u_{i-1,j} + u_{i+1,j} + u_{i,j-1} + u_{i,j+1}\bigr) and why averaging neighbours encodes the Laplacian.
  3. State and use the mean-value property and the maximum principle to predict, without computation, whether an interior point can ever be hotter than the boundary.
  4. Solve a steady-state heat problem by hand on a small grid and on a large grid with Python / PyTorch.
  5. Recognise where steady-state heat models appear in engineering — chip cooling, building insulation, planetary temperatures, image inpainting.

From Time Evolution to Equilibrium

"Wait long enough, and every temperature settles down. What remains is a frozen portrait of how heat balances itself."

Heat does not stand still. Pour hot coffee into a cold mug and the temperature changes everywhere in the system — the mug warms, the coffee cools, the air carries energy away. The transient stage of this story is told by the heat equation, tu=α2u\partial_t u = \alpha\,\nabla^2 u. But in many practical situations we do not care about the transient at all. We care about what the temperature looks like once nothing is changing any more: the engine block has been running for hours, the heat sink has reached its operating temperature, the planet has been receiving sunlight for billions of years.

When nothing changes any more, tu=0\partial_t u = 0. Plug that into the heat equation and the time derivative on the left collapses to zero, leaving

α2u=02u=0.\alpha\,\nabla^2 u = 0 \quad\Longleftrightarrow\quad \nabla^2 u = 0.

This is Laplace's equation. Every problem in this chapter — electrostatic potential, incompressible flow, gravitational potential in empty space — reduces to the same PDE because they all describe equilibrium states. Steady-state heat is simply the most physically intuitive entry point.


Warm-Up: The 1D Rod

Before going to two dimensions, settle the intuition in one. Take a thin uniform rod of length LL, perfectly insulated along its sides, held at temperature TLT_L on the left end and TRT_R on the right. Heat flows from hot to cold along the rod until the temperature u(x,t)u(x, t) stops changing.

In 1D the Laplacian 2u\nabla^2 u is just u(x)u''(x), so the steady-state equation becomes

u(x)=0,u(0)=TL,u(L)=TR.u''(x) = 0, \qquad u(0) = T_L, \qquad u(L) = T_R.

Integrate twice. The first integration gives u(x)=C1u'(x) = C_1, a constant slope. The second gives u(x)=C1x+C2u(x) = C_1 x + C_2, a straight line. The two boundary conditions pin C2=TLC_2 = T_L and C1=(TRTL)/LC_1 = (T_R - T_L)/L, so

u(x)  =  TL  +  TRTLLx.u(x) \;=\; T_L \;+\; \frac{T_R - T_L}{L}\, x.
The 1D moral. "Steady state" means "the second derivative vanishes." In 1D that forces the profile to be a straight line — there is just one shape it can take. The heat flux q=ku(x)=k(TLTR)/Lq = -k\,u'(x) = k\,(T_L - T_R)/L is also constant: just as much heat enters from the left as leaves from the right, every second.

Two dimensions are richer. The constraint uxx+uyy=0u_{xx} + u_{yy} = 0 still forces smoothness, but the field has room to bend — it can be steep in one direction and shallow in the perpendicular one. The rest of this section explores how that extra freedom unfolds.


Why Laplace's Equation Governs Steady-State Heat

Let us be slightly more careful. The 2D heat equation is

tu(x,y,t)  =  α(x2u+y2u).\partial_t u(x, y, t) \;=\; \alpha\,\bigl(\partial_x^2 u + \partial_y^2 u\bigr).

Here u(x,y,t)u(x, y, t) is the temperature at point (x,y)(x, y) at time tt, and α>0\alpha > 0 is the thermal diffusivity — large for copper, small for wood, ridiculous for ice cream.

Suppose the boundary temperatures uΩu\bigl|_{\partial \Omega} are held constant in time (a heater on one side, an ice bath on the other). Intuitively the interior temperature should approach a unique equilibrium — and it does. As tt \to \infty, u(x,y,t)u(x,y)u(x, y, t) \to u_\infty(x, y), and the limit obeys

  2u  =  x2u+y2u  =  0  \boxed{\;\nabla^2 u_\infty \;=\; \partial_x^2 u_\infty + \partial_y^2 u_\infty \;=\; 0\;}

with the same boundary data. This is a boundary value problem: no initial condition is needed because all memory of the past has decayed away.

There is also a direct, time-independent derivation that is worth carrying in your head. In equilibrium, the net heat flux into any small region must be zero (otherwise its temperature would change and we would not be in equilibrium). Fourier's law says the flux is q=ku\mathbf{q} = -k\,\nabla u. The divergence theorem turns "net flux through the boundary" into qdA\int \nabla \cdot \mathbf{q}\,dA, which equals k2udA-k \int \nabla^2 u\,dA. Setting this to zero for every region forces 2u=0\nabla^2 u = 0 pointwise. The Laplacian is just the "net flow out minus flow in" per unit area.


The Relaxation Rule and Why It Works

Discretise the plane on a square grid of spacing hh. The standard second-order finite differences give

x2uui,j12ui,j+ui,j+1h2,y2uui1,j2ui,j+ui+1,jh2.\partial_x^2 u \approx \frac{u_{i,j-1} - 2u_{i,j} + u_{i,j+1}}{h^2}, \qquad \partial_y^2 u \approx \frac{u_{i-1,j} - 2u_{i,j} + u_{i+1,j}}{h^2}.

Add them and set the sum to zero. The h2h^2 cancels, and we are left with

ui1,j+ui+1,j+ui,j1+ui,j+14ui,j  =  0,u_{i-1,j} + u_{i+1,j} + u_{i,j-1} + u_{i,j+1} - 4\,u_{i,j} \;=\; 0,

which rearranges into the celebrated five-point stencil:

ui,j  =  14(ui1,j+ui+1,j+ui,j1+ui,j+1).u_{i,j} \;=\; \tfrac{1}{4}\bigl(u_{i-1,j} + u_{i+1,j} + u_{i,j-1} + u_{i,j+1}\bigr).
The intuition in one sentence: at equilibrium, every cell is the average of its four neighbours. The heat flowing in from above-plus-below must equal the heat flowing out to left-plus-right, otherwise the cell would be heating or cooling.

That algebraic identity is also an algorithm. Pick any initial guess. Sweep through every interior cell and overwrite it with the average of its four current neighbours. Repeat. The field relaxes to the unique steady state. This is called Jacobi iteration, and it is the cousin of gradient descent on the energy functional E[u]=12u2dAE[u] = \tfrac{1}{2}\int |\nabla u|^2\,dA.


The Mean-Value Property

The discrete rule above is a finite-difference shadow of an exact continuous statement called the mean-value property of harmonic functions:

u(x0,y0)  =  12πrCr(x0,y0)udsu(x_0, y_0) \;=\; \frac{1}{2\pi r}\oint_{C_r(x_0, y_0)} u\,ds

for every disk of radius rr centred at (x0,y0)(x_0, y_0) that fits inside the domain. In words: the value at the centre equals the average of the values on the circle around it.

This is one of the most elegant facts in calculus. It says that harmonic functions are perfectly smooth and democratic: no single point can defy its neighbours, because it is forced to be their average. Slide the circle around the field below and watch the equality hold no matter where you put it.

Notice that the equality is exact — the slight numerical difference you see is purely discretisation error from sampling the continuous field on a finite grid.


The Maximum Principle: No Bumps Inside

From the mean-value property, an immediate corollary falls out that engineers rely on every day: the maximum principle.

Maximum principle (heat version). Suppose uu is harmonic on a domain Ω\Omega. Then the maximum and minimum of uu are both attained on the boundary Ω\partial \Omega, not in the interior.

Why? If uu had an interior local maximum at some point pp, then u(p)u(p) would be strictly greater than every nearby value — but the mean-value property forces u(p)u(p) to equal the average of those nearby values. Contradiction. The same argument rules out interior local minima.

Physically: no interior point can be hotter than every single point on the boundary. If your chip is dissipating heat purely through its boundary, and the package surface is at 80°C, then nowhere in the silicon can exceed 80°C in steady state. The only way to get an interior hotspot is to add an internal heat source, which turns Laplace into Poisson's equation 2u=f\nabla^2 u = -f — the subject of Chapter 33.


Worked Example: 3×3 Grid by Hand

Before letting the computer iterate ten thousand times, do two passes by hand on a tiny grid. This builds enormous intuition for what the solver is actually doing.

Walk through Jacobi iteration on a 5×5 grid (3×3 interior)

Setup. We use a 5×5 grid: a 3×3 interior of unknowns plus a one-cell-thick boundary on all sides. Boundary conditions: top = 100°, bottom = left = right = 0°. Seed the interior with the boundary average (100+0+0+0)/4=25(100 + 0 + 0 + 0)/4 = 25.

Initial field:
 100  100  100  100  100
   0   25   25   25    0
   0   25   25   25    0
   0   25   25   25    0
   0    0    0    0    0

Iteration 1. Apply ui,j=14(uN+uS+uE+uW)u_{i,j} = \tfrac{1}{4}(u_{N} + u_{S} + u_{E} + u_{W}) using the values from the previous field. For example, the top-left interior cell at position (1, 1):

u1,1(1)  =  14(100+25+0+25)  =  1504  =  37.5u^{(1)}_{1,1} \;=\; \tfrac{1}{4}(100 + 25 + 0 + 25) \;=\; \tfrac{150}{4} \;=\; 37.5

The top-middle cell at (1, 2):

u1,2(1)  =  14(100+25+25+25)  =  43.75u^{(1)}_{1,2} \;=\; \tfrac{1}{4}(100 + 25 + 25 + 25) \;=\; 43.75

Filling all nine interior cells the same way gives:

After iteration 1:
 100   100      100     100    100
   0    37.50   43.75   37.50    0
   0    18.75   25.00   18.75    0
   0    12.50   18.75   12.50    0
   0     0       0       0       0

Iteration 2. Repeat using the iteration-1 field. For example,

u1,2(2)  =  14(100+25+37.5+37.5)  =  50.u^{(2)}_{1,2} \;=\; \tfrac{1}{4}(100 + 25 + 37.5 + 37.5) \;=\; 50.
After iteration 2:
 100    100      100      100    100
   0     40.625   50.000   40.625   0
   0     18.750   25.000   18.750   0
   0      9.375   12.500    9.375   0
   0      0        0         0      0

The centre. Notice that the centre cell u2,2u_{2,2} stays at exactly 25.0 in every iteration. This is not a coincidence — it is the exact analytic value for this geometry. Here is why:

By symmetry, the centre temperature for the "top hot, others cold" problem equals some number cc. By rotating the box, the same number cc is the centre value for "bottom hot", "left hot", and "right hot" problems. By linearity of Laplace's equation, the sum of those four solutions solves the all-sides-at-100° problem, whose answer is the constant 100°. Therefore 4c=1004c = 100, so c=25c = 25.

After 20 Jacobi iterations the rest of the field still has not quite converged (the cells nearest the hot edge are still climbing toward their true values around 43° and 52°), but the centre is already exact. The mean-value property is doing real work here.


Interactive Solver

Here is the same algorithm scaled up to a 60×60 grid. Drag the four boundary sliders, press Play to watch the field relax, or step one iteration at a time. Click anywhere on the field to read off the temperature at that point.

A few experiments to do with this visualiser:

  • Set the four sides to 100, 0, 100, 0. The diagonal symmetry forces the centre to be exactly 50 — confirm by clicking the middle.
  • Set three sides to 0 and the fourth to 100. The centre converges to 25 (the rotation-symmetry argument from the worked example).
  • Make all four sides equal. The solution is a constant equal to that value — the only harmonic function with constant Dirichlet data on a closed boundary.
  • Watch the residual. It decays roughly geometrically, which reflects the slow eigenvalue of the Jacobi iteration matrix on a square grid (about 1π2/(2N2)1 - \pi^2/(2N^2)).

Python: Solving with Jacobi Iteration

Now let us write the same algorithm explicitly. Plain Python with NumPy makes every step transparent. Read every line carefully — the entire body of the function is just average your neighbours, repeated until nothing changes.

Steady-state heat via Jacobi iteration
🐍python
1NumPy is the entire toolbox here

We will work with a 2D array u of shape (N, N). Each entry stores the temperature at one grid point. The entire algorithm is one local averaging rule, so vectorized slicing in NumPy is enough — no SciPy needed.

3What the four inputs mean

top, bottom, left, right are the fixed temperatures held on the four edges of the square (Dirichlet boundary conditions). N is the grid resolution. tol is how small the largest change between successive iterations must become before we declare convergence. max_iter is a safety cap so a bad problem never loops forever.

EXECUTION STATE
top = 100 (constant temperature on top edge)
bottom = 0 (constant temperature on bottom edge)
left = 0 (constant temperature on left edge)
right = 0 (constant temperature on right edge)
N = 51 (grid is 51 × 51 = 2601 points)
tol = 1e-5 (stop when max change per cell < 1e-5)
10Seed the interior with the average of the four boundaries

Any starting guess converges to the same answer, but seeding with the boundary average puts every interior cell roughly in the right ballpark, so the algorithm converges much faster.

EXAMPLE
(100 + 0 + 0 + 0) / 4 = 25.0  →  every interior cell starts at 25.0
13Pin the four edges — they NEVER change

Dirichlet boundary conditions mean the boundary temperatures are imposed from outside (e.g. by a heater and an ice bath). Inside the loop we only touch interior cells; the boundary rows and columns stay at their assigned values forever.

EXECUTION STATE
u[0, :] = = 100 (top row, all columns)
u[-1, :] = = 0 (bottom row)
u[:, 0] = = 0 (left column)
u[:, -1] = = 0 (right column)
18The Jacobi loop: keep relaxing until nothing moves

Each pass replaces every interior temperature with the average of its four neighbours. If u already satisfies the discrete Laplace equation, the average equals u — no change happens. So the algorithm grinds away until it reaches a fixed point of the averaging rule, which is exactly the steady-state solution.

20Copy first — Jacobi reads from the OLD field

Jacobi iteration computes each new value from the old neighbour values, so we must not overwrite u during the sweep. We make u_new a fresh copy and write the new averages into u_new[1:-1, 1:-1]. (Gauss-Seidel skips this copy and overwrites in place — it converges roughly twice as fast but is harder to vectorize.)

21Vectorised five-point stencil — the heart of the algorithm

We replace nine nested Python loops with one NumPy expression using array slicing. u[:-2, 1:-1] is every interior cell SHIFTED UP by one row, so for each interior position (i, j) the value at that slot is u[i-1, j] — the north neighbour. Similarly u[2:, 1:-1] is the south neighbour, u[1:-1, :-2] is the west, u[1:-1, 2:] is the east. Average those four shifted views and assign in one shot.

EXECUTION STATE
u[:-2, 1:-1] = north neighbour for every interior cell
u[2:, 1:-1] = south neighbour
u[1:-1, :-2] = west neighbour
u[1:-1, 2:] = east neighbour
24Residual measures how far we are from equilibrium

If the maximum change anywhere on the grid is below the tolerance, every cell is essentially equal to the average of its neighbours, which is exactly the discrete form of ∇²u = 0. That is our stopping criterion.

EXAMPLE
np.max(np.abs(u_new - u))  →  shrinks roughly like 0.97^k for this geometry
28Sanity check via a symmetry argument

Here is a beautiful fact you can verify in your head: by linearity of Laplace's equation and the four-fold symmetry of the square, the centre temperature for the 'one hot side, three cold sides' problem is exactly (Thot)/4. If you set all four sides to 100°, the solution is a constant 100° (no gradients) — and that constant solution is the sum of four single-hot-side problems, each contributing the same centre value c. So 4c = 100, giving c = 25°.

32 lines without explanation
1import numpy as np
2
3def steady_state_heat(top, bottom, left, right, N=50, tol=1e-5, max_iter=20000):
4    """
5    Solve Laplace's equation  u_xx + u_yy = 0  on a unit square
6    with Dirichlet boundary conditions (constant temperature on each side)
7    via Jacobi iteration.
8
9    Returns the converged temperature field of shape (N, N).
10    """
11    u = np.full((N, N), (top + bottom + left + right) / 4, dtype=np.float64)
12
13    # Apply Dirichlet boundary conditions (these never change)
14    u[0, :]   = top      # row 0 is the top edge
15    u[-1, :]  = bottom
16    u[:, 0]   = left
17    u[:, -1]  = right
18
19    for k in range(max_iter):
20        # Average each interior cell against its four neighbours
21        u_new = u.copy()
22        u_new[1:-1, 1:-1] = 0.25 * (
23            u[:-2, 1:-1] + u[2:, 1:-1] + u[1:-1, :-2] + u[1:-1, 2:]
24        )
25        residual = np.max(np.abs(u_new - u))
26        u = u_new
27        if residual < tol:
28            print(f"Converged in {k + 1} iterations  (residual = {residual:.2e})")
29            return u
30
31    print(f"Stopped at max_iter ({max_iter}). Residual = {residual:.2e}")
32    return u
33
34
35# Try the classical "100° on top, 0° elsewhere" problem.
36u = steady_state_heat(top=100, bottom=0, left=0, right=0, N=51)
37
38# By a beautiful symmetry argument, the centre temperature is exactly 25°:
39# four superimposed problems (one hot side each) sum to a constant 100° field,
40# so by linearity each one contributes 25° at the centre.
41print(f"u(centre) = {u[25, 25]:.4f}      # should be very close to 25.0000")

Running this for N=51N = 51 converges in about 2,5002{,}500 iterations to a residual of 10510^{-5}. The reported centre temperature is 25.0000 to four decimals — exactly the symmetry prediction.


PyTorch: The Same Idea, Tensorized

The five-point stencil is a tiny convolution. That observation makes the GPU port absurdly short — we describe the averaging rule as a 3×3 kernel and let torch.nn.functional.conv2d do the work. The boundary still needs to be re-imposed by hand because conv2d does not know it is special.

Steady-state heat as a tiny convolution (GPU)
🐍python
1Two imports do everything

torch gives us GPU-backed tensor arithmetic. torch.nn.functional.conv2d gives us a five-point stencil as a single fused kernel call — much faster than slicing because the data never has to leave the GPU memory cache.

4Same signature, but auto-routed to GPU when available

The device argument lets the same function run on CPU during prototyping and on GPU for production-sized grids. For N = 1024 the GPU version is typically 50–200× faster than the NumPy version because every iteration is one kernel launch instead of millions of cache misses.

13Tensor shape is (batch, channels, height, width)

PyTorch convolutions expect a 4D tensor. We use batch = 1 and channels = 1 because there is exactly one temperature field. The .view(1, 1, 3, 3) on the kernel similarly turns a 3×3 matrix into the (out_channels, in_channels, kH, kW) layout that conv2d wants.

EXECUTION STATE
u.shape = torch.Size([1, 1, 128, 128])
kernel.shape = torch.Size([1, 1, 3, 3])
22Why this 3×3 kernel implements the same averaging rule

Look at the kernel. Its 0.25 entries sit at the N, S, E, W positions — exactly the four neighbours used by Jacobi. The centre weight is 0. When PyTorch slides this kernel over the field, each output pixel is 0.25·(N + S + E + W) — the four-neighbour mean. We have just turned an O(N²) double loop into one optimized convolution.

EXAMPLE
\frac{1}{4}\,\bigl(u_{i-1,j} + u_{i+1,j} + u_{i,j-1} + u_{i,j+1}\bigr)
32Padding = 1 keeps the same spatial size

Without padding the output would be (N − 2) × (N − 2) because the kernel cannot reach the very edge. With padding = 1, PyTorch surrounds the input with zeros, runs the convolution, then crops back to N × N. The boundary rows of u_new are corrupted by this — that is exactly why we re-apply the Dirichlet BCs in the next four lines.

35Re-impose the boundary every iteration

The convolution does not know that the boundary cells are special. After convolving we overwrite the four edges with the prescribed boundary temperatures, restoring the Dirichlet conditions. This is harmless and cheap.

39Same convergence test, executed on the GPU

(u_new - u).abs().max() reduces a full N×N tensor to a single scalar without leaving the device. .item() pulls that scalar back to Python so we can branch on it.

45Bring the field back to NumPy at the end

Inside the loop we keep everything on the GPU for speed. Only after convergence do we squeeze away the batch + channel axes and copy to CPU NumPy for plotting and downstream analysis.

42 lines without explanation
1import torch
2import torch.nn.functional as F
3
4def steady_state_heat_torch(top, bottom, left, right, N=128, tol=1e-5, max_iter=20000,
5                            device="cuda" if torch.cuda.is_available() else "cpu"):
6    """
7    Solve Laplace's equation on a unit square with constant Dirichlet BCs
8    using PyTorch tensors and a 3x3 averaging convolution.
9
10    Identical math to the NumPy version, but every iteration is one fused
11    GPU kernel — orders of magnitude faster for large N.
12    """
13    seed = (top + bottom + left + right) / 4.0
14    u = torch.full((1, 1, N, N), seed, dtype=torch.float32, device=device)
15
16    # Pin Dirichlet boundary conditions
17    u[..., 0,  :]  = top
18    u[..., -1, :]  = bottom
19    u[..., :,  0]  = left
20    u[..., :, -1]  = right
21
22    # Five-point stencil  =  3x3 conv kernel that picks N, S, E, W and averages
23    kernel = torch.tensor(
24        [[0.0, 0.25, 0.0],
25         [0.25, 0.0, 0.25],
26         [0.0, 0.25, 0.0]],
27        dtype=torch.float32, device=device,
28    ).view(1, 1, 3, 3)
29
30    for k in range(max_iter):
31        # Convolve with stride 1, padding 1 to keep the same shape
32        u_new = F.conv2d(u, kernel, padding=1)
33
34        # Re-apply boundary conditions (the conv would have rewritten them)
35        u_new[..., 0,  :]  = top
36        u_new[..., -1, :]  = bottom
37        u_new[..., :,  0]  = left
38        u_new[..., :, -1]  = right
39
40        residual = (u_new - u).abs().max().item()
41        u = u_new
42        if residual < tol:
43            print(f"Converged in {k + 1} iterations  (residual = {residual:.2e})")
44            return u.squeeze().cpu().numpy()
45
46    return u.squeeze().cpu().numpy()
47
48
49u = steady_state_heat_torch(top=100, bottom=0, left=0, right=0, N=129)
50print("u(centre) =", float(u[64, 64]))   # ≈ 25.0  (same 4-side superposition rule)

For N=1024N = 1024 the PyTorch version finishes in seconds on a consumer GPU while NumPy takes minutes. The algorithm is identical — the only thing that changed is who is doing the arithmetic.

Why this matters for ML practitioners. The same averaging kernel appears in image processing as a blur, in graph neural networks as a diffusion step, and in diffusion models as the score-matching denoiser. They are all discrete approximations of solutions to Laplace-like PDEs. Recognising the structure in one place teaches you the others.

Where This Shows Up in the Real World

Steady-state heat models are everywhere. A few representative examples:

SettingQuantityBoundary data
CPU / GPU die coolingSilicon temperature in steady operationHeat-sink contact temperature, ambient
Building thermal designWall and ceiling temperatures in winterIndoor 20°C / outdoor −5°C
Geothermal gradientCrust temperature with depthSurface ≈ 15°C / Moho ≈ 600°C
Heat-sink fin designFin surface temperature distributionBase temperature, convective ambient
Image inpaintingMissing pixels in a photoKnown pixels surrounding the hole
Cartography / shadingSmoothly interpolated elevationKnown contour-line values

The image-inpainting connection (a small detour)

Image inpainting — filling in a missing region of a photograph — is mathematically identical to a steady-state heat problem. Treat the known pixels as boundary conditions and let the missing pixels relax to the harmonic interpolant. The Jacobi iteration we just wrote, applied to a colour image with a hole cut out, is literally Poisson image editing without sources. Adding sources gives you Photoshop's seamless-cloning feature, which is Poisson's equation — the next chapter.


Summary

  1. Steady-state heat distributions are exactly the harmonic functions: solutions to 2u=0\nabla^2 u = 0 with prescribed boundary temperatures.
  2. The discrete five-point rule ui,j=14neighboursuu_{i,j} = \tfrac{1}{4}\sum_{\text{neighbours}} u is both the discrete Laplacian-equals-zero condition and the iterative algorithm that finds it.
  3. Harmonic functions obey the mean-value property — the centre of any disk equals the average around the rim — which forces all extrema to live on the boundary.
  4. The same algorithm runs in NumPy (CPU) and as a 3×3 convolution in PyTorch (GPU). The math is identical; only the data plumbing changes.
  5. Recognising harmonic interpolation in disguise — heat sinks, image inpainting, terrain shading, graph smoothing — turns a single PDE into a dozen practical tools.
Loading comments...