Chapter 28
22 min read
Section 236 of 353

Boundary Value Problems

Laplace's Equation

Learning Objectives

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

  1. State the three classical types of boundary conditions for Laplace's equation (Dirichlet, Neumann, Robin) and recognise which one a physical setup demands.
  2. Explain why a PDE alone — even one as rigid as 2u=0\nabla^2 u = 0 — has infinitely many solutions, and how boundary data picks out exactly one.
  3. Use the maximum principle to argue uniqueness and to bound the interior of a harmonic function in terms of its boundary.
  4. Check the compatibility condition Ωgds=0\oint_{\partial\Omega} g\,ds = 0 that a pure Neumann problem must satisfy.
  5. Implement a Jacobi relaxation solver from scratch in Python, and a differentiable version in PyTorch that you can use inside a learning loop.

The Big Picture

In section 28.1 we introduced Laplace's equation 2u=uxx+uyy=0\nabla^2 u = u_{xx} + u_{yy} = 0 and saw that its solutions — harmonic functions — model equilibrium: steady-state heat, electrostatic potential, ideal-fluid streamfunctions, gravitational potential outside masses. Equilibrium is what is left after every transient has died out.

A natural question follows immediately: given that the interior is in equilibrium, what is it in equilibrium with? The answer is the boundary. A drumhead at rest is in equilibrium with the rim it is glued to. A metal plate's steady temperature is in equilibrium with whatever the edges are doing. A capacitor's electric potential is in equilibrium with the voltages on its plates.

Slogan. Laplace's equation is the law of the interior. Boundary conditions are the law of the edge. A Boundary Value Problem (BVP) is the two of them welded together — and only then does the problem have a unique answer.

Why a PDE Needs Boundary Conditions

For an ordinary differential equation, you fix the solution with initial conditions: tell me where the particle is and how fast it is moving at t=0t = 0, and I will tell you x(t)x(t) for all later time.

For Laplace's equation there is no time. Time has already finished — we are sitting in the equilibrium that remains. So we cannot pin the answer down with initial conditions. Instead we pin it down with spatial conditions on the edge of the region.

Here is the most striking demonstration. The function u(x,y)=0u(x, y) = 0 is harmonic. So is u(x,y)=xu(x, y) = x. So is u(x,y)=x2y2u(x, y) = x^2 - y^2. So is u(x,y)=excos(y)u(x, y) = e^{x} \cos(y). All four solve the same PDE uxx+uyy=0u_{xx} + u_{yy} = 0 identically. The PDE alone cannot distinguish them — they look different only at the boundary.

Intuition. Think of 2u=0\nabla^2 u = 0 as the rule "every point equals the average of its neighbours" (the mean-value property — we will prove this in section 28.1). Many different fields satisfy that rule. The boundary is what tells the field where to start averaging from.

The Three Classical Boundary Conditions

On the boundary Ω\partial\Omega of our region ΩR2\Omega \subset \mathbb{R}^2 we can prescribe the value of uu, the outward normal derivative u/n\partial u / \partial n, or a linear combination of the two. These give the three classical boundary conditions.

Dirichlet: Prescribed Value

A Dirichlet boundary condition prescribes the value of the solution on the boundary:

u(x)=f(x)for all xΩ.u(\mathbf{x}) = f(\mathbf{x}) \quad \text{for all } \mathbf{x} \in \partial\Omega.

Physical picture. A metal plate whose edges are welded to ice baths and steam pipes: the temperature on the edge is whatever the bath dictates, no matter what is happening inside. Electrostatically, a Dirichlet boundary is a conductor held at a fixed voltage by a battery.

Dirichlet conditions are the most rigid: the edge value is locked. They give the cleanest existence and uniqueness theory.

Neumann: Prescribed Flux

A Neumann boundary condition prescribes the outward normal derivative:

un(x)=g(x)for all xΩ,\dfrac{\partial u}{\partial n}(\mathbf{x}) = g(\mathbf{x}) \quad \text{for all } \mathbf{x} \in \partial\Omega,

where n\mathbf{n} is the outward unit normal to Ω\partial\Omega.

Physical picture. Now we are not specifying the temperature on the edge — we are specifying the rate at which heat flows through it. If g=0g = 0, the boundary is perfectly insulated: no heat enters or leaves. If g>0g > 0 at some point, heat is being pumped out there. Electrostatically, prescribing u/n\partial u / \partial n on a surface is the same as prescribing the surface charge density (up to a constant).

Neumann conditions have a subtlety: if you Neumann-specify every part of the boundary, the solution is only unique up to an additive constant (you cannot tell what temperature it is — only what the heat flux is). And the data gg must satisfy the compatibility condition Ωgds=0\oint_{\partial\Omega} g\,ds = 0 we will derive below.

Robin: A Mix of Both

A Robin (or "third-kind") boundary condition is a linear combination:

αu+βun=hon Ω,α,β0.\alpha\, u + \beta\, \dfrac{\partial u}{\partial n} = h \quad \text{on } \partial\Omega, \quad \alpha,\beta \neq 0.

Physical picture. Newton's law of cooling. Suppose the boundary is a thin wall exchanging heat with surroundings at temperature TT_\infty. Heat flux through the wall is proportional to the temperature difference:

kun=hconv(uT).-k\dfrac{\partial u}{\partial n} = h_{\text{conv}}\,(u - T_\infty).

Rearranged, this is exactly the Robin form. As hconvh_{\text{conv}} \to \infty Robin degenerates to Dirichlet (the wall instantly matches the outside temperature). As hconv0h_{\text{conv}} \to 0 Robin degenerates to Neumann with g=0g = 0 (the wall is a perfect insulator).

Mixed vs. Robin — Don't Confuse Them

Two phrases sound similar but mean different things:

TermMeaningExample
Mixed BVPDifferent sides of the boundary carry different types (some Dirichlet, others Neumann).Top: u = 100 (Dirichlet). Sides: insulated (Neumann, g = 0).
Robin BVPA linear combination of u and ∂u/∂n on the same side of the boundary.All sides obey α u + β ∂u/∂n = h (Newton cooling).

What "Well-Posed" Really Means

Jacques Hadamard introduced three criteria a sensible BVP should meet. We call the problem well-posed if all three hold:

  1. Existence. A solution exists for any reasonable boundary data.
  2. Uniqueness. No two distinct solutions share the same boundary data.
  3. Continuous dependence. Small changes in the boundary data produce small changes in the solution. Tiny measurement errors cannot blow up into wildly different temperatures.

The Dirichlet problem for Laplace's equation on a bounded well-behaved domain satisfies all three. The Neumann problem satisfies all three once the compatibility condition holds and we agree to consider solutions modulo an additive constant. Robin is well-posed whenever α,β>0\alpha, \beta > 0.

Ill-posed problems are not unsolvable — they are physically wrong-headed. The Cauchy problem for Laplace's equation (specify both uu and u/n\partial u / \partial n on a curve and evolve away from it) is the textbook example of an ill-posed problem: tiny perturbations in the data cause arbitrarily large changes in the solution. Inverse problems in imaging and geophysics inherit this sensitivity and need regularisation.

The Maximum Principle (the Secret Weapon)

Harmonic functions have a remarkable shape constraint. The strong maximum principle says:

Theorem. Let Ω\Omega be a bounded connected open set in Rn\mathbb{R}^n and suppose uu is harmonic in Ω\Omega and continuous on Ω\overline{\Omega}. Then uu attains its maximum and minimum on the boundary Ω\partial\Omega. Moreover, if uu attains its max or min at any interior point, then uu is constant.

Why does this happen? Because of the mean-value property: u(x0)u(\mathbf{x}_0) is the average of uu on any small disk around x0\mathbf{x}_0. If x0\mathbf{x}_0 were strictly larger than every value in its neighbourhood, it could not equal their average — a contradiction. So a strict interior maximum is impossible.

Uniqueness from the Maximum Principle

The maximum principle gives a one-line proof of uniqueness for the Dirichlet BVP. Suppose u1u_1 and u2u_2 both solve 2u=0\nabla^2 u = 0 in Ω\Omega with the same boundary data u=fu = f on Ω\partial\Omega. Let w=u1u2w = u_1 - u_2. Then ww is harmonic in Ω\Omega with w=0w = 0 on Ω\partial\Omega. By the maximum principle, maxw=minw=0\max w = \min w = 0, so w0w \equiv 0 in Ω\Omega — meaning u1u2u_1 \equiv u_2. The Dirichlet problem has at most one solution.

Interactive: Maximum Principle Demo

Below is a numerical solver for Laplace's equation on the unit square with four piecewise-constant Dirichlet boundaries. Drag the probe around the interior and watch the inequality minΩuu(x)maxΩu\min_{\partial\Omega} u \leq u(\mathbf{x}) \leq \max_{\partial\Omega} u hold at every point. You can never find an interior point hotter than the hottest edge or colder than the coldest edge.

Loading maximum-principle demo…

The Neumann Compatibility Condition

For the pure Neumann problem

2u=0  in  Ω,un=g  on  Ω,\nabla^2 u = 0 \;\text{in}\; \Omega, \qquad \dfrac{\partial u}{\partial n} = g \;\text{on}\; \partial\Omega,

the data gg cannot be arbitrary. Take the PDE and integrate over Ω\Omega:

Ω2udA=Ω ⁣ ⁣(u)dA=Ωu ⁣ ⁣nds=Ωunds.\int_\Omega \nabla^2 u\, dA = \int_\Omega \nabla\!\cdot\!(\nabla u)\, dA = \oint_{\partial\Omega} \nabla u\!\cdot\!\mathbf{n}\, ds = \oint_{\partial\Omega} \dfrac{\partial u}{\partial n}\, ds.

The leftmost integral is zero because 2u=0\nabla^2 u = 0 in Ω\Omega. So:

  Ωgds=0  \boxed{\;\oint_{\partial\Omega} g\, ds = 0\;}

Physical interpretation. For a steady temperature field, the net heat flux through the entire boundary must vanish — otherwise heat would be accumulating or being lost, and the interior could not be steady. If you try to pump heat into a fully sealed room without letting any out, there is no steady-state temperature; the room just keeps heating up.

Even when the compatibility condition holds, the Neumann solution is unique only up to an additive constant: if uu solves the problem, so does u+Cu + C. You have to know the temperature at some reference point to pin CC down.

Interactive: Compatibility Check

Slide the four normal-derivative values. The widget shows the net flux gds\oint g\,ds and turns green only when the data is compatible.

Loading compatibility demo…

Interactive: Solve a BVP Yourself

The widget below is a full Jacobi relaxation solver on the unit square. You can switch each of the four edges between Dirichlet and Neumann, adjust the boundary values, and press Run to watch the field relax to equilibrium. The red border marks Dirichlet edges; the blue border marks Neumann edges. The iteration counter tells you how many Jacobi sweeps have been applied so far.

Loading Laplace BVP solver…

Things to try. Pick the first preset (hot top, cold bottom, cold sides) and watch a thermal "dome" develop near the top edge. Switch to the third preset (insulated sides) and notice that the solution becomes purely vertical: u=yu = y. Try the all-Neumann case (set every side to Neumann) — if the four fluxes do not sum to zero, the solver still iterates, but the compatibility warning lights up and the field drifts without settling.

Worked Example (Step by Step)

Let's trace the Jacobi solver by hand on a tiny grid you can fit on a napkin. Take the unit square with N=5N = 5 grid points per axis (so 3×3=93 \times 3 = 9 interior unknowns). Use Dirichlet boundaries: top edge u=1u = 1, the other three edges u=0u = 0. We start with u=0u = 0 everywhere in the interior.

Click to expand the hand trace
Set-up. Grid indices are (i,j)(i, j) with i=0,,4i = 0,\ldots,4 (rows, y-direction) and j=0,,4j = 0,\ldots,4 (columns, x-direction). Row i=4i = 4 is the top. Initial state (interior = 0, top boundary = 1):
row i=4   [1   1   1   1   1]   <- top boundary (hot)
row i=3   [0   0   0   0   0]
row i=2   [0   0   0   0   0]
row i=1   [0   0   0   0   0]
row i=0   [0   0   0   0   0]   <- bottom (cold)
            j=0 j=1 j=2 j=3 j=4
Iteration 1. Each interior cell is replaced by 14(uN+uS+uE+uW)\tfrac{1}{4}(u_N + u_S + u_E + u_W). Only the row just below the top sees a nonzero neighbour (the top row, equal to 1). Every other interior cell averages four zeros and stays at zero.
row i=3   [0  0.25 0.25 0.25  0]
row i=2   [0  0    0    0     0]
row i=1   [0  0    0    0     0]
Iteration 2. Now row 3 has nonzero neighbours, so row 2 starts filling in. Row 3's center cell (i=3,j=2)(i=3, j=2) averages (1+0.25+0.25+0)/4=0.375(1 + 0.25 + 0.25 + 0) / 4 = 0.375. Row 2's center cell averages (0.25+0+0+0)/4=0.0625(0.25 + 0 + 0 + 0)/4 = 0.0625.
row i=3   [0  0.3125 0.375  0.3125 0]
row i=2   [0  0.0625 0.0625 0.0625 0]
row i=1   [0  0      0      0      0]
Iteration 3. Row 1 finally feels the top. Heat propagates one row per iteration, exactly like an explicit diffusion step. The center column of row 3 averages (1+0.0625+0.3125+0.3125)/40.4219(1 + 0.0625 + 0.3125 + 0.3125)/4 \approx 0.4219.
row i=3   [0  0.359375 0.421875 0.359375 0]
row i=2   [0  0.09375  0.125    0.09375  0]
row i=1   [0  0.015625 0.015625 0.015625 0]
After many iterations. The field has reached equilibrium. The exact discrete solution for the centre point is available by symmetry. Consider four problems, each with top, bottom, left, right held at 1 in turn and the other three sides at 0. By linearity, summing all four gives a problem with all sides at 1, whose unique solution is u1u \equiv 1. By symmetry, the centre of each of the four problems carries the same value cc, and 4c=14c = 1, so c=1/4c = 1/4.
u(centre) = 0.25   (exact, by symmetry + linearity)
Jacobi after 2048 iterations on a 33x33 grid: 0.24998
The numerical value matches the exact answer to four decimals — a satisfying confirmation that Jacobi is solving the right problem.

Python Implementation (Jacobi Relaxation)

Below is the same algorithm we just traced by hand, written in plain Python plus NumPy. Hover the cards on the left to highlight the corresponding code line.

Jacobi Solver for Laplace's Equation with Mixed BCs
🐍laplace_bvp_jacobi.py
1Import NumPy

We only need NumPy. The whole solver is one big array update plus four boundary-condition lines, so vectorised slicing is perfect.

3laplace_bvp_jacobi — the master function

Discretise the unit square into an N x N grid and find the harmonic function u that matches the given boundary conditions. Jacobi relaxation replaces each interior value with the average of its four neighbours, which is exactly the discrete version of the mean-value property of harmonic functions.

EXECUTION STATE
bc = dict of (kind, value) per side
N = grid points per axis, e.g. 33
tol = 1e-6 (convergence threshold)
max_iter = 20000 (safety cap)
21Grid spacing h

h = 1 / (N - 1) is the physical distance between two neighbouring grid points. For N = 33, h ≈ 0.03125. h shows up in the Neumann boundary update because Neumann conditions involve a derivative.

EXECUTION STATE
N = 33
h = 1/(N-1) = 0.03125
22Initial guess u = 0

We start with zeros everywhere. Jacobi will not care about the initial guess — given enough iterations it converges to the same answer regardless. But a smart initial guess (e.g. linear interpolation of boundary data) cuts iterations roughly in half.

24apply_bc — one helper for all four edges

Defined as a nested function so the main loop can rewrite the boundary once per sweep. For Dirichlet sides we just overwrite the boundary row/column with the prescribed value. For Neumann sides we use the ghost-point trick: enforce (u_boundary - u_neighbor) / h = g by setting u_boundary = u_neighbor + h * g. This is first-order accurate; second-order versions exist but are not needed for the demo.

25Top edge (y = 1)

u[-1, :] selects the very last row, which in our convention is the top boundary. Dirichlet branch overwrites every cell of that row with the prescribed value. Neumann branch makes the top row a copy of the second-from-top row shifted by h * g.

EXECUTION STATE
bc["top"] = ("dirichlet", 1.0)
u[-1, :] after Dirichlet = [1, 1, 1, ..., 1]
32Bottom edge (y = 0)

Same shape as the top, but the row index is 0 and the outward normal points in the -y direction. The ghost-point sign convention works out identically because in both cases the outward normal points away from the interior.

39Left edge (x = 0)

Now we hit a column instead of a row: u[:, 0]. Dirichlet overwrites the entire left column. Neumann couples it to its eastern neighbour u[:, 1].

46Right edge (x = 1)

Final edge — column index -1, outward normal +x. After this call u satisfies the boundary conditions exactly (Dirichlet) or approximately (Neumann, first-order).

53Initial boundary application

Before the loop starts, paint the boundary onto u so that the first interior sweep already pulls in correct boundary information.

55Main iteration loop

Each iteration is one Jacobi sweep over every interior point. We keep iterating until either (a) the largest pointwise change is below tol, or (b) we hit max_iter. For an N x N grid Jacobi takes roughly N^2 iterations to converge — about 1000 for N = 33.

LOOP TRACE · 4 iterations
iter = 1 (starting from zeros)
u_old = boundary set, interior 0
row next to top boundary = ≈ 0.25 (= 1/4 of the hot edge)
all other rows = still 0
max |u - u_old| = ≈ 0.25
iter = 2
row 2 below top = ≈ 0.0625 (= 1/4 of the 0.25 row)
row next to top = ≈ 0.3125 (averaging in its new neighbours)
max |u - u_old| = ≈ 0.0625
iter = 3
centre cell = ≈ 0.015625
u stays smooth — no oscillations = harmonic functions cannot oscillate
iter ≈ 1500 (converged)
u at centre (0.5, 0.5) = ≈ 0.5 (insulated sides + symmetric)
max |u - u_old| = < 1e-7
56Snapshot u_old

Jacobi uses the OLD field on the right-hand side to compute the NEW field, so we must keep a copy. Gauss–Seidel sweeps in place using already-updated values and converges roughly twice as fast, but is harder to vectorise.

58The Jacobi update — one vectorised line

The discrete Laplacian stencil says (u_N + u_S + u_E + u_W - 4 u_C) / h^2 = 0, which solves for u_C = (u_N + u_S + u_E + u_W) / 4. That is exactly this line. The slicing u_old[:-2, 1:-1] picks every interior cell&apos;s south neighbour in one shot; the four slices together hit all four neighbours of every interior cell simultaneously.

EXECUTION STATE
u_old[:-2, 1:-1] =
south neighbours
u_old[2:, 1:-1] =
north neighbours
u_old[1:-1, :-2] =
west  neighbours
u_old[1:-1, 2:] =
east  neighbours
64Re-apply boundary conditions

The Jacobi sweep only touches interior cells. Neumann boundaries depend on the latest interior values, so we must re-apply them every iteration. Dirichlet boundaries are constant and could be applied just once, but doing them every iteration is harmless and keeps the code uniform.

65Convergence test

err is the L∞ norm of the change. When this drops below tol the relaxation has stalled, meaning the discrete Laplacian is (approximately) zero everywhere — which is exactly the discrete version of ∇² u = 0.

EXECUTION STATE
err at iter 1 = ≈ 0.25
err at iter 100 = ≈ 0.005
err at convergence = < 1e-7
67Return u and iteration count

u is the discrete harmonic function; iters tells us how many sweeps we actually needed. For the demo BC (hot top, cold bottom, insulated sides) N = 33 converges in ~1500 iterations to 1e-7 tolerance.

71Demo boundary conditions

Mixed BVP: top and bottom are Dirichlet (fixed temperatures), left and right are Neumann with zero flux (perfect insulation). Physically: a metal plate whose top edge is welded to a 1 °C reservoir, whose bottom edge is held at 0 °C, and whose sides cannot lose any heat. The steady state must depend only on y.

78Sanity check — symmetric problem

With this BC, the exact steady state is u(x, y) = y (a linear ramp). The Jacobi solver should converge to u(centre) = 0.5 — and the printed value confirms it to six decimals. This is the simplest non-trivial check that the solver is working.

EXECUTION STATE
u(centre) numerical = 0.500000
u(centre) analytic = 0.5
63 lines without explanation
1import numpy as np
2
3def laplace_bvp_jacobi(bc, N=33, tol=1e-6, max_iter=20000):
4    """
5    Solve Laplace's equation  div(grad u) = 0  on the unit square (0,1)x(0,1)
6    by Jacobi relaxation on an N x N grid.
7
8    Parameters
9    ----------
10    bc : dict with keys "top", "bottom", "left", "right".
11         Each value is a tuple ("dirichlet", value)  or  ("neumann", flux).
12    N        : number of grid points along each axis (including boundary).
13    tol      : convergence tolerance on max |u_new - u_old|.
14    max_iter : safety cap on iterations.
15
16    Returns
17    -------
18    u    : the converged solution as an (N, N) array, u[i, j] = u(x_j, y_i).
19    iters: number of iterations actually used.
20    """
21    h = 1.0 / (N - 1)
22    u = np.zeros((N, N))
23
24    def apply_bc(u):
25        # top row  (y = 1)
26        kind, v = bc["top"]
27        if kind == "dirichlet":
28            u[-1, :] = v
29        else:                                  # outward normal = +y
30            u[-1, :] = u[-2, :] + h * v        # ghost-point form
31
32        # bottom row (y = 0)
33        kind, v = bc["bottom"]
34        if kind == "dirichlet":
35            u[0, :] = v
36        else:                                  # outward normal = -y
37            u[0, :] = u[1, :] + h * v
38
39        # left column (x = 0)
40        kind, v = bc["left"]
41        if kind == "dirichlet":
42            u[:, 0] = v
43        else:                                  # outward normal = -x
44            u[:, 0] = u[:, 1] + h * v
45
46        # right column (x = 1)
47        kind, v = bc["right"]
48        if kind == "dirichlet":
49            u[:, -1] = v
50        else:                                  # outward normal = +x
51            u[:, -1] = u[:, -2] + h * v
52
53    apply_bc(u)
54
55    for it in range(1, max_iter + 1):
56        u_old = u.copy()
57        # Jacobi sweep:  u_{i,j} <- 1/4 (north + south + east + west)
58        u[1:-1, 1:-1] = 0.25 * (
59            u_old[:-2, 1:-1] +     # south neighbor
60            u_old[2:,  1:-1] +     # north neighbor
61            u_old[1:-1, :-2] +     # west  neighbor
62            u_old[1:-1, 2:]        # east  neighbor
63        )
64        apply_bc(u)
65        err = np.max(np.abs(u - u_old))
66        if err < tol:
67            return u, it
68    return u, max_iter
69
70
71# --- Demo: hot top, cold bottom, insulated sides ---
72bc = {
73    "top":    ("dirichlet", 1.0),
74    "bottom": ("dirichlet", 0.0),
75    "left":   ("neumann",   0.0),
76    "right":  ("neumann",   0.0),
77}
78u, n_iter = laplace_bvp_jacobi(bc, N=33, tol=1e-7)
79print(f"Converged in {n_iter} iterations.")
80print(f"u(center)       = {u[16, 16]:.6f}")  # expected ~ 0.5 by symmetry
81print(f"max |u| on boundary = {max(u[0].max(), u[-1].max()):.3f}")

PyTorch Implementation (Differentiable)

Rewriting the solver in PyTorch unlocks something the NumPy version cannot do: backprop through the BVP. If we treat the boundary values as learnable parameters, the gradient of a downstream loss (e.g. desired interior temperature) flows all the way back to the boundary. This is the foundation of physics-informed learning and shape/topology optimisation.

Differentiable Laplace BVP solver
🐍laplace_bvp_torch.py
1Import torch

We swap NumPy for PyTorch. Two things change: (1) the same array slicing now records a computation graph, so we can call .backward() on a loss; (2) we automatically pick up GPU acceleration when the input tensors live on CUDA.

3laplace_bvp_torch — differentiable Jacobi

Same algorithm as before, but every operation is a tensor op tracked by autograd. The boundary tensors top, bottom, left, right can carry requires_grad=True; gradients flow from any downstream scalar back through the relaxation iterations to those boundary values.

EXECUTION STATE
top, bottom, left, right = (N,) tensors (possibly requires_grad=True)
N = grid size 33
n_iter = 400 Jacobi sweeps
10Match device and dtype

If the input lives on cuda:0, the whole solver runs on cuda:0. If it is float64, we use float64 everywhere. This single line keeps the function portable between CPU and GPU.

12Initial zero field

Same starting point as the NumPy version. PyTorch creates the tensor as a leaf with no gradient — gradients only enter through the boundary slices below.

15Paint the four boundaries

Each row/column of u is overwritten by the corresponding boundary tensor. Because those tensors require gradients, every element of u that touches the boundary inherits the graph: the top row is literally a view into the top tensor.

EXECUTION STATE
u[-1, :] = = top (tracked)
u[0, :] = = bottom
u[:, 0] = = left
u[:, -1] = = right
20Relaxation loop

n_iter Jacobi sweeps. Every sweep is one tensor add + a scalar multiply, so the backward pass through n_iter sweeps has length proportional to n_iter — which means the memory cost of autograd can be large. In practice you would use checkpointing or an implicit solver (linear solve) for very large grids.

LOOP TRACE · 3 iterations
sweep = 0
u interior = all zeros
row next to top = 0.25 * top (carries grad)
sweep = 200
u(centre) = still rising toward 0.25
computation graph length = 200 tensor ops deep
sweep = 400 (return)
u(centre) = ≈ 0.25 (well-converged)
u.grad_fn = MaskedScatterBackward (or similar)
22Out-of-place update

Important PyTorch detail: in-place updates of a tensor that already has a graph break autograd. We .clone() to a fresh tensor and write into that. The cost is one extra allocation per sweep — negligible compared to the matmul-style cost of the stencil itself.

23Discrete Laplacian average

Identical slicing to the NumPy version. Each sliced view is a leaf in the autograd graph; the average is a Mean-like op. After this line, every interior cell&apos;s value is a smooth function of the four boundary tensors.

28Re-glue boundary

We rewrite the boundary rows of u_new with the original tensors so that the graph from u to top, bottom, left, right is kept alive. Without this re-glue, after one sweep the boundary cells would still hold values, but their grad-link to the input tensors would have been lost.

35Return the converged field

u now satisfies ∇² u ≈ 0 with the prescribed boundary data, AND it is a differentiable function of that boundary data. The next block uses this fact.

39Inverse design problem

We want the temperature at the centre of the plate to be 0.7. The bottom, left, right edges are held at 0. What top edge produces u(centre) = 0.7? This is an inverse boundary problem — and because the forward solver is differentiable, we can solve it with gradient descent.

41Trainable top edge

We declare top as a learnable parameter, initialised at 1.0 everywhere. Each of its 33 entries can be optimised independently — meaning we can also discover a non-uniform top temperature.

EXECUTION STATE
top.requires_grad = True
top initial mean = 1.0
45Adam optimiser

Adam is overkill for this tiny problem but illustrates the pattern: any PyTorch optimiser works because the solver is just another differentiable op. lr = 0.05 converges in ~150 steps.

46Outer optimisation loop

For each gradient step: (1) run the forward solver, (2) read u at the centre cell, (3) compute squared error against the target, (4) backprop through every Jacobi sweep, (5) update top.

LOOP TRACE · 3 iterations
step = 0
u(centre) = ≈ 0.25 (top = 1 everywhere)
loss = (0.25 - 0.7)^2 = ≈ 0.2025
step = 40
u(centre) = ≈ 0.55
loss = ≈ 0.022
top.mean() = growing past 1.5
step = 150 (converged)
u(centre) = ≈ 0.700
loss = < 1e-6
top.mean() = ≈ 2.8
49Loss = squared interior error

(u[16, 16] - 0.7)^2 is a scalar tensor that depends on every iteration of the Jacobi loop. Calling loss.backward() unrolls the entire 400-step solver and pushes gradients all the way to top.

50zero_grad / backward / step

Standard PyTorch trio. zero_grad clears the leftover gradient from the previous step, backward fills in top.grad, and step uses Adam&apos;s update rule to nudge top.

56Final reporting

Detaching and averaging shows the typical top-edge temperature the optimiser found. Because the problem is severely under-determined (33 parameters vs. 1 scalar target), there is no unique answer — the optimiser finds one that works.

39 lines without explanation
1import torch
2
3def laplace_bvp_torch(top, bottom, left, right, N=33, n_iter=2000):
4    """
5    Differentiable Jacobi solver for Laplace's equation on the unit square.
6    All four boundary values are torch tensors that may carry gradients —
7    so we can backprop a downstream loss (e.g. desired interior pattern)
8    back through the solver to the boundary data.
9
10    All four sides are Dirichlet here for simplicity.
11    """
12    device = top.device
13    dtype  = top.dtype
14    u = torch.zeros(N, N, dtype=dtype, device=device)
15
16    # Paint the boundary
17    u[-1, :] = top
18    u[ 0, :] = bottom
19    u[:,  0] = left
20    u[:, -1] = right
21
22    for _ in range(n_iter):
23        # discrete Laplacian average — runs on GPU if u is on GPU
24        u_new = u.clone()
25        u_new[1:-1, 1:-1] = 0.25 * (
26            u[:-2, 1:-1] + u[2:, 1:-1] +
27            u[1:-1, :-2] + u[1:-1, 2:]
28        )
29        # re-paint boundary so it stays glued to the input tensors
30        u_new[-1, :] = top
31        u_new[ 0, :] = bottom
32        u_new[:,  0] = left
33        u_new[:, -1] = right
34        u = u_new
35
36    return u
37
38
39# --- We want u(centre) = target.  Optimise the top edge to achieve it. ---
40target_centre = 0.7
41
42top    = torch.full((33,), 1.0, requires_grad=True)
43bottom = torch.zeros(33)
44left   = torch.zeros(33)
45right  = torch.zeros(33)
46
47opt = torch.optim.Adam([top], lr=0.05)
48for step in range(200):
49    u = laplace_bvp_torch(top, bottom, left, right, N=33, n_iter=400)
50    loss = (u[16, 16] - target_centre) ** 2
51    opt.zero_grad()
52    loss.backward()
53    opt.step()
54    if step % 40 == 0:
55        print(f"step {step:3d}  loss={loss.item():.5f}  u_centre={u[16,16].item():.4f}")
56print(f"final mean(top) = {top.detach().mean().item():.4f}")

Real-World Applications

FielduBoundary conditionType
Steady-state heatTemperatureFixed temperature on furnace wallDirichlet
Steady-state heatTemperatureInsulated surface (no heat flux)Neumann (g = 0)
Steady-state heatTemperatureWall cooled by ambient air (Newton cooling)Robin
ElectrostaticsPotential VConductor at fixed voltageDirichlet
ElectrostaticsPotential VSurface charge density specifiedNeumann
Ideal fluid flowStream function ψVelocity tangent to body surface (no penetration)Dirichlet (ψ constant on body)
Soap-film geometryHeight uHeight fixed by the wire frameDirichlet
Image inpaintingPixel intensityKnown pixels around the holeDirichlet

Image inpainting is a fun one: replace a missing region with the solution of Laplace's equation whose Dirichlet data is the surrounding known pixels. The mean-value property smoothly interpolates the hole — colours blend, gradients match, no jarring edges. Modern deep-learning inpainters are more powerful (they can hallucinate structure), but the Laplace-equation inpainter is still the right thing to use for smooth backgrounds.

Common Pitfalls

  • Forgetting the compatibility condition. If you set up a pure Neumann problem with gds0\oint g\,ds \neq 0, no solution exists. The solver will diverge or drift; the discretised linear system is singular.
  • Forgetting the constant freedom in pure Neumann. Even compatible Neumann data leaves uu defined only up to +C+C. Add one additional pin like "average of u over Ω\Omega is zero" to make it unique.
  • Mixing up ∂u/∂n and ∂u/∂x. The Neumann condition uses the outward normal derivative. On the right edge of a rectangle, this is +u/x+\partial u/\partial x; on the left edge it is u/x-\partial u/\partial x.
  • First-order Neumann in code. The ghost-point trick ubd=unbr+hgu_{\text{bd}} = u_{\text{nbr}} + h g is only first-order accurate. For high precision, use second-order one-sided differences ubd=43unbr,113unbr,2+23hgu_{\text{bd}} = \tfrac{4}{3} u_{\text{nbr,1}} - \tfrac{1}{3} u_{\text{nbr,2}} + \tfrac{2}{3} h g (Taylor expand a quadratic to confirm).
  • Jacobi is slow. Convergence is O(N2)O(N^2) iterations. For large grids use multigrid, conjugate gradient, or a direct sparse solver.
  • Re-entrant corners. On non-convex domains (think of an L-shape) the solution can have a singularity at the inward corner: ur2/3u \sim r^{2/3}. The PDE still well-posed in the variational sense, but pointwise convergence near the corner is slow and grid-refinement is essential.

Test Your Understanding

Summary

ConceptStatementWhy it matters
Dirichlet BCu = f on ∂ΩPins the value on the edge — well-posed and unique.
Neumann BC∂u/∂n = g on ∂ΩPins the flux — unique up to a constant; needs ∮ g ds = 0.
Robin BCα u + β ∂u/∂n = hModels Newton cooling; interpolates Dirichlet and Neumann.
Mixed BVPDifferent sides carry different BC typesMost physical setups (insulated sides + fixed top, etc.).
Maximum principleMax and min of harmonic u live on ∂ΩGives one-line uniqueness proof; bounds the interior.
Compatibility∮ g ds = 0 for pure NeumannNet flux must balance, or no steady state exists.
Jacobi relaxationu_{i,j} ← (u_N + u_S + u_E + u_W)/4Discrete mean-value property; converges to the harmonic field.

Key Takeaways

  1. A PDE on its own is under-determined; boundary conditions turn it into a well-posed problem.
  2. The three classical types — Dirichlet, Neumann, Robin — cover essentially every physical scenario you will meet in heat conduction, electrostatics, fluid flow, and elastic membranes.
  3. The maximum principle is the secret weapon: it gives uniqueness, monotonicity, and pointwise bounds, all in one stroke.
  4. Pure Neumann problems need compatibility: Ωgds=0\oint_{\partial\Omega} g\,ds = 0. Net flux must vanish, otherwise no steady state exists.
  5. Jacobi relaxation is the discrete embodiment of the mean-value property — and writing it in PyTorch makes the entire BVP differentiable, which is the foundation of modern physics-informed deep learning.
Boundary Value Problems in One Sentence:
"Laplace's equation tells the interior to be the average of itself; boundary conditions tell it what to average toward."
Coming next. In section 28.3 we leave abstract boundary types behind and solve a concrete problem: Laplace's equation on a rectangle, by separation of variables and Fourier series. The maximum principle and compatibility conditions you have built here will keep guiding us — but the algebra will get specific.
Loading comments...